460 lines
10 KiB
Vue
460 lines
10 KiB
Vue
<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
|
||
// 读只读参数特征(MCM_INFO_UUID,TLV 同 INFO_RSP 全部项),取温度+风扇
|
||
const info = await McmInfo.readDeviceInfo()
|
||
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>
|