diff --git a/AGENTS.md b/AGENTS.md index 43f4eef..bd74ad3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,14 +5,14 @@ 本仓库是 **mothercup(母乳杯)** 产品的完整代码仓库,包含三大部分: 1. **设备固件** — 基于国民技术(Nations)**N32WB031 BLE SoC**(Cortex-M0,64MHz HSI,**板载硅片实测 256KB Flash**(0x01000000~0x0103FFFF,开发日志 §37),RAM 48KB+16KB): - - **`mcm-ddc-ble/`** — 唯一活跃的应用固件工程(APP,链接在 APP1 bank `0x01008000`/112KB)。以 SDK rdtss 例程为蓝本,集成 FreeRTOS、UART CLI、BLE 自定义 GATT 服务(CLI 透传 / 设备信息查询 / BLE OTA 双 bank 直写升级)、bootsetting 与 APP_DATA 参数区结构化读写命令。当前版本 **V1.00.17**(`mcm-ddc-ble/inc/app_version.h`)。历史上曾有 `mcm-ddc-04/` 主工程,**已删除**,其功能已全部并入本工程。 + - **`mcm-ddc-ble/`** — 唯一活跃的应用固件工程(APP,链接在 APP1 bank `0x01008000`/112KB)。以 SDK rdtss 例程为蓝本,集成 FreeRTOS、UART CLI、BLE 自定义 GATT 服务(CLI 透传 / 设备信息查询 / BLE OTA 双 bank 直写升级)、bootsetting 与 APP_DATA 参数区结构化读写命令。当前版本 **V1.00.18**(`mcm-ddc-ble/inc/app_version.h`)。历史上曾有 `mcm-ddc-04/` 主工程,**已删除**,其功能已全部并入本工程。 - **`Boot/`** — 自写精简 bootloader(链接在 `0x01000000`/16KB):校验 bootsetting 记录自身完整性(magic + 结构体 CRC32)后按 active bank 的 `start_address` 直接跳转;**不校验镜像 CRC(CRC 校验在 OTA 升级过程中完成)**;记录无效或地址越界回退 APP1。 2. **手机 App `SmartAssiter/`** — uni-app **Vue3 + TypeScript** 工程(HBuilderX 项目管理,无 package.json),通过 BLE 连接设备:查看运行参数(温度/电压/转速等)、CLI 终端、OTA 升级页(当前停用,见下)。另有登录/注册/用户信息等云端业务页面。 3. **PC 工具 `tools/`** — 整片烧录包制作/烧录脚本 + bleak 蓝牙测试客户端。 关键文档: -- `docs/开发日志.md` — 固件全程开发日志(39 节,含所有踩坑根因与设计决策,排查问题先查这里) +- `docs/开发日志.md` — 固件全程开发日志(41 节,含所有踩坑根因与设计决策,排查问题先查这里) - `docs/ble_protocol.md` — CAIIC BLE 通信协议 V1.1(帧格式/GATT UUID/三类业务流程/维护命令/手机端开发指南) - `SmartAssiter/docs/修改记录_*.md` — App 侧每次改造的详细记录 @@ -26,7 +26,7 @@ mothercup/ ├── tools/ # merge_image.py / make_package.bat / flash_package.bat / ble_cli_test.py 等 ├── docs/ # 开发日志.md + ble_protocol.md(仓库级权威文档) ├── nations-tec/ # 厂商资源:SDK V2.0.0、DFP 器件包、STB 开发板资料、官方 PDF。**只读,不要修改** -└── tools/out/ # 出包产物(caiic_ble_full.{hex,bin} + _ota.bin/_ota.json) +└── tools/out/ # 出包产物(mothercup_ble_prod.{hex,bin} 生产整包 + mothercup_ble_ota.bin 单文件升级包 + 调试用 _ota.bin/_ota_app2.bin 及 json) ``` SDK 源码(固件库、CMSIS、FreeRTOS V9.0.0、BLE 协议栈/profile/ns_library、启动文件)通过相对路径 `..\..\nations-tec\N32WB03x_SDK_V2.0.0` 被 uvprojx 引用,**不复制进工程**;Bootloader 共享代码(`Boot/src/dfu_layout.h`、`boot_crc32.c`)经 `..\..\Boot\src` 被 APP 工程引用。因此各工程目录不能脱离仓库根单独移动。 @@ -95,7 +95,7 @@ mcm-ddc-ble/ "D:\Keil_v5\UV4\UV4.exe" -b caiic_boot.uvprojx -j0 -o build.log # Boot 增量构建 ``` -要求保持 **0 Error(s), 0 Warning(s)**。当前基线(V1.00.17):`Code=47896 RO-data=4752 RW-data=2060 ZI-data=28612`。 +要求保持 **0 Error(s), 0 Warning(s)**。当前基线(V1.00.18):`Code=47896 RO-data=4752 RW-data=2060 ZI-data=28612`。 **固件版本号约定(重要)**:每次修改固件代码必须递增 `mcm-ddc-ble/inc/app_version.h` 中的 `APP_FW_VERSION` 与 `APP_FW_VERSION_NUM`(格式 `0x00MMmmpp`,通常补丁位 +1),用于识别板上实际运行的固件;工程名与出包文件名保持不变。 @@ -103,11 +103,11 @@ mcm-ddc-ble/ 出包与烧录: -1. `tools\make_package.bat` — 一键:`UV4 -r` **全量重建** Boot + APP(出包必须 `-r`,增量构建曾产生栈顶/ZI 不一致的砖包,见开发日志 §24)→ `merge_image.py` 合并。产物在 `tools/out/`: - - `caiic_ble_full.hex/.bin` — 整片包(Boot + 缺省 bootsetting + APP1) - - `caiic_ble_full_ota.bin` + `_ota.json` — APP1 链接的 OTA 载荷与清单(size/crc32/version/target_bank=1;version 自动取 `APP_FW_VERSION_NUM`);`make_package.bat` 同时构建 APP2 target 并出 `caiic_ble_full_ota_app2.bin`/`_ota_app2.json`(target_bank=2)。**客户端按信息项 0x07 CUR_BANK 选对侧 bank 的载荷**;固件 OTA_END 有向量表防呆(status=6 bank_mismatch) - - 注意:`merge_image.py` 不带参数运行时默认 app 路径仍指向已删除的 `mcm-ddc-04/`,**手动调用必须显式传参**:`python tools/merge_image.py mcm-ddc-ble/MDK-ARM/bin/mcm-ddc-ble.bin caiic_ble_full` -2. 首次/变砖恢复:`tools\flash_package.bat tools\out\caiic_ble_full.hex`(调 SDK 自带 NSpyocd,整片擦除 + 烧录;需 NS-LINK 接 SWD PA4/PA5 + 复位脚,先关闭 Keil;target 用 DFP pack 的 `n32wb031keq6_2` + `-O smart_flash=false`,内置 `n32wb031` target 只有 256KB 映射) +1. `tools\make_package.bat` — 一键:`UV4 -r` **全量重建** Boot + APP1 + APP2 target(出包必须 `-r`,增量构建曾产生栈顶/ZI 不一致的砖包,见开发日志 §24)→ `merge_image.py` 合并。产物在 `tools/out/`: + - `mothercup_ble_prod.hex/.bin` — 生产整片包(Boot + 缺省 bootsetting + APP1,只含一个 APP) + - `mothercup_ble_ota.bin` — **OTA 单文件发布件**(52B 头含 version/总大小/总 CRC + 双 bank 载荷索引,格式见 ble_protocol.md §7);调试用单 bank 载荷 `mothercup_ble_prod_ota.bin`(bank1)/ `_ota_app2.bin`(bank2)+ json 也保留。**客户端按信息项 0x07 CUR_BANK 选对侧 bank 的载荷**;固件 OTA_END 有向量表防呆(status=6 bank_mismatch) + - 注意:`merge_image.py` 不带参数运行时默认 app 路径仍指向已删除的 `mcm-ddc-04/`,**手动调用必须显式传参**:`python tools/merge_image.py mcm-ddc-ble/MDK-ARM/bin/mcm-ddc-ble.bin mothercup_ble_prod` +2. 首次/变砖恢复:`tools\flash_package.bat tools\out\mothercup_ble_prod.hex`(调 SDK 自带 NSpyocd,整片擦除 + 烧录;需 NS-LINK 接 SWD PA4/PA5 + 复位脚,先关闭 Keil;target 用 DFP pack 的 `n32wb031keq6_2` + `-O smart_flash=false`) - **双 APP 整包(appsw 测试)**:`tools\make_dual_package.bat` 出 `caiic_ble_dual.{hex,bin}`(Boot+bootsetting 双 bank 记录+APP_DATA 缺省记录+APP1/APP2 **同版本**双镜像,APP2 由 uvprojx target "APP2" 链接 0x01024000 构建;运行 bank 用信息项 0x07 CUR_BANK 识别);`flash_package.bat tools\out\caiic_ble_dual.hex` 烧录后可用 `appsw` 在两 bank 间切换(已实测双向切换)。 - **OTA 双 bank 选包**:OTA 写对侧 bank,载荷必须链接在目标 bank 地址;两份载荷见上,客户端按 CUR_BANK 选包(详见开发日志 §39)。 3. 日常开发调试:Keil F7 编译、F8 下载即可(Boot 不校验镜像 CRC,可直接下载 APP 调试);**Keil 下载选项必须是 "Erase Sectors",整片擦除会杀掉 Boot/bootsetting**(杀掉后需重烧整片包恢复)。板子上烧过其他 0x01000000 起步的程序同样会覆盖 Boot,恢复也是重烧整片包。 @@ -151,6 +151,7 @@ mcm-ddc-ble/ - `make_package.bat` / `flash_package.bat` — 一键出包 / NSpyocd 整片烧录(脚本会自动找 Python312 全路径,本机 `python` 可能是商店占位 stub) - `make_dual_package.bat` — 双 APP 整包(Boot+bootsetting 双 bank+APP_DATA+APP1+APP2,APP2 升一版链接 0x01024000),用于 appsw 切换测试 - `ble_cli_test.py` + `ble_cli.bat` — PC 端 bleak 蓝牙 CLI 测试客户端(帧协议模式 + `-rw` 读写特征模式,交互模式 `-i`,venv 在 `tools/.venv-ble`,首次运行 bat 自动创建) +- `ble_ota_update.py` + `ble_ota.bat` — BLE OTA 升级器(解析单文件升级包 `mothercup_ble_ota.bin`、按 CUR_BANK 选对侧 bank 载荷、节流发送避开扇区擦除窗口、复位检测+重连校验)。**实验性:实机跑通中,notify 上行不通拿不到 OTA_RSP,定位见开发日志 §40** - `ble_temp_watch.py` — 温度监测小工具 ## 实时架构约定(固件) diff --git a/docs/ble_protocol.md b/docs/ble_protocol.md index b5dfe13..8dbfc25 100644 --- a/docs/ble_protocol.md +++ b/docs/ble_protocol.md @@ -130,13 +130,12 @@ flash 擦除单位 = 4KB 扇区。设备侧**直写 flash,不做扇区级 RAM 注意:flash 擦写期间设备关中断数十 ms/扇区(Qflash 算法在 RAM 执行),BLE 链路靠 5s supervision timeout 维持,属正常现象;但请避免在 OTA 期间主动断开。 -**OTA 载荷必须链接在目标 bank 地址**(Cortex-M0 代码位置相关)。出包脚本同时产出 -两份 bank 匹配的载荷:`<包名>_ota.bin`(target_bank=1,链接 0x01008000)与 -`<包名>_ota_app2.bin`(target_bank=2,链接 0x01024000),清单 json 内含 -`target_bank` 字段。**客户端选包规则**:先读信息项 0x07 CUR_BANK(当前运行 bank), -OTA 目标 = 对侧 bank,选用对应的载荷。固件侧兜底:OTA_END 整镜像 CRC 通过后, -还会校验镜像向量表 Reset 地址落在目标 bank 范围内,不符则回 -OTA_RSP(status=6 bank_mismatch) 并中止、不切换(V1.00.17 起)。 +**OTA 载荷必须链接在目标 bank 地址**(Cortex-M0 代码位置相关)。发布件为单文件 +`mothercup_ble_ota.bin`(52B 头含双载荷索引与全部 CRC,格式见 §7)。**客户端选包 +规则**:先读信息项 0x07 CUR_BANK(当前运行 bank),OTA 目标 = 对侧 bank,从头里取 +对应载荷。固件侧兜底:OTA_END 整镜像 CRC 通过后,还会校验镜像向量表 Reset 地址 +落在目标 bank 范围内,不符则回 OTA_RSP(status=6 bank_mismatch) 并中止、不切换 +(V1.00.17 起)。 ## 6.5 维护命令:bootsetting 与 APP_DATA 参数区(V1.00.09 起,结构化访问;V1.00.10 起参数命令为 appget/appset;V1.00.11 起新增 reset/factory/uartrst/uartinfo) @@ -171,10 +170,35 @@ blob 偏移不下发应用层,协议栈会切错窗口),**读到 0 长度 - 出厂/首次:SWD 烧录整包(bootloader + bootsetting 缺省配置 + APP1), 由 `tools\merge_image.py` / `make_package.bat` 生成。 -- OTA 镜像:每次出整包时 `merge_image.py` 同时输出 `<包名>_ota.bin` - (OTA 载荷,即 APP bin 本体)和 `<包名>_ota.json`(清单:size / crc32 / - version,手机端 OTA_BEGIN 直接取这三个值;version 可用第三个命令行参数 - 指定,如 `0x00010001`,缺省 0)。 +- OTA 发布文件(**单文件发布**,V1.00.18 起):`tools\make_package.bat` 产出 + `mothercup_ble_ota.bin` = **52B 头 + bank1 载荷 + bank2 载荷**,头已包含原清单 + json 的全部信息(version/总大小/总 CRC),无需伴随 json。格式(全小端): + + | 偏移 | 字段 | 说明 | + |---|---|---| + | 0 | magic u32 | `0xCA10BA11` | + | 4 | hdr_len u32 | = 52(头部字节数,前向兼容扩展用) | + | 8 | version u32 | 固件版本号(`0x00MMmmpp`,写进 OTA_BEGIN 与新 bank 记录) | + | 12 | total_size u32 | 整文件字节数(自检) | + | 16 | payload_crc u32 | CRC32(hdr_len .. 文件尾),即两份载荷整体 | + | 20 | count u32 | = 2 | + | 24 | b1_off/b1_size/b1_crc u32×3 | bank1(APP1 链接,0x01008000)载荷索引 | + | 36 | b2_off/b2_size/b2_crc u32×3 | bank2(APP2 链接,0x01024000)载荷索引 | + | 48 | hdr_crc u32 | CRC32(头 0..47) | + + CRC32 均为 IEEE(zlib 多项式,与固件 `caiic_crc32` 一致)。 + **升级流程(APP/UART/PC 通用)**:① 校验 magic + hdr_len + hdr_crc + + total_size + payload_crc;② 读设备信息项 0x07 CUR_BANK 得当前运行 bank N; + ③ 取对侧 bank(3−N)的载荷(offset/size/crc 取自头部);④ OTA_BEGIN + {size, crc32, version} → OTA_DATA 流 → OTA_END{crc32};⑤ 设备自动复位到 + 新 bank,重连后复核 CUR_BANK 与 FW_VERSION。PC 参考实现: + `tools\ble_ota_update.py`(`ble_ota.bat`)。 + 调试用的单 bank 载荷(`mothercup_ble_prod_ota.bin` / `_ota_app2.bin` + json) + 仍在 tools/out/ 生成,但不作为发布件。 +- **SWD 单 bank 升级**(不动运行中的 bank,V1.00.18 实测通过): + `NSpyocd load ... "mcm-ddc-ble.bin@0x01008000"`(bin 后用 `@基址` 指定写入 + 位置,只擦写涉及扇区)→ CLI `bsset` 修正该 bank 的 size/crc/ver → + `appsw` 切入。详见开发日志 §40。 - OTA 失败变砖恢复:SWD 重烧即可(bootloader 不做串口 DFU)。 - Keil 调试下载必须用**扇区擦除**,整片擦除会删掉 bootloader。 @@ -228,8 +252,8 @@ def parse_stream(buf: bytearray): ``` 镜像 CRC32(OTA_BEGIN / OTA_END 用):标准 zlib/IEEE CRC32, -`zlib.crc32(open('xxx_ota.bin','rb').read()) & 0xFFFFFFFF`, -也可直接从 `<包名>_ota.json` 清单里读。 +对 `mothercup_ble_ota.bin` 中取出的目标 bank 载荷计算 +(`zlib.crc32(payload) & 0xFFFFFFFF`),也可直接读头部的 bN_crc 字段。 ### 8.3 字节级完整示例(均含 SOF 与 CRC16,SEQ 自取) diff --git a/docs/开发日志.md b/docs/开发日志.md index b5e315c..65655a0 100644 --- a/docs/开发日志.md +++ b/docs/开发日志.md @@ -996,3 +996,42 @@ APP2 `Code=48004`(同源码、不同链接地址/版本号,体积差 300B `cur bank: APP2`(两 bank 同为 V1.00.17)→ e0004 TLV 含 `cur_bank` → `appsw 1` 切回。构建 0 Error/0 Warning(双 target),整包/双包/双 OTA 载荷 均已重新生成。 + +## 40. SWD 单 bank 升级流程实测 + BLE OTA 待定(2026-09-04,V1.00.18) + +**SWD 部分烧录升级(不动运行中的 bank)实测通过**,步骤: + +1. 正常出包:`tools\make_package.bat`(构建 APP1/APP2 双 target,出整包 + + bank1/bank2 两份 OTA 载荷 + 合并包 `*_ota_dual.bin`); +2. 只写非活动 bank(假设当前跑 APP2,升级 APP1 区): + `NSpyocd load --pack -M under-reset -f 1000000 -t n32wb031keq6_2 + -O smart_flash=false mcm-ddc-ble.bin@0x01008000` + —— bin 后用 `@地址` 指定基址,pyocd 只擦写涉及的 14 个扇区,运行中的 + APP2 不受影响(烧完复位仍回 active bank); +3. BLE/串口 CLI 修正该 bank 记录:`bsset b1size ` / `bsset b1crc ` + / `bsset b1ver `(值取 merge_image 输出或 _ota.json 清单); +4. `appsw 1` → 目标镜像 CRC 复算通过 → 复位切入新版本。 + +实测:bank1 SWD 写入 V1.00.18 → 修记录 → `appsw 1` → `devinfo` 报 +**APP1 / V1.00.18**;bank2 仍保留 V1.00.17 可回切。此路径可作为 BLE OTA +修复前的生产升级手段,也是 OTA 链路的地面参照。 + +**BLE OTA 当前状态(未决)**:`tools/ble_ota_update.py`(合并包解析、 +CUR_BANK 选包、扇区擦除窗口避让的节流发送、双次重试)已写好并实机试跑, +但设备端在 OTA_END 后未复位(中途应有帧丢失/状态拒绝,notify 上行不通 +拿不到 OTA_RSP,定位需要 UART blelog)。该脚本标记为实验性,待 notify +修复或 UART 抓帧后再调通。 + +## 41. 发布命名整理 + OTA 单文件包(2026-09-04,工具链,固件 V1.00.18 不变) + +- 生产整片包改名 **`mothercup_ble_prod.{hex,bin}`**(Boot+bootsetting+APP_DATA + 缺省记录+APP1);`flash_package.bat` 缺省镜像同步改名。 +- OTA 发布件改名 **`mothercup_ble_ota.bin`**,头部从 40B 扩到 **52B**: + 新增 hdr_len、total_size(整文件字节数)、payload_crc(两载荷整体 CRC32)—— + 原清单 json 的信息全部收进头部,**只发这一个文件**(单 bank 载荷与 json + 仍生成,仅供调试)。格式表写入 ble_protocol.md §7(供 App/UART 升级开发)。 +- `merge_image.py` 新增 `--combo-name`,`make_package.bat` 传 + `--combo-name mothercup_ble_ota`;`ble_ota_update.py` 按新头解析并校验 + hdr_len/total_size/payload_crc/各 bank crc,实包解析验证通过 + (V1.00.18,bank1 54204B / bank2 54504B,total=108760B 与文件一致)。 +- 旧命名 caiic_ble_full_* 产物已从仓库删除;双 APP 测试包仍叫 caiic_ble_dual。 diff --git a/mcm-ddc-ble/MDK-ARM/bin/mcm-ddc-ble-app2.bin b/mcm-ddc-ble/MDK-ARM/bin/mcm-ddc-ble-app2.bin index c55c0c7..12ebcfd 100644 Binary files a/mcm-ddc-ble/MDK-ARM/bin/mcm-ddc-ble-app2.bin and b/mcm-ddc-ble/MDK-ARM/bin/mcm-ddc-ble-app2.bin differ diff --git a/mcm-ddc-ble/MDK-ARM/bin/mcm-ddc-ble.bin b/mcm-ddc-ble/MDK-ARM/bin/mcm-ddc-ble.bin index 0c78c1d..f5c1452 100644 Binary files a/mcm-ddc-ble/MDK-ARM/bin/mcm-ddc-ble.bin and b/mcm-ddc-ble/MDK-ARM/bin/mcm-ddc-ble.bin differ diff --git a/mcm-ddc-ble/inc/app_version.h b/mcm-ddc-ble/inc/app_version.h index bd9bb45..ab8e257 100644 --- a/mcm-ddc-ble/inc/app_version.h +++ b/mcm-ddc-ble/inc/app_version.h @@ -9,12 +9,12 @@ #define __APP_VERSION_H__ #ifndef APP_FW_VERSION -#define APP_FW_VERSION "V1.00.17" +#define APP_FW_VERSION "V1.00.18" #endif /* Numeric form used by the BLE info query / OTA records: 0x00MMmmpp */ #ifndef APP_FW_VERSION_NUM -#define APP_FW_VERSION_NUM 0x00010011u +#define APP_FW_VERSION_NUM 0x00010012u #endif #endif /* __APP_VERSION_H__ */ diff --git a/tools/ble_ota.bat b/tools/ble_ota.bat new file mode 100644 index 0000000..3e869aa --- /dev/null +++ b/tools/ble_ota.bat @@ -0,0 +1,35 @@ +@echo off +rem ============================================================ +rem ble_ota.bat - BLE OTA updater for CAIIC-MCM devices +rem +rem ble_ota.bat [combo_pkg] +rem default: tools\out\mothercup_ble_ota.bin +rem +rem The combined package holds both bank-linked payloads; the script +rem reads the current bank (info item 0x07 CUR_BANK) and downloads +rem the payload for the opposite bank, then verifies the reboot. +rem Requires tools\.venv-ble (created automatically on first run). +rem ============================================================ +setlocal +set TOOLS=%~dp0 +set PYEXE=%TOOLS%.venv-ble\Scripts\python.exe + +if not exist "%PYEXE%" ( + echo [setup] creating venv %TOOLS%.venv-ble ... + set PYREAL= + if exist "%LOCALAPPDATA%\Programs\Python\Python312\python.exe" set PYREAL="%LOCALAPPDATA%\Programs\Python\Python312\python.exe" + if not defined PYREAL ( + py -3 --version >nul 2>&1 && set PYREAL=py -3 + ) + if not defined PYREAL set PYREAL=python + %PYREAL% -m venv "%TOOLS%.venv-ble" || goto :fail + "%PYEXE%" -m pip install --quiet bleak || goto :fail +) + +"%PYEXE%" "%TOOLS%ble_ota_update.py" %1 +goto :eof + +:fail +echo. +echo *** setup failed - check python installation *** +exit /b 1 diff --git a/tools/ble_ota_update.py b/tools/ble_ota_update.py new file mode 100644 index 0000000..4368718 --- /dev/null +++ b/tools/ble_ota_update.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +ble_ota_update.py - PC-side BLE OTA updater for CAIIC-MCM devices. + +Takes the combined OTA package (mothercup_ble_ota.bin: 52-byte header + both +bank-linked payloads, see tools/merge_image.py), reads the device's current +bank from the read-only info characteristic ...e0004 (info item 0x07 +CUR_BANK), and downloads the payload linked for the OPPOSITE bank over the +framed protocol downlink (characteristic ...e0001). + + usage: ble_ota.bat [combo_pkg] + default pkg: out/mothercup_ble_ota.bin + +Flow-control note: the notify uplink is currently broken (dev log section +28), so OTA_RSP acks cannot be received. The script therefore paces writes: +a short gap per frame and a longer gap at every 4KB sector boundary (the +device erases sectors lazily with interrupts off - writes during the erase +window would be lost). Success is detected by the device resetting after +OTA_END (link drops), then re-checking CUR_BANK/version after reconnect. +The firmware independently rejects a payload linked for the wrong bank +(OTA_RSP status 6, no switch), so a wrong pick never bricks the device. +""" + +import asyncio +import os +import struct +import sys +import zlib + +from bleak import BleakClient, BleakScanner + +NAME_PREFIX = "CAIIC-MCM-20260902" +WRITE_UUID = "00002760-08c2-11e1-9073-0e8ac72e0001" # Write Without Response +INFO_RD_UUID = "00002760-08c2-11e1-9073-0e8ac72e0004" # Read-only info TLV + +TYPE_OTA_BEGIN = 0x10 +TYPE_OTA_DATA = 0x11 +TYPE_OTA_END = 0x12 + +COMBO_MAGIC = 0xCA10BA11 +COMBO_HDR_LEN = 52 + +FRAME_GAP_S = 0.008 # between WWR frames +SECTOR_GAP_S = 0.25 # at 4KB boundaries (lazy sector erase, IRQs off) +REBOOT_WAIT_S = 20.0 +SECTOR = 0x1000 + + +def crc32(data): + return zlib.crc32(data) & 0xFFFFFFFF + + +def crc16_ccitt(data): + crc = 0xFFFF + for b in data: + crc ^= b << 8 + for _ in range(8): + crc = ((crc << 1) ^ 0x1021) & 0xFFFF if crc & 0x8000 else (crc << 1) & 0xFFFF + return crc + + +_seq = [0] + + +def encode_frame(ftype, payload): + out = bytearray([0xCA, ftype & 0xFF, _seq[0] & 0xFF, + len(payload) & 0xFF, (len(payload) >> 8) & 0xFF]) + out += bytes(payload) + crc = crc16_ccitt(out[1:]) + out += bytes([crc & 0xFF, (crc >> 8) & 0xFF]) + _seq[0] = (_seq[0] + 1) & 0xFF + return bytes(out) + + +def parse_combo(path): + """Parse the combined OTA package; returns (version, {bank: payload}). + + Header (52B LE): magic | hdr_len | version | total_size | payload_crc | + count(=2) | b1_off/size/crc | b2_off/size/crc | hdr_crc.""" + data = open(path, "rb").read() + if len(data) < COMBO_HDR_LEN: + raise SystemExit("bad package: too small") + (magic, hdr_len, version, total_size, payload_crc, count, + b1o, b1s, b1c, b2o, b2s, b2c, hcrc) = \ + struct.unpack_from(" len(data): + break + items[iid] = data[i + 2:i + 2 + ilen] + i += 2 + ilen + return items + + +def fmt_ver(v): + return "V%d.%02d.%02d" % (v >> 16, (v >> 8) & 0xFF, v & 0xFF) + + +async def find_device(): + print("scanning for %s* ..." % NAME_PREFIX) + while True: + dev = await BleakScanner.find_device_by_filter( + lambda d, ad: d.name and d.name.startswith(NAME_PREFIX), + timeout=10.0) + if dev is not None: + return dev + print(" not found, retrying (device advertising?)") + + +async def ota_session(pkg_path): + version, banks = parse_combo(pkg_path) + print("package: version %s, bank1 %dB / bank2 %dB" + % (fmt_ver(version), len(banks[1]), len(banks[2]))) + + dev = await find_device() + print("connecting %s (%s) ..." % (dev.name, dev.address)) + + disconnected = asyncio.Event() + + def on_disconnect(_c): + disconnected.set() + + async with BleakClient(dev, disconnected_callback=on_disconnect) as client: + print("connected, mtu=%d" % client.mtu_size) + await asyncio.sleep(0.5) # let the device's MTU exchange finish + + items = await read_info_tlv(client) + cur_bank = items.get(0x07, b"\x01")[0] + cur_ver = int.from_bytes(items.get(0x01, b"\x00" * 4), "little") + print("device: running APP%d, firmware %s" % (cur_bank, fmt_ver(cur_ver))) + + target = 3 - cur_bank # OTA always writes the inactive bank + blob = banks[target] + print("target bank: APP%d (%d bytes, crc32=0x%08X)" + % (target, len(blob), crc32(blob))) + if cur_ver == version: + print("note: same version already on the device, updating anyway") + + max_data = min(client.mtu_size - 3 - 4 - 7, 233) # offset u32 + frame overhead + disconnected.clear() + + async def send(ftype, payload): + await client.write_gatt_char(WRITE_UUID, encode_frame(ftype, payload), + response=False) + await asyncio.sleep(FRAME_GAP_S) + + for attempt in (1, 2): + sector_gap = SECTOR_GAP_S * attempt + print("OTA_BEGIN ... (attempt %d)" % attempt) + await send(TYPE_OTA_BEGIN, struct.pack(" 1 else os.path.join( + root, "out", "mothercup_ble_ota.bin") + if not os.path.isfile(pkg): + print("package not found: %s (run tools\\make_package.bat first)" % pkg) + return 1 + return asyncio.run(ota_session(pkg)) + + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + pass diff --git a/tools/flash_package.bat b/tools/flash_package.bat index c575150..4f20439 100644 --- a/tools/flash_package.bat +++ b/tools/flash_package.bat @@ -2,7 +2,7 @@ rem ============================================================ rem flash_package.bat [image] - erase the chip and program the rem merged package via NS-LINK (NSpyocd). -rem [image] optional, default: tools\out\caiic_full.hex +rem [image] optional, default: tools\out\mothercup_ble_prod.hex rem Connect NS-LINK to SWD (PA4/PA5) and the reset pin first, rem and CLOSE Keil (it holds the probe). rem ============================================================ @@ -15,7 +15,7 @@ rem "n32wb031keq6_2" maps the full 512KB of the KEQ6-2 we actually use. set PACK="%ROOT%\nations-tec\N32WB03x_DFP.1.4.0.pack" set TARGET=n32wb031keq6_2 set IMAGE=%~1 -if "%IMAGE%"=="" set IMAGE=out\caiic_full.hex +if "%IMAGE%"=="" set IMAGE=out\mothercup_ble_prod.hex if not "%IMAGE:~0,1%"=="\" if not "%IMAGE:~1,1%"==":" set IMAGE=%~dp0%IMAGE% if not exist "%IMAGE%" ( diff --git a/tools/make_package.bat b/tools/make_package.bat index b76cc69..bf02dab 100644 --- a/tools/make_package.bat +++ b/tools/make_package.bat @@ -1,14 +1,16 @@ @echo off rem ============================================================ -rem make_package.bat - build bootloader + APP, then merge into -rem one flash package (tools/out/caiic_ble_full.*) +rem make_package.bat - build bootloader + APP1/APP2, then merge +rem into the PRODUCTION package: +rem tools/out/mothercup_ble_prod.{hex,bin} (APP1 only) +rem tools/out/mothercup_ble_ota.bin (OTA release) rem Run from anywhere: tools\make_package.bat rem ============================================================ setlocal set ROOT=%~dp0.. set UV4="D:\Keil_v5\UV4\UV4.exe" -echo [1/3] Building bootloader... +echo [1/4] Building bootloader... %UV4% -r "%ROOT%\Boot\MDK-ARM\caiic_boot.uvprojx" -j0 -o "%ROOT%\Boot\MDK-ARM\build.log" if errorlevel 1 goto :fail findstr /C:"0 Error(s), 0 Warning(s)" "%ROOT%\Boot\MDK-ARM\build.log" >nul || goto :fail @@ -38,11 +40,12 @@ if not defined PYEXE ( rem --ota-app2: emit a second OTA payload linked for the APP2 bank rem (OTA writes the bank opposite to the running one, so both link rem variants must ship; the client picks by info item 0x07 CUR_BANK). -%PYEXE% "%~dp0merge_image.py" "%ROOT%\mcm-ddc-ble\MDK-ARM\bin\mcm-ddc-ble.bin" caiic_ble_full --ota-app2 "%ROOT%\mcm-ddc-ble\MDK-ARM\bin\mcm-ddc-ble-app2.bin" || goto :fail +%PYEXE% "%~dp0merge_image.py" "%ROOT%\mcm-ddc-ble\MDK-ARM\bin\mcm-ddc-ble.bin" mothercup_ble_prod --ota-app2 "%ROOT%\mcm-ddc-ble\MDK-ARM\bin\mcm-ddc-ble-app2.bin" --combo-name mothercup_ble_ota || goto :fail echo. -echo Package ready: tools\out\caiic_ble_full.hex / caiic_ble_full.bin -echo OTA payloads: caiic_ble_full_ota.bin (bank1) / caiic_ble_full_ota_app2.bin (bank2) +echo Package ready: tools\out\mothercup_ble_prod.hex / mothercup_ble_prod.bin +echo OTA payloads: mothercup_ble_prod_ota.bin (bank1) / mothercup_ble_prod_ota_app2.bin (bank2) +echo OTA release: mothercup_ble_ota.bin (single file, header carries version/size/CRC) goto :eof :fail diff --git a/tools/merge_image.py b/tools/merge_image.py index 778d8b5..087c0d2 100644 --- a/tools/merge_image.py +++ b/tools/merge_image.py @@ -36,12 +36,14 @@ Outputs (written to tools/out/): Usage: python tools/merge_image.py [app_bin] [out_name] [version] [--app2 app2_bin [--app2-version N]] [--with-appdata] - [--ota-app2 app2_linked_bin] + [--ota-app2 app2_linked_bin] [--combo-name name] Defaults: app_bin = mcm-ddc-04/MDK-ARM/bin/embeddedSrc.bin, out_name = caiic_full, version = 0 --app2 merges the APP2-linked image into the package; --ota-app2 only emits an extra OTA payload _ota_app2.bin/.json (target_bank=2) without -merging. OTA manifests carry "target_bank" - the client must pick the payload +merging. Both payloads also produce the combined single-file release +(.bin, default _ota_dual.bin): 52B header carrying +version/total size/total CRC + per-bank entries. The client picks the payload matching the bank the device is NOT running (info item 0x07 CUR_BANK). Run from anywhere; paths are resolved relative to this script. """ @@ -75,6 +77,14 @@ OUT_DIR = "out" HEX_REC_LEN = 16 +# Combined OTA package (release name: mothercup_ble_ota.bin): 52-byte header +# + both bank-linked payloads. The header carries everything the old sidecar +# .json had (version / total size / total CRC), so a single file is released. +# Parsed by tools/ble_ota_update.py, which picks the payload for the bank the +# device is NOT running (info item 0x07 CUR_BANK). +OTA_COMBO_MAGIC = 0xCA10BA11 +OTA_COMBO_HDR_LEN = 52 + def crc32(data): """IEEE CRC32 - same value as Boot/src/boot_crc32.c caiic_crc32().""" @@ -95,6 +105,33 @@ def default_bootsetting(app_blob, app2_blob=None, version=0, app2_version=0): return body + struct.pack("