V1.00.20: OTA_END ack guaranteed delivery via deferred reset

- app_ota.c: after END ok, wait until the RSP is consumed by the host
  (BLE e0005 read-out / UART TX done) plus 300ms grace before resetting,
  2s timeout fallback; polled from the BLE schedule task (rwip_schedule
  must keep running for the ATT read to be processed)
- main.c: app_ota_reset_poll() in the BLE schedule task loop
- docs: ble_protocol.md 6.6 note, dev log section 43, AGENTS.md sync
This commit is contained in:
evan.liu 2026-09-04 17:35:01 +08:00
parent 266fcec731
commit 520838ce65
35 changed files with 12812 additions and 11822 deletions

View File

@ -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.18**(`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.20**(`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` — 固件全程开发日志(41 节,含所有踩坑根因与设计决策,排查问题先查这里)
- `docs/开发日志.md` — 固件全程开发日志(43 节,含所有踩坑根因与设计决策,排查问题先查这里)
- `docs/ble_protocol.md` — CAIIC BLE 通信协议 V1.1(帧格式/GATT UUID/三类业务流程/维护命令/手机端开发指南)
- `SmartAssiter/docs/修改记录_*.md` — 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.18):`Code=47896 RO-data=4752 RW-data=2060 ZI-data=28612`。
要求保持 **0 Error(s), 0 Warning(s)**。当前基线(V1.00.20):`Code=48840 RO-data=4868 RW-data=2076 ZI-data=29116`。
**固件版本号约定(重要)**:每次修改固件代码必须递增 `mcm-ddc-ble/inc/app_version.h` 中的 `APP_FW_VERSION` 与 `APP_FW_VERSION_NUM`(格式 `0x00MMmmpp`,通常补丁位 +1),用于识别板上实际运行的固件;工程名与出包文件名保持不变。
@ -115,7 +115,7 @@ mcm-ddc-ble/
验证运行:串口助手接 PB6(TX)/PB7(RX),115200 8N1,复位后应看到 banner 与 `caiic->` 提示符(`help` 查看命令);手机应能搜到广播名 `CAIIC-MCM-20260902`。
## BLE 接口与协议(重要:当前双轨状态)
## BLE 接口与协议
协议权威文档:`docs/ble_protocol.md`(V1.1)。广播名 `CAIIC-MCM-20260902`,自定义 128-bit UUID 服务 `00002760-08c2-11e1-9073-0e8ac72e1001`(SDK rdtss 服务),特征:
@ -125,9 +125,10 @@ mcm-ddc-ble/
| `...c72ee002` | Notify | 帧协议上行应答(CLI_RSP / INFO_RSP / OTA_RSP) |
| `...c72ee003` | Read + Write(带响应) | **CLI 读写特征**:写原始命令行(≤63B)→ **分包读回**输出文本(每片 ≤att_mtu−2,0 长度包=结束,V1.00.13 起;此前单读 ≤512B),V1.00.02 新增 |
| `...c72ee004` | Read | **只读参数特征**:读出全部信息项 TLV(同 INFO_RSP(all)),V1.00.04 新增 |
| `...c72ee005` | Read + Write(带响应) | **OTA 专用特征**(V1.00.19 新增):写携带一帧 OTA 传输帧 → 读同一特征取回 OTA_RSP(轮询至非空),见 ble_protocol.md §6.6 |
- **已知未决问题**:固件 notify 上行链路不通(帧协议应答收不到,下行写入正常),见开发日志 §28 末尾。因此 **App(SmartAssiter)已整体切换为只使用 `...e0003`/`...e0004` 两个读写特征**,删除了 notify/帧协议代码;**App 的 OTA 页当前停用**(OTA 应答依赖 notify)。固件侧帧协议与 OTA 代码仍完整保留,PC 端 `tools/ble_cli_test.py` 两种方式都支持。修复 notify 是本仓库最重要的待办。
- 帧格式:`0xCA | TYPE | SEQ | LEN(LE16) | PAYLOAD | CRC16-CCITT(0x1021/0xFFFF, LE)`;OTA 为双 bank 直写、扇区级 ack 流控、END 整镜像 zlib CRC32 校验后更新 bootsetting 并复位。详见 ble_protocol.md。
- **notify 上行已修复(V1.00.19)**:根因是固件帧 CRC16 漏算最后一个 payload 字节(3+payLen vs 协议规定的 4+payLen),所有帧被静默丢弃,与 notify 硬件链路无关(开发日志 §42)。帧协议通道(e0001/e0002)已实测恢复。App(SmartAssiter)当前仍走 `...e0003`/`...e0004` 读写特征;OTA 页可基于 `...e0005` 重做(PC 参考实现 `tools/ble_ota_update.py`)。
- 帧格式:`0xCA | TYPE | SEQ | LEN(LE16) | PAYLOAD | CRC16-CCITT(0x1021/0xFFFF, LE)`,**CRC 覆盖 TYPE..PAYLOAD 共 4+payLen 字节**;OTA 为双 bank 直写、**逐帧锁步 ack**、END 整镜像 zlib CRC32 + 向量表 bank 校验后更新 bootsetting 并复位。OTA 传输层通道无关:BLE e0005 / UART 二进制模式(CLI `ota`)/ 旧帧协议通道,详见 ble_protocol.md §6.6。
- 连接后设备主动发起 MTU=247 交换;失败退化为默认 20B/包,协议按字节流重组,两种情形都正确。
- 信息项 id:0x01 固件版本 u32 / 0x02 芯片温度 i16(0.1°C,ADC CH7) / 0x03 风扇转速 u16(0xFFFF=无硬件,`app_fan_get_rpm()` 弱符号,产品板重写即可) / 0x04 电压 u16(mV,ADC CH6) / 0x05 运行时长 u32(s) / 0x06 剩余堆 u32(B) / 0x07 当前运行 bank u8(1=APP1,2=APP2,OTA 选包依据)。
- 调试手段:CLI 命令 `blelog on` 后,串口打印每个收/发协议帧 hex dump、连接/断开、CCCD 订阅事件(走 printf,不经 BLE 通道,避免自激)。
@ -151,7 +152,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_ota_update.py` + `ble_ota.bat` — BLE OTA 升级器(解析单文件升级包 `mothercup_ble_ota.bin`、按 CUR_BANK 选对侧 bank 载荷、经 OTA 特征 `...e0005` 逐帧锁步、复位检测+重连校验;`--uart COMx` 走串口二进制模式)。**BLE 通道已实测通过(§42);UART 通道待验证(串口线接触问题,待恢复后测)**
- `ble_temp_watch.py` — 温度监测小工具
## 实时架构约定(固件)

View File

@ -17,6 +17,7 @@
| 上行特征(设备→手机) | `00002760-08C2-11E1-9073-0E8AC72EE002` | Notify(需写 CCCD=0x0001 使能) |
| CLI 读写特征 | `00002760-08C2-11E1-9073-0E8AC72EE003` | Read + Write(带响应):写原始命令行(≤63B)→ 分包读回输出文本(见 §6.5) |
| 只读参数特征 | `00002760-08C2-11E1-9073-0E8AC72EE004` | Read:全部信息项 TLV(同 INFO_RSP(all),见 §5) |
| OTA 特征 | `00002760-08C2-11E1-9073-0E8AC72EE005` | Read + Write(带响应):OTA 传输帧下行(写)→ OTA_RSP 帧读回(见 §6.6) |
MTU:**设备在连接建立后主动发起 MTU 交换(请求 247)**;手机端也可自行发起。
一次 BLE 写入/通知的最大数据量 = att_mtu − 3(默认 MTU 23 时为 20B)。
@ -49,6 +50,7 @@ MTU:**设备在连接建立后主动发起 MTU 交换(请求 247)**;手
| 0x10 | 手机→设备 | OTA_BEGIN | {total_size u32, image_crc32 u32, version u32} |
| 0x11 | 手机→设备 | OTA_DATA | {offset u32, data…}(offset 必须严格连续) |
| 0x12 | 手机→设备 | OTA_END | {crc32 u32}(与 OTA_BEGIN 中一致) |
| 0x13 | 手机→设备 | OTA_ABORT | 空 payload;中止会话(设备回 OTA_RSP(ok)) |
| 0x1F | 设备→手机 | OTA_RSP | {cmd_echo u8, status u8, offset u32} |
| 0x20 | 手机→设备 | INFO_QUERY | {item_id u8}×n;空 payload 或单个 0xFF = 查询全部 |
| 0x21 | 设备→手机 | INFO_RSP | TLV 序列 {id u8, len u8, value…}×n |
@ -115,9 +117,10 @@ flash 擦除单位 = 4KB 扇区。设备侧**直写 flash,不做扇区级 RAM
2. **OTA_DATA**:从 offset=0 起严格顺序发送,每帧 {offset, data}。
- 单帧 data 建议 ≤ 233B(MTU 247 时一次写入 244B = 帧开销 7 + offset 4 + data 233);
MTU 23 时单帧总长度不得超过 20B。
- 设备收到即写入 flash;每写满一个 4KB 扇区回一帧 OTA_RSP(0x11, ok, 已写 offset)
——兼作流控与进度显示。
- 手机端节奏:可按扇区等 ack 发送,也可连续发送;若收到 status=2 的应答,
- 设备收到即写入 flash;**每一帧都回 OTA_RSP(0x11, ok, 新 offset)**——
逐帧锁步流控(V1.00.19 起;此前为每 4KB 扇区一答)。ack 往返时间天然覆盖
新扇区的惰性擦除窗口(数十 ms 关中断)。
- 主机节奏:逐帧等 ack(锁步)。若应答 status=2(offset 乱序),
从应答中的 offset 处重发即可重新同步。
3. **OTA_END**:{crc32}。设备 flush 尾部(0xFF 补齐到 4 字节对齐后写最后一个扇区),
从 flash 回读整镜像复算 CRC32,与手机端比对:
@ -137,6 +140,33 @@ flash 擦除单位 = 4KB 扇区。设备侧**直写 flash,不做扇区级 RAM
落在目标 bank 范围内,不符则回 OTA_RSP(status=6 bank_mismatch) 并中止、不切换
(V1.00.17 起)。
## 6.6 OTA 传输层:通道无关设计(V1.00.19 起)
OTA 会话(§6 的 BEGIN/DATA/END/ABORT → OTA_RSP)跑在 0xCA 帧上,帧格式与
字节流重组规则和 §2 完全一致,因此**同一份协议可以跑在不同物理通道上**;
设备侧 OTA 引擎与通道解耦(应答经通道注册的 sink 出口):
| 通道 | 下行(主机→设备) | 上行(设备→主机) |
|---|---|---|
| **BLE OTA 特征** `...e0005` | 每次"写带响应"携带一帧(≤ att_mtu−3) | 写完**读同一特征**取回 OTA_RSP 帧;读应答可能先于设备处理完成返回空,**读需轮询至非空**(擦扇区时数十 ms) |
| **UART 串口**(115200 8N1) | CLI 先发文本命令 `ota` 进入二进制帧模式,随后原始帧字节流 | OTA_RSP 帧直接从 TX 发出;3s 无帧自动退出回 CLI |
| BLE 旧帧协议通道(e0001/e0002 notify) | 0xCA 帧写入 e0001 | notify 上行(V1.00.19 修复 CRC 后可用,见开发日志 §42) |
锁步规则:每发一帧必须等到对应 OTA_RSP 再发下一帧(ack 里 offset =
设备期望的下一字节;status≠0 时按其 offset 重发可重新同步)。
**CRC16 覆盖范围 = TYPE 到 PAYLOAD 末尾(含 SEQ 与 LEN 两字节,共
4+payLen 字节)**——V1.00.19 之前固件漏算最后一个 payload 字节导致全部
帧被丢弃(§42 根因),新旧固件与主机实现必须统一按 4+payLen 计算。
PC 参考实现:`tools\ble_ota_update.py`(`ble_ota.bat`;BLE 默认,
`--uart COMx` 走串口)。
**END 应答可靠送达(V1.00.20 起)**:OTA_END 校验通过后,设备先发出 ok
应答,**等主机取走该应答**(BLE:e0005 读取消耗;UART:TX 发完)并留
300ms 宽限后才复位进入新 bank,2s 超时兜底。因此主机在正常链路上必定能
收到 END 应答;收不到应视为链路异常,但仍可按"等待重启后重连复核 CUR_BANK
与版本"兜底(PC 工具两种处理都保留)。
## 6.5 维护命令:bootsetting 与 APP_DATA 参数区(V1.00.09 起,结构化访问;V1.00.10 起参数命令为 appget/appset;V1.00.11 起新增 reset/factory/uartrst/uartinfo)
bootsetting 与 APP_DATA 保留区(0x01006000/8KB)的读写以 CLI 命令形式提供,

View File

@ -1035,3 +1035,60 @@ CUR_BANK 选包、扇区擦除窗口避让的节流发送、双次重试)已
hdr_len/total_size/payload_crc/各 bank crc,实包解析验证通过
(V1.00.18,bank1 54204B / bank2 54504B,total=108760B 与文件一致)。
- 旧命名 caiic_ble_full_* 产物已从仓库删除;双 APP 测试包仍叫 caiic_ble_dual。
## 42. OTA 传输层通道无关化 + 帧 CRC 根因修复(2026-09-04,V1.00.19)
**本仓库最大遗留问题(notify 上行不通)的根因找到并修复了**:固件 0xCA 帧的
CRC16 只覆盖了 `3+payLen` 字节(TYPE/SEQ/LEN + 除最后一字节外的 payload),
而协议文档与 PC 端实现都是 `4+payLen`(TYPE..PAYLOAD 全量)——**所有帧在设备侧
CRC 校验失败被静默丢弃**,表现为"下行写入正常、没有任何应答",此前一直以为
是 notify 上行链路问题。修复 4 处(app_ble_proto.c TX/RX、app_ota.c 信道
TX/RX 各一处 `3u`→`4u`)后,notify 上行立即恢复(`ble_cli -f version` 实测
收到 CLI_RSP 帧流 + CLI_RSP_END),e0005 OTA 信道也随之打通。
本次内容:
- **OTA 传输层通道无关设计**(ble_protocol.md §6.6):0xCA 帧 + OTA 消息集
(BEGIN/DATA/END/ABORT→OTA_RSP)与物理通道解耦;`app_ota.c` 应答改为
通道注册的 sink 出口(`app_ota_rsp_sink_fn`),NULL 时回落旧 notify 路径;
通道侧自含字节流重组器(只收 OTA 类型)。
- **BLE 独立 OTA 特征 `...e0005`**(Write With Response + Read,不与 CLI/参数
特征混用):写携带一帧,读同一特征取回 OTA_RSP(读即消费;读要先于设备
处理完成返回空时主机需轮询,实测需要)。
- **UART OTA 通道**:CLI 命令 `ota` 进入二进制帧模式(无回显、3s 空闲自动
退出回 CLI 并中止会话),RSP 帧直接从 TX 发出。
- **流控改为逐帧锁步**:原"每写满 4KB 扇区才回 ack"导致锁步主机在第一个
DATA 帧就等不到应答;改为每帧回 OTA_RSP(ok, 新 offset),ack 往返天然覆盖
扇区擦除窗口(旧 WWR 盲发丢帧问题同时消解)。
- PC 端 `ble_ota_update.py` 重写为双通道(BLE 默认 / `--uart COMx`),
`ble_ota.bat` 包装;BEGIN/DATA/END 逐帧校验 echo/status/offset,
END 的 ack 丢失(设备先复位)按"等重启复核"处理。
实测:BLE e0005 通道完整 OTA 通过(55KB @ ~1.9kB/s,35s,复位后重连核对
CUR_BANK 翻转 + 版本正确);notify 帧协议通道恢复(CLI_RSP 正常);
UART 通道待串口线恢复后验证。
## 43. OTA_END 应答可靠送达:延迟复位(2026-09-04,V1.00.20)
**问题**:OTA 成功后设备在发出 END 的 ok 应答后仅固定延时 200ms 就
`NVIC_SystemReset()`。V1.00.19 锁步信道下,BLE 通道的 OTA_RSP 只是写入
e0005 读回缓冲,**主机还没发起 ATT 读**,200ms 后设备已复位——PC 端永远
收不到 END ack(实测报 `END ack lost WinError -2147023673`,只能靠重启后
重连复核兜底)。UART 通道同理存在复位抢在应答读完之前的窗口。
**修复**(`app_ota.c`):END 成功后不再原地延时复位,改为**延迟复位**:
- 新增"应答已消费"标志 `s_rsp_consumed`:BLE 通道在
`app_ota_chan_rsp_ble()` 读取消耗(主机读到非空 RSP)时置位;UART 通道
在 `ota_uart_sink()` 阻塞式 DMA TX 返回(字节已出移位寄存器)时置位。
- END ok 应答发出后置 `s_reset_pending` + 时间戳即返回;**不能在
`ota_on_end()` 里死等**——e0005 的 ATT 读处理本身也要 BLE 调度任务继续
跑 `rwip_schedule()`,阻塞会导致永远等不到读。
- BLE 调度任务循环新增 `app_ota_reset_poll()`:应答已消费且过了 300ms
宽限(让控制器真正把 ATT read response 发出空中)即复位;2s 超时无条件
兜底复位(防旧客户端/异常下设备挂死)。旧 notify 路径顺带获益:等待窗口
内 `ble_up_poll()` 持续排空 notify 缓冲。
实测:`ble_ota.bat` 完整 OTA 后**直接收到 END ack**(输出 `OTA_END done`,
不再出现 ack lost),设备复位进 APP2,重连核对 CUR_BANK 翻转 + 版本正确,
PASS。基线:Code=48840 RO-data=4868 RW-data=2076 ZI-data=29116。

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -24,12 +24,15 @@ extern "C" {
#include <stdint.h>
/* Frame types */
#define BLE_FRAME_SOF 0xCA /* frame start byte */
#define BLE_FRAME_HDR_LEN 5u /* SOF+TYPE+SEQ+LEN(2) */
#define BLE_FRAME_CLI_REQ 0x01 /* phone -> device: one command line */
#define BLE_FRAME_CLI_RSP 0x02 /* device -> phone: response text chunk */
#define BLE_FRAME_CLI_RSP_END 0x03 /* device -> phone: {status u8} */
#define BLE_FRAME_OTA_BEGIN 0x10 /* phone -> device: {size, crc32, version} */
#define BLE_FRAME_OTA_DATA 0x11 /* phone -> device: {offset, data...} */
#define BLE_FRAME_OTA_END 0x12 /* phone -> device: {crc32} */
#define BLE_FRAME_OTA_ABORT 0x13 /* phone -> device: abort session (no payload) */
#define BLE_FRAME_OTA_RSP 0x1F /* device -> phone: {cmd, status, offset} */
#define BLE_FRAME_INFO_QUERY 0x20 /* phone -> device: {item_id}*n (empty/0xFF = all) */
#define BLE_FRAME_INFO_RSP 0x21 /* device -> phone: TLV {id, len, value}*n */

View File

@ -18,6 +18,9 @@ void AppCli_Init(void);
/* Feed CLI input bytes from a non-UART source (BLE downlink, task context). */
void AppCli_InputBytes(const uint8_t* data, uint16_t len);
/* Switch the UART frontend into OTA binary frame mode (CLI "ota" command). */
void app_cli_ota_mode_enter(void);
#ifdef __cplusplus
}
#endif

