BLE协议V1.1适配:INFO_QUERY信息查询、显式GATT UUID、修复notify数据解析(NotityData);页面调整:Status并入Device、CLI终端改为底部tab;增加[MCM]收发报文日志
This commit is contained in:
parent
b4e8cafa61
commit
4af93dca6c
18
App.uvue
18
App.uvue
@ -10,13 +10,17 @@ import * as McmChannel from "@/src/mcm/McmChannel.uts"
|
|||||||
|
|
||||||
globalData: {
|
globalData: {
|
||||||
ble: new BleLib(), // 蓝牙全局变量
|
ble: new BleLib(), // 蓝牙全局变量
|
||||||
// 接收蓝牙数据的回调
|
// 接收蓝牙数据的回调(显式指定 CAIIC-MCM 服务/notify UUID,不依赖插件自动识别;
|
||||||
|
// 用当前接口 onNotityBleData,旧接口 onNotityReadBleData 插件已标记弃用)
|
||||||
setBleNotity(){
|
setBleNotity(){
|
||||||
getApp().globalData.ble.onNotityReadBleData(getApp().globalData.ble.getSericUUID(),
|
getApp().globalData.ble.onNotityBleData(Ble.MCM_SERVICE_UUID,
|
||||||
getApp().globalData.ble.getNotityUUID(),
|
Ble.MCM_NOTIFY_UUID,
|
||||||
true,
|
true,
|
||||||
function(res:MyApiResult){
|
function(res:MyApiResult){
|
||||||
// console.log(res)
|
if (res.type != 0) {
|
||||||
|
// 1000=订阅成功 1001=订阅失败(type=0 的 notify 数据由 McmChannel 打印)
|
||||||
|
console.log("[MCM] notify subscribe result: type=" + res.type + " msg=" + res.message)
|
||||||
|
}
|
||||||
uni.$emit("bleData", res)
|
uni.$emit("bleData", res)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -25,12 +29,10 @@ import * as McmChannel from "@/src/mcm/McmChannel.uts"
|
|||||||
// 连接蓝牙
|
// 连接蓝牙
|
||||||
ConnectBle(address: string, autoReconnect: boolean){
|
ConnectBle(address: string, autoReconnect: boolean){
|
||||||
getApp().globalData.ble.connect(address, autoReconnect, function(res:MyApiResult){
|
getApp().globalData.ble.connect(address, autoReconnect, function(res:MyApiResult){
|
||||||
// console.log(res)
|
console.log("[MCM] connect " + address + ": type=" + res.type + " msg=" + res.message)
|
||||||
if(res.type==0){
|
if(res.type==0){
|
||||||
getApp().globalData.ble.scanServices(function(resSevice:MyApiResult){
|
getApp().globalData.ble.scanServices(function(resSevice:MyApiResult){
|
||||||
// console.log(resSevice);
|
console.log("[MCM] scanServices: type=" + resSevice.type + " msg=" + resSevice.message)
|
||||||
// 显式选中 CAIIC-MCM 的 CLS 服务组合(自动识别可能选错)
|
|
||||||
Ble.selectService("0000CA10")
|
|
||||||
getApp().globalData.setBleNotity()
|
getApp().globalData.setBleNotity()
|
||||||
|
|
||||||
// getApp().globalData.ble.readRssi(function(b:MyApiResult){
|
// getApp().globalData.ble.readRssi(function(b:MyApiResult){
|
||||||
|
|||||||
72
docs/修改记录_2026-09-03_BLE协议V1.1适配.md
Normal file
72
docs/修改记录_2026-09-03_BLE协议V1.1适配.md
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# 修改记录 2026-09-03:BLE 协议 V1.1 适配
|
||||||
|
|
||||||
|
设备协议升级到 `D:\caic-workspace\mothercup\docs\ble_protocol.md`(CAIIC BLE 通信协议 V1.1),
|
||||||
|
APP 侧同步适配。差异与改动如下。
|
||||||
|
|
||||||
|
## 协议差异(V1.0 → V1.1)
|
||||||
|
|
||||||
|
- GATT 更换为 SDK rdtss 自定义 128-bit UUID:Service `00002760-08C2-11E1-9073-0E8AC72E1001`,
|
||||||
|
下行写特征(Write Without Response),上行 notify;广播名 `CAIIC-MCM-20260902`。
|
||||||
|
MTU=247 由设备连接后主动发起,手机端也可自行 requestMtu。
|
||||||
|
**注意**:协议文档表格中下行/上行 UUID 写作 `...C72EE001` / `...C72EE002`,
|
||||||
|
与固件 `app_rdtss.h` 实际值 `...C72E0001` / `...C72E0002` 不符,APP 以固件为准
|
||||||
|
(协议文档此处需修正)。
|
||||||
|
- 新增设备信息查询帧:`INFO_QUERY(0x20)` / `INFO_RSP(0x21)`,payload 为 TLV 序列
|
||||||
|
`{id u8, len u8, value…}×n`(数值小端)。信息项:0x01 FW_VERSION(u32)、
|
||||||
|
0x02 CHIP_TEMP(i16, 0.1℃, 0x7FFF=读取失败)、0x03 FAN_RPM(u16, 0xFFFF=无硬件)、
|
||||||
|
0x04 VDD_MV(u16, 0=失败)、0x05 UPTIME_S(u32)、0x06 FREE_HEAP(u32)。
|
||||||
|
空 payload 或单个 0xFF = 查询全部;设备不认识的 id 不出现在应答中。
|
||||||
|
- 帧格式(SOF 0xCA / CRC16-CCITT)、CLI 透传(0x01/0x02/0x03)、OTA(0x10/0x11/0x12/0x1F)
|
||||||
|
与 V1.0 一致,既有 `FrameCodec`/`McmChannel` CLI 通道/`OtaManager` 无需改动。
|
||||||
|
|
||||||
|
## 修改文件
|
||||||
|
|
||||||
|
- `src/mcm/FrameCodec.uts` — 新增 `TYPE_INFO_QUERY=0x20` / `TYPE_INFO_RSP=0x21` 常量,
|
||||||
|
新增 `readU16LE` / `readI16LE`(i16 处理符号位)解包工具。
|
||||||
|
- `src/mcm/McmChannel.uts` — 新增 INFO_QUERY 请求-响应通道:`queryInfo(items, timeoutMs)`
|
||||||
|
串行执行(infoChain),单 pending infoJob,超时/发送失败/断连 resolve null;
|
||||||
|
`dispatchFrame` 分发 INFO_RSP;`resetChannel` 清理挂起的信息查询。
|
||||||
|
**修复 notify 数据解析**:插件 `onNotityReadBleData` 数据回调(type=0)的 `res.data`
|
||||||
|
是 `NotityData` 结构(`{data: number[], mac, serviceId, characteristicsId}`),
|
||||||
|
此前按裸 `number[]` 解析导致设备应答字节永远进不了帧重组器,表现为
|
||||||
|
"写入成功但 help 无应答(status=-1)"。
|
||||||
|
CLI 命令**不追加** `\r\n`:固件 `app_ble_proto.c` 把 CLI_REQ payload 直接交给
|
||||||
|
`cli_exec_line`(只按空格/Tab 分词),尾随 `\r\n` 会被当作命令名一部分导致
|
||||||
|
unknown command(曾按"行编辑器需要结束符"的假设加过,查固件源码后撤销)。
|
||||||
|
- `src/BluetoothManager.uts` — 新增 `MCM_SERVICE_UUID` / `MCM_WRITE_UUID` /
|
||||||
|
`MCM_NOTIFY_UUID` 常量(取固件 `app_rdtss.h` 实际值,小写);
|
||||||
|
`sendDataBytes` 与 `onNotityReadBleData` 改用显式 UUID,不再依赖插件的
|
||||||
|
service/notify/write 自动识别(下行特征只有 Write Without Response 属性,
|
||||||
|
自动识别的写特征可能为空,表现为"发送 help 无应答")。
|
||||||
|
notify 订阅从旧接口 `onNotityReadBleData` 换成插件推荐的 `onNotityBleData`
|
||||||
|
(旧接口已标记弃用,数据统一走 `NotityData` 结构)。
|
||||||
|
- `App.uvue` — `setBleNotity()` 改用 `Ble.MCM_SERVICE_UUID` / `Ble.MCM_NOTIFY_UUID`
|
||||||
|
显式订阅 notify;`ConnectBle()` 移除 `selectService(...)` 调用(已不再需要)。
|
||||||
|
- 调试日志:BLE 收发全链路加了 `[MCM]` 前缀的 console 打印——`McmChannel` 的
|
||||||
|
TX 帧(完整 hex)、写入失败、RX notify 原始字节、RX 重组帧;
|
||||||
|
`App.uvue` 的 connect/scanServices/notify 订阅结果。真机调试时在 HBuilderX
|
||||||
|
控制台可直接观察报文流。
|
||||||
|
- `src/mcm/McmInfo.uts`(新增)— 信息项 id 常量、`parseTlv`(未知 id 按 len 跳过、
|
||||||
|
长度不符项忽略、0x7FFF/0xFFFF/0 等无效值归一为 null)、`DeviceInfo` 类型、
|
||||||
|
`queryDeviceInfo(items)`(空数组=查全部)、`formatVersion`(u32 → "V1.00.01")。
|
||||||
|
- `src/mcm/McmCommands.uts` — `getTemperature`/`getFanSpeed`/`getVersion` 改走
|
||||||
|
INFO_QUERY(函数签名不变);废弃未实现的 CLI `temp`/`fan`/`version` 命令常量;
|
||||||
|
`getSysInfo`/`getBankInfo`/`execCli` 保持 CLI 透传不变。
|
||||||
|
- `pages/tabbar/home.uvue` — 扫描过滤前缀更新为 `CAIIC-MCM-20260902`;
|
||||||
|
参数轮询合并为一次 `queryDeviceInfo([CHIP_TEMP, FAN_RPM])`(温度显示 1 位小数,
|
||||||
|
风扇无硬件显示 `N/A`)。
|
||||||
|
|
||||||
|
## 未改动
|
||||||
|
|
||||||
|
- `src/mcm/OtaManager.uts`:OTA 流程与 V1.1 第 6 节一致(BEGIN→DATA 扇区级 ack 流控→END
|
||||||
|
整镜像 CRC32 校验,status=2 按应答 offset 重同步)。
|
||||||
|
- `pages/tabbar/sysStatus.uvue` / `pages/tabbar/ota.uvue` / CLI 调试终端:
|
||||||
|
调用的 `McmCommands` API 签名不变,无需修改。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- 用 Node 脚本按协议 8.2 节参考实现复算 8.3 节全部字节级示例(CLI_REQ "help"、
|
||||||
|
INFO_QUERY 查全部/温度+风扇、OTA_BEGIN/OTA_DATA/OTA_END),与 `encodeFrame`/CRC16
|
||||||
|
输出逐字节一致;INFO_RSP 样例(26.6℃ + 无风扇)经 `parseTlv` 解析结果正确。
|
||||||
|
- 真机回归(HBuilderX 编译):扫描连接 `CAIIC-MCM-20260902`、主页温度/风扇显示、
|
||||||
|
系统状态页、CLI 终端、本地 bin OTA 升级。
|
||||||
34
docs/修改记录_2026-09-03_页面结构调整.md
Normal file
34
docs/修改记录_2026-09-03_页面结构调整.md
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# 修改记录 2026-09-03:页面结构调整(Status 合并进 Device,CLI 终端改为底部 Tab)
|
||||||
|
|
||||||
|
## 改动
|
||||||
|
|
||||||
|
- **Status 页合并进 Device 页**:`pages/tabbar/home.uvue` 新增 sysinfo 控制台卡片
|
||||||
|
(固件版本/运行 Bank 卡片下方),`loadDeviceInfo()` 顺带拉取 `sysinfo`;
|
||||||
|
已连接时下拉刷新改为重新拉取设备信息(版本/bank/sysinfo);断开时清空 sysinfo 显示。
|
||||||
|
新增 `consoleTitle`/`consoleText` 样式(沿用原 sysStatus 页样式)。
|
||||||
|
- **CLI 终端改为底部 Tab**:`pages/bleTest/bluetoothOperation/bluetoothOperation.uvue`
|
||||||
|
进入 tabbar(text "CLI",图标复用原 Status 的 `mapDefault/mapActive.png`)。
|
||||||
|
- 连接地址不再有页面参数来源:`onConnect()` 在未连接时取 `bleInfo` 中最近连接过的
|
||||||
|
MAC,没有记录则提示先去 Device 页扫描连接;
|
||||||
|
- 生命周期钩子由 `onPageShow/onPageHide` 改为 tabbar 适用的 `onShow/onHide`;
|
||||||
|
- `onShow` 时若应用内已连接(如在 Device 页连上后切过来)自动进入可用状态。
|
||||||
|
- **my 页移除 "CLI Terminal" 入口**(已成为 tab),删除不再使用的 `onBluetooth`、
|
||||||
|
Ble 相关 import 与 `cardBle/cardBox/textType` 样式。
|
||||||
|
|
||||||
|
## pages.json
|
||||||
|
|
||||||
|
- tabbar 四 tab:Device / OTA / CLI / my(原 Status 位替换为 CLI)。
|
||||||
|
- 移除 `pages/tabbar/sysStatus` 与 `pages/bleTest/bluetoothList/bluetoothList` 的页面注册;
|
||||||
|
`bluetoothOperation` 保留注册(现为 tab 页),导航栏背景色与整体风格统一为 `#3A6FA1`。
|
||||||
|
|
||||||
|
## 删除
|
||||||
|
|
||||||
|
- `pages/tabbar/sysStatus.uvue`(内容已并入 home.uvue)。
|
||||||
|
- `pages/bleTest/bluetoothList/`(扫描选设备页:唯一入口是 my 页的 CLI Terminal,
|
||||||
|
设备选择统一由 Device 主页承担后该页成为死代码)。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- 无 CLI 构建手段,需 HBuilderX 编译真机验证:四个 tab 切换正常;Device 页连接后
|
||||||
|
sysinfo 控制台有内容;切到 CLI tab 自动可用、发 `help` 有应答;未连接时 CLI tab
|
||||||
|
点 connect 给出提示。
|
||||||
30
pages.json
30
pages.json
@ -8,14 +8,6 @@
|
|||||||
"enablePullDownRefresh": true
|
"enablePullDownRefresh": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"path": "pages/tabbar/sysStatus",
|
|
||||||
"style": {
|
|
||||||
"navigationBarTitleText": "System Status",
|
|
||||||
"navigationBarBackgroundColor": "#3A6FA1",
|
|
||||||
"enablePullDownRefresh": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "pages/tabbar/ota",
|
"path": "pages/tabbar/ota",
|
||||||
"style": {
|
"style": {
|
||||||
@ -53,17 +45,11 @@
|
|||||||
"navigationBarBackgroundColor": "#3A6FA1"
|
"navigationBarBackgroundColor": "#3A6FA1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"path": "pages/bleTest/bluetoothList/bluetoothList",
|
|
||||||
"style": {
|
|
||||||
"navigationBarTitleText": "bluetooth list",
|
|
||||||
"enablePullDownRefresh": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "pages/bleTest/bluetoothOperation/bluetoothOperation",
|
"path": "pages/bleTest/bluetoothOperation/bluetoothOperation",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "CLI Terminal"
|
"navigationBarTitleText": "CLI Terminal",
|
||||||
|
"navigationBarBackgroundColor": "#3A6FA1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -79,18 +65,18 @@
|
|||||||
"iconPath": "/static/tabbar/homeDefault.png",
|
"iconPath": "/static/tabbar/homeDefault.png",
|
||||||
"selectedIconPath": "/static/tabbar/homeActive.png"
|
"selectedIconPath": "/static/tabbar/homeActive.png"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"pagePath": "pages/tabbar/sysStatus",
|
|
||||||
"text": "Status",
|
|
||||||
"iconPath": "/static/tabbar/mapDefault.png",
|
|
||||||
"selectedIconPath": "/static/tabbar/mapActive.png"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"pagePath": "pages/tabbar/ota",
|
"pagePath": "pages/tabbar/ota",
|
||||||
"text": "OTA",
|
"text": "OTA",
|
||||||
"iconPath": "/static/tabbar/chargeDefault.png",
|
"iconPath": "/static/tabbar/chargeDefault.png",
|
||||||
"selectedIconPath": "/static/tabbar/chargeActive.png"
|
"selectedIconPath": "/static/tabbar/chargeActive.png"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"pagePath": "pages/bleTest/bluetoothOperation/bluetoothOperation",
|
||||||
|
"text": "CLI",
|
||||||
|
"iconPath": "/static/tabbar/mapDefault.png",
|
||||||
|
"selectedIconPath": "/static/tabbar/mapActive.png"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"pagePath": "pages/tabbar/my",
|
"pagePath": "pages/tabbar/my",
|
||||||
"text": "my",
|
"text": "my",
|
||||||
|
|||||||
@ -1,142 +0,0 @@
|
|||||||
<template>
|
|
||||||
<!-- #ifdef APP -->
|
|
||||||
<scroll-view style="flex:1">
|
|
||||||
<!-- #endif -->
|
|
||||||
<view class="page">
|
|
||||||
<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 }}</text>
|
|
||||||
<text>{{ item.device.address }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<!-- #ifdef APP -->
|
|
||||||
</scroll-view>
|
|
||||||
<!-- #endif -->
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="uts">
|
|
||||||
import {BleLib,MyApiResult,BleScanResult,BleServices,ScanRssiBR,WNuuid} from "@/uni_modules/android-ble"
|
|
||||||
import * as Ble from "../../../src/BluetoothManager"
|
|
||||||
|
|
||||||
const scanArray = ref<BleScanResult[]>([]) // 蓝牙扫描结果
|
|
||||||
const refreshing = ref<boolean>(false)
|
|
||||||
|
|
||||||
// 连接蓝牙
|
|
||||||
function onLinkBluetooth(item: BleScanResult) {
|
|
||||||
console.log(item.device.name)
|
|
||||||
uni.navigateTo({
|
|
||||||
url: `/pages/bleTest/bluetoothOperation/bluetoothOperation?address=${encodeURIComponent(item.device.address)}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function onBleStarScan() {
|
|
||||||
console.log("list")
|
|
||||||
if (refreshing.value) return
|
|
||||||
refreshing.value = true
|
|
||||||
|
|
||||||
const scanMs = 10*1000
|
|
||||||
scanArray.value = []
|
|
||||||
Ble.startScan(scanMs,function(res:MyApiResult){
|
|
||||||
// type 为0 扫描成功 10001 表示无蓝牙权限 10002 表示蓝牙未开启
|
|
||||||
// console.log(res.type)
|
|
||||||
if(res.type==0){
|
|
||||||
var scan = res.data as BleScanResult;
|
|
||||||
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(() => {
|
|
||||||
refreshing.value = false
|
|
||||||
uni.stopPullDownRefresh()
|
|
||||||
}, scanMs + 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======= 生命周期 ========
|
|
||||||
onLoad((options: UTSJSONObject | null) => {
|
|
||||||
if (options != null) {
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onPageShow(() => {
|
|
||||||
onBleStarScan()
|
|
||||||
})
|
|
||||||
|
|
||||||
onPageHide(() => {
|
|
||||||
Ble.stopScan()
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnload(() => {
|
|
||||||
console.log("stop")
|
|
||||||
Ble.stopScan()
|
|
||||||
})
|
|
||||||
|
|
||||||
// 下拉刷新
|
|
||||||
onPullDownRefresh(() => {
|
|
||||||
onBleStarScan()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.page{
|
|
||||||
display:flex;
|
|
||||||
flex-direction:column;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 卡片 */
|
|
||||||
.card {
|
|
||||||
border-radius: 36rpx;
|
|
||||||
background-color: #ffffff;
|
|
||||||
padding: 20rpx;
|
|
||||||
margin-top: 20rpx;
|
|
||||||
/* 阴影效果 */
|
|
||||||
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
|
|
||||||
/* 横向布局:左图右文 */
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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; /* 很关键:让 text-overflow 生效 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.displyType {
|
|
||||||
color: #0000ff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -50,6 +50,7 @@ import * as Ble from "../../../src/BluetoothManager"
|
|||||||
import * as McmChannel from "../../../src/mcm/McmChannel.uts"
|
import * as McmChannel from "../../../src/mcm/McmChannel.uts"
|
||||||
import * as McmCmd from "../../../src/mcm/McmCommands.uts"
|
import * as McmCmd from "../../../src/mcm/McmCommands.uts"
|
||||||
import { CliResult } from "../../../src/mcm/McmChannel.uts"
|
import { CliResult } from "../../../src/mcm/McmChannel.uts"
|
||||||
|
import * as BleINfo from "../../../common/bleInfo.uts"
|
||||||
|
|
||||||
const bleAddress = ref('')
|
const bleAddress = ref('')
|
||||||
const bleIsConnect = ref(false) // 蓝牙连接标志 true-已连接 false-未连接
|
const bleIsConnect = ref(false) // 蓝牙连接标志 true-已连接 false-未连接
|
||||||
@ -120,19 +121,29 @@ function onDisConnect() {
|
|||||||
McmChannel.resetChannel()
|
McmChannel.resetChannel()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 连接蓝牙(走全局 ConnectBle:connect → scanServices → 选中 CA10 服务 → 订阅 notify)
|
// 连接蓝牙(走全局 ConnectBle:connect → scanServices → 订阅 notify,显式 CAIIC-MCM UUID)
|
||||||
|
// 本页是 tab 页,地址取最近连接过的设备(bleInfo),没有则提示去 Device 页连接
|
||||||
function onConnect() {
|
function onConnect() {
|
||||||
if (Ble.isConnected()) {
|
if (Ble.isConnected()) {
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
bleIsConnect.value = true
|
bleIsConnect.value = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let addr = bleAddress.value
|
||||||
|
if (addr.length == 0) {
|
||||||
|
addr = BleINfo.getMac() ?? ""
|
||||||
|
}
|
||||||
|
if (addr.length == 0) {
|
||||||
|
pushMsg("message", "No device. Please scan and connect a device on the Device tab first")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bleAddress.value = addr
|
||||||
uni.showLoading({
|
uni.showLoading({
|
||||||
title: "Connecting...",
|
title: "Connecting...",
|
||||||
mask: true
|
mask: true
|
||||||
})
|
})
|
||||||
pushMsg("start", "connect: " + bleAddress.value)
|
pushMsg("start", "connect: " + addr)
|
||||||
getApp().globalData.ConnectBle(bleAddress.value, false)
|
getApp().globalData.ConnectBle(addr, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送 CLI 命令
|
// 发送 CLI 命令
|
||||||
@ -190,11 +201,16 @@ onLoad((options: UTSJSONObject | null) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onPageShow(() => {
|
onShow(() => {
|
||||||
subscribe()
|
subscribe()
|
||||||
|
// 从其他 tab 切回时同步连接状态
|
||||||
|
if (Ble.isConnected() && !bleIsConnect.value) {
|
||||||
|
bleIsConnect.value = true
|
||||||
|
pushMsg("message", "already connected: " + Ble.getConnectMac())
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onPageHide(() => {
|
onHide(() => {
|
||||||
unsubscribe()
|
unsubscribe()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -41,6 +41,12 @@
|
|||||||
</view>
|
</view>
|
||||||
</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-if="!connected">
|
||||||
<view v-for="(item, index) in scanArray" :key="index" class="card">
|
<view v-for="(item, index) in scanArray" :key="index" class="card">
|
||||||
@ -70,9 +76,10 @@ import { MyApiResult, BleScanResult } from "@/uni_modules/android-ble"
|
|||||||
import * as Ble from "../../src/BluetoothManager"
|
import * as Ble from "../../src/BluetoothManager"
|
||||||
import * as McmChannel from "../../src/mcm/McmChannel.uts"
|
import * as McmChannel from "../../src/mcm/McmChannel.uts"
|
||||||
import * as McmCmd from "../../src/mcm/McmCommands.uts"
|
import * as McmCmd from "../../src/mcm/McmCommands.uts"
|
||||||
|
import * as McmInfo from "../../src/mcm/McmInfo.uts"
|
||||||
import * as BleINfo from "../../common/bleInfo.uts"
|
import * as BleINfo from "../../common/bleInfo.uts"
|
||||||
|
|
||||||
const DEVICE_NAME_FILTER = "CAIIC-MCM"
|
const DEVICE_NAME_FILTER = "CAIIC-MCM-20260902"
|
||||||
const POLL_INTERVAL = 2000
|
const POLL_INTERVAL = 2000
|
||||||
|
|
||||||
const scanArray = ref<BleScanResult[]>([])
|
const scanArray = ref<BleScanResult[]>([])
|
||||||
@ -85,6 +92,7 @@ const temperature = ref<string>("--")
|
|||||||
const fanSpeed = ref<string>("--")
|
const fanSpeed = ref<string>("--")
|
||||||
const fwVersion = ref<string>("--")
|
const fwVersion = ref<string>("--")
|
||||||
const bankInfo = ref<string>("--")
|
const bankInfo = ref<string>("--")
|
||||||
|
const sysInfo = ref<string>("--")
|
||||||
|
|
||||||
let pollTimer = 0
|
let pollTimer = 0
|
||||||
let pageVisible = false
|
let pageVisible = false
|
||||||
@ -93,11 +101,12 @@ let pageVisible = false
|
|||||||
|
|
||||||
async function pollParams() {
|
async function pollParams() {
|
||||||
if (!connected.value || !pageVisible) return
|
if (!connected.value || !pageVisible) return
|
||||||
const temp = await McmCmd.getTemperature()
|
// 一次 INFO_QUERY 同时取温度+风扇(协议 V1.1 第 5 节)
|
||||||
temperature.value = temp != null ? temp.toString() : "--"
|
const info = await McmInfo.queryDeviceInfo([McmInfo.INFO_CHIP_TEMP, McmInfo.INFO_FAN_RPM])
|
||||||
if (!connected.value || !pageVisible) return
|
const t = info.chipTemp
|
||||||
const fan = await McmCmd.getFanSpeed()
|
temperature.value = t != null ? t.toFixed(1) : "--"
|
||||||
fanSpeed.value = fan != null ? fan.toString() : "--"
|
const f = info.fanRpm
|
||||||
|
fanSpeed.value = f != null ? f.toString() : "N/A"
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopPolling() {
|
function stopPolling() {
|
||||||
@ -129,6 +138,8 @@ async function loadDeviceInfo() {
|
|||||||
// bankinfo 原文较长,取首个 "APP1/APP2" 关键词展示
|
// bankinfo 原文较长,取首个 "APP1/APP2" 关键词展示
|
||||||
bankInfo.value = b.indexOf("APP2") >= 0 ? "APP2" : (b.indexOf("APP1") >= 0 ? "APP1" : b)
|
bankInfo.value = b.indexOf("APP2") >= 0 ? "APP2" : (b.indexOf("APP1") >= 0 ? "APP1" : b)
|
||||||
}
|
}
|
||||||
|
const s = await McmCmd.getSysInfo()
|
||||||
|
sysInfo.value = s != null ? s : "--"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================= 蓝牙事件 =================
|
// ================= 蓝牙事件 =================
|
||||||
@ -157,6 +168,7 @@ function onDisConnectUi() {
|
|||||||
fanSpeed.value = "--"
|
fanSpeed.value = "--"
|
||||||
fwVersion.value = "--"
|
fwVersion.value = "--"
|
||||||
bankInfo.value = "--"
|
bankInfo.value = "--"
|
||||||
|
sysInfo.value = "--"
|
||||||
}
|
}
|
||||||
|
|
||||||
function onBleStatueListener(state: number) {
|
function onBleStatueListener(state: number) {
|
||||||
@ -188,6 +200,8 @@ function onBleStarScan() {
|
|||||||
Ble.startScan(scanMs, function(res: MyApiResult) {
|
Ble.startScan(scanMs, function(res: MyApiResult) {
|
||||||
if (res.type == 0) {
|
if (res.type == 0) {
|
||||||
const scan = res.data as BleScanResult
|
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++) {
|
for (let i = 0; i < scanArray.value.length; i++) {
|
||||||
if (scanArray.value[i].device.address == scan.device.address) return
|
if (scanArray.value[i].device.address == scan.device.address) return
|
||||||
@ -277,7 +291,10 @@ onPullDownRefresh(() => {
|
|||||||
if (!connected.value) {
|
if (!connected.value) {
|
||||||
onBleStarScan()
|
onBleStarScan()
|
||||||
} else {
|
} else {
|
||||||
uni.stopPullDownRefresh()
|
// 已连接:下拉刷新设备信息(版本/bank/sysinfo)
|
||||||
|
loadDeviceInfo().then(() => {
|
||||||
|
uni.stopPullDownRefresh()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@ -424,4 +441,17 @@ onPullDownRefresh(() => {
|
|||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
color: #999999;
|
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>
|
</style>
|
||||||
|
|||||||
@ -26,14 +26,6 @@
|
|||||||
<view class="verType">
|
<view class="verType">
|
||||||
<text class="colorType">{{ "version: 1.0.1" }}</text>
|
<text class="colorType">{{ "version: 1.0.1" }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="cardBle">
|
|
||||||
<!-- CLI 调试终端 -->
|
|
||||||
<view class="cardBox" @tap="onBluetooth">
|
|
||||||
<uni-icons type="link" size="20" class="input-icon" />
|
|
||||||
<text class="textType">CLI Terminal</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
<!-- #ifdef APP -->
|
<!-- #ifdef APP -->
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
@ -45,8 +37,6 @@
|
|||||||
import { checkToken, getUserName } from '../../common/auth';
|
import { checkToken, getUserName } from '../../common/auth';
|
||||||
import { SERVER_LIST, getBaseUrl, setBaseUrl } from '../../api/httpInstance';
|
import { SERVER_LIST, getBaseUrl, setBaseUrl } from '../../api/httpInstance';
|
||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import {BleLib,MyApiResult,BleScanResult,BleServices,ScanRssiBR,WNuuid} from "@/uni_modules/android-ble"
|
|
||||||
import * as Ble from "../../src/BluetoothManager"
|
|
||||||
|
|
||||||
const isLogin = ref(false)
|
const isLogin = ref(false)
|
||||||
const userInfo = ref("login/register")
|
const userInfo = ref("login/register")
|
||||||
@ -114,22 +104,6 @@ function onCardTap () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CLI 调试终端(先扫描选择设备,连接后进入终端)
|
|
||||||
function onBluetooth() {
|
|
||||||
var open=Ble.isEnabled();// true 为蓝牙开启 false 为关闭
|
|
||||||
if(open) {
|
|
||||||
uni.navigateTo({
|
|
||||||
url: "/pages/bleTest/bluetoothList/bluetoothList"
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
uni.showToast({
|
|
||||||
title: 'please turn on bluetooth',
|
|
||||||
icon: 'error',
|
|
||||||
duration: 2000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======= 生命周期 ========
|
// ======= 生命周期 ========
|
||||||
onLoad((options: UTSJSONObject | null) => {
|
onLoad((options: UTSJSONObject | null) => {
|
||||||
if (options != null) {
|
if (options != null) {
|
||||||
@ -193,32 +167,6 @@ onUnload(() => {
|
|||||||
color: #333333;
|
color: #333333;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 卡片 */
|
|
||||||
.cardBle {
|
|
||||||
border-radius: 36rpx;
|
|
||||||
background-color: #ffffff;
|
|
||||||
padding: 20rpx;
|
|
||||||
margin-top: 20rpx;
|
|
||||||
/* 阴影效果 */
|
|
||||||
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
|
|
||||||
|
|
||||||
/* 横向布局:左图右文 */
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardBox {
|
|
||||||
padding: 35rpx;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.textType {
|
|
||||||
font-size: 25rpx;
|
|
||||||
}
|
|
||||||
.verType {
|
.verType {
|
||||||
margin-top: 500rpx;
|
margin-top: 500rpx;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -1,196 +0,0 @@
|
|||||||
<template>
|
|
||||||
<!-- #ifdef APP -->
|
|
||||||
<scroll-view style="flex:1">
|
|
||||||
<!-- #endif -->
|
|
||||||
<view class="page">
|
|
||||||
|
|
||||||
<view class="card" v-if="!connected">
|
|
||||||
<text class="tipText">Not connected. Please connect a CAIIC-MCM device on the Device tab first</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<template v-if="connected">
|
|
||||||
<!-- 概要 -->
|
|
||||||
<view class="card">
|
|
||||||
<view class="infoRow">
|
|
||||||
<text class="infoLabel">Firmware</text>
|
|
||||||
<text class="infoValue">{{ version }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="infoRow">
|
|
||||||
<text class="infoLabel">Bank Info</text>
|
|
||||||
<text class="infoValue">{{ bankInfo }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="btnRow">
|
|
||||||
<button class="btn" size="mini" :disabled="loading" @tap="refreshAll">
|
|
||||||
{{ loading ? 'Loading...' : 'Refresh' }}
|
|
||||||
</button>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- sysinfo 控制台输出 -->
|
|
||||||
<view class="card">
|
|
||||||
<text class="consoleTitle">sysinfo (system runtime status)</text>
|
|
||||||
<text class="consoleText">{{ sysInfo }}</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"
|
|
||||||
|
|
||||||
const connected = ref<boolean>(false)
|
|
||||||
const loading = ref<boolean>(false)
|
|
||||||
const version = ref<string>("--")
|
|
||||||
const bankInfo = ref<string>("--")
|
|
||||||
const sysInfo = ref<string>("--")
|
|
||||||
|
|
||||||
async function refreshAll() {
|
|
||||||
uni.stopPullDownRefresh()
|
|
||||||
if (!Ble.isConnected()) {
|
|
||||||
connected.value = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
connected.value = true
|
|
||||||
loading.value = true
|
|
||||||
|
|
||||||
const v = await McmCmd.getVersion()
|
|
||||||
version.value = v != null ? v : "--"
|
|
||||||
|
|
||||||
const b = await McmCmd.getBankInfo()
|
|
||||||
bankInfo.value = b != null ? b.trim() : "--"
|
|
||||||
|
|
||||||
const s = await McmCmd.getSysInfo()
|
|
||||||
sysInfo.value = s != null ? s : "--"
|
|
||||||
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= 蓝牙状态监听 =================
|
|
||||||
|
|
||||||
function onBleStatueListener(state: number) {
|
|
||||||
if (state == 10000 || state == 10001) {
|
|
||||||
connected.value = false
|
|
||||||
version.value = "--"
|
|
||||||
bankInfo.value = "--"
|
|
||||||
sysInfo.value = "--"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
if (Ble.isConnected()) {
|
|
||||||
refreshAll()
|
|
||||||
} else {
|
|
||||||
connected.value = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onHide(() => {
|
|
||||||
unsubscribe()
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnload(() => {
|
|
||||||
unsubscribe()
|
|
||||||
})
|
|
||||||
|
|
||||||
onPullDownRefresh(() => {
|
|
||||||
refreshAll()
|
|
||||||
})
|
|
||||||
</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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.consoleTitle {
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #888888;
|
|
||||||
margin-bottom: 12rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.consoleText {
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: #333333;
|
|
||||||
font-family: monospace;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,5 +1,11 @@
|
|||||||
import {BleLib,MyApiResult,BleScanResult,BleServices,ScanRssiBR,WNuuid,ScanPara,WriteData} from "@/uni_modules/android-ble"
|
import {BleLib,MyApiResult,BleScanResult,BleServices,ScanRssiBR,WNuuid,ScanPara,WriteData} from "@/uni_modules/android-ble"
|
||||||
|
|
||||||
|
// CAIIC-MCM 设备 GATT(固件 mcm-ddc-ble app_rdtss.h 实际值,小写与 Android UUID.toString 格式一致)
|
||||||
|
// 注意:ble_protocol.md V1.1 表格写作 ...C72EE001/...C72EE002,与固件实际 ...C72E0001/...C72E0002 不符,以固件为准
|
||||||
|
export const MCM_SERVICE_UUID = "00002760-08c2-11e1-9073-0e8ac72e1001"
|
||||||
|
export const MCM_WRITE_UUID = "00002760-08c2-11e1-9073-0e8ac72e0001" // Write Without Response
|
||||||
|
export const MCM_NOTIFY_UUID = "00002760-08c2-11e1-9073-0e8ac72e0002" // Notify
|
||||||
|
|
||||||
// 统一拿到全局 ble 单例(仍然是 App.globalData.ble)
|
// 统一拿到全局 ble 单例(仍然是 App.globalData.ble)
|
||||||
function ble(): BleLib {
|
function ble(): BleLib {
|
||||||
const app = getApp()
|
const app = getApp()
|
||||||
@ -93,10 +99,11 @@ export function disConnect() {
|
|||||||
ble().close()
|
ble().close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开启服务消息通知
|
// 开启服务消息通知(显式使用 CAIIC-MCM 的服务/notify UUID;onNotityBleData 为当前接口,
|
||||||
|
// 旧接口 onNotityReadBleData 插件已标记弃用)
|
||||||
export function onNotityReadBleData(readNotification: boolean , cb: (res: MyApiResult) => void) {
|
export function onNotityReadBleData(readNotification: boolean , cb: (res: MyApiResult) => void) {
|
||||||
const cBle = ble()
|
const cBle = ble()
|
||||||
cBle.onNotityReadBleData(cBle.getSericUUID(), cBle.getNotityUUID(), readNotification, cb)
|
cBle.onNotityBleData(MCM_SERVICE_UUID, MCM_NOTIFY_UUID, readNotification, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取自动识别读写服务uuid
|
// 获取自动识别读写服务uuid
|
||||||
@ -122,12 +129,14 @@ export function sendData(data: number[], cb: (res: MyApiResult) => void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 发送数据(显式写类型):writeType 0=WRITE(有响应) 1=WRITE_NO_RESPONSE(无响应)
|
// 发送数据(显式写类型):writeType 0=WRITE(有响应) 1=WRITE_NO_RESPONSE(无响应)
|
||||||
// CAIIC-MCM 设备的下行特征仅支持 Write Without Response,必须用 writeType=1
|
// CAIIC-MCM 设备的下行特征仅支持 Write Without Response,必须用 writeType=1。
|
||||||
|
// 服务/特征 UUID 用固件实际值显式指定,不依赖插件自动识别
|
||||||
|
//(下行特征只有 WRITE_NO_RESPONSE 属性,自动识别的 wuuid 可能为空导致写入失败)
|
||||||
export function sendDataBytes(data: number[], writeType: number, cb: (res: MyApiResult) => void) {
|
export function sendDataBytes(data: number[], writeType: number, cb: (res: MyApiResult) => void) {
|
||||||
const b = ble()
|
const b = ble()
|
||||||
const wd: WriteData = {
|
const wd: WriteData = {
|
||||||
serviceId: b.getServiceUUID(),
|
serviceId: MCM_SERVICE_UUID,
|
||||||
characteristicId: b.getWriteUUID(),
|
characteristicId: MCM_WRITE_UUID,
|
||||||
writeType: writeType,
|
writeType: writeType,
|
||||||
data: data,
|
data: data,
|
||||||
fenbao: false
|
fenbao: false
|
||||||
@ -135,7 +144,7 @@ export function sendDataBytes(data: number[], writeType: number, cb: (res: MyApi
|
|||||||
b.sendData(wd, cb)
|
b.sendData(wd, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按服务 UUID 前缀选择 service+notify+write 组合(如 "0000CA10"),找到返回 true
|
// 按服务 UUID 前缀选择 service+notify+write 组合(如 "00002760"),找到返回 true
|
||||||
export function selectService(serviceUuidPrefix: string): boolean {
|
export function selectService(serviceUuidPrefix: string): boolean {
|
||||||
const b = ble()
|
const b = ble()
|
||||||
const list = b.getUUIDs() as any as WNuuid[]
|
const list = b.getUUIDs() as any as WNuuid[]
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* CAIIC BLE 协议 V1.0 帧编解码
|
* CAIIC BLE 协议 V1.1 帧编解码
|
||||||
* 帧格式(小端):[0]=0xCA SOF, [1]=TYPE, [2]=SEQ, [3:4]=LEN(u16 LE),
|
* 帧格式(小端):[0]=0xCA SOF, [1]=TYPE, [2]=SEQ, [3:4]=LEN(u16 LE),
|
||||||
* [5..]=PAYLOAD(LEN 字节), 末尾 2B CRC16-CCITT(覆盖 TYPE..PAYLOAD, LE)
|
* [5..]=PAYLOAD(LEN 字节), 末尾 2B CRC16-CCITT(覆盖 TYPE..PAYLOAD, LE)
|
||||||
* 帧开销 7 字节(头 5 + CRC 2),payload 上限 480。
|
* 帧开销 7 字节(头 5 + CRC 2),payload 上限 480。
|
||||||
@ -19,6 +19,8 @@ export const TYPE_OTA_BEGIN = 0x10 // {total_size u32, image_crc32 u32, versi
|
|||||||
export const TYPE_OTA_DATA = 0x11 // {offset u32, data…}
|
export const TYPE_OTA_DATA = 0x11 // {offset u32, data…}
|
||||||
export const TYPE_OTA_END = 0x12 // {crc32 u32}
|
export const TYPE_OTA_END = 0x12 // {crc32 u32}
|
||||||
export const TYPE_OTA_RSP = 0x1F // {cmd_echo u8, status u8, offset u32}
|
export const TYPE_OTA_RSP = 0x1F // {cmd_echo u8, status u8, offset u32}
|
||||||
|
export const TYPE_INFO_QUERY = 0x20 // {item_id u8}×n;空 payload 或单个 0xFF = 查询全部
|
||||||
|
export const TYPE_INFO_RSP = 0x21 // TLV 序列 {id u8, len u8, value…}×n
|
||||||
|
|
||||||
export type BleFrame = {
|
export type BleFrame = {
|
||||||
type: number,
|
type: number,
|
||||||
@ -111,3 +113,14 @@ export function readU32LE(data: number[], offset: number): number {
|
|||||||
(data[offset + 2] & 0xFF) * 65536 +
|
(data[offset + 2] & 0xFF) * 65536 +
|
||||||
(data[offset + 3] & 0xFF) * 16777216
|
(data[offset + 3] & 0xFF) * 16777216
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 小端 u16 解包 */
|
||||||
|
export function readU16LE(data: number[], offset: number): number {
|
||||||
|
return (data[offset] & 0xFF) + (data[offset + 1] & 0xFF) * 256
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 小端 i16 解包(处理符号位) */
|
||||||
|
export function readI16LE(data: number[], offset: number): number {
|
||||||
|
const v = readU16LE(data, offset)
|
||||||
|
return v >= 0x8000 ? v - 0x10000 : v
|
||||||
|
}
|
||||||
|
|||||||
@ -8,12 +8,12 @@
|
|||||||
* 约定:所有接口的 Promise 只 resolve 不 reject,失败通过返回值表达(uts 兼容性考虑)。
|
* 约定:所有接口的 Promise 只 resolve 不 reject,失败通过返回值表达(uts 兼容性考虑)。
|
||||||
*/
|
*/
|
||||||
import * as Ble from "../BluetoothManager.uts"
|
import * as Ble from "../BluetoothManager.uts"
|
||||||
import { MyApiResult } from "@/uni_modules/android-ble"
|
import { MyApiResult, NotityData } from "@/uni_modules/android-ble"
|
||||||
import {
|
import {
|
||||||
FrameDecoder, BleFrame, encodeFrame,
|
FrameDecoder, BleFrame, encodeFrame,
|
||||||
TYPE_CLI_REQ, TYPE_CLI_RSP, TYPE_CLI_RSP_END, TYPE_OTA_RSP
|
TYPE_CLI_REQ, TYPE_CLI_RSP, TYPE_CLI_RSP_END, TYPE_OTA_RSP, TYPE_INFO_QUERY, TYPE_INFO_RSP
|
||||||
} from "./FrameCodec.uts"
|
} from "./FrameCodec.uts"
|
||||||
import { asciiToBytes, bytesToAscii } from "../commonFunctions.uts"
|
import { asciiToBytes, bytesToAscii, bytesToHex } from "../commonFunctions.uts"
|
||||||
|
|
||||||
export type CliResult = {
|
export type CliResult = {
|
||||||
text: string, // 聚合的应答文本
|
text: string, // 聚合的应答文本
|
||||||
@ -26,6 +26,11 @@ type CliJob = {
|
|||||||
timer: number
|
timer: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InfoJob = {
|
||||||
|
resolve: (payload: number[] | null) => void,
|
||||||
|
timer: number
|
||||||
|
}
|
||||||
|
|
||||||
const CLI_MAX_CMD_BYTES = 63
|
const CLI_MAX_CMD_BYTES = 63
|
||||||
|
|
||||||
let txSeq = 0
|
let txSeq = 0
|
||||||
@ -37,12 +42,16 @@ let cliJob: CliJob | null = null
|
|||||||
let cliChain: Promise<boolean> = Promise.resolve(true)
|
let cliChain: Promise<boolean> = Promise.resolve(true)
|
||||||
let writeChain: Promise<boolean> = Promise.resolve(true)
|
let writeChain: Promise<boolean> = Promise.resolve(true)
|
||||||
|
|
||||||
|
let infoJob: InfoJob | null = null
|
||||||
|
let infoChain: Promise<boolean> = Promise.resolve(true)
|
||||||
|
|
||||||
let otaRspHandler: ((payload: number[]) => void) | null = null
|
let otaRspHandler: ((payload: number[]) => void) | null = null
|
||||||
let disconnectHandler: (() => void) | null = null
|
let disconnectHandler: (() => void) | null = null
|
||||||
|
|
||||||
// ================= 接收 =================
|
// ================= 接收 =================
|
||||||
|
|
||||||
function dispatchFrame(frame: BleFrame) {
|
function dispatchFrame(frame: BleFrame) {
|
||||||
|
console.log("[MCM] RX frame type=0x" + frame.type.toString(16) + " seq=" + frame.seq + " payload=" + bytesToHex(frame.payload))
|
||||||
if (frame.type == TYPE_CLI_RSP) {
|
if (frame.type == TYPE_CLI_RSP) {
|
||||||
const job = cliJob
|
const job = cliJob
|
||||||
if (job != null) {
|
if (job != null) {
|
||||||
@ -65,13 +74,23 @@ function dispatchFrame(frame: BleFrame) {
|
|||||||
if (h != null) {
|
if (h != null) {
|
||||||
h(frame.payload)
|
h(frame.payload)
|
||||||
}
|
}
|
||||||
|
} else if (frame.type == TYPE_INFO_RSP) {
|
||||||
|
const job = infoJob
|
||||||
|
if (job != null) {
|
||||||
|
infoJob = null
|
||||||
|
clearTimeout(job.timer)
|
||||||
|
job.resolve(frame.payload)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onBleData(res: MyApiResult) {
|
function onBleData(res: MyApiResult) {
|
||||||
if (res.type != 0) return
|
if (res.type != 0) return
|
||||||
const data = res.data as number[]
|
// 插件 notify 回调的 data 是 NotityData 结构({data: number[], mac, serviceId, ...}),
|
||||||
const frames = decoder.feed(data)
|
// 不是裸字节数组,需取 .data 字段
|
||||||
|
const nd = res.data as NotityData
|
||||||
|
console.log("[MCM] RX notify " + nd.data.length + "B: " + bytesToHex(nd.data))
|
||||||
|
const frames = decoder.feed(nd.data)
|
||||||
for (let i = 0; i < frames.length; i++) {
|
for (let i = 0; i < frames.length; i++) {
|
||||||
dispatchFrame(frames[i])
|
dispatchFrame(frames[i])
|
||||||
}
|
}
|
||||||
@ -86,7 +105,7 @@ function onBleState(state: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 断连/异常时复位通道:挂起的 CLI 命令以 -4 结束 */
|
/** 断连/异常时复位通道:挂起的 CLI 命令以 -4 结束,挂起的信息查询以 null 结束 */
|
||||||
export function resetChannel() {
|
export function resetChannel() {
|
||||||
decoder.reset()
|
decoder.reset()
|
||||||
txSeq = 0
|
txSeq = 0
|
||||||
@ -100,6 +119,12 @@ export function resetChannel() {
|
|||||||
}
|
}
|
||||||
job.resolve({ text: text, status: -4 } as CliResult)
|
job.resolve({ text: text, status: -4 } as CliResult)
|
||||||
}
|
}
|
||||||
|
const iJob = infoJob
|
||||||
|
if (iJob != null) {
|
||||||
|
infoJob = null
|
||||||
|
clearTimeout(iJob.timer)
|
||||||
|
iJob.resolve(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================= 生命周期 =================
|
// ================= 生命周期 =================
|
||||||
@ -144,6 +169,7 @@ function sendOnce(chunk: number[]): Promise<MyApiResult> {
|
|||||||
async function sendFrameImpl(type: number, payload: number[]): Promise<boolean> {
|
async function sendFrameImpl(type: number, payload: number[]): Promise<boolean> {
|
||||||
const frame = encodeFrame(type, txSeq & 0xFF, payload)
|
const frame = encodeFrame(type, txSeq & 0xFF, payload)
|
||||||
txSeq = (txSeq + 1) & 0xFF
|
txSeq = (txSeq + 1) & 0xFF
|
||||||
|
console.log("[MCM] TX frame type=0x" + type.toString(16) + " len=" + frame.length + ": " + bytesToHex(frame))
|
||||||
let offset = 0
|
let offset = 0
|
||||||
while (offset < frame.length) {
|
while (offset < frame.length) {
|
||||||
const chunkLen = Math.min(frame.length - offset, maxWriteBytes)
|
const chunkLen = Math.min(frame.length - offset, maxWriteBytes)
|
||||||
@ -153,6 +179,7 @@ async function sendFrameImpl(type: number, payload: number[]): Promise<boolean>
|
|||||||
}
|
}
|
||||||
const res = await sendOnce(chunk)
|
const res = await sendOnce(chunk)
|
||||||
if (res.type != 0) {
|
if (res.type != 0) {
|
||||||
|
console.log("[MCM] TX write failed: type=" + res.type + " msg=" + res.message)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
offset += chunkLen
|
offset += chunkLen
|
||||||
@ -169,6 +196,7 @@ export function sendFrame(type: number, payload: number[]): Promise<boolean> {
|
|||||||
|
|
||||||
function sendCliImpl(cmd: string, timeoutMs: number): Promise<CliResult> {
|
function sendCliImpl(cmd: string, timeoutMs: number): Promise<CliResult> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
// 固件 CLI_REQ 为整行 payload 直接执行(协议 V1.1:无结束符)
|
||||||
const payload = asciiToBytes(cmd)
|
const payload = asciiToBytes(cmd)
|
||||||
if (payload.length > CLI_MAX_CMD_BYTES) {
|
if (payload.length > CLI_MAX_CMD_BYTES) {
|
||||||
resolve({ text: "", status: -2 } as CliResult)
|
resolve({ text: "", status: -2 } as CliResult)
|
||||||
@ -207,3 +235,40 @@ export function sendCli(cmd: string, timeoutMs: number = 3000): Promise<CliResul
|
|||||||
cliChain = p.then((): boolean => true)
|
cliChain = p.then((): boolean => true)
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================= 信息查询(INFO_QUERY / INFO_RSP) =================
|
||||||
|
|
||||||
|
function queryInfoImpl(items: number[], timeoutMs: number): Promise<number[] | null> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const job: InfoJob = {
|
||||||
|
resolve: resolve,
|
||||||
|
timer: 0
|
||||||
|
} as InfoJob
|
||||||
|
job.timer = setTimeout(() => {
|
||||||
|
if (infoJob == job) {
|
||||||
|
infoJob = null
|
||||||
|
resolve(null)
|
||||||
|
}
|
||||||
|
}, timeoutMs)
|
||||||
|
infoJob = job
|
||||||
|
// payload = {item_id u8}×n;空数组 = 查询全部
|
||||||
|
sendFrame(TYPE_INFO_QUERY, items).then((ok) => {
|
||||||
|
if (!ok && infoJob == job) {
|
||||||
|
infoJob = null
|
||||||
|
clearTimeout(job.timer)
|
||||||
|
resolve(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备信息查询:串行执行,返回 INFO_RSP 的 TLV payload;
|
||||||
|
* 超时/发送失败/断连返回 null;只 resolve 不 reject
|
||||||
|
*/
|
||||||
|
export function queryInfo(items: number[], timeoutMs: number = 2000): Promise<number[] | null> {
|
||||||
|
attach()
|
||||||
|
const p = infoChain.then((): Promise<number[] | null> => queryInfoImpl(items, timeoutMs))
|
||||||
|
infoChain = p.then((): boolean => true)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|||||||
@ -1,34 +1,22 @@
|
|||||||
/**
|
/**
|
||||||
* CAIIC-MCM 业务命令封装(基于 CLI 透传)
|
* CAIIC-MCM 业务命令封装
|
||||||
* 命令名集中在顶部常量,固件侧命令定名后只需改这里。
|
* 温度/风扇/固件版本走 INFO_QUERY(协议 V1.1 第 5 节);
|
||||||
* 注意:固件当前已实现 version/sysinfo/bankinfo;
|
* sysinfo/bankinfo 等系统状态仍走 CLI 透传,命令名集中在顶部常量。
|
||||||
* 温度/风扇命令(默认 temp/fan)待固件新增,解析失败返回 null 由页面显示 "--"。
|
|
||||||
*/
|
*/
|
||||||
import { sendCli, CliResult } from "./McmChannel.uts"
|
import { sendCli, CliResult } from "./McmChannel.uts"
|
||||||
|
import {
|
||||||
|
queryDeviceInfo, formatVersion,
|
||||||
|
INFO_FW_VERSION, INFO_CHIP_TEMP, INFO_FAN_RPM
|
||||||
|
} from "./McmInfo.uts"
|
||||||
|
|
||||||
const CMD_VERSION = "version"
|
|
||||||
const CMD_SYSINFO = "sysinfo"
|
const CMD_SYSINFO = "sysinfo"
|
||||||
const CMD_BANKINFO = "bankinfo"
|
const CMD_BANKINFO = "bankinfo"
|
||||||
const CMD_TEMPERATURE = "temp"
|
|
||||||
const CMD_FAN_SPEED = "fan"
|
|
||||||
|
|
||||||
/** 从应答文本中提取第一个数值(整数或小数),提取不到返回 null */
|
/** 固件版本号,如 "V1.00.01"(来自 INFO_QUERY FW_VERSION);失败返回 null */
|
||||||
function parseFirstNumber(text: string): number | null {
|
|
||||||
const m = text.match(/-?\d+(\.\d+)?/)
|
|
||||||
if (m == null) return null
|
|
||||||
const m0 = m[0]
|
|
||||||
if (m0 == null) return null
|
|
||||||
const v = parseFloat(m0)
|
|
||||||
if (isNaN(v)) return null
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 固件版本号原文,如 "V1.00.01";失败返回 null */
|
|
||||||
export async function getVersion(): Promise<string | null> {
|
export async function getVersion(): Promise<string | null> {
|
||||||
const r: CliResult = await sendCli(CMD_VERSION)
|
const info = await queryDeviceInfo([INFO_FW_VERSION])
|
||||||
if (r.status != 0) return null
|
const v = info.fwVersion
|
||||||
const t = r.text.trim()
|
return v != null ? formatVersion(v) : null
|
||||||
return t.length > 0 ? t : null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 系统运行状态原文(运行时间/任务数/heap/任务表),失败返回 null */
|
/** 系统运行状态原文(运行时间/任务数/heap/任务表),失败返回 null */
|
||||||
@ -45,18 +33,16 @@ export async function getBankInfo(): Promise<string | null> {
|
|||||||
return r.text
|
return r.text
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 温度(单位由固件定义,℃),失败返回 null */
|
/** 温度(℃),读取失败返回 null */
|
||||||
export async function getTemperature(): Promise<number | null> {
|
export async function getTemperature(): Promise<number | null> {
|
||||||
const r: CliResult = await sendCli(CMD_TEMPERATURE)
|
const info = await queryDeviceInfo([INFO_CHIP_TEMP])
|
||||||
if (r.status != 0) return null
|
return info.chipTemp
|
||||||
return parseFirstNumber(r.text)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 风扇转速(RPM),失败返回 null */
|
/** 风扇转速(RPM),无测速硬件/读取失败返回 null */
|
||||||
export async function getFanSpeed(): Promise<number | null> {
|
export async function getFanSpeed(): Promise<number | null> {
|
||||||
const r: CliResult = await sendCli(CMD_FAN_SPEED)
|
const info = await queryDeviceInfo([INFO_FAN_RPM])
|
||||||
if (r.status != 0) return null
|
return info.fanRpm
|
||||||
return parseFirstNumber(r.text)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 通用 CLI 命令(调试终端用) */
|
/** 通用 CLI 命令(调试终端用) */
|
||||||
|
|||||||
85
src/mcm/McmInfo.uts
Normal file
85
src/mcm/McmInfo.uts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
/**
|
||||||
|
* CAIIC-MCM 设备信息查询(协议 V1.1 第 5 节:INFO_QUERY / INFO_RSP)
|
||||||
|
* INFO_RSP payload 为 TLV 序列 {id u8, len u8, value…}×n,数值均小端;
|
||||||
|
* 设备不认识的 id 不出现在应答中;解析时未知 id 按 len 跳过。
|
||||||
|
*/
|
||||||
|
import { queryInfo } from "./McmChannel.uts"
|
||||||
|
import { readU16LE, readI16LE, readU32LE } from "./FrameCodec.uts"
|
||||||
|
|
||||||
|
// 信息项 id
|
||||||
|
export const INFO_FW_VERSION = 0x01 // u32 固件版本号,如 0x00010001 = V1.00.01
|
||||||
|
export const INFO_CHIP_TEMP = 0x02 // i16 芯片温度,单位 0.1℃(0x7FFF = 读取失败)
|
||||||
|
export const INFO_FAN_RPM = 0x03 // u16 风扇转速 rpm(0xFFFF = 无此硬件/未接入)
|
||||||
|
export const INFO_VDD_MV = 0x04 // u16 电源电压 mV(0 = 读取失败)
|
||||||
|
export const INFO_UPTIME_S = 0x05 // u32 系统运行时间 s
|
||||||
|
export const INFO_FREE_HEAP = 0x06 // u32 FreeRTOS 堆剩余 B
|
||||||
|
|
||||||
|
export type DeviceInfo = {
|
||||||
|
fwVersion: number | null, // 原始 u32 版本号
|
||||||
|
chipTemp: number | null, // 温度,已换算为 ℃
|
||||||
|
fanRpm: number | null, // 风扇转速 rpm;null = 无硬件/未应答
|
||||||
|
vddMv: number | null, // 电源电压 mV
|
||||||
|
uptimeS: number | null, // 运行时间 s
|
||||||
|
freeHeap: number | null // 堆剩余 B
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyInfo(): DeviceInfo {
|
||||||
|
return {
|
||||||
|
fwVersion: null,
|
||||||
|
chipTemp: null,
|
||||||
|
fanRpm: null,
|
||||||
|
vddMv: null,
|
||||||
|
uptimeS: null,
|
||||||
|
freeHeap: null
|
||||||
|
} as DeviceInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析 INFO_RSP 的 TLV payload;未知 id 按 len 跳过,数值长度不符的项忽略 */
|
||||||
|
export function parseTlv(payload: number[]): DeviceInfo {
|
||||||
|
const info = emptyInfo()
|
||||||
|
let i = 0
|
||||||
|
while (i + 2 <= payload.length) {
|
||||||
|
const id = payload[i]
|
||||||
|
const len = payload[i + 1]
|
||||||
|
if (i + 2 + len > payload.length) break // 长度越界,丢弃剩余
|
||||||
|
const off = i + 2
|
||||||
|
if (id == INFO_FW_VERSION && len == 4) {
|
||||||
|
info.fwVersion = readU32LE(payload, off)
|
||||||
|
} else if (id == INFO_CHIP_TEMP && len == 2) {
|
||||||
|
const raw = readI16LE(payload, off)
|
||||||
|
info.chipTemp = raw == 0x7FFF ? null : raw / 10.0
|
||||||
|
} else if (id == INFO_FAN_RPM && len == 2) {
|
||||||
|
const raw = readU16LE(payload, off)
|
||||||
|
info.fanRpm = raw == 0xFFFF ? null : raw
|
||||||
|
} else if (id == INFO_VDD_MV && len == 2) {
|
||||||
|
const raw = readU16LE(payload, off)
|
||||||
|
info.vddMv = raw == 0 ? null : raw
|
||||||
|
} else if (id == INFO_UPTIME_S && len == 4) {
|
||||||
|
info.uptimeS = readU32LE(payload, off)
|
||||||
|
} else if (id == INFO_FREE_HEAP && len == 4) {
|
||||||
|
info.freeHeap = readU32LE(payload, off)
|
||||||
|
}
|
||||||
|
i += 2 + len
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询设备信息;items 为空数组 = 查询全部。
|
||||||
|
* 超时/断连/发送失败返回各字段全 null 的 DeviceInfo。
|
||||||
|
*/
|
||||||
|
export async function queryDeviceInfo(items: number[]): Promise<DeviceInfo> {
|
||||||
|
const payload = await queryInfo(items, 2000)
|
||||||
|
if (payload == null) return emptyInfo()
|
||||||
|
return parseTlv(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 固件版本号 u32 格式化,如 0x00010001 → "V1.00.01" */
|
||||||
|
export function formatVersion(v: number): string {
|
||||||
|
const major = Math.floor(v / 65536) % 256
|
||||||
|
const minor = Math.floor(v / 256) % 256
|
||||||
|
const patch = v % 256
|
||||||
|
const ms = minor < 10 ? "0" + minor : "" + minor
|
||||||
|
const ps = patch < 10 ? "0" + patch : "" + patch
|
||||||
|
return "V" + major + "." + ms + "." + ps
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user