diff --git a/App.uvue b/App.uvue
index 3d59d38..689a41b 100644
--- a/App.uvue
+++ b/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){
diff --git a/docs/修改记录_2026-09-03_BLE协议V1.1适配.md b/docs/修改记录_2026-09-03_BLE协议V1.1适配.md
new file mode 100644
index 0000000..586454f
--- /dev/null
+++ b/docs/修改记录_2026-09-03_BLE协议V1.1适配.md
@@ -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 升级。
diff --git a/docs/修改记录_2026-09-03_页面结构调整.md b/docs/修改记录_2026-09-03_页面结构调整.md
new file mode 100644
index 0000000..59274ab
--- /dev/null
+++ b/docs/修改记录_2026-09-03_页面结构调整.md
@@ -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 给出提示。
diff --git a/pages.json b/pages.json
index 0471059..09b824f 100644
--- a/pages.json
+++ b/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",
diff --git a/pages/bleTest/bluetoothList/bluetoothList.uvue b/pages/bleTest/bluetoothList/bluetoothList.uvue
deleted file mode 100644
index 1480fcd..0000000
--- a/pages/bleTest/bluetoothList/bluetoothList.uvue
+++ /dev/null
@@ -1,142 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
- {{ item.device.name }}
- {{ item.device.address }}
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/pages/bleTest/bluetoothOperation/bluetoothOperation.uvue b/pages/bleTest/bluetoothOperation/bluetoothOperation.uvue
index b904814..2587da7 100644
--- a/pages/bleTest/bluetoothOperation/bluetoothOperation.uvue
+++ b/pages/bleTest/bluetoothOperation/bluetoothOperation.uvue
@@ -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()
})
diff --git a/pages/tabbar/home.uvue b/pages/tabbar/home.uvue
index de3c5b3..d84d6e3 100644
--- a/pages/tabbar/home.uvue
+++ b/pages/tabbar/home.uvue
@@ -41,6 +41,12 @@
+
+
+ sysinfo (system runtime status)
+ {{ sysInfo }}
+
+
@@ -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([])
@@ -85,6 +92,7 @@ const temperature = ref("--")
const fanSpeed = ref("--")
const fwVersion = ref("--")
const bankInfo = ref("--")
+const sysInfo = ref("--")
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()
+ })
}
})
@@ -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;
+}
diff --git a/pages/tabbar/my.uvue b/pages/tabbar/my.uvue
index b7a8fda..ebf046c 100644
--- a/pages/tabbar/my.uvue
+++ b/pages/tabbar/my.uvue
@@ -26,14 +26,6 @@
{{ "version: 1.0.1" }}
-
-
-
-
-
- CLI Terminal
-
-
@@ -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;
diff --git a/pages/tabbar/sysStatus.uvue b/pages/tabbar/sysStatus.uvue
deleted file mode 100644
index 3ade3f1..0000000
--- a/pages/tabbar/sysStatus.uvue
+++ /dev/null
@@ -1,196 +0,0 @@
-
-
-
-
-
-
-
- Not connected. Please connect a CAIIC-MCM device on the Device tab first
-
-
-
-
-
-
- Firmware
- {{ version }}
-
-
- Bank Info
- {{ bankInfo }}
-
-
-
-
-
-
-
-
- sysinfo (system runtime status)
- {{ sysInfo }}
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/BluetoothManager.uts b/src/BluetoothManager.uts
index 16d4a59..d319411 100644
--- a/src/BluetoothManager.uts
+++ b/src/BluetoothManager.uts
@@ -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[]
diff --git a/src/mcm/FrameCodec.uts b/src/mcm/FrameCodec.uts
index ea7c6a9..e0711b2 100644
--- a/src/mcm/FrameCodec.uts
+++ b/src/mcm/FrameCodec.uts
@@ -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
+}
diff --git a/src/mcm/McmChannel.uts b/src/mcm/McmChannel.uts
index 7504f20..d811a8c 100644
--- a/src/mcm/McmChannel.uts
+++ b/src/mcm/McmChannel.uts
@@ -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 = Promise.resolve(true)
let writeChain: Promise = Promise.resolve(true)
+let infoJob: InfoJob | null = null
+let infoChain: Promise = 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 {
async function sendFrameImpl(type: number, payload: number[]): Promise {
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
}
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 {
function sendCliImpl(cmd: string, timeoutMs: number): Promise {
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 true)
return p
}
+
+// ================= 信息查询(INFO_QUERY / INFO_RSP) =================
+
+function queryInfoImpl(items: number[], timeoutMs: number): Promise {
+ 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 {
+ attach()
+ const p = infoChain.then((): Promise => queryInfoImpl(items, timeoutMs))
+ infoChain = p.then((): boolean => true)
+ return p
+}
diff --git a/src/mcm/McmCommands.uts b/src/mcm/McmCommands.uts
index 490c8ca..89b1eac 100644
--- a/src/mcm/McmCommands.uts
+++ b/src/mcm/McmCommands.uts
@@ -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 {
- 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 {
return r.text
}
-/** 温度(单位由固件定义,℃),失败返回 null */
+/** 温度(℃),读取失败返回 null */
export async function getTemperature(): Promise {
- 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 {
- 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 命令(调试终端用) */
diff --git a/src/mcm/McmInfo.uts b/src/mcm/McmInfo.uts
new file mode 100644
index 0000000..17a613c
--- /dev/null
+++ b/src/mcm/McmInfo.uts
@@ -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 {
+ 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
+}