View File

@ -17,8 +17,9 @@ extern "C" {
#include <stdint.h>
/**
* @brief Handle one OTA frame (BLE_FRAME_OTA_BEGIN / OTA_DATA / OTA_END).
* Sends OTA_RSP frames through app_ble_proto_send_frame().
* @brief Handle one OTA frame (BLE_FRAME_OTA_BEGIN / OTA_DATA / OTA_END /
* OTA_ABORT). Sends OTA_RSP frames through the channel sink
* (app_ota_chan_rx) or, without a sink, app_ble_proto_send_frame().
*/
void app_ota_handle_frame(uint8_t type, uint8_t seq, const uint8_t* payload, uint16_t len);
@ -33,6 +34,35 @@ void app_ota_abort(void);
*/
uint32_t app_ota_current_bank_base(void);
/* ------------------------------------------------------------------ */
/* Transport channels (ble_protocol.md §6.6): the engine is fed via */
/* app_ota_chan_rx(); RSP frames go to the channel's sink. */
/* ------------------------------------------------------------------ */
/** Response sink: receives one complete 0xCA-framed OTA_RSP. */
typedef void (*app_ota_rsp_sink_fn)(const uint8_t* frame, uint16_t len);
/** Generic channel byte input with an explicit sink. */
void app_ota_chan_rx(const uint8_t* data, uint16_t len, app_ota_rsp_sink_fn sink);
/** BLE OTA characteristic (...e0005): write = frame in, read = RSP out. */
void app_ota_chan_rx_ble(const uint8_t* data, uint16_t len);
const uint8_t* app_ota_chan_rsp_ble(uint16_t* out_len);
/** UART binary mode (CLI "ota"): one received byte in; RSP frames on TX. */
void app_ota_chan_rx_uart(uint8_t ch);
/** Drop the reassembler state (channel idle timeout / link loss). */
void app_ota_chan_idle(void);
/**
* @brief Poll the deferred post-OTA_END reset: after a successful OTA_END
* the device resets only once the END response has been consumed by
* the host (plus a short grace), with a timeout fallback. Call from
* the BLE schedule task loop.
*/
void app_ota_reset_poll(void);
#ifdef __cplusplus
}
#endif

