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:
parent
266fcec731
commit
520838ce65
15
AGENTS.md
15
AGENTS.md
@ -5,14 +5,14 @@
|
|||||||
本仓库是 **mothercup(母乳杯)** 产品的完整代码仓库,包含三大部分:
|
本仓库是 **mothercup(母乳杯)** 产品的完整代码仓库,包含三大部分:
|
||||||
|
|
||||||
1. **设备固件** — 基于国民技术(Nations)**N32WB031 BLE SoC**(Cortex-M0,64MHz HSI,**板载硅片实测 256KB Flash**(0x01000000~0x0103FFFF,开发日志 §37),RAM 48KB+16KB):
|
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。
|
- **`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 升级页(当前停用,见下)。另有登录/注册/用户信息等云端业务页面。
|
2. **手机 App `SmartAssiter/`** — uni-app **Vue3 + TypeScript** 工程(HBuilderX 项目管理,无 package.json),通过 BLE 连接设备:查看运行参数(温度/电压/转速等)、CLI 终端、OTA 升级页(当前停用,见下)。另有登录/注册/用户信息等云端业务页面。
|
||||||
3. **PC 工具 `tools/`** — 整片烧录包制作/烧录脚本 + bleak 蓝牙测试客户端。
|
3. **PC 工具 `tools/`** — 整片烧录包制作/烧录脚本 + bleak 蓝牙测试客户端。
|
||||||
|
|
||||||
关键文档:
|
关键文档:
|
||||||
|
|
||||||
- `docs/开发日志.md` — 固件全程开发日志(41 节,含所有踩坑根因与设计决策,排查问题先查这里)
|
- `docs/开发日志.md` — 固件全程开发日志(43 节,含所有踩坑根因与设计决策,排查问题先查这里)
|
||||||
- `docs/ble_protocol.md` — CAIIC BLE 通信协议 V1.1(帧格式/GATT UUID/三类业务流程/维护命令/手机端开发指南)
|
- `docs/ble_protocol.md` — CAIIC BLE 通信协议 V1.1(帧格式/GATT UUID/三类业务流程/维护命令/手机端开发指南)
|
||||||
- `SmartAssiter/docs/修改记录_*.md` — App 侧每次改造的详细记录
|
- `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 增量构建
|
"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),用于识别板上实际运行的固件;工程名与出包文件名保持不变。
|
**固件版本号约定(重要)**:每次修改固件代码必须递增 `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`。
|
验证运行:串口助手接 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 服务),特征:
|
协议权威文档:`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) |
|
| `...c72ee002` | Notify | 帧协议上行应答(CLI_RSP / INFO_RSP / OTA_RSP) |
|
||||||
| `...c72ee003` | Read + Write(带响应) | **CLI 读写特征**:写原始命令行(≤63B)→ **分包读回**输出文本(每片 ≤att_mtu−2,0 长度包=结束,V1.00.13 起;此前单读 ≤512B),V1.00.02 新增 |
|
| `...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 新增 |
|
| `...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 是本仓库最重要的待办。
|
- **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)`;OTA 为双 bank 直写、扇区级 ack 流控、END 整镜像 zlib CRC32 校验后更新 bootsetting 并复位。详见 ble_protocol.md。
|
- 帧格式:`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/包,协议按字节流重组,两种情形都正确。
|
- 连接后设备主动发起 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 选包依据)。
|
- 信息项 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 通道,避免自激)。
|
- 调试手段: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_package.bat` / `flash_package.bat` — 一键出包 / NSpyocd 整片烧录(脚本会自动找 Python312 全路径,本机 `python` 可能是商店占位 stub)
|
||||||
- `make_dual_package.bat` — 双 APP 整包(Boot+bootsetting 双 bank+APP_DATA+APP1+APP2,APP2 升一版链接 0x01024000),用于 appsw 切换测试
|
- `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_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` — 温度监测小工具
|
- `ble_temp_watch.py` — 温度监测小工具
|
||||||
|
|
||||||
## 实时架构约定(固件)
|
## 实时架构约定(固件)
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
| 上行特征(设备→手机) | `00002760-08C2-11E1-9073-0E8AC72EE002` | Notify(需写 CCCD=0x0001 使能) |
|
| 上行特征(设备→手机) | `00002760-08C2-11E1-9073-0E8AC72EE002` | Notify(需写 CCCD=0x0001 使能) |
|
||||||
| CLI 读写特征 | `00002760-08C2-11E1-9073-0E8AC72EE003` | Read + Write(带响应):写原始命令行(≤63B)→ 分包读回输出文本(见 §6.5) |
|
| CLI 读写特征 | `00002760-08C2-11E1-9073-0E8AC72EE003` | Read + Write(带响应):写原始命令行(≤63B)→ 分包读回输出文本(见 §6.5) |
|
||||||
| 只读参数特征 | `00002760-08C2-11E1-9073-0E8AC72EE004` | Read:全部信息项 TLV(同 INFO_RSP(all),见 §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)**;手机端也可自行发起。
|
MTU:**设备在连接建立后主动发起 MTU 交换(请求 247)**;手机端也可自行发起。
|
||||||
一次 BLE 写入/通知的最大数据量 = att_mtu − 3(默认 MTU 23 时为 20B)。
|
一次 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} |
|
| 0x10 | 手机→设备 | OTA_BEGIN | {total_size u32, image_crc32 u32, version u32} |
|
||||||
| 0x11 | 手机→设备 | OTA_DATA | {offset u32, data…}(offset 必须严格连续) |
|
| 0x11 | 手机→设备 | OTA_DATA | {offset u32, data…}(offset 必须严格连续) |
|
||||||
| 0x12 | 手机→设备 | OTA_END | {crc32 u32}(与 OTA_BEGIN 中一致) |
|
| 0x12 | 手机→设备 | OTA_END | {crc32 u32}(与 OTA_BEGIN 中一致) |
|
||||||
|
| 0x13 | 手机→设备 | OTA_ABORT | 空 payload;中止会话(设备回 OTA_RSP(ok)) |
|
||||||
| 0x1F | 设备→手机 | OTA_RSP | {cmd_echo u8, status u8, offset u32} |
|
| 0x1F | 设备→手机 | OTA_RSP | {cmd_echo u8, status u8, offset u32} |
|
||||||
| 0x20 | 手机→设备 | INFO_QUERY | {item_id u8}×n;空 payload 或单个 0xFF = 查询全部 |
|
| 0x20 | 手机→设备 | INFO_QUERY | {item_id u8}×n;空 payload 或单个 0xFF = 查询全部 |
|
||||||
| 0x21 | 设备→手机 | INFO_RSP | TLV 序列 {id u8, len u8, value…}×n |
|
| 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}。
|
2. **OTA_DATA**:从 offset=0 起严格顺序发送,每帧 {offset, data}。
|
||||||
- 单帧 data 建议 ≤ 233B(MTU 247 时一次写入 244B = 帧开销 7 + offset 4 + data 233);
|
- 单帧 data 建议 ≤ 233B(MTU 247 时一次写入 244B = 帧开销 7 + offset 4 + data 233);
|
||||||
MTU 23 时单帧总长度不得超过 20B。
|
MTU 23 时单帧总长度不得超过 20B。
|
||||||
- 设备收到即写入 flash;每写满一个 4KB 扇区回一帧 OTA_RSP(0x11, ok, 已写 offset)
|
- 设备收到即写入 flash;**每一帧都回 OTA_RSP(0x11, ok, 新 offset)**——
|
||||||
——兼作流控与进度显示。
|
逐帧锁步流控(V1.00.19 起;此前为每 4KB 扇区一答)。ack 往返时间天然覆盖
|
||||||
- 手机端节奏:可按扇区等 ack 发送,也可连续发送;若收到 status=2 的应答,
|
新扇区的惰性擦除窗口(数十 ms 关中断)。
|
||||||
|
- 主机节奏:逐帧等 ack(锁步)。若应答 status=2(offset 乱序),
|
||||||
从应答中的 offset 处重发即可重新同步。
|
从应答中的 offset 处重发即可重新同步。
|
||||||
3. **OTA_END**:{crc32}。设备 flush 尾部(0xFF 补齐到 4 字节对齐后写最后一个扇区),
|
3. **OTA_END**:{crc32}。设备 flush 尾部(0xFF 补齐到 4 字节对齐后写最后一个扇区),
|
||||||
从 flash 回读整镜像复算 CRC32,与手机端比对:
|
从 flash 回读整镜像复算 CRC32,与手机端比对:
|
||||||
@ -137,6 +140,33 @@ flash 擦除单位 = 4KB 扇区。设备侧**直写 flash,不做扇区级 RAM
|
|||||||
落在目标 bank 范围内,不符则回 OTA_RSP(status=6 bank_mismatch) 并中止、不切换
|
落在目标 bank 范围内,不符则回 OTA_RSP(status=6 bank_mismatch) 并中止、不切换
|
||||||
(V1.00.17 起)。
|
(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)
|
## 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 命令形式提供,
|
bootsetting 与 APP_DATA 保留区(0x01006000/8KB)的读写以 CLI 命令形式提供,
|
||||||
|
|||||||
57
docs/开发日志.md
57
docs/开发日志.md
@ -1035,3 +1035,60 @@ CUR_BANK 选包、扇区擦除窗口避让的节流发送、双次重试)已
|
|||||||
hdr_len/total_size/payload_crc/各 bank crc,实包解析验证通过
|
hdr_len/total_size/payload_crc/各 bank crc,实包解析验证通过
|
||||||
(V1.00.18,bank1 54204B / bank2 54504B,total=108760B 与文件一致)。
|
(V1.00.18,bank1 54204B / bank2 54504B,total=108760B 与文件一致)。
|
||||||
- 旧命名 caiic_ble_full_* 产物已从仓库删除;双 APP 测试包仍叫 caiic_ble_dual。
|
- 旧命名 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
Binary file not shown.
Binary file not shown.
@ -24,12 +24,15 @@ extern "C" {
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
/* Frame types */
|
/* 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_REQ 0x01 /* phone -> device: one command line */
|
||||||
#define BLE_FRAME_CLI_RSP 0x02 /* device -> phone: response text chunk */
|
#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_CLI_RSP_END 0x03 /* device -> phone: {status u8} */
|
||||||
#define BLE_FRAME_OTA_BEGIN 0x10 /* phone -> device: {size, crc32, version} */
|
#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_DATA 0x11 /* phone -> device: {offset, data...} */
|
||||||
#define BLE_FRAME_OTA_END 0x12 /* phone -> device: {crc32} */
|
#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_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_QUERY 0x20 /* phone -> device: {item_id}*n (empty/0xFF = all) */
|
||||||
#define BLE_FRAME_INFO_RSP 0x21 /* device -> phone: TLV {id, len, value}*n */
|
#define BLE_FRAME_INFO_RSP 0x21 /* device -> phone: TLV {id, len, value}*n */
|
||||||
|
|||||||
@ -18,6 +18,9 @@ void AppCli_Init(void);
|
|||||||
/* Feed CLI input bytes from a non-UART source (BLE downlink, task context). */
|
/* Feed CLI input bytes from a non-UART source (BLE downlink, task context). */
|
||||||
void AppCli_InputBytes(const uint8_t* data, uint16_t len);
|
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
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -17,8 +17,9 @@ extern "C" {
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Handle one OTA frame (BLE_FRAME_OTA_BEGIN / OTA_DATA / OTA_END).
|
* @brief Handle one OTA frame (BLE_FRAME_OTA_BEGIN / OTA_DATA / OTA_END /
|
||||||
* Sends OTA_RSP frames through app_ble_proto_send_frame().
|
* 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);
|
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);
|
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
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -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_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_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_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
|
/// rdtss Service Attributes Indexes
|
||||||
@ -105,6 +106,9 @@ enum
|
|||||||
RDTSS_IDX_INFO_CHAR,
|
RDTSS_IDX_INFO_CHAR,
|
||||||
RDTSS_IDX_INFO_VAL,
|
RDTSS_IDX_INFO_VAL,
|
||||||
|
|
||||||
|
RDTSS_IDX_OTA_CHAR,
|
||||||
|
RDTSS_IDX_OTA_VAL,
|
||||||
|
|
||||||
RDTSS_IDX_NB,
|
RDTSS_IDX_NB,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -9,12 +9,12 @@
|
|||||||
#define __APP_VERSION_H__
|
#define __APP_VERSION_H__
|
||||||
|
|
||||||
#ifndef APP_FW_VERSION
|
#ifndef APP_FW_VERSION
|
||||||
#define APP_FW_VERSION "V1.00.18"
|
#define APP_FW_VERSION "V1.00.20"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/* Numeric form used by the BLE info query / OTA records: 0x00MMmmpp */
|
/* Numeric form used by the BLE info query / OTA records: 0x00MMmmpp */
|
||||||
#ifndef APP_FW_VERSION_NUM
|
#ifndef APP_FW_VERSION_NUM
|
||||||
#define APP_FW_VERSION_NUM 0x00010012u
|
#define APP_FW_VERSION_NUM 0x00010014u
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#endif /* __APP_VERSION_H__ */
|
#endif /* __APP_VERSION_H__ */
|
||||||
|
|||||||
@ -23,9 +23,7 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
/* Frame layout */
|
/* Frame layout (BLE_FRAME_SOF / BLE_FRAME_HDR_LEN live in app_ble_proto.h) */
|
||||||
#define BLE_FRAME_SOF 0xCA
|
|
||||||
#define BLE_FRAME_HDR_LEN 5 /* SOF+TYPE+SEQ+LEN(2) */
|
|
||||||
#define BLE_FRAME_CRC_LEN 2
|
#define BLE_FRAME_CRC_LEN 2
|
||||||
#define BLE_FRAME_OVERHEAD (BLE_FRAME_HDR_LEN + BLE_FRAME_CRC_LEN)
|
#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);
|
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] = (uint8_t)(crc & 0xFFu);
|
||||||
s_txFrame[5 + len + 1] = (uint8_t)(crc >> 8);
|
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)
|
if (s_rxHave == s_rxNeed)
|
||||||
{
|
{
|
||||||
uint16_t payLen = (uint16_t)s_rxBuf[3] | ((uint16_t)s_rxBuf[4] << 8);
|
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 crcRecv = (uint16_t)s_rxBuf[5 + payLen] |
|
||||||
((uint16_t)s_rxBuf[5 + payLen + 1] << 8);
|
((uint16_t)s_rxBuf[5 + payLen + 1] << 8);
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
#include "app_cli.h"
|
#include "app_cli.h"
|
||||||
#include "cli_core.h"
|
#include "cli_core.h"
|
||||||
#include "bsp_usart.h"
|
#include "bsp_usart.h"
|
||||||
|
#include "app_ota.h"
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
@ -24,6 +25,22 @@
|
|||||||
#define CLI_HISTORY_SIZE 8 /* number of remembered command lines */
|
#define CLI_HISTORY_SIZE 8 /* number of remembered command lines */
|
||||||
#define CLI_PROMPT "caiic->"
|
#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 */
|
/* Key codes */
|
||||||
#define CLI_KEY_TAB 0x09
|
#define CLI_KEY_TAB 0x09
|
||||||
#define CLI_KEY_ESC 0x1B
|
#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.
|
* @brief CLI task: receive bytes, edit the line, dispatch on terminator.
|
||||||
* TAB completes the command name, UP/DOWN arrows walk the history.
|
* 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)
|
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 swallow = 0; /* complementary terminator to swallow once */
|
||||||
uint8_t escState = 0; /* 0=normal, 1=got ESC, 2=got ESC '[' */
|
uint8_t escState = 0; /* 0=normal, 1=got ESC, 2=got ESC '[' */
|
||||||
int histNav = -1; /* -1 = editing, 0.. = history entry (0=newest) */
|
int histNav = -1; /* -1 = editing, 0.. = history entry (0=newest) */
|
||||||
|
uint32_t otaIdleMs = 0;
|
||||||
uint8_t ch;
|
uint8_t ch;
|
||||||
(void)argument;
|
(void)argument;
|
||||||
|
|
||||||
@ -222,6 +243,27 @@ static void CliTask(void* argument)
|
|||||||
|
|
||||||
for (;;)
|
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)
|
if (bsp_usart_read_byte(&ch, BSP_USART_WAIT_FOREVER) == 0)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* @file app_ota.c
|
* @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
|
* Flow: OTA_BEGIN (size/crc/version) -> OTA_DATA* (strictly sequential
|
||||||
* offsets, acked per completed 4KB flash sector) -> OTA_END (flush the
|
* offsets, EVERY frame acked with the new offset - lockstep flow control)
|
||||||
* sub-word tail, verify whole-image CRC32 against flash, update bootsetting,
|
* -> OTA_END (flush the sub-word tail, verify whole-image CRC32 against
|
||||||
* reset).
|
* flash, verify the vector table targets the right bank, update
|
||||||
|
* bootsetting, reset). OTA_ABORT aborts the session.
|
||||||
*
|
*
|
||||||
* Direct-write design (no 4KB staging buffer):
|
* Direct-write design (no 4KB staging buffer):
|
||||||
* - Sectors of the target bank are erased lazily: the first received byte
|
* - Sectors of the target bank are erased lazily: the first received byte
|
||||||
@ -22,6 +24,7 @@
|
|||||||
*/
|
*/
|
||||||
#include "app_ota.h"
|
#include "app_ota.h"
|
||||||
#include "app_ble_proto.h"
|
#include "app_ble_proto.h"
|
||||||
|
#include "bsp_usart.h"
|
||||||
#include "dfu_layout.h"
|
#include "dfu_layout.h"
|
||||||
#include "boot_crc32.h"
|
#include "boot_crc32.h"
|
||||||
#include "n32wb03x.h"
|
#include "n32wb03x.h"
|
||||||
@ -37,7 +40,14 @@
|
|||||||
extern uint32_t Image$$ER_IROM1$$Base;
|
extern uint32_t Image$$ER_IROM1$$Base;
|
||||||
|
|
||||||
#define OTA_MIN_IMAGE_SIZE 4u
|
#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 */
|
/* OTA session state */
|
||||||
static uint8_t s_active; /* session in progress */
|
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_offset; /* bytes received so far */
|
||||||
static uint32_t s_prog; /* bytes programmed to flash */
|
static uint32_t s_prog; /* bytes programmed to flash */
|
||||||
static uint32_t s_erased; /* bytes erased (sector granularity) */
|
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 uint8_t s_word_buf[4]; /* sub-word alignment staging */
|
||||||
static uint32_t s_word_fill;
|
static uint32_t s_word_fill;
|
||||||
static uint8_t s_qflash_ready;
|
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 */
|
/* Helpers */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
@ -63,6 +77,39 @@ static uint32_t ota_rd32(const uint8_t* p)
|
|||||||
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
|
((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)}.
|
* @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[3] = (uint8_t)((offset >> 8) & 0xFFu);
|
||||||
p[4] = (uint8_t)((offset >> 16) & 0xFFu);
|
p[4] = (uint8_t)((offset >> 16) & 0xFFu);
|
||||||
p[5] = (uint8_t)((offset >> 24) & 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));
|
(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_offset = 0;
|
||||||
s_prog = 0;
|
s_prog = 0;
|
||||||
s_erased = 0;
|
s_erased = 0;
|
||||||
s_ack_sector = 0;
|
|
||||||
s_word_fill = 0;
|
s_word_fill = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,18 +190,6 @@ static uint8_t ota_program(const uint8_t* data, uint32_t len)
|
|||||||
return 0;
|
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 */
|
/* 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_offset = 0;
|
||||||
s_prog = 0;
|
s_prog = 0;
|
||||||
s_erased = 0;
|
s_erased = 0;
|
||||||
s_ack_sector = 0;
|
|
||||||
s_word_fill = 0;
|
s_word_fill = 0;
|
||||||
|
|
||||||
ota_respond(seq, BLE_FRAME_OTA_BEGIN, BLE_OTA_ST_OK, 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.
|
* @brief OTA_DATA: {offset u32 + data}. Data must arrive strictly in order.
|
||||||
* Payload is programmed to flash immediately (sectors are erased
|
* Payload is programmed to flash immediately (sectors are erased
|
||||||
* lazily); an OTA_RSP(ok) is sent per completed 4KB sector (flow
|
* lazily); EVERY frame is acked with the new offset (lockstep flow
|
||||||
* control / progress). Errors are acked immediately and the state is
|
* 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.
|
* kept so the peer can resend.
|
||||||
*/
|
*/
|
||||||
static void ota_on_data(uint8_t seq, const uint8_t* p, uint16_t len)
|
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();
|
app_ota_abort();
|
||||||
return;
|
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,12 +456,34 @@ static void ota_on_end(uint8_t seq, const uint8_t* p, uint16_t len)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s_rsp_consumed = 0;
|
||||||
ota_respond(seq, BLE_FRAME_OTA_END, BLE_OTA_ST_OK, s_total_size);
|
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 */
|
/* Defer the reset until the host has fetched the END response (polled
|
||||||
vTaskDelay(pdMS_TO_TICKS(OTA_RESET_DELAY_MS));
|
* 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();
|
NVIC_SystemReset();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void app_ota_handle_frame(uint8_t type, uint8_t seq, const uint8_t* payload, uint16_t len)
|
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:
|
case BLE_FRAME_OTA_END:
|
||||||
ota_on_end(seq, payload, len);
|
ota_on_end(seq, payload, len);
|
||||||
break;
|
break;
|
||||||
|
case BLE_FRAME_OTA_ABORT:
|
||||||
|
app_ota_abort();
|
||||||
|
ota_respond(seq, BLE_FRAME_OTA_ABORT, BLE_OTA_ST_OK, s_offset);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -75,6 +75,7 @@
|
|||||||
#include "rdtss.h"
|
#include "rdtss.h"
|
||||||
#include "app_ble_proto.h"
|
#include "app_ble_proto.h"
|
||||||
#include "app_info.h"
|
#include "app_info.h"
|
||||||
|
#include "app_ota.h"
|
||||||
#include "ble_up.h"
|
#include "ble_up.h"
|
||||||
|
|
||||||
/* Private typedef -----------------------------------------------------------*/
|
/* 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
|
/* Device info Characteristic Value (read-only): TLV snapshot of all
|
||||||
* info items, identical content to an INFO_RSP(all) frame */
|
* 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 },
|
[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 -----------------------------------------------*/
|
/* 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
|
// CLI read/write characteristic: return the last command's output text
|
||||||
data = app_ble_proto_cli_rsp(&len);
|
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)
|
else if (req_value->att_idx == RDTSS_IDX_INFO_VAL)
|
||||||
{
|
{
|
||||||
// device info characteristic: TLV snapshot of all info items
|
// 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
|
// back through an ATT read of this same characteristic
|
||||||
app_ble_proto_cli_exec(ind_value->value, ind_value->length);
|
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;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
#include "app_ble_proto.h"
|
#include "app_ble_proto.h"
|
||||||
#include "app_params.h"
|
#include "app_params.h"
|
||||||
#include "app_bootset.h"
|
#include "app_bootset.h"
|
||||||
|
#include "app_cli.h"
|
||||||
#include "n32wb03x.h" /* SystemTrimValueGet (temp debug) */
|
#include "n32wb03x.h" /* SystemTrimValueGet (temp debug) */
|
||||||
|
|
||||||
#include <stdio.h>
|
#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");
|
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 */
|
/* Command table; add new commands here */
|
||||||
static const CliCmd_t s_cmds[] = {
|
static const CliCmd_t s_cmds[] = {
|
||||||
{"help", "help [cmd]: list commands / show one command's help", CmdHelp},
|
{"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},
|
{"bsdump", "bsdump: show bootsetting record + crc check", AppBootset_CmdBsdump},
|
||||||
{"bsset", "bsset <field> <val>: write bootsetting field (careful!)", AppBootset_CmdBsset},
|
{"bsset", "bsset <field> <val>: write bootsetting field (careful!)", AppBootset_CmdBsset},
|
||||||
{"appsw", "appsw [1|2]: switch boot bank (crc verified) + reboot", AppBootset_CmdAppswitch},
|
{"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},
|
{"reset", "reset: system reset (reboot)", CmdReset},
|
||||||
{"factory", "factory: restore params to defaults and reboot", CmdFactory},
|
{"factory", "factory: restore params to defaults and reboot", CmdFactory},
|
||||||
{"uartrst", "uartrst: re-init USART1 (115200 8N1), flush rx queue", CmdUartRst},
|
{"uartrst", "uartrst: re-init USART1 (115200 8N1), flush rx queue", CmdUartRst},
|
||||||
|
|||||||
@ -81,6 +81,7 @@
|
|||||||
#include "app_cli.h"
|
#include "app_cli.h"
|
||||||
#include "app_ble_proto.h"
|
#include "app_ble_proto.h"
|
||||||
#include "app_params.h"
|
#include "app_params.h"
|
||||||
|
#include "app_ota.h"
|
||||||
#include "app_version.h"
|
#include "app_version.h"
|
||||||
#include "app_user_config.h"
|
#include "app_user_config.h"
|
||||||
|
|
||||||
@ -124,6 +125,8 @@ static void ble_schedule_task(void *pvParameters)
|
|||||||
rwip_schedule();
|
rwip_schedule();
|
||||||
/*flush pending CLI uplink frames (notify, paced by cfm)*/
|
/*flush pending CLI uplink frames (notify, paced by cfm)*/
|
||||||
ble_up_poll();
|
ble_up_poll();
|
||||||
|
/*deferred post-OTA_END reset once the response reached the host*/
|
||||||
|
app_ota_reset_poll();
|
||||||
vTaskDelay(pdMS_TO_TICKS(1));
|
vTaskDelay(pdMS_TO_TICKS(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,9 @@
|
|||||||
rem ============================================================
|
rem ============================================================
|
||||||
rem ble_ota.bat - BLE OTA updater for CAIIC-MCM devices
|
rem ble_ota.bat - BLE OTA updater for CAIIC-MCM devices
|
||||||
rem
|
rem
|
||||||
rem ble_ota.bat [combo_pkg]
|
rem ble_ota.bat [mothercup_ble_ota.bin] BLE channel (...e0005)
|
||||||
rem default: tools\out\mothercup_ble_ota.bin
|
rem ble_ota.bat --uart COM4 [mothercup_ble_ota.bin] UART channel
|
||||||
|
rem default pkg: tools\out\mothercup_ble_ota.bin
|
||||||
rem
|
rem
|
||||||
rem The combined package holds both bank-linked payloads; the script
|
rem The combined package holds both bank-linked payloads; the script
|
||||||
rem reads the current bank (info item 0x07 CUR_BANK) and downloads
|
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%" -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
|
goto :eof
|
||||||
|
|
||||||
:fail
|
:fail
|
||||||
|
|||||||
@ -1,25 +1,27 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- 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
|
Two channels:
|
||||||
bank-linked payloads, see tools/merge_image.py), reads the device's current
|
BLE (default): dedicated OTA characteristic ...e0005 - one frame per
|
||||||
bank from the read-only info characteristic ...e0004 (info item 0x07
|
write-with-response, the framed OTA_RSP is read back from
|
||||||
CUR_BANK), and downloads the payload linked for the OPPOSITE bank over the
|
the same characteristic (lockstep, no notify needed).
|
||||||
framed protocol downlink (characteristic ...e0001).
|
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]
|
Both channels select the payload by CUR_BANK (info item 0x07, read-only
|
||||||
default pkg: out/mothercup_ble_ota.bin
|
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
|
usage: ble_ota.bat [mothercup_ble_ota.bin]
|
||||||
28), so OTA_RSP acks cannot be received. The script therefore paces writes:
|
ble_ota.bat --uart COM4 [mothercup_ble_ota.bin]
|
||||||
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
|
Success = the device resets after OTA_END and comes back on the new bank
|
||||||
window would be lost). Success is detected by the device resetting after
|
with the new version (verified by a second CUR_BANK/FW_VERSION read).
|
||||||
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 asyncio
|
||||||
@ -28,23 +30,23 @@ import struct
|
|||||||
import sys
|
import sys
|
||||||
import zlib
|
import zlib
|
||||||
|
|
||||||
from bleak import BleakClient, BleakScanner
|
|
||||||
|
|
||||||
NAME_PREFIX = "CAIIC-MCM-20260902"
|
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
|
INFO_RD_UUID = "00002760-08c2-11e1-9073-0e8ac72e0004" # Read-only info TLV
|
||||||
|
|
||||||
TYPE_OTA_BEGIN = 0x10
|
TYPE_OTA_BEGIN = 0x10
|
||||||
TYPE_OTA_DATA = 0x11
|
TYPE_OTA_DATA = 0x11
|
||||||
TYPE_OTA_END = 0x12
|
TYPE_OTA_END = 0x12
|
||||||
|
TYPE_OTA_ABORT = 0x13
|
||||||
|
TYPE_OTA_RSP = 0x1F
|
||||||
|
|
||||||
COMBO_MAGIC = 0xCA10BA11
|
COMBO_MAGIC = 0xCA10BA11
|
||||||
COMBO_HDR_LEN = 52
|
COMBO_HDR_LEN = 52
|
||||||
|
|
||||||
FRAME_GAP_S = 0.008 # between WWR frames
|
RSP_TIMEOUT_S = 5.0
|
||||||
SECTOR_GAP_S = 0.25 # at 4KB boundaries (lazy sector erase, IRQs off)
|
|
||||||
REBOOT_WAIT_S = 20.0
|
REBOOT_WAIT_S = 20.0
|
||||||
SECTOR = 0x1000
|
|
||||||
|
_seq = [0]
|
||||||
|
|
||||||
|
|
||||||
def crc32(data):
|
def crc32(data):
|
||||||
@ -60,9 +62,6 @@ def crc16_ccitt(data):
|
|||||||
return crc
|
return crc
|
||||||
|
|
||||||
|
|
||||||
_seq = [0]
|
|
||||||
|
|
||||||
|
|
||||||
def encode_frame(ftype, payload):
|
def encode_frame(ftype, payload):
|
||||||
out = bytearray([0xCA, ftype & 0xFF, _seq[0] & 0xFF,
|
out = bytearray([0xCA, ftype & 0xFF, _seq[0] & 0xFF,
|
||||||
len(payload) & 0xFF, (len(payload) >> 8) & 0xFF])
|
len(payload) & 0xFF, (len(payload) >> 8) & 0xFF])
|
||||||
@ -73,6 +72,113 @@ def encode_frame(ftype, payload):
|
|||||||
return bytes(out)
|
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):
|
def parse_combo(path):
|
||||||
"""Parse the combined OTA package; returns (version, {bank: payload}).
|
"""Parse the combined OTA package; returns (version, {bank: payload}).
|
||||||
|
|
||||||
@ -102,9 +208,11 @@ def parse_combo(path):
|
|||||||
return version, banks
|
return version, banks
|
||||||
|
|
||||||
|
|
||||||
async def read_info_tlv(client):
|
def fmt_ver(v):
|
||||||
"""Read ...e0004 and decode the TLV into {item_id: raw_bytes}."""
|
return "V%d.%02d.%02d" % (v >> 16, (v >> 8) & 0xFF, v & 0xFF)
|
||||||
data = bytes(await client.read_gatt_char(INFO_RD_UUID))
|
|
||||||
|
|
||||||
|
def decode_tlv(data):
|
||||||
items = {}
|
items = {}
|
||||||
i = 0
|
i = 0
|
||||||
while i + 2 <= len(data):
|
while i + 2 <= len(data):
|
||||||
@ -116,11 +224,66 @@ async def read_info_tlv(client):
|
|||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
def fmt_ver(v):
|
async def ota_session(xfer, chunk, blob, version, target):
|
||||||
return "V%d.%02d.%02d" % (v >> 16, (v >> 8) & 0xFF, v & 0xFF)
|
"""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():
|
async def find_device():
|
||||||
|
from bleak import BleakScanner
|
||||||
print("scanning for %s* ..." % NAME_PREFIX)
|
print("scanning for %s* ..." % NAME_PREFIX)
|
||||||
while True:
|
while True:
|
||||||
dev = await BleakScanner.find_device_by_filter(
|
dev = await BleakScanner.find_device_by_filter(
|
||||||
@ -131,88 +294,58 @@ async def find_device():
|
|||||||
print(" not found, retrying (device advertising?)")
|
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)
|
version, banks = parse_combo(pkg_path)
|
||||||
print("package: version %s, bank1 %dB / bank2 %dB"
|
print("package: version %s, bank1 %dB / bank2 %dB"
|
||||||
% (fmt_ver(version), len(banks[1]), len(banks[2])))
|
% (fmt_ver(version), len(banks[1]), len(banks[2])))
|
||||||
|
|
||||||
dev = await find_device()
|
dev = await find_device()
|
||||||
print("connecting %s (%s) ..." % (dev.name, dev.address))
|
print("connecting %s (%s) ..." % (dev.name, dev.address))
|
||||||
|
|
||||||
disconnected = asyncio.Event()
|
disconnected = asyncio.Event()
|
||||||
|
|
||||||
def on_disconnect(_c):
|
async with BleakClient(dev, disconnected_callback=lambda _c: disconnected.set()) as client:
|
||||||
disconnected.set()
|
|
||||||
|
|
||||||
async with BleakClient(dev, disconnected_callback=on_disconnect) as client:
|
|
||||||
print("connected, mtu=%d" % client.mtu_size)
|
print("connected, mtu=%d" % client.mtu_size)
|
||||||
await asyncio.sleep(0.5) # let the device's MTU exchange finish
|
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_bank = items.get(0x07, b"\x01")[0]
|
||||||
cur_ver = int.from_bytes(items.get(0x01, b"\x00" * 4), "little")
|
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)))
|
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]
|
blob = banks[target]
|
||||||
print("target bank: APP%d (%d bytes, crc32=0x%08X)"
|
print("target bank: APP%d (%d bytes, crc32=0x%08X)"
|
||||||
% (target, len(blob), crc32(blob)))
|
% (target, len(blob), crc32(blob)))
|
||||||
if cur_ver == version:
|
if cur_ver == version:
|
||||||
print("note: same version already on the device, updating anyway")
|
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()
|
disconnected.clear()
|
||||||
|
# one 0xCA frame per ATT write; data = mtu-3 minus frame overhead(7+4)
|
||||||
async def send(ftype, payload):
|
chunk = min(client.mtu_size - 3 - 11, 222)
|
||||||
await client.write_gatt_char(WRITE_UUID, encode_frame(ftype, payload),
|
try:
|
||||||
response=False)
|
await ota_session(transport.xfer, chunk, blob, version, target)
|
||||||
await asyncio.sleep(FRAME_GAP_S)
|
except Exception as exc:
|
||||||
|
print("FAIL: %s" % exc)
|
||||||
for attempt in (1, 2):
|
try:
|
||||||
sector_gap = SECTOR_GAP_S * attempt
|
await transport.xfer(encode_frame(TYPE_OTA_ABORT, b""))
|
||||||
print("OTA_BEGIN ... (attempt %d)" % attempt)
|
except Exception:
|
||||||
await send(TYPE_OTA_BEGIN, struct.pack("<III", len(blob), crc32(blob), version))
|
pass
|
||||||
|
return 1
|
||||||
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)))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await asyncio.wait_for(disconnected.wait(), REBOOT_WAIT_S)
|
await asyncio.wait_for(disconnected.wait(), REBOOT_WAIT_S)
|
||||||
break
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
if attempt == 2:
|
print("FAIL: device did not reboot within %.0fs" % REBOOT_WAIT_S)
|
||||||
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:
|
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
dt = asyncio.get_event_loop().time() - t0
|
print("reconnecting to verify ...")
|
||||||
print("device rebooted (transfer %.1fs). verifying new bank ..." % dt)
|
|
||||||
|
|
||||||
dev = await find_device()
|
dev = await find_device()
|
||||||
async with BleakClient(dev) as client:
|
async with BleakClient(dev) as client:
|
||||||
await asyncio.sleep(0.5)
|
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_bank = items.get(0x07, b"\x00")[0]
|
||||||
new_ver = int.from_bytes(items.get(0x01, b"\x00" * 4), "little")
|
new_ver = int.from_bytes(items.get(0x01, b"\x00" * 4), "little")
|
||||||
|
|
||||||
@ -224,14 +357,55 @@ async def ota_session(pkg_path):
|
|||||||
return 1
|
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():
|
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__))
|
root = os.path.dirname(os.path.abspath(__file__))
|
||||||
pkg = sys.argv[1] if len(sys.argv) > 1 else os.path.join(
|
pkg = args[0] if args else os.path.join(root, "out", "mothercup_ble_ota.bin")
|
||||||
root, "out", "mothercup_ble_ota.bin")
|
|
||||||
if not os.path.isfile(pkg):
|
if not os.path.isfile(pkg):
|
||||||
print("package not found: %s (run tools\\make_package.bat first)" % pkg)
|
print("package not found: %s (run tools\\make_package.bat first)" % pkg)
|
||||||
return 1
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"file": "caiic_ble_dual_ota.bin",
|
"file": "caiic_ble_dual_ota.bin",
|
||||||
"target_bank": 1,
|
"target_bank": 1,
|
||||||
"size": 54204,
|
"size": 55264,
|
||||||
"crc32": "0xF0A05A7B",
|
"crc32": "0x5A24EBF6",
|
||||||
"version": 65554
|
"version": 65556
|
||||||
}
|
}
|
||||||
Binary file not shown.
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"file": "caiic_ble_dual_ota_app2.bin",
|
"file": "caiic_ble_dual_ota_app2.bin",
|
||||||
"target_bank": 2,
|
"target_bank": 2,
|
||||||
"size": 54504,
|
"size": 55564,
|
||||||
"crc32": "0xDA5C52E3",
|
"crc32": "0x97ED5D57",
|
||||||
"version": 65554
|
"version": 65556
|
||||||
}
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"file": "mothercup_ble_prod_ota.bin",
|
"file": "mothercup_ble_prod_ota.bin",
|
||||||
"target_bank": 1,
|
"target_bank": 1,
|
||||||
"size": 54204,
|
"size": 55264,
|
||||||
"crc32": "0xF0A05A7B",
|
"crc32": "0x5A24EBF6",
|
||||||
"version": 65554
|
"version": 65556
|
||||||
}
|
}
|
||||||
Binary file not shown.
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"file": "mothercup_ble_prod_ota_app2.bin",
|
"file": "mothercup_ble_prod_ota_app2.bin",
|
||||||
"target_bank": 2,
|
"target_bank": 2,
|
||||||
"size": 54504,
|
"size": 55564,
|
||||||
"crc32": "0xDA5C52E3",
|
"crc32": "0x97ED5D57",
|
||||||
"version": 65554
|
"version": 65556
|
||||||
}
|
}
|
||||||
20
tools/uart_cap.py
Normal file
20
tools/uart_cap.py
Normal 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)
|
||||||
Loading…
x
Reference in New Issue
Block a user