381 lines
8.8 KiB
Plaintext
381 lines
8.8 KiB
Plaintext
<template>
|
||
<!-- #ifdef APP -->
|
||
<scroll-view style="flex:1">
|
||
<!-- #endif -->
|
||
<view class="page">
|
||
|
||
<view class="card" v-if="!connected">
|
||
<text class="tipText">设备未连接,请先在"设备"页连接 CAIIC-MCM</text>
|
||
</view>
|
||
|
||
<template v-if="connected">
|
||
<!-- 当前设备信息 -->
|
||
<view class="card">
|
||
<view class="infoRow">
|
||
<text class="infoLabel">当前固件版本</text>
|
||
<text class="infoValue">{{ curVersion }}</text>
|
||
</view>
|
||
<view class="infoRow">
|
||
<text class="infoLabel">运行 Bank</text>
|
||
<text class="infoValue">{{ bankInfo }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 固件文件 -->
|
||
<view class="card">
|
||
<text class="sectionTitle">固件文件(Keil 产物 bin,≤224KB)</text>
|
||
<input class="input" v-model="filePath" placeholder="/sdcard/Download/embeddedSrc.bin" :disabled="upgrading" />
|
||
<view class="btnRow">
|
||
<button class="btn" size="mini" :disabled="upgrading" @tap="onLoadFile">读取文件</button>
|
||
</view>
|
||
<view class="infoRow" v-if="fileLoaded">
|
||
<text class="infoLabel">文件大小</text>
|
||
<text class="infoValue">{{ fileSize }} 字节</text>
|
||
</view>
|
||
<view class="infoRow" v-if="fileLoaded">
|
||
<text class="infoLabel">镜像 CRC32</text>
|
||
<text class="infoValue">0x{{ fileCrcHex }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 目标版本 -->
|
||
<view class="card">
|
||
<text class="sectionTitle">目标版本号(如 1.00.02)</text>
|
||
<input class="input" v-model="versionText" placeholder="1.00.02" :disabled="upgrading" />
|
||
</view>
|
||
|
||
<!-- 升级控制 -->
|
||
<view class="card">
|
||
<view class="progressBox">
|
||
<view class="progressBar" :style="'width:' + progress + '%'"></view>
|
||
</view>
|
||
<text class="progressText">{{ progress }}%</text>
|
||
<view class="btnRow">
|
||
<button class="btn" size="mini" v-if="!upgrading" :disabled="!fileLoaded" @tap="onStartOta">开始升级</button>
|
||
<button class="btn btnWarn" size="mini" v-if="upgrading" @tap="onAbortOta">中止升级</button>
|
||
</view>
|
||
<text class="warnText" v-if="upgrading">升级中请勿断开连接或退出应用</text>
|
||
</view>
|
||
|
||
<!-- 日志 -->
|
||
<view class="card" v-if="logs.length > 0">
|
||
<text class="sectionTitle">升级日志</text>
|
||
<text class="logText" v-for="(log, index) in logs" :key="index">{{ log }}</text>
|
||
</view>
|
||
</template>
|
||
|
||
</view>
|
||
<!-- #ifdef APP -->
|
||
</scroll-view>
|
||
<!-- #endif -->
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { MyApiResult } from "@/uni_modules/android-ble"
|
||
import * as Ble from "../../src/BluetoothManager"
|
||
import * as McmCmd from "../../src/mcm/McmCommands.uts"
|
||
import * as Ota from "../../src/mcm/OtaManager.uts"
|
||
import { OtaCallbacks } from "../../src/mcm/OtaManager.uts"
|
||
import { crc32Ieee } from "../../src/mcm/Crc.uts"
|
||
|
||
// #ifdef APP-ANDROID
|
||
import File from 'java.io.File'
|
||
import FileInputStream from 'java.io.FileInputStream'
|
||
// #endif
|
||
|
||
const connected = ref<boolean>(false)
|
||
const curVersion = ref<string>("--")
|
||
const bankInfo = ref<string>("--")
|
||
const filePath = ref<string>("/sdcard/Download/embeddedSrc.bin")
|
||
const fileLoaded = ref<boolean>(false)
|
||
const fileSize = ref<number>(0)
|
||
const fileCrcHex = ref<string>("")
|
||
const versionText = ref<string>("")
|
||
const upgrading = ref<boolean>(false)
|
||
const progress = ref<number>(0)
|
||
const logs = ref<string[]>([])
|
||
|
||
let imageBytes = [] as number[]
|
||
|
||
function addLog(msg: string) {
|
||
logs.value.push(msg)
|
||
}
|
||
|
||
// ================= 文件读取(Android 本地文件) =================
|
||
|
||
function readFileBytes(path: string): number[] | null {
|
||
// #ifdef APP-ANDROID
|
||
try {
|
||
const f = new File(path)
|
||
if (!f.exists()) return null
|
||
const len: Int = f.length().toInt()
|
||
if (len <= 0) return null
|
||
const fis = new FileInputStream(f)
|
||
const buf = new ByteArray(len)
|
||
let readTotal: Int = 0
|
||
while (readTotal < len) {
|
||
const n = fis.read(buf, readTotal, len - readTotal)
|
||
if (n <= 0) break
|
||
readTotal += n
|
||
}
|
||
fis.close()
|
||
if (readTotal != len) return null
|
||
const out = [] as number[]
|
||
var i: Int = 0
|
||
while (i < len) {
|
||
out.push(buf[i].toInt() & 0xFF)
|
||
i++
|
||
}
|
||
return out
|
||
} catch (e) {
|
||
console.log("readFileBytes error", e)
|
||
return null
|
||
}
|
||
// #endif
|
||
// #ifndef APP-ANDROID
|
||
return null
|
||
// #endif
|
||
}
|
||
|
||
function onLoadFile() {
|
||
const bytes = readFileBytes(filePath.value.trim())
|
||
if (bytes == null) {
|
||
fileLoaded.value = false
|
||
uni.showToast({ title: '文件读取失败,请检查路径', icon: 'error' })
|
||
return
|
||
}
|
||
if (bytes.length > 0x38000) {
|
||
fileLoaded.value = false
|
||
uni.showToast({ title: '文件超过 224KB 上限', icon: 'error' })
|
||
return
|
||
}
|
||
imageBytes = bytes
|
||
fileSize.value = bytes.length
|
||
fileCrcHex.value = crc32Ieee(bytes).toString(16)
|
||
fileLoaded.value = true
|
||
addLog("已加载文件: " + bytes.length + " 字节, CRC32=0x" + fileCrcHex.value)
|
||
}
|
||
|
||
// ================= 版本号解析 "1.00.02" -> 0x010002 =================
|
||
|
||
function parseVersion(v: string): number | null {
|
||
const parts = v.trim().split(".")
|
||
if (parts.length != 3) return null
|
||
const a = parseInt(parts[0])
|
||
const b = parseInt(parts[1])
|
||
const c = parseInt(parts[2])
|
||
if (isNaN(a) || isNaN(b) || isNaN(c)) return null
|
||
if (a < 0 || a > 255 || b < 0 || b > 255 || c < 0 || c > 255) return null
|
||
return a * 65536 + b * 256 + c
|
||
}
|
||
|
||
// ================= OTA =================
|
||
|
||
async function onStartOta() {
|
||
const version = parseVersion(versionText.value)
|
||
if (version == null) {
|
||
uni.showToast({ title: '版本号格式错误(如 1.00.02)', icon: 'error' })
|
||
return
|
||
}
|
||
if (!Ble.isConnected()) {
|
||
uni.showToast({ title: '设备未连接', icon: 'error' })
|
||
return
|
||
}
|
||
upgrading.value = true
|
||
progress.value = 0
|
||
logs.value = []
|
||
addLog("开始升级,目标版本 " + versionText.value)
|
||
const cbs: OtaCallbacks = {
|
||
onProgress: (percent: number) => {
|
||
progress.value = percent
|
||
},
|
||
onState: (msg: string) => {
|
||
addLog(msg)
|
||
}
|
||
} as OtaCallbacks
|
||
const ok = await Ota.start(imageBytes, version, cbs)
|
||
upgrading.value = false
|
||
if (ok) {
|
||
uni.showToast({ title: '升级成功,设备将自动复位', icon: 'success' })
|
||
} else {
|
||
uni.showToast({ title: '升级失败', icon: 'error' })
|
||
}
|
||
}
|
||
|
||
function onAbortOta() {
|
||
Ota.abort()
|
||
addLog("用户请求中止 ...")
|
||
}
|
||
|
||
// ================= 设备信息 =================
|
||
|
||
async function loadDeviceInfo() {
|
||
const v = await McmCmd.getVersion()
|
||
curVersion.value = v != null ? v : "--"
|
||
const b = await McmCmd.getBankInfo()
|
||
if (b != null) {
|
||
bankInfo.value = b.indexOf("APP2") >= 0 ? "APP2" : (b.indexOf("APP1") >= 0 ? "APP1" : "--")
|
||
}
|
||
}
|
||
|
||
// ================= 蓝牙状态监听 =================
|
||
|
||
function onBleStatueListener(state: number) {
|
||
if (state == 10000 || state == 10001) {
|
||
connected.value = false
|
||
if (upgrading.value) {
|
||
addLog("连接断开,升级中止")
|
||
upgrading.value = false
|
||
}
|
||
}
|
||
}
|
||
|
||
let subscribed = false
|
||
function subscribe() {
|
||
if (subscribed) return
|
||
uni.$on("bleState", onBleStatueListener)
|
||
subscribed = true
|
||
}
|
||
|
||
function unsubscribe() {
|
||
if (!subscribed) return
|
||
uni.$off("bleState", onBleStatueListener)
|
||
subscribed = false
|
||
}
|
||
|
||
// ================= 生命周期 =================
|
||
|
||
onLoad((options: UTSJSONObject | null) => {
|
||
subscribe()
|
||
})
|
||
|
||
onShow(() => {
|
||
subscribe()
|
||
connected.value = Ble.isConnected()
|
||
if (connected.value) {
|
||
loadDeviceInfo()
|
||
}
|
||
})
|
||
|
||
onHide(() => {
|
||
unsubscribe()
|
||
})
|
||
|
||
onUnload(() => {
|
||
unsubscribe()
|
||
// 离开页面时若仍在升级则中止(升级期间不建议离开)
|
||
if (Ota.isRunning()) {
|
||
Ota.abort()
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page{
|
||
display:flex;
|
||
flex-direction:column;
|
||
padding: 0 20rpx;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.card {
|
||
border-radius: 36rpx;
|
||
background-color: #ffffff;
|
||
padding: 24rpx;
|
||
margin-top: 20rpx;
|
||
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.tipText {
|
||
font-size: 26rpx;
|
||
color: #999999;
|
||
text-align: center;
|
||
padding: 20rpx 0;
|
||
}
|
||
|
||
.sectionTitle {
|
||
font-size: 26rpx;
|
||
color: #888888;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.input {
|
||
border: 1px solid #dddddd;
|
||
border-radius: 12rpx;
|
||
padding: 16rpx;
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.infoRow {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
padding: 8rpx 0;
|
||
}
|
||
|
||
.infoLabel {
|
||
font-size: 26rpx;
|
||
color: #888888;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.infoValue {
|
||
font-size: 24rpx;
|
||
color: #333333;
|
||
flex: 1;
|
||
text-align: right;
|
||
margin-left: 20rpx;
|
||
}
|
||
|
||
.btnRow {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: flex-end;
|
||
margin-top: 16rpx;
|
||
}
|
||
|
||
.btn {
|
||
background-color: #3A6FA1;
|
||
color: #ffffff;
|
||
}
|
||
.btnWarn {
|
||
background-color: #dd524d;
|
||
}
|
||
|
||
.progressBox {
|
||
height: 24rpx;
|
||
border-radius: 12rpx;
|
||
background-color: #eeeeee;
|
||
overflow: hidden;
|
||
margin-top: 8rpx;
|
||
}
|
||
|
||
.progressBar {
|
||
height: 100%;
|
||
background-color: #3A6FA1;
|
||
}
|
||
|
||
.progressText {
|
||
font-size: 26rpx;
|
||
color: #3A6FA1;
|
||
text-align: center;
|
||
margin-top: 8rpx;
|
||
}
|
||
|
||
.warnText {
|
||
font-size: 24rpx;
|
||
color: #dd524d;
|
||
text-align: center;
|
||
margin-top: 12rpx;
|
||
}
|
||
|
||
.logText {
|
||
font-size: 22rpx;
|
||
color: #333333;
|
||
font-family: monospace;
|
||
line-height: 1.5;
|
||
}
|
||
</style>
|