View File

@ -84,6 +84,7 @@
#define ATT_CHAR_AM_SPEED_NTF_128 {0x02,0x00,0x2E,0xC7,0x8a,0x0E, 0x73,0x90, 0xE1,0x11, 0xC2,0x08, 0x60,0x27,0x00,0x00} /*!< Characteristic value UUID */
#define ATT_CHAR_AM_SPEED_CLI_128 {0x03,0x00,0x2E,0xC7,0x8a,0x0E, 0x73,0x90, 0xE1,0x11, 0xC2,0x08, 0x60,0x27,0x00,0x00} /*!< CLI Characteristic value UUID (read + write) */
#define ATT_CHAR_AM_SPEED_INFO_128 {0x04,0x00,0x2E,0xC7,0x8a,0x0E, 0x73,0x90, 0xE1,0x11, 0xC2,0x08, 0x60,0x27,0x00,0x00} /*!< Device info Characteristic value UUID (read-only TLV) */
#define ATT_CHAR_AM_SPEED_OTA_128 {0x05,0x00,0x2E,0xC7,0x8a,0x0E, 0x73,0x90, 0xE1,0x11, 0xC2,0x08, 0x60,0x27,0x00,0x00} /*!< OTA Characteristic value UUID (write frame in, read RSP out) */
/// rdtss Service Attributes Indexes
@ -105,6 +106,9 @@ enum
RDTSS_IDX_INFO_CHAR,
RDTSS_IDX_INFO_VAL,
RDTSS_IDX_OTA_CHAR,
RDTSS_IDX_OTA_VAL,
RDTSS_IDX_NB,
};

View File

@ -9,12 +9,12 @@
#define __APP_VERSION_H__
#ifndef APP_FW_VERSION
#define APP_FW_VERSION "V1.00.18"
#define APP_FW_VERSION "V1.00.20"
#endif
/* Numeric form used by the BLE info query / OTA records: 0x00MMmmpp */
#ifndef APP_FW_VERSION_NUM
#define APP_FW_VERSION_NUM 0x00010012u
#define APP_FW_VERSION_NUM 0x00010014u
#endif
#endif /* __APP_VERSION_H__ */

View File

