From 4af93dca6c42f8ea07a8a63757885c47399ceef2 Mon Sep 17 00:00:00 2001 From: "evan.liu" <583229071@qq.com> Date: Thu, 3 Sep 2026 08:44:04 +0800 Subject: [PATCH] =?UTF-8?q?BLE=E5=8D=8F=E8=AE=AEV1.1=E9=80=82=E9=85=8D?= =?UTF-8?q?=EF=BC=9AINFO=5FQUERY=E4=BF=A1=E6=81=AF=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E3=80=81=E6=98=BE=E5=BC=8FGATT=20UUID=E3=80=81=E4=BF=AE?= =?UTF-8?q?=E5=A4=8Dnotify=E6=95=B0=E6=8D=AE=E8=A7=A3=E6=9E=90(NotityData)?= =?UTF-8?q?=EF=BC=9B=E9=A1=B5=E9=9D=A2=E8=B0=83=E6=95=B4=EF=BC=9AStatus?= =?UTF-8?q?=E5=B9=B6=E5=85=A5Device=E3=80=81CLI=E7=BB=88=E7=AB=AF=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E5=BA=95=E9=83=A8tab=EF=BC=9B=E5=A2=9E=E5=8A=A0[MCM]?= =?UTF-8?q?=E6=94=B6=E5=8F=91=E6=8A=A5=E6=96=87=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App.uvue | 18 +- docs/修改记录_2026-09-03_BLE协议V1.1适配.md | 72 +++++++ docs/修改记录_2026-09-03_页面结构调整.md | 34 +++ pages.json | 30 +-- .../bleTest/bluetoothList/bluetoothList.uvue | 142 ------------- .../bluetoothOperation.uvue | 26 ++- pages/tabbar/home.uvue | 44 +++- pages/tabbar/my.uvue | 52 ----- pages/tabbar/sysStatus.uvue | 196 ------------------ src/BluetoothManager.uts | 21 +- src/mcm/FrameCodec.uts | 15 +- src/mcm/McmChannel.uts | 77 ++++++- src/mcm/McmCommands.uts | 48 ++--- src/mcm/McmInfo.uts | 85 ++++++++ 14 files changed, 384 insertions(+), 476 deletions(-) create mode 100644 docs/修改记录_2026-09-03_BLE协议V1.1适配.md create mode 100644 docs/修改记录_2026-09-03_页面结构调整.md delete mode 100644 pages/bleTest/bluetoothList/bluetoothList.uvue delete mode 100644 pages/tabbar/sysStatus.uvue create mode 100644 src/mcm/McmInfo.uts 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 @@ - - - - - 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 @@ - - - - - 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 +}