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: {
|
||||
ble: new BleLib(), // 蓝牙全局变量
|
||||
// 接收蓝牙数据的回调
|
||||
// 接收蓝牙数据的回调(显式指定 CAIIC-MCM 服务/notify UUID,不依赖插件自动识别;
|
||||
// 用当前接口 onNotityBleData,旧接口 onNotityReadBleData 插件已标记弃用)
|
||||
setBleNotity(){
|
||||
getApp().globalData.ble.onNotityReadBleData(getApp().globalData.ble.getSericUUID(),
|
||||
getApp().globalData.ble.getNotityUUID(),
|
||||
getApp().globalData.ble.onNotityBleData(Ble.MCM_SERVICE_UUID,
|
||||
Ble.MCM_NOTIFY_UUID,
|
||||
true,
|
||||
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)
|
||||
}
|
||||
)
|
||||
@ -25,12 +29,10 @@ import * as McmChannel from "@/src/mcm/McmChannel.uts"
|
||||
// 连接蓝牙
|
||||
ConnectBle(address: string, autoReconnect: boolean){
|
||||
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){
|
||||
getApp().globalData.ble.scanServices(function(resSevice:MyApiResult){
|
||||
// console.log(resSevice);
|
||||
// 显式选中 CAIIC-MCM 的 CLS 服务组合(自动识别可能选错)
|
||||
Ble.selectService("0000CA10")
|
||||
console.log("[MCM] scanServices: type=" + resSevice.type + " msg=" + resSevice.message)
|
||||
getApp().globalData.setBleNotity()
|
||||
|
||||
// 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
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/tabbar/sysStatus",
|
||||
"style": {
|
||||
"navigationBarTitleText": "System Status",
|
||||
"navigationBarBackgroundColor": "#3A6FA1",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/tabbar/ota",
|
||||
"style": {
|
||||
@ -53,17 +45,11 @@
|
||||
"navigationBarBackgroundColor": "#3A6FA1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/bleTest/bluetoothList/bluetoothList",
|
||||
"style": {
|
||||
"navigationBarTitleText": "bluetooth list",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/bleTest/bluetoothOperation/bluetoothOperation",
|
||||
"style": {
|
||||
"navigationBarTitleText": "CLI Terminal"
|
||||
"navigationBarTitleText": "CLI Terminal",
|
||||
"navigationBarBackgroundColor": "#3A6FA1"
|
||||
}
|
||||
}
|
||||
],
|
||||
@ -79,18 +65,18 @@
|
||||
"iconPath": "/static/tabbar/homeDefault.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",
|
||||
"text": "OTA",
|
||||
"iconPath": "/static/tabbar/chargeDefault.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",
|
||||
"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 McmCmd from "../../../src/mcm/McmCommands.uts"
|
||||
import { CliResult } from "../../../src/mcm/McmChannel.uts"
|
||||
import * as BleINfo from "../../../common/bleInfo.uts"
|
||||
|
||||
const bleAddress = ref('')
|
||||
const bleIsConnect = ref(false) // 蓝牙连接标志 true-已连接 false-未连接
|
||||
@ -120,19 +121,29 @@ function onDisConnect() {
|
||||
McmChannel.resetChannel()
|
||||
}
|
||||
|
||||
// 连接蓝牙(走全局 ConnectBle:connect → scanServices → 选中 CA10 服务 → 订阅 notify)
|
||||
// 连接蓝牙(走全局 ConnectBle:connect → scanServices → 订阅 notify,显式 CAIIC-MCM UUID)
|
||||
// 本页是 tab 页,地址取最近连接过的设备(bleInfo),没有则提示去 Device 页连接
|
||||
function onConnect() {
|
||||
if (Ble.isConnected()) {
|
||||
uni.hideLoading()
|
||||
bleIsConnect.value = true
|
||||
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({
|
||||
title: "Connecting...",
|
||||
mask: true
|
||||
})
|
||||
pushMsg("start", "connect: " + bleAddress.value)
|
||||
getApp().globalData.ConnectBle(bleAddress.value, false)
|
||||
pushMsg("start", "connect: " + addr)
|
||||
getApp().globalData.ConnectBle(addr, false)
|
||||
}
|
||||
|
||||
// 发送 CLI 命令
|
||||
@ -190,11 +201,16 @@ onLoad((options: UTSJSONObject | null) => {
|
||||
}
|
||||
})
|
||||
|
||||
onPageShow(() => {
|
||||
onShow(() => {
|
||||
subscribe()
|
||||
// 从其他 tab 切回时同步连接状态
|
||||
if (Ble.isConnected() && !bleIsConnect.value) {
|
||||
bleIsConnect.value = true
|
||||
pushMsg("message", "already connected: " + Ble.getConnectMac())
|
||||
}
|
||||
})
|
||||
|
||||
onPageHide(() => {
|
||||
onHide(() => {
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
|
||||
@ -41,6 +41,12 @@
|
||||
</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">
|
||||
@ -70,9 +76,10 @@ import { MyApiResult, BleScanResult } from "@/uni_modules/android-ble"
|
||||
import * as Ble from "../../src/BluetoothManager"
|
||||
import * as McmChannel from "../../src/mcm/McmChannel.uts"
|
||||
import * as McmCmd from "../../src/mcm/McmCommands.uts"
|
||||
import * as McmInfo from "../../src/mcm/McmInfo.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 scanArray = ref<BleScanResult[]>([])
|
||||
@ -85,6 +92,7 @@ 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
|
||||
@ -93,11 +101,12 @@ let pageVisible = false
|
||||
|
||||
async function pollParams() {
|
||||
if (!connected.value || !pageVisible) return
|
||||
const temp = await McmCmd.getTemperature()
|
||||
temperature.value = temp != null ? temp.toString() : "--"
|
||||
if (!connected.value || !pageVisible) return
|
||||
const fan = await McmCmd.getFanSpeed()
|
||||
fanSpeed.value = fan != null ? fan.toString() : "--"
|
||||
// 一次 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() {
|
||||
@ -129,6 +138,8 @@ async function loadDeviceInfo() {
|
||||
// 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 : "--"
|
||||
}
|
||||
|
||||
// ================= 蓝牙事件 =================
|
||||
@ -157,6 +168,7 @@ function onDisConnectUi() {
|
||||
fanSpeed.value = "--"
|
||||
fwVersion.value = "--"
|
||||
bankInfo.value = "--"
|
||||
sysInfo.value = "--"
|
||||
}
|
||||
|
||||
function onBleStatueListener(state: number) {
|
||||
@ -188,6 +200,8 @@ function onBleStarScan() {
|
||||
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
|
||||
@ -277,7 +291,10 @@ onPullDownRefresh(() => {
|
||||
if (!connected.value) {
|
||||
onBleStarScan()
|
||||
} else {
|
||||
uni.stopPullDownRefresh()
|
||||
// 已连接:下拉刷新设备信息(版本/bank/sysinfo)
|
||||
loadDeviceInfo().then(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@ -424,4 +441,17 @@ onPullDownRefresh(() => {
|
||||
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>
|
||||
|
||||
@ -26,14 +26,6 @@
|
||||
<view class="verType">
|
||||
<text class="colorType">{{ "version: 1.0.1" }}</text>
|
||||
</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>
|
||||
<!-- #ifdef APP -->
|
||||
</scroll-view>
|
||||
@ -45,8 +37,6 @@
|
||||
import { checkToken, getUserName } from '../../common/auth';
|
||||
import { SERVER_LIST, getBaseUrl, setBaseUrl } from '../../api/httpInstance';
|
||||
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 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) => {
|
||||
if (options != null) {
|
||||
@ -193,32 +167,6 @@ onUnload(() => {
|
||||
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 {
|
||||
margin-top: 500rpx;
|
||||
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"
|
||||
|
||||
// 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)
|
||||
function ble(): BleLib {
|
||||
const app = getApp()
|
||||
@ -93,10 +99,11 @@ export function disConnect() {
|
||||
ble().close()
|
||||
}
|
||||
|
||||
// 开启服务消息通知
|
||||
// 开启服务消息通知(显式使用 CAIIC-MCM 的服务/notify UUID;onNotityBleData 为当前接口,
|
||||
// 旧接口 onNotityReadBleData 插件已标记弃用)
|
||||
export function onNotityReadBleData(readNotification: boolean , cb: (res: MyApiResult) => void) {
|
||||
const cBle = ble()
|
||||
cBle.onNotityReadBleData(cBle.getSericUUID(), cBle.getNotityUUID(), readNotification, cb)
|
||||
cBle.onNotityBleData(MCM_SERVICE_UUID, MCM_NOTIFY_UUID, readNotification, cb)
|
||||
}
|
||||
|
||||
// 获取自动识别读写服务uuid
|
||||
@ -122,12 +129,14 @@ export function sendData(data: number[], cb: (res: MyApiResult) => void) {
|
||||
}
|
||||
|
||||
// 发送数据(显式写类型):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) {
|
||||
const b = ble()
|
||||
const wd: WriteData = {
|
||||
serviceId: b.getServiceUUID(),
|
||||
characteristicId: b.getWriteUUID(),
|
||||
serviceId: MCM_SERVICE_UUID,
|
||||
characteristicId: MCM_WRITE_UUID,
|
||||
writeType: writeType,
|
||||
data: data,
|
||||
fenbao: false
|
||||
@ -135,7 +144,7 @@ export function sendDataBytes(data: number[], writeType: number, cb: (res: MyApi
|
||||
b.sendData(wd, cb)
|
||||
}
|
||||
|
||||
// 按服务 UUID 前缀选择 service+notify+write 组合(如 "0000CA10"),找到返回 true
|
||||
// 按服务 UUID 前缀选择 service+notify+write 组合(如 "00002760"),找到返回 true
|
||||
export function selectService(serviceUuidPrefix: string): boolean {
|
||||
const b = ble()
|
||||
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),
|
||||
* [5..]=PAYLOAD(LEN 字节), 末尾 2B CRC16-CCITT(覆盖 TYPE..PAYLOAD, LE)
|
||||
* 帧开销 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_END = 0x12 // {crc32 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 = {
|
||||
type: number,
|
||||
@ -111,3 +113,14 @@ export function readU32LE(data: number[], offset: number): number {
|
||||
(data[offset + 2] & 0xFF) * 65536 +
|
||||
(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 兼容性考虑)。
|
||||
*/
|
||||
import * as Ble from "../BluetoothManager.uts"
|
||||
import { MyApiResult } from "@/uni_modules/android-ble"
|
||||
import { MyApiResult, NotityData } from "@/uni_modules/android-ble"
|
||||
import {
|
||||
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"
|
||||
import { asciiToBytes, bytesToAscii } from "../commonFunctions.uts"
|
||||
import { asciiToBytes, bytesToAscii, bytesToHex } from "../commonFunctions.uts"
|
||||
|
||||
export type CliResult = {
|
||||
text: string, // 聚合的应答文本
|
||||
@ -26,6 +26,11 @@ type CliJob = {
|
||||
timer: number
|
||||
}
|
||||
|
||||
type InfoJob = {
|
||||
resolve: (payload: number[] | null) => void,
|
||||
timer: number
|
||||
}
|
||||
|
||||
const CLI_MAX_CMD_BYTES = 63
|
||||
|
||||
let txSeq = 0
|
||||
@ -37,12 +42,16 @@ let cliJob: CliJob | null = null
|
||||
let cliChain: 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 disconnectHandler: (() => void) | null = null
|
||||
|
||||
// ================= 接收 =================
|
||||
|
||||
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) {
|
||||
const job = cliJob
|
||||
if (job != null) {
|
||||
@ -65,13 +74,23 @@ function dispatchFrame(frame: BleFrame) {
|
||||
if (h != null) {
|
||||
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) {
|
||||
if (res.type != 0) return
|
||||
const data = res.data as number[]
|
||||
const frames = decoder.feed(data)
|
||||
// 插件 notify 回调的 data 是 NotityData 结构({data: number[], mac, serviceId, ...}),
|
||||
// 不是裸字节数组,需取 .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++) {
|
||||
dispatchFrame(frames[i])
|
||||
}
|
||||
@ -86,7 +105,7 @@ function onBleState(state: number) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 断连/异常时复位通道:挂起的 CLI 命令以 -4 结束 */
|
||||
/** 断连/异常时复位通道:挂起的 CLI 命令以 -4 结束,挂起的信息查询以 null 结束 */
|
||||
export function resetChannel() {
|
||||
decoder.reset()
|
||||
txSeq = 0
|
||||
@ -100,6 +119,12 @@ export function resetChannel() {
|
||||
}
|
||||
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> {
|
||||
const frame = encodeFrame(type, txSeq & 0xFF, payload)
|
||||
txSeq = (txSeq + 1) & 0xFF
|
||||
console.log("[MCM] TX frame type=0x" + type.toString(16) + " len=" + frame.length + ": " + bytesToHex(frame))
|
||||
let offset = 0
|
||||
while (offset < frame.length) {
|
||||
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)
|
||||
if (res.type != 0) {
|
||||
console.log("[MCM] TX write failed: type=" + res.type + " msg=" + res.message)
|
||||
return false
|
||||
}
|
||||
offset += chunkLen
|
||||
@ -169,6 +196,7 @@ export function sendFrame(type: number, payload: number[]): Promise<boolean> {
|
||||
|
||||
function sendCliImpl(cmd: string, timeoutMs: number): Promise<CliResult> {
|
||||
return new Promise((resolve) => {
|
||||
// 固件 CLI_REQ 为整行 payload 直接执行(协议 V1.1:无结束符)
|
||||
const payload = asciiToBytes(cmd)
|
||||
if (payload.length > CLI_MAX_CMD_BYTES) {
|
||||
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)
|
||||
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 透传)
|
||||
* 命令名集中在顶部常量,固件侧命令定名后只需改这里。
|
||||
* 注意:固件当前已实现 version/sysinfo/bankinfo;
|
||||
* 温度/风扇命令(默认 temp/fan)待固件新增,解析失败返回 null 由页面显示 "--"。
|
||||
* CAIIC-MCM 业务命令封装
|
||||
* 温度/风扇/固件版本走 INFO_QUERY(协议 V1.1 第 5 节);
|
||||
* sysinfo/bankinfo 等系统状态仍走 CLI 透传,命令名集中在顶部常量。
|
||||
*/
|
||||
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_BANKINFO = "bankinfo"
|
||||
const CMD_TEMPERATURE = "temp"
|
||||
const CMD_FAN_SPEED = "fan"
|
||||
|
||||
/** 从应答文本中提取第一个数值(整数或小数),提取不到返回 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 */
|
||||
/** 固件版本号,如 "V1.00.01"(来自 INFO_QUERY FW_VERSION);失败返回 null */
|
||||
export async function getVersion(): Promise<string | null> {
|
||||
const r: CliResult = await sendCli(CMD_VERSION)
|
||||
if (r.status != 0) return null
|
||||
const t = r.text.trim()
|
||||
return t.length > 0 ? t : null
|
||||
const info = await queryDeviceInfo([INFO_FW_VERSION])
|
||||
const v = info.fwVersion
|
||||
return v != null ? formatVersion(v) : null
|
||||
}
|
||||
|
||||
/** 系统运行状态原文(运行时间/任务数/heap/任务表),失败返回 null */
|
||||
@ -45,18 +33,16 @@ export async function getBankInfo(): Promise<string | null> {
|
||||
return r.text
|
||||
}
|
||||
|
||||
/** 温度(单位由固件定义,℃),失败返回 null */
|
||||
/** 温度(℃),读取失败返回 null */
|
||||
export async function getTemperature(): Promise<number | null> {
|
||||
const r: CliResult = await sendCli(CMD_TEMPERATURE)
|
||||
if (r.status != 0) return null
|
||||
return parseFirstNumber(r.text)
|
||||
const info = await queryDeviceInfo([INFO_CHIP_TEMP])
|
||||
return info.chipTemp
|
||||
}
|
||||
|
||||
/** 风扇转速(RPM),失败返回 null */
|
||||
/** 风扇转速(RPM),无测速硬件/读取失败返回 null */
|
||||
export async function getFanSpeed(): Promise<number | null> {
|
||||
const r: CliResult = await sendCli(CMD_FAN_SPEED)
|
||||
if (r.status != 0) return null
|
||||
return parseFirstNumber(r.text)
|
||||
const info = await queryDeviceInfo([INFO_FAN_RPM])
|
||||
return info.fanRpm
|
||||
}
|
||||
|
||||
/** 通用 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