@ -23,9 +23,7 @@
#include <string.h>
#include <stdio.h>
/* Frame layout */
#define BLE_FRAME_SOF 0xCA
#define BLE_FRAME_HDR_LEN 5 /* SOF+TYPE+SEQ+LEN(2) */
/* Frame layout (BLE_FRAME_SOF / BLE_FRAME_HDR_LEN live in app_ble_proto.h) */
#define BLE_FRAME_CRC_LEN 2
#define BLE_FRAME_OVERHEAD (BLE_FRAME_HDR_LEN + BLE_FRAME_CRC_LEN)
@ -138,7 +136,7 @@ int app_ble_proto_send_frame(uint8_t type, uint8_t seq, const uint8_t* payload,
{
memcpy(&s_txFrame[5], payload, len);
}
crc = proto_crc16(&s_txFrame[1], (uint16_t)(3u + len)); /* TYPE..PAYLOAD */
crc = proto_crc16(&s_txFrame[1], (uint16_t)(4u + len)); /* TYPE..PAYLOAD (incl. SEQ+LEN) */
s_txFrame[5 + len] = (uint8_t)(crc & 0xFFu);
s_txFrame[5 + len + 1] = (uint8_t)(crc >> 8);
@ -352,7 +350,7 @@ void app_ble_proto_rx_bytes(const uint8_t* data, uint16_t len)
if (s_rxHave == s_rxNeed)
{
uint16_t payLen = (uint16_t)s_rxBuf[3] | ((uint16_t)s_rxBuf[4] << 8);
uint16_t crcCalc = proto_crc16(&s_rxBuf[1], (uint16_t)(3u + payLen));
uint16_t crcCalc = proto_crc16(&s_rxBuf[1], (uint16_t)(4u + payLen));
uint16_t crcRecv = (uint16_t)s_rxBuf[5 + payLen] |
((uint16_t)s_rxBuf[5 + payLen + 1] << 8);

View File

@ -12,6 +12,7 @@
#include "app_cli.h"
#include "cli_core.h"
#include "bsp_usart.h"
#include "app_ota.h"
#include <string.h>
@ -24,6 +25,22 @@
#define CLI_HISTORY_SIZE 8 /* number of remembered command lines */
#define CLI_PROMPT "caiic->"
/* UART OTA binary mode: raw 0xCA frames in, framed OTA_RSP out (no echo).
* Auto-exit after this much inactivity (ms). */
#define OTA_MODE_IDLE_MS 3000u
#define OTA_MODE_POLL_MS 100u
static uint8_t s_otaMode;
/**
* @brief Switch the UART frontend into OTA binary frame mode. Called by the
* "ota" CLI command (any CLI context; takes effect in CliTask).
*/
void app_cli_ota_mode_enter(void)
{
s_otaMode = 1;
}
/* Key codes */
#define CLI_KEY_TAB 0x09
#define CLI_KEY_ESC 0x1B
@ -206,6 +223,9 @@ static uint16_t cli_tab_complete(char* line, uint16_t len)
/**
* @brief CLI task: receive bytes, edit the line, dispatch on terminator.
* TAB completes the command name, UP/DOWN arrows walk the history.
* In OTA binary mode (CLI "ota"), bytes feed the OTA frame channel
* raw - no echo/editing; the mode exits on 3s of inactivity (or on
* device reset after a successful OTA_END).
*/
static void CliTask(void* argument)
{
@ -215,6 +235,7 @@ static void CliTask(void* argument)
uint8_t swallow = 0; /* complementary terminator to swallow once */
uint8_t escState = 0; /* 0=normal, 1=got ESC, 2=got ESC '[' */
int histNav = -1; /* -1 = editing, 0.. = history entry (0=newest) */
uint32_t otaIdleMs = 0;
uint8_t ch;
(void)argument;
@ -222,6 +243,27 @@ static void CliTask(void* argument)
for (;;)
{
/* OTA binary frame mode: raw bytes to the OTA channel */
while (s_otaMode)
{
if (bsp_usart_read_byte(&ch, OTA_MODE_POLL_MS) != 0)
{
app_ota_chan_rx_uart(ch);
otaIdleMs = 0;
continue;
}
otaIdleMs += OTA_MODE_POLL_MS;
if (otaIdleMs >= OTA_MODE_IDLE_MS)
{
s_otaMode = 0;
app_ota_chan_idle();
app_ota_abort(); /* discard a half-finished session */
bsp_usart_tx_lock();
cli_uart_write("\r\n[ota] idle timeout, back to CLI\r\n" CLI_PROMPT);
bsp_usart_tx_unlock();
}
}
if (bsp_usart_read_byte(&ch, BSP_USART_WAIT_FOREVER) == 0)
{
continue;

View File

@ -1,11 +1,13 @@
/**
* @file app_ota.c
* @brief BLE OTA receiver implementation.
* @brief OTA receiver implementation (channel-agnostic: BLE OTA
* characteristic, UART binary mode, legacy notify frame path).
*
* Flow: OTA_BEGIN (size/crc/version) -> OTA_DATA* (strictly sequential
* offsets, acked per completed 4KB flash sector) -> OTA_END (flush the
* sub-word tail, verify whole-image CRC32 against flash, update bootsetting,
* reset).
* offsets, EVERY frame acked with the new offset - lockstep flow control)
* -> OTA_END (flush the sub-word tail, verify whole-image CRC32 against
* flash, verify the vector table targets the right bank, update
* bootsetting, reset). OTA_ABORT aborts the session.
*
* Direct-write design (no 4KB staging buffer):
* - Sectors of the target bank are erased lazily: the first received byte
@ -22,6 +24,7 @@
*/
#include "app_ota.h"
#include "app_ble_proto.h"
#include "bsp_usart.h"
#include "dfu_layout.h"
#include "boot_crc32.h"
#include "n32wb03x.h"
@ -37,7 +40,14 @@
extern uint32_t Image$$ER_IROM1$$Base;
#define OTA_MIN_IMAGE_SIZE 4u
#define OTA_RESET_DELAY_MS 200u
/* Deferred reset after a successful OTA_END: the END response must reach
* the host before the chip resets, otherwise the ack is lost (BLE: the RSP
* sits in the read-back buffer until the host's ATT read fetches it).
* Reset once the response was consumed AND a short grace elapsed (lets the
* controller actually emit the ATT read response), or unconditionally after
* the timeout so the device never hangs. */
#define OTA_RESET_GRACE_MS 300u
#define OTA_RESET_TIMEOUT_MS 2000u
/* OTA session state */
static uint8_t s_active; /* session in progress */
@ -48,11 +58,15 @@ static uint32_t s_version;
static uint32_t s_offset; /* bytes received so far */
static uint32_t s_prog; /* bytes programmed to flash */
static uint32_t s_erased; /* bytes erased (sector granularity) */
static uint32_t s_ack_sector; /* last acked sector index */
static uint8_t s_word_buf[4]; /* sub-word alignment staging */
static uint32_t s_word_fill;
static uint8_t s_qflash_ready;
/* Deferred post-OTA_END reset (see OTA_RESET_GRACE_MS above) */
static volatile uint8_t s_rsp_consumed; /* END RSP taken by the host */
static volatile uint8_t s_reset_pending; /* reset scheduled, polled by the BLE task */
static TickType_t s_reset_stamp;
/* ------------------------------------------------------------------ */
/* Helpers */
/* ------------------------------------------------------------------ */
@ -63,6 +77,39 @@ static uint32_t ota_rd32(const uint8_t* p)
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
/* ------------------------------------------------------------------ */
/* Channel-agnostic response routing */
/* */
/* The OTA engine speaks 0xCA-framed OTA_RSP messages. Each transport */
/* channel (legacy notify, BLE OTA characteristic, UART binary mode) */
/* registers a sink before feeding frames in; the sink receives one */
/* complete framed OTA_RSP. NULL sink = legacy notify path via */
/* app_ble_proto_send_frame(). */
/* ------------------------------------------------------------------ */
static app_ota_rsp_sink_fn s_rsp_sink;
/**
* @brief CRC16-CCITT (poly 0x1021, init 0xFFFF) - same as the frame
* protocol's proto_crc16 (app_ble_proto.c).
*/
static uint16_t ota_crc16(const uint8_t* d, uint16_t len)
{
uint16_t crc = 0xFFFFu;
while (len--)
{
uint8_t bit;
crc ^= (uint16_t)*d++ << 8;
for (bit = 0; bit < 8u; bit++)
{
crc = (crc & 0x8000u) ? (uint16_t)((crc << 1) ^ 0x1021u)
: (uint16_t)(crc << 1);
}
}
return crc;
}
/**
* @brief Send an OTA_RSP frame: {cmd_echo, status, offset(LE)}.
*/
@ -76,6 +123,25 @@ static void ota_respond(uint8_t seq, uint8_t cmd_echo, uint8_t status, uint32_t
p[3] = (uint8_t)((offset >> 8) & 0xFFu);
p[4] = (uint8_t)((offset >> 16) & 0xFFu);
p[5] = (uint8_t)((offset >> 24) & 0xFFu);
if (s_rsp_sink != NULL)
{
/* Wrap into a 0xCA frame and hand to the channel */
uint8_t fr[5 + sizeof(p) + 2];
uint16_t crc;
fr[0] = BLE_FRAME_SOF;
fr[1] = BLE_FRAME_OTA_RSP;
fr[2] = seq;
fr[3] = sizeof(p) & 0xFFu;
fr[4] = 0;
memcpy(fr + 5, p, sizeof(p));
crc = ota_crc16(fr + 1, (uint16_t)(4u + sizeof(p))); /* TYPE..PAYLOAD */
fr[5 + sizeof(p)] = (uint8_t)(crc & 0xFFu);
fr[5 + sizeof(p) + 1] = (uint8_t)(crc >> 8);
s_rsp_sink(fr, (uint16_t)sizeof(fr));
return;
}
(void)app_ble_proto_send_frame(BLE_FRAME_OTA_RSP, seq, p, sizeof(p));
}
@ -90,7 +156,6 @@ void app_ota_abort(void)
s_offset = 0;
s_prog = 0;
s_erased = 0;
s_ack_sector = 0;
s_word_fill = 0;
}
@ -125,18 +190,6 @@ static uint8_t ota_program(const uint8_t* data, uint32_t len)
return 0;
}
/**
* @brief Sector-level flow control: ack once per completed 4KB sector.
*/
static void ota_sector_ack(uint8_t seq)
{
if (s_prog / CAIIC_FLASH_SECTOR_SIZE > s_ack_sector)
{
s_ack_sector = s_prog / CAIIC_FLASH_SECTOR_SIZE;
ota_respond(seq, BLE_FRAME_OTA_DATA, BLE_OTA_ST_OK, s_prog);
}
}
/* ------------------------------------------------------------------ */
/* Frame handlers */
/* ------------------------------------------------------------------ */
@ -195,7 +248,6 @@ static void ota_on_begin(uint8_t seq, const uint8_t* p, uint16_t len)
s_offset = 0;
s_prog = 0;
s_erased = 0;
s_ack_sector = 0;
s_word_fill = 0;
ota_respond(seq, BLE_FRAME_OTA_BEGIN, BLE_OTA_ST_OK, 0);
@ -204,8 +256,10 @@ static void ota_on_begin(uint8_t seq, const uint8_t* p, uint16_t len)
/**
* @brief OTA_DATA: {offset u32 + data}. Data must arrive strictly in order.
* Payload is programmed to flash immediately (sectors are erased
* lazily); an OTA_RSP(ok) is sent per completed 4KB sector (flow
* control / progress). Errors are acked immediately and the state is
* lazily); EVERY frame is acked with the new offset (lockstep flow
* control on the BLE/UART OTA channels; the erase time of a fresh
* sector is covered by the ack round trip).
* Errors are acked immediately and the state is
* kept so the peer can resend.
*/
static void ota_on_data(uint8_t seq, const uint8_t* p, uint16_t len)
@ -280,7 +334,8 @@ static void ota_on_data(uint8_t seq, const uint8_t* p, uint16_t len)
app_ota_abort();
return;
}
ota_sector_ack(seq);
/* Lockstep ack: every DATA frame is answered with the new offset */
ota_respond(seq, BLE_FRAME_OTA_DATA, BLE_OTA_ST_OK, s_offset);
}
/**
@ -401,11 +456,33 @@ static void ota_on_end(uint8_t seq, const uint8_t* p, uint16_t len)
return;
}
s_rsp_consumed = 0;
ota_respond(seq, BLE_FRAME_OTA_END, BLE_OTA_ST_OK, s_total_size);
/* Give the notify time to go out, then reset into the new bank */
vTaskDelay(pdMS_TO_TICKS(OTA_RESET_DELAY_MS));
/* Defer the reset until the host has fetched the END response (polled
* from the BLE schedule task, which keeps running meanwhile so the ATT
* read / notify flush can actually happen) */
s_reset_stamp = xTaskGetTickCount();
s_reset_pending = 1;
}
/**
* @brief Poll the deferred post-OTA_END reset. Must be called from the BLE
* schedule task loop (it keeps scheduling BLE events while the host
* fetches the END response).
*/
void app_ota_reset_poll(void)
{
if (s_reset_pending)
{
TickType_t elapsed = xTaskGetTickCount() - s_reset_stamp;
if ((s_rsp_consumed && elapsed >= pdMS_TO_TICKS(OTA_RESET_GRACE_MS)) ||
elapsed >= pdMS_TO_TICKS(OTA_RESET_TIMEOUT_MS))
{
NVIC_SystemReset();
}
}
}
void app_ota_handle_frame(uint8_t type, uint8_t seq, const uint8_t* payload, uint16_t len)
@ -421,7 +498,170 @@ void app_ota_handle_frame(uint8_t type, uint8_t seq, const uint8_t* payload, uin
case BLE_FRAME_OTA_END:
ota_on_end(seq, payload, len);
break;
case BLE_FRAME_OTA_ABORT:
app_ota_abort();
ota_respond(seq, BLE_FRAME_OTA_ABORT, BLE_OTA_ST_OK, s_offset);
break;
default:
break;
}
}
/* ------------------------------------------------------------------ */
/* Transport channels (docs/ble_protocol.md section 6.6) */
/* */
/* The OTA engine is channel-agnostic. Each channel feeds raw bytes */
/* through the reassembler below and gets OTA_RSP frames via its sink: */
/* - BLE OTA characteristic (...e0005): write-with-response carries */
/* one frame; the RSP is fetched by an ATT read (lockstep, no */
/* notify needed). */
/* - UART binary mode (CLI "ota"): frames on the raw byte stream, */
/* RSP frames go straight out on TX. */
/* ------------------------------------------------------------------ */
#define OTA_CHAN_MAX_PAYLOAD 480u /* same limit as the frame protocol */
enum
{
CHAN_RX_WAIT_SOF = 0,
CHAN_RX_HEADER,
CHAN_RX_BODY,
};
static uint8_t s_chanBuf[BLE_FRAME_HDR_LEN + OTA_CHAN_MAX_PAYLOAD + 2u];
static uint16_t s_chanHave;
static uint16_t s_chanNeed;
static uint8_t s_chanState;
/**
* @brief Feed channel bytes into the OTA frame reassembler.
* @param sink response sink for this channel (NULL = legacy notify path)
*/
void app_ota_chan_rx(const uint8_t* data, uint16_t len, app_ota_rsp_sink_fn sink)
{
uint16_t i;
s_rsp_sink = sink;
for (i = 0; i < len; i++)
{
uint8_t ch = data[i];
switch (s_chanState)
{
case CHAN_RX_WAIT_SOF:
if (ch == BLE_FRAME_SOF)
{
s_chanBuf[0] = ch;
s_chanHave = 1;
s_chanNeed = BLE_FRAME_HDR_LEN;
s_chanState = CHAN_RX_HEADER;
}
break;
case CHAN_RX_HEADER:
s_chanBuf[s_chanHave++] = ch;
if (s_chanHave == BLE_FRAME_HDR_LEN)
{
uint16_t payLen = (uint16_t)s_chanBuf[3] |
((uint16_t)s_chanBuf[4] << 8);
if (payLen > OTA_CHAN_MAX_PAYLOAD)
{
s_chanState = CHAN_RX_WAIT_SOF;
s_chanHave = 0;
}
else
{
s_chanNeed = (uint16_t)(BLE_FRAME_HDR_LEN + payLen + 2u);
s_chanState = (s_chanNeed == s_chanHave) ? CHAN_RX_WAIT_SOF
: CHAN_RX_BODY;
}
}
break;
case CHAN_RX_BODY:
s_chanBuf[s_chanHave++] = ch;
if (s_chanHave == s_chanNeed)
{
uint16_t payLen = (uint16_t)s_chanBuf[3] |
((uint16_t)s_chanBuf[4] << 8);
uint16_t crc = ota_crc16(&s_chanBuf[1],
(uint16_t)(4u + payLen));
uint16_t rxCrc = (uint16_t)s_chanBuf[5 + payLen] |
((uint16_t)s_chanBuf[5 + payLen + 1] << 8);
if (crc == rxCrc)
{
app_ota_handle_frame(s_chanBuf[1], s_chanBuf[2],
&s_chanBuf[5], payLen);
}
s_chanState = CHAN_RX_WAIT_SOF;
s_chanHave = 0;
}
break;
default:
s_chanState = CHAN_RX_WAIT_SOF;
s_chanHave = 0;
break;
}
}
}
/* --- BLE OTA characteristic (...e0005): write frames in, read RSP out --- */
#define OTA_BLE_RSP_BUF_SIZE 16u /* largest RSP frame = 7B hdr + 6B */
static uint8_t s_bleRsp[OTA_BLE_RSP_BUF_SIZE];
static uint16_t s_bleRspLen;
static void ota_ble_sink(const uint8_t* frame, uint16_t len)
{
if (len <= OTA_BLE_RSP_BUF_SIZE)
{
memcpy(s_bleRsp, frame, len);
s_bleRspLen = len;
}
}
void app_ota_chan_rx_ble(const uint8_t* data, uint16_t len)
{
app_ota_chan_rx(data, len, ota_ble_sink);
}
const uint8_t* app_ota_chan_rsp_ble(uint16_t* out_len)
{
*out_len = s_bleRspLen;
if (s_bleRspLen != 0u)
{
s_rsp_consumed = 1; /* host has fetched the END response */
}
s_bleRspLen = 0; /* consumed by the read */
return s_bleRsp;
}
/* --- UART binary mode (CLI "ota"): RSP frames straight out on TX --- */
static void ota_uart_sink(const uint8_t* frame, uint16_t len)
{
bsp_usart_tx_lock();
bsp_usart_write_dma(frame, len);
bsp_usart_tx_unlock();
/* Blocking DMA TX: the frame is physically out when we get here */
s_rsp_consumed = 1;
}
void app_ota_chan_rx_uart(uint8_t ch)
{
app_ota_chan_rx(&ch, 1, ota_uart_sink);
}
/**
* @brief Channel idle timeout helper: dropping the channel state also
* aborts a half-finished session (called when UART binary mode or
* the BLE link goes away mid-session).
*/
void app_ota_chan_idle(void)
{
s_chanState = CHAN_RX_WAIT_SOF;
s_chanHave = 0;
}

