evan.liu 07509a2387 迁移uni-app x到uni-app vue3,蓝牙去第三方插件改uni内置BLE API
- uni-app x官方无内置蓝牙API(文档明确见插件市场),为实现去插件化,工程整体迁移为uni-app vue3(vue/ts),App端内置完整BLE API,标准基座即可运行
- 新建src/BluetoothManager.ts:基于uni内置BLE API(createBLEConnection/notifyBLECharacteristicValueChange/writeBLECharacteristicValue(writeNoResponse)/setBLEMTU/getBLEDeviceRSSI等),保持原插件type约定,页面业务逻辑不变
- 删除uni_modules/android-ble及未使用的lime-*、uni-wifi;保留uni-icons、uni-scss
- uts模块全部移植为ts(src/mcm、api、common):UTSJSONObject改Record<string,any>,适配vue3 getStorageSync空值返回空串的差异
- 7个页面uvue转vue:模板不变,显式import ref与生命周期钩子;OTA固件读取由java.io改为plus.io+uni.base64ToArrayBuffer
- manifest.json去uni-app-x改app-plus结构,补Android 12+蓝牙权限;删除platformConfig.json;运行改用标准基座
2026-09-04 06:02:29 +08:00

460 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- #ifdef APP -->
<scroll-view style="flex:1">
<!-- #endif -->
<view class="page">
<!-- 连接状态卡片 -->
<view class="card">
<view class="statusRow">
<view class="statusDot" :class="connected ? 'dotOn' : 'dotOff'"></view>
<text class="statusText">{{ connected ? 'Connected' : (connecting ? 'Connecting...' : 'Disconnected') }}</text>
<text class="deviceText" v-if="connected">{{ deviceName }} ({{ deviceMac }})</text>
</view>
<view class="btnRow">
<button class="btn" size="mini" v-if="!connected && !connecting" @tap="onBleStarScan">{{ scanning ? 'Scanning...' : 'Scan' }}</button>
<button class="btn btnWarn" size="mini" v-if="connected" @tap="onDisconnect">Disconnect</button>
</view>
</view>
<!-- 参数显示 -->
<view class="card paramCard" v-if="connected">
<view class="paramItem">
<text class="paramLabel">Temperature</text>
<text class="paramValue">{{ temperature }}<text class="paramUnit"> ℃</text></text>
</view>
<view class="paramItem">
<text class="paramLabel">Fan Speed</text>
<text class="paramValue">{{ fanSpeed }}<text class="paramUnit"> RPM</text></text>
</view>
</view>
<!-- 设备信息 -->
<view class="card" v-if="connected">
<view class="infoRow">
<text class="infoLabel">Firmware</text>
<text class="infoValue">{{ fwVersion }}</text>
</view>
<view class="infoRow">
<text class="infoLabel">Bank</text>
<text class="infoValue">{{ bankInfo }}</text>
</view>
</view>
<!-- sysinfo 控制台输出(原 System Status 页内容) -->
<view class="card" v-if="connected">
<text class="consoleTitle">sysinfo (system runtime status)</text>
<text class="consoleText">{{ sysInfo }}</text>
</view>
<!-- 扫描结果列表 -->
<view v-if="!connected">
<view v-for="(item, index) in scanArray" :key="index" class="card">
<view class="cardBox" @tap="onLinkBluetooth(item)">
<view class="leftIcon">
<uni-icons type="paperclip" size="20"/>
</view>
<view class="rightContent">
<text>{{ item.device.name ?? 'N/A' }}</text>
<text>{{ item.device.address }}</text>
</view>
</view>
</view>
<view v-if="!scanning && scanArray.length == 0" class="emptyBox">
<text class="emptyText">No device found. Tap "Scan" to retry</text>
</view>
</view>
</view>
<!-- #ifdef APP -->
</scroll-view>
<!-- #endif -->
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { onLoad, onShow, onHide, onUnload, onPullDownRefresh } from '@dcloudio/uni-app'
import { MyApiResult, BleScanResult } from "../../src/BluetoothManager"
import * as Ble from "../../src/BluetoothManager"
import * as McmChannel from "../../src/mcm/McmChannel"
import * as McmCmd from "../../src/mcm/McmCommands"
import * as McmInfo from "../../src/mcm/McmInfo"
import * as BleINfo from "../../common/bleInfo"
const DEVICE_NAME_FILTER = "CAIIC-MCM-20260902"
const POLL_INTERVAL = 2000
const scanArray = ref<BleScanResult[]>([])
const scanning = ref<boolean>(false)
const connecting = ref<boolean>(false)
const connected = ref<boolean>(false)
const deviceName = ref<string>("")
const deviceMac = ref<string>("")
const temperature = ref<string>("--")
const fanSpeed = ref<string>("--")
const fwVersion = ref<string>("--")
const bankInfo = ref<string>("--")
const sysInfo = ref<string>("--")
let pollTimer = 0
let pageVisible = false
// ================= 参数轮询 =================
async function pollParams() {
if (!connected.value || !pageVisible) return
// 一次 INFO_QUERY 同时取温度+风扇(协议 V1.1 第 5 节)
const info = await McmInfo.queryDeviceInfo([McmInfo.INFO_CHIP_TEMP, McmInfo.INFO_FAN_RPM])
const t = info.chipTemp
temperature.value = t != null ? t.toFixed(1) : "--"
const f = info.fanRpm
fanSpeed.value = f != null ? f.toString() : "N/A"
}
function stopPolling() {
if (pollTimer != 0) {
clearTimeout(pollTimer)
pollTimer = 0
}
}
function startPolling() {
stopPolling()
// 注意:uts 的 setTimeout 不接受 async 回调,用 then 链式调度
pollTimer = setTimeout(() => {
pollParams().then(() => {
if (connected.value && pageVisible) {
startPolling()
}
})
}, POLL_INTERVAL)
}
// ================= 设备信息 =================
async function loadDeviceInfo() {
const v = await McmCmd.getVersion()
if (v != null) fwVersion.value = v
const b = await McmCmd.getBankInfo()
if (b != null) {
// bankinfo 原文较长,取首个 "APP1/APP2" 关键词展示
bankInfo.value = b.indexOf("APP2") >= 0 ? "APP2" : (b.indexOf("APP1") >= 0 ? "APP1" : b)
}
const s = await McmCmd.getSysInfo()
sysInfo.value = s != null ? s : "--"
}
// ================= 蓝牙事件 =================
async function onBleDataListener(res: MyApiResult) {
if (res.type == 1000) {
// notify 订阅成功:协商 MTU 247(写切片 244),iOS 无效保持 20
Ble.setMtu(247, (resMtu: MyApiResult) => {
if (resMtu.type == 0) {
McmChannel.setMtuSize(247)
}
})
connected.value = true
connecting.value = false
deviceMac.value = Ble.getConnectMac()
loadDeviceInfo()
startPolling()
}
}
function onDisConnectUi() {
connected.value = false
connecting.value = false
stopPolling()
temperature.value = "--"
fanSpeed.value = "--"
fwVersion.value = "--"
bankInfo.value = "--"
sysInfo.value = "--"
}
function onBleStatueListener(state: number) {
console.log("home bleState = ", state)
if (state == 0) {
connecting.value = false
} else if (state == 10000) { // 连接失败
connecting.value = false
connected.value = false
uni.showToast({ title: 'Connection failed', icon: 'error' })
} else if (state == 10001) { // 已断开
onDisConnectUi()
}
}
// ================= 扫描与连接 =================
function onBleStarScan() {
if (scanning.value) return
if (!Ble.isEnabled()) {
Ble.openBluetooth((res: MyApiResult) => {
if (res.type == 0) onBleStarScan()
})
return
}
scanning.value = true
scanArray.value = []
const scanMs = 10 * 1000
Ble.startScan(scanMs, function(res: MyApiResult) {
if (res.type == 0) {
const scan = res.data as BleScanResult
const name = scan.device.name ?? ""
if (name.indexOf(DEVICE_NAME_FILTER) != 0) return // 只列出自家设备(CAIIC-MCM-20260902 前缀)
// 去重
for (let i = 0; i < scanArray.value.length; i++) {
if (scanArray.value[i].device.address == scan.device.address) return
}
scanArray.value.push(scan)
} else if (res.type == 10003) {
uni.showToast({ title: 'No Bluetooth permission', icon: 'none' })
} else if (res.type == 10002) {
uni.showToast({ title: 'Bluetooth is not turned on', icon: 'none' })
}
})
setTimeout(() => {
scanning.value = false
uni.stopPullDownRefresh()
}, scanMs + 200)
}
function onLinkBluetooth(item: BleScanResult) {
Ble.stopScan()
scanning.value = false
connecting.value = true
deviceName.value = item.device.name ?? DEVICE_NAME_FILTER
BleINfo.setName(deviceName.value)
BleINfo.setMac(item.device.address)
(getApp().globalData as any).ConnectBle(item.device.address, false)
}
function onDisconnect() {
stopPolling()
Ble.disConnect()
onDisConnectUi()
}
// ================= 生命周期 =================
let subscribed = false
function subscribe() {
if (subscribed) return
uni.$on("bleData", onBleDataListener)
uni.$on("bleState", onBleStatueListener)
subscribed = true
}
function unsubscribe() {
if (!subscribed) return
uni.$off("bleData", onBleDataListener)
uni.$off("bleState", onBleStatueListener)
subscribed = false
}
onLoad((options: any) => {
subscribe()
})
onShow(() => {
subscribe()
pageVisible = true
if (Ble.isConnected()) {
if (!connected.value) {
// 应用内已连接(如从其他页返回)
connected.value = true
deviceMac.value = Ble.getConnectMac()
deviceName.value = BleINfo.getName() ?? ""
loadDeviceInfo()
}
startPolling()
} else {
onBleStarScan()
}
})
onHide(() => {
pageVisible = false
Ble.stopScan()
stopPolling()
unsubscribe()
})
onUnload(() => {
pageVisible = false
Ble.stopScan()
stopPolling()
unsubscribe()
})
onPullDownRefresh(() => {
if (!connected.value) {
onBleStarScan()
} else {
// 已连接:下拉刷新设备信息(版本/bank/sysinfo)
loadDeviceInfo().then(() => {
uni.stopPullDownRefresh()
})
}
})
</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;
}
.statusRow {
display: flex;
flex-direction: row;
align-items: center;
}
.statusDot {
width: 16rpx;
height: 16rpx;
border-radius: 8rpx;
margin-right: 12rpx;
}
.dotOn { background-color: #4cd964; }
.dotOff { background-color: #cccccc; }
.statusText {
font-size: 30rpx;
font-weight: bold;
}
.deviceText {
font-size: 24rpx;
color: #888888;
margin-left: 16rpx;
flex: 1;
}
.btnRow {
display: flex;
flex-direction: row;
justify-content: flex-end;
margin-top: 16rpx;
}
.btn {
background-color: #3A6FA1;
color: #ffffff;
}
.btnWarn {
background-color: #dd524d;
}
.paramCard {
flex-direction: row;
justify-content: space-around;
}
.paramItem {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.paramLabel {
font-size: 26rpx;
color: #888888;
}
.paramValue {
font-size: 52rpx;
font-weight: bold;
color: #3A6FA1;
margin-top: 8rpx;
}
.paramUnit {
font-size: 24rpx;
font-weight: normal;
color: #888888;
}
.infoRow {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 8rpx 0;
}
.infoLabel {
font-size: 26rpx;
color: #888888;
}
.infoValue {
font-size: 26rpx;
color: #333333;
flex: 1;
text-align: right;
margin-left: 20rpx;
}
.cardBox {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.leftIcon {
width: 28px;
display: flex;
justify-content: center;
align-items: center;
margin-right: 10px;
}
.rightContent {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.emptyBox {
padding: 60rpx 0;
display: flex;
justify-content: center;
}
.emptyText {
font-size: 26rpx;
color: #999999;
}
.consoleTitle {
font-size: 26rpx;
color: #888888;
margin-bottom: 12rpx;
}
.consoleText {
font-size: 22rpx;
color: #333333;
font-family: monospace;
line-height: 1.5;
}
</style>