View File

@ -75,6 +75,7 @@
#include "rdtss.h"
#include "app_ble_proto.h"
#include "app_info.h"
#include "app_ota.h"
#include "ble_up.h"
/* Private typedef -----------------------------------------------------------*/
@ -113,6 +114,12 @@ const struct attm_desc_128 app_rdts_att_db[RDTSS_IDX_NB] =
/* Device info Characteristic Value (read-only): TLV snapshot of all
* info items, identical content to an INFO_RSP(all) frame */
[10] = {ATT_CHAR_AM_SPEED_INFO_128, PERM(RD, ENABLE), PERM(RI, ENABLE)| PERM_VAL(UUID_LEN, 0x02), 0x200 },
/* Characteristic Declaration */
[11] = {{0x03,0x28}, PERM(RD, ENABLE) | PERM(WRITE_REQ, ENABLE), 0, 0 },
/* OTA Characteristic Value: one 0xCA transport frame in
* (write-with-response), the OTA_RSP frame out (ATT read) */
[12] = {ATT_CHAR_AM_SPEED_OTA_128, PERM(RD, ENABLE) | PERM(WRITE_REQ, ENABLE), PERM(RI, ENABLE)| PERM_VAL(UUID_LEN, 0x02), 0x200 },
};
/* Private function prototypes -----------------------------------------------*/
@ -176,6 +183,11 @@ static int rdtss_value_req_ind_handler(ke_msg_id_t const msgid,
// CLI read/write characteristic: return the last command's output text
data = app_ble_proto_cli_rsp(&len);
}
else if (req_value->att_idx == RDTSS_IDX_OTA_VAL)
{
// OTA channel: fetch the pending framed OTA_RSP (consumed on read)
data = app_ota_chan_rsp_ble(&len);
}
else if (req_value->att_idx == RDTSS_IDX_INFO_VAL)
{
// device info characteristic: TLV snapshot of all info items
@ -277,6 +289,12 @@ static int rdtss_val_write_ind_handler(ke_msg_id_t const msgid,
// back through an ATT read of this same characteristic
app_ble_proto_cli_exec(ind_value->value, ind_value->length);
break;
case RDTSS_IDX_OTA_VAL:
// OTA transport frame (write-with-response); the OTA_RSP frame is
// fetched back through an ATT read of this same characteristic
app_ota_chan_rx_ble(ind_value->value, ind_value->length);
break;
default:
break;

View File

@ -14,6 +14,7 @@
#include "app_ble_proto.h"
#include "app_params.h"
#include "app_bootset.h"
#include "app_cli.h"
#include "n32wb03x.h" /* SystemTrimValueGet (temp debug) */
#include <stdio.h>
@ -304,6 +305,22 @@ static void CmdUartInfo(int argc, char* argv[])
cli_write("flow control: none; tx: DMA CH1 (polled); rx: RXDNE irq queue\r\n");
}
/**
* @brief "ota": switch the UART frontend into OTA binary frame mode
* (0xCA OTA frames on the raw stream, framed RSP on TX, 3s idle
* timeout back to CLI). The BLE OTA channel (...e0005) is always
* available and does not need this command.
*/
static void CmdOta(int argc, char* argv[])
{
(void)argc;
(void)argv;
cli_write("ota: UART switches to binary frame mode (3s idle timeout)\r\n");
cli_write("send 0xCA OTA frames now (ble_protocol.md section 6.6)\r\n");
app_cli_ota_mode_enter();
}
/* Command table; add new commands here */
static const CliCmd_t s_cmds[] = {
{"help", "help [cmd]: list commands / show one command's help", CmdHelp},
@ -317,6 +334,7 @@ static const CliCmd_t s_cmds[] = {
{"bsdump", "bsdump: show bootsetting record + crc check", AppBootset_CmdBsdump},
{"bsset", "bsset <field> <val>: write bootsetting field (careful!)", AppBootset_CmdBsset},
{"appsw", "appsw [1|2]: switch boot bank (crc verified) + reboot", AppBootset_CmdAppswitch},
{"ota", "ota: UART enters OTA binary frame mode (see help ota)", CmdOta},
{"reset", "reset: system reset (reboot)", CmdReset},
{"factory", "factory: restore params to defaults and reboot", CmdFactory},
{"uartrst", "uartrst: re-init USART1 (115200 8N1), flush rx queue", CmdUartRst},

View File

@ -81,6 +81,7 @@
#include "app_cli.h"
#include "app_ble_proto.h"
#include "app_params.h"
#include "app_ota.h"
#include "app_version.h"
#include "app_user_config.h"
@ -124,6 +125,8 @@ static void ble_schedule_task(void *pvParameters)
rwip_schedule();
/*flush pending CLI uplink frames (notify, paced by cfm)*/
ble_up_poll();
/*deferred post-OTA_END reset once the response reached the host*/
app_ota_reset_poll();
vTaskDelay(pdMS_TO_TICKS(1));
}
}

View File

@ -2,8 +2,9 @@
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 ble_ota.bat [mothercup_ble_ota.bin] BLE channel (...e0005)
rem ble_ota.bat --uart COM4 [mothercup_ble_ota.bin] UART channel
rem default pkg: 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
@ -26,7 +27,7 @@ if not exist "%PYEXE%" (
"%PYEXE%" -m pip install --quiet bleak || goto :fail
)
"%PYEXE%" "%TOOLS%ble_ota_update.py" %1
"%PYEXE%" "%TOOLS%ble_ota_update.py" %1 %2 %3
goto :eof
:fail

View File

@ -1,25 +1,27 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
ble_ota_update.py - PC-side BLE OTA updater for CAIIC-MCM devices.
ble_ota_update.py - PC-side OTA updater for CAIIC-MCM devices, transport
protocol per docs/ble_protocol.md section 6.6 (0xCA frames, OTA_BEGIN/DATA/
END/ABORT -> framed OTA_RSP acks).
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).
Two channels:
BLE (default): dedicated OTA characteristic ...e0005 - one frame per
write-with-response, the framed OTA_RSP is read back from
the same characteristic (lockstep, no notify needed).
UART (--uart): e.g. --uart COM4 - the CLI command "ota" switches the
serial link to binary frame mode; same frames on the wire.
usage: ble_ota.bat [combo_pkg]
default pkg: out/mothercup_ble_ota.bin
Both channels select the payload by CUR_BANK (info item 0x07, read-only
characteristic ...e0004 / CLI "devinfo"): OTA always writes the INACTIVE
bank with the image linked for it, from the single-file package
(mothercup_ble_ota.bin, 52B header + both payloads).
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.
usage: ble_ota.bat [mothercup_ble_ota.bin]
ble_ota.bat --uart COM4 [mothercup_ble_ota.bin]
Success = the device resets after OTA_END and comes back on the new bank
with the new version (verified by a second CUR_BANK/FW_VERSION read).
"""
import asyncio
@ -28,23 +30,23 @@ 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
OTA_RW_UUID = "00002760-08c2-11e1-9073-0e8ac72e0005" # OTA: write frame, read RSP
INFO_RD_UUID = "00002760-08c2-11e1-9073-0e8ac72e0004" # Read-only info TLV
TYPE_OTA_BEGIN = 0x10
TYPE_OTA_DATA = 0x11
TYPE_OTA_END = 0x12
TYPE_OTA_ABORT = 0x13
TYPE_OTA_RSP = 0x1F
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)
RSP_TIMEOUT_S = 5.0
REBOOT_WAIT_S = 20.0
SECTOR = 0x1000
_seq = [0]
def crc32(data):
@ -60,9 +62,6 @@ def crc16_ccitt(data):
return crc
_seq = [0]
def encode_frame(ftype, payload):
out = bytearray([0xCA, ftype & 0xFF, _seq[0] & 0xFF,
len(payload) & 0xFF, (len(payload) >> 8) & 0xFF])
@ -73,6 +72,113 @@ def encode_frame(ftype, payload):
return bytes(out)
class FrameDecoder:
"""0xCA byte-stream reassembly (same rules as the firmware)."""
def __init__(self):
self.buf = bytearray()
def feed(self, data):
frames = []
self.buf += bytes(data)
while True:
while self.buf and self.buf[0] != 0xCA:
del self.buf[0]
if len(self.buf) < 5:
break
plen = self.buf[3] | (self.buf[4] << 8)
if plen > 480:
del self.buf[0]
continue
total = 5 + plen + 2
if len(self.buf) < total:
break
crc = crc16_ccitt(self.buf[1:5 + plen])
rx = self.buf[5 + plen] | (self.buf[5 + plen + 1] << 8)
if crc != rx:
del self.buf[0]
continue
frames.append((self.buf[1], self.buf[2], bytes(self.buf[5:5 + plen])))
del self.buf[:total]
return frames
def parse_rsp(payload):
"""OTA_RSP payload: {cmd_echo u8, status u8, offset u32 LE}."""
if len(payload) < 6:
raise RuntimeError("short OTA_RSP")
return payload[0], payload[1], int.from_bytes(payload[2:6], "little")
# ---------------------------------------------------------------------------
# Transports: xfer(frame_bytes) -> rsp frame bytes (lockstep request/ack)
# ---------------------------------------------------------------------------
class BleTransport:
"""BLE OTA characteristic ...e0005: write frame, then read the RSP.
The ATT write confirmation can complete before the device's BLE task has
finished processing the write indication (and a DATA frame may trigger a
flash sector erase, tens of ms), so the RSP read polls until non-empty."""
def __init__(self, client):
self.client = client
async def xfer(self, frame):
await self.client.write_gatt_char(OTA_RW_UUID, frame, response=True)
deadline = asyncio.get_event_loop().time() + RSP_TIMEOUT_S
while True:
rsp = bytes(await self.client.read_gatt_char(OTA_RW_UUID))
if rsp:
return rsp
if asyncio.get_event_loop().time() > deadline:
raise asyncio.TimeoutError("no OTA_RSP on BLE read")
await asyncio.sleep(0.02)
class UartTransport:
"""UART binary mode: frames on the wire, RSP frames come back on RX."""
def __init__(self, port, baud=115200):
import serial
self.ser = serial.Serial(port, baud, timeout=0.1)
self.dec = FrameDecoder()
async def enter_ota_mode(self):
"""Text CLI first: devinfo tells us the running bank ("cur bank:
APPn"); then "ota" switches the frontend to binary frame mode."""
self.ser.write(b"devinfo\r")
await asyncio.sleep(0.5)
text = self.ser.read(self.ser.in_waiting or 1).decode("ascii", "replace")
bank = 0
for line in text.splitlines():
if "cur bank:" in line:
bank = 2 if "APP2" in line else 1
self.ser.write(b"ota\r")
await asyncio.sleep(0.4)
self.ser.read(self.ser.in_waiting or 1) # drain echo/notice text
return bank
async def xfer(self, frame):
self.ser.write(frame)
deadline = asyncio.get_event_loop().time() + RSP_TIMEOUT_S
while True:
data = await asyncio.to_thread(self.ser.read, 256)
for ftype, seq, payload in self.dec.feed(data):
if ftype == TYPE_OTA_RSP:
# hand back a raw re-encoded frame (uniform with BLE)
body = bytes([ftype, seq, len(payload) & 0xFF,
(len(payload) >> 8) & 0xFF]) + payload
crc = crc16_ccitt(body)
return bytes([0xCA]) + body + bytes([crc & 0xFF, crc >> 8])
if asyncio.get_event_loop().time() > deadline:
raise asyncio.TimeoutError("no OTA_RSP on UART")
# ---------------------------------------------------------------------------
# Common helpers
# ---------------------------------------------------------------------------
def parse_combo(path):
"""Parse the combined OTA package; returns (version, {bank: payload}).
@ -102,9 +208,11 @@ def parse_combo(path):
return version, banks
async def read_info_tlv(client):
"""Read ...e0004 and decode the TLV into {item_id: raw_bytes}."""
data = bytes(await client.read_gatt_char(INFO_RD_UUID))
def fmt_ver(v):
return "V%d.%02d.%02d" % (v >> 16, (v >> 8) & 0xFF, v & 0xFF)
def decode_tlv(data):
items = {}
i = 0
while i + 2 <= len(data):
@ -116,11 +224,66 @@ async def read_info_tlv(client):
return items
def fmt_ver(v):
return "V%d.%02d.%02d" % (v >> 16, (v >> 8) & 0xFF, v & 0xFF)
async def ota_session(xfer, chunk, blob, version, target):
"""Lockstep OTA: BEGIN -> DATA* -> END, every frame acked by OTA_RSP."""
async def call(ftype, payload):
stage = {TYPE_OTA_BEGIN: "BEGIN", TYPE_OTA_DATA: "DATA",
TYPE_OTA_END: "END", TYPE_OTA_ABORT: "ABORT"}.get(ftype, "?")
# transport errors (TimeoutError/OSError) propagate unwrapped so the
# END handler can tell "ack lost due to reboot" apart from rejection
rsp = await xfer(encode_frame(ftype, payload))
frames = FrameDecoder().feed(rsp)
if len(frames) != 1 or frames[0][0] != TYPE_OTA_RSP:
raise RuntimeError("%s: bad RSP frame: %s" % (stage, rsp.hex(" ")))
echo, status, offset = parse_rsp(frames[0][2])
if echo != ftype:
raise RuntimeError("%s: RSP echo 0x%02X != 0x%02X" % (stage, echo, ftype))
return status, offset
status, _ = await call(TYPE_OTA_BEGIN,
struct.pack("<III", len(blob), crc32(blob), version))
if status != 0:
raise RuntimeError("OTA_BEGIN rejected, status=%d" % status)
sent = 0
t0 = asyncio.get_event_loop().time()
while sent < len(blob):
n = min(chunk, len(blob) - sent)
status, next_off = await call(TYPE_OTA_DATA,
struct.pack("<I", sent) + blob[sent:sent + n])
if status != 0:
raise RuntimeError("OTA_DATA rejected at %d, status=%d (device "
"expects offset %d)" % (sent, status, next_off))
sent += n
if sent % 0x4000 < chunk or sent == len(blob):
dt = asyncio.get_event_loop().time() - t0
print(" %d / %d bytes (%.0f%%, %.1f kB/s)"
% (sent, len(blob), 100.0 * sent / len(blob),
sent / 1024.0 / max(dt, 0.001)))
try:
status, _ = await call(TYPE_OTA_END, struct.pack("<I", crc32(blob)))
if status != 0:
raise RuntimeError("OTA_END rejected, status=%d "
"(4=crc_fail, 6=bank_mismatch)" % status)
except (asyncio.TimeoutError, OSError) as exc:
# the device may reset before the END write/read ack completes
# (observed on WinRT: "operation cancelled"); the reboot wait below
# is the real verdict
print("OTA_END ack lost (%s) - waiting for reboot anyway ..." % exc)
dt = asyncio.get_event_loop().time() - t0
print("OTA_END done (%.1fs total), device reboots into APP%d ..."
% (dt, target))
return True
# ---------------------------------------------------------------------------
# Channel frontends
# ---------------------------------------------------------------------------
async def find_device():
from bleak import BleakScanner
print("scanning for %s* ..." % NAME_PREFIX)
while True:
dev = await BleakScanner.find_device_by_filter(
@ -131,88 +294,58 @@ async def find_device():
print(" not found, retrying (device advertising?)")
async def ota_session(pkg_path):
async def run_ble(pkg_path):
from bleak import BleakClient
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:
async with BleakClient(dev, disconnected_callback=lambda _c: disconnected.set()) 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)
items = decode_tlv(bytes(await client.read_gatt_char(INFO_RD_UUID)))
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
target = 3 - cur_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
transport = BleTransport(client)
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("<III", len(blob), crc32(blob), version))
sent = 0
t0 = asyncio.get_event_loop().time()
while sent < len(blob):
first_of_sector = (sent % SECTOR) == 0
n = min(max_data, len(blob) - sent, SECTOR - (sent % SECTOR))
await send(TYPE_OTA_DATA, struct.pack("<I", sent) + blob[sent:sent + n])
sent += n
if sent % SECTOR == 0 or sent == len(blob):
print(" %d / %d bytes (%.0f%%)"
% (sent, len(blob), 100.0 * sent / len(blob)))
# the first frame touching a NEW sector triggers its lazy
# erase (IRQs off, tens of ms) - frames sent during the erase
# are lost, so wait right after that first frame
if first_of_sector and sent < len(blob):
await asyncio.sleep(sector_gap)
print("OTA_END (crc32=0x%08X), waiting for device reboot ..."
% crc32(blob))
await send(TYPE_OTA_END, struct.pack("<I", crc32(blob)))
# one 0xCA frame per ATT write; data = mtu-3 minus frame overhead(7+4)
chunk = min(client.mtu_size - 3 - 11, 222)
try:
await ota_session(transport.xfer, chunk, blob, version, target)
except Exception as exc:
print("FAIL: %s" % exc)
try:
await transport.xfer(encode_frame(TYPE_OTA_ABORT, b""))
except Exception:
pass
return 1
try:
await asyncio.wait_for(disconnected.wait(), REBOOT_WAIT_S)
break
except asyncio.TimeoutError:
if attempt == 2:
print("FAIL: device did not reboot - OTA rejected "
"(crc/bank mismatch?). Check UART with 'blelog on'.")
return 1
print(" no reboot; retrying the whole image "
"(frames were probably lost in an erase window)")
else:
print("FAIL: device did not reboot within %.0fs" % REBOOT_WAIT_S)
return 1
dt = asyncio.get_event_loop().time() - t0
print("device rebooted (transfer %.1fs). verifying new bank ..." % dt)
print("reconnecting to verify ...")
dev = await find_device()
async with BleakClient(dev) as client:
await asyncio.sleep(0.5)
items = await read_info_tlv(client)
items = decode_tlv(bytes(await client.read_gatt_char(INFO_RD_UUID)))
new_bank = items.get(0x07, b"\x00")[0]
new_ver = int.from_bytes(items.get(0x01, b"\x00" * 4), "little")
@ -224,14 +357,55 @@ async def ota_session(pkg_path):
return 1
async def run_uart(port, 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])))
transport = UartTransport(port)
cur_bank = await transport.enter_ota_mode()
if cur_bank in (1, 2):
print("device: running APP%d" % cur_bank)
target = 3 - cur_bank
else:
# no CUR_BANK readback; default to bank2 and let the device's
# bank_mismatch check reject a wrong guess (never bricks)
print("warning: could not read cur bank via CLI, guessing APP1")
target = 2
blob = banks[target]
print("target bank: APP%d (%d bytes, crc32=0x%08X)"
% (target, len(blob), crc32(blob)))
try:
await ota_session(transport.xfer, 222, blob, version, target)
except Exception as exc:
print("FAIL: %s" % exc)
try:
await transport.xfer(encode_frame(TYPE_OTA_ABORT, b""))
except Exception:
pass
return 1
print("device resets into the new bank; rerun with the serial CLI "
"'devinfo' to confirm (cur bank / version).")
return 0
def main():
args = [a for a in sys.argv[1:] if not a.startswith("--")]
uart = None
if "--uart" in sys.argv[1:]:
i = sys.argv[1:].index("--uart")
uart = sys.argv[1:][i + 1]
args = [a for j, a in enumerate(sys.argv[1:]) if j not in (i, i + 1)]
root = os.path.dirname(os.path.abspath(__file__))
pkg = sys.argv[1] if len(sys.argv) > 1 else os.path.join(
root, "out", "mothercup_ble_ota.bin")
pkg = args[0] if args 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 uart:
return asyncio.run(run_uart(uart, pkg))
return asyncio.run(run_ble(pkg))
if __name__ == "__main__":

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -1,7 +1,7 @@
{
"file": "caiic_ble_dual_ota.bin",
"target_bank": 1,
"size": 54204,
"crc32": "0xF0A05A7B",
"version": 65554
"size": 55264,
"crc32": "0x5A24EBF6",
"version": 65556
}

View File

@ -1,7 +1,7 @@
{
"file": "caiic_ble_dual_ota_app2.bin",
"target_bank": 2,
"size": 54504,
"crc32": "0xDA5C52E3",
"version": 65554
"size": 55564,
"crc32": "0x97ED5D57",
"version": 65556
}

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -1,7 +1,7 @@
{
"file": "mothercup_ble_prod_ota.bin",
"target_bank": 1,
"size": 54204,
"crc32": "0xF0A05A7B",
"version": 65554
"size": 55264,
"crc32": "0x5A24EBF6",
"version": 65556
}

View File

@ -1,7 +1,7 @@
{
"file": "mothercup_ble_prod_ota_app2.bin",
"target_bank": 2,
"size": 54504,
"crc32": "0xDA5C52E3",
"version": 65554
"size": 55564,
"crc32": "0x97ED5D57",
"version": 65556
}

20
tools/uart_cap.py Normal file
View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
"""Capture UART to a file for N seconds (OTA/BLE debug sessions)."""
import sys
import time
import serial
port = sys.argv[1] if len(sys.argv) > 1 else r"\\.\COM4"
out = sys.argv[2]
dur = float(sys.argv[3]) if len(sys.argv) > 3 else 120
ser = serial.Serial(port, 115200, timeout=0.2)
t0 = time.time()
with open(out, "wb") as f:
while time.time() - t0 < dur:
data = ser.read(4096)
if data:
f.write(data)
f.flush()
print("captured to", out)