V1.00.21: UART OTA channel verified + mode-switch handshake + loss-tolerant lockstep
- UART binary mode switch mechanism: enter handshake marker '[ota] binary mode ON', explicit exit via OTA_ABORT (immediate), 10s idle fallback (was 3s, must exceed the host retry window); host resync by CR probe - fix: otaIdleMs not re-armed on mode entry - every second 'ota' entry was kicked out by an instant idle timeout (leftover count from prior session) - lossy-link tolerance: device drops partial frames after a 100ms byte gap so host resends re-align; OTA session is now single-owner (second BEGIN on another channel gets BAD_STATE) - ble_ota_update.py: wait for the enter marker instead of blind sleeps, per-frame resend (2s x3), BAD_STATE+expected-offset treated as implicit ack for duplicate DATA/BEGIN (resync after ack loss), stage-labeled timeouts with raw-rx dump - verified on hardware: UART OTA full pass (55.7KB/41s, 4-5 lost frames auto-recovered) + BLE OTA regression pass; docs: dev log 44, ble_protocol.md 6.6 rework, AGENTS.md sync
This commit is contained in:
parent
520838ce65
commit
efc7bd129f
@ -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.20**(`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.21**(`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` — 固件全程开发日志(43 节,含所有踩坑根因与设计决策,排查问题先查这里)
|
||||
- `docs/开发日志.md` — 固件全程开发日志(44 节,含所有踩坑根因与设计决策,排查问题先查这里)
|
||||
- `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.20):`Code=48840 RO-data=4868 RW-data=2076 ZI-data=29116`。
|
||||
要求保持 **0 Error(s), 0 Warning(s)**。当前基线(V1.00.21):`Code=48872 RO-data=4980 RW-data=2076 ZI-data=29116`。
|
||||
|
||||
**固件版本号约定(重要)**:每次修改固件代码必须递增 `mcm-ddc-ble/inc/app_version.h` 中的 `APP_FW_VERSION` 与 `APP_FW_VERSION_NUM`(格式 `0x00MMmmpp`,通常补丁位 +1),用于识别板上实际运行的固件;工程名与出包文件名保持不变。
|
||||
|
||||
@ -152,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 载荷、经 OTA 特征 `...e0005` 逐帧锁步、复位检测+重连校验;`--uart COMx` 走串口二进制模式)。**BLE 通道已实测通过(§42);UART 通道待验证(串口线接触问题,待恢复后测)**
|
||||
- `ble_ota_update.py` + `ble_ota.bat` — BLE OTA 升级器(解析单文件升级包 `mothercup_ble_ota.bin`、按 CUR_BANK 选对侧 bank 载荷、经 OTA 特征 `...e0005` 逐帧锁步、复位检测+重连校验;`--uart COMx` 走串口二进制模式:握手标记进入/ABORT 退出/丢帧锁步重传再同步)。**BLE 与 UART 双通道均已实测通过(§42/§44)**
|
||||
- `ble_temp_watch.py` — 温度监测小工具
|
||||
|
||||
## 实时架构约定(固件)
|
||||
|
||||
@ -149,7 +149,21 @@ OTA 会话(§6 的 BEGIN/DATA/END/ABORT → OTA_RSP)跑在 0xCA 帧上,帧
|
||||
| 通道 | 下行(主机→设备) | 上行(设备→主机) |
|
||||
|---|---|---|
|
||||
| **BLE OTA 特征** `...e0005` | 每次"写带响应"携带一帧(≤ att_mtu−3) | 写完**读同一特征**取回 OTA_RSP 帧;读应答可能先于设备处理完成返回空,**读需轮询至非空**(擦扇区时数十 ms) |
|
||||
| **UART 串口**(115200 8N1) | CLI 先发文本命令 `ota` 进入二进制帧模式,随后原始帧字节流 | OTA_RSP 帧直接从 TX 发出;3s 无帧自动退出回 CLI |
|
||||
| **UART 串口**(115200 8N1) | CLI 先发文本命令 `ota`,**等到设备回标记行 `[ota] binary mode ON` 再发帧**(握手,防帧被当文本命令吃掉),随后原始帧字节流 | OTA_RSP 帧直接从 TX 发出;**OTA_ABORT 立即退出**回 CLI,10s 无帧兜底退出 |
|
||||
|
||||
UART 模式切换与容错(V1.00.21 起):
|
||||
|
||||
- **进入握手**:`ota` 命令的标记行是 go-ahead;标记之前设备仍在文本 CLI,
|
||||
提前发二进制帧会被行编辑器吃掉(帧内 0x0D 甚至会触发垃圾命令执行)。
|
||||
- **退出三路**:END 成功 → 复位;OTA_ABORT → 回完 ack 立即回文本 CLI;
|
||||
10s 空闲兜底(host 失联自救)。host 再同步:发 `\r`,CLI 态立即回
|
||||
`caiic->`;二进制态无响应,等兜底超时退出。
|
||||
- **丢帧恢复(锁步重传)**:任一帧的 ack 超时(PC 参考实现 2s)就直接
|
||||
重发同一帧,最多 3 次。重复 DATA 会被设备回 `BAD_STATE + 期望 offset`
|
||||
——若该 offset 恰好等于本帧发完后的下一 offset,说明设备早已收到、只是
|
||||
ack 丢了,**视为 ack 继续**;重复 BEGIN 同理视为会话已建立。设备侧对
|
||||
100ms 字节间断的半帧自动丢弃重组器状态,保证重发帧能重新对齐;
|
||||
会话全程只允许一个通道占用(他通道 BEGIN 回 BAD_STATE)。
|
||||
| BLE 旧帧协议通道(e0001/e0002 notify) | 0xCA 帧写入 e0001 | notify 上行(V1.00.19 修复 CRC 后可用,见开发日志 §42) |
|
||||
|
||||
锁步规则:每发一帧必须等到对应 OTA_RSP 再发下一帧(ack 里 offset =
|
||||
|
||||
40
docs/开发日志.md
40
docs/开发日志.md
@ -1092,3 +1092,43 @@ e0005 读回缓冲,**主机还没发起 ATT 读**,200ms 后设备已复位
|
||||
实测:`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。
|
||||
|
||||
## 44. UART OTA 通道调通:模式切换机制 + 锁步重传容错(2026-09-04,V1.00.21)
|
||||
|
||||
UART OTA 首轮实测失败("no OTA_RSP on UART"),排查过程与最终设计如下。
|
||||
|
||||
**模式切换机制(来回切换的完整答案)**:串口是 CLI 文本通道,切二进制
|
||||
需要明确的状态机,否则主机与设备模式错位、二进制帧被文本行编辑器吃掉
|
||||
(帧内 0x0D 会触发执行垃圾命令)。定案(ble_protocol.md §6.6):
|
||||
|
||||
- 进入握手:`ota` 命令回标记行 `[ota] binary mode ON`,主机必须等到标记
|
||||
再发帧(此前 PC 工具靠固定 sleep 猜,时序上不可靠);
|
||||
- 退出三路:END 成功→复位(V1.00.20 延迟复位保证 ack 送达);OTA_ABORT→
|
||||
回完 ack 立即回 CLI(新增,`app_ota_chan_rx_uart()` 返回值驱动前端退出);
|
||||
10s 空闲兜底(原 3s 太短,必须大于主机重传窗口);
|
||||
- 再同步:主机随时发 `\r`,CLI 态立即回提示符;二进制态无响应,等兜底退出;
|
||||
- 会话占用保护:OTA 会话全局唯一,他通道 BEGIN 回 BAD_STATE(此前会踩状态)。
|
||||
|
||||
**实测抓到的两个真 bug**:
|
||||
|
||||
1. `otaIdleMs` 进入二进制模式时未清零——第一次 `ota` 正常,**第二次进入
|
||||
必在 100ms 内被"空闲超时"踢出**(上一段会话残留的计数值 ≥ 阈值)。
|
||||
表现为帧字节走文本路径被逐字符回显(只有可打印字符回显,这是定位
|
||||
突破口)。修复:每次进入 while(s_otaMode) 前清零。
|
||||
2. 丢整帧:约 1~3% 的 233B 数据帧在主机 write() 成功后设备侧一个字节都
|
||||
收不到(设备 3s 空闲退出、CLI 仍活为证),随机发生——**物理链路接触
|
||||
不良**(用户已预警过串口线问题)。协议必须容忍:PC 工具实现锁步重传
|
||||
(ack 2s 超时直接重发同帧,≤3 次);利用协议已有的 offset 报告语义做
|
||||
再同步——重复 DATA 回 BAD_STATE+期望 offset,若等于本帧结束位置说明
|
||||
设备早收到只是 ack 丢了,视为 ack 继续;重复 BEGIN 视为会话已建立。
|
||||
设备侧配套:100ms 字节间断自动丢弃重组器半帧状态,保证重发帧重新对齐。
|
||||
|
||||
**调试教训**:排查时在逐字节路径加 `printf("[rx %02X]")` 观测,结果打印
|
||||
本身(轮询 TX ~500µs/字节)把 RX 消费拖慢 6 倍,64 深队列在 ~80 字节处
|
||||
溢出丢尾——**观测手段本身制造了新的丢帧**,误以为 CRC/协议问题。高速
|
||||
链路排查禁用逐字节打印。
|
||||
|
||||
实测(release 固件):UART OTA 全程 PASS(55.7KB/41s,途中 4~5 次丢帧
|
||||
全部自动重传恢复,END ack 正常,复位进 APP2 复核通过);BLE OTA 回归
|
||||
PASS(APP1,30s)。基线:Code=48872 RO-data=4980 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.
@ -49,8 +49,10 @@ void app_ota_chan_rx(const uint8_t* data, uint16_t len, app_ota_rsp_sink_fn sink
|
||||
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);
|
||||
/** UART binary mode (CLI "ota"): one received byte in; RSP frames on TX.
|
||||
* @return non-zero when the host sent OTA_ABORT - the frontend should
|
||||
* leave binary mode and return to the text CLI immediately. */
|
||||
uint8_t app_ota_chan_rx_uart(uint8_t ch);
|
||||
|
||||
/** Drop the reassembler state (channel idle timeout / link loss). */
|
||||
void app_ota_chan_idle(void);
|
||||
|
||||
@ -9,12 +9,12 @@
|
||||
#define __APP_VERSION_H__
|
||||
|
||||
#ifndef APP_FW_VERSION
|
||||
#define APP_FW_VERSION "V1.00.20"
|
||||
#define APP_FW_VERSION "V1.00.21"
|
||||
#endif
|
||||
|
||||
/* Numeric form used by the BLE info query / OTA records: 0x00MMmmpp */
|
||||
#ifndef APP_FW_VERSION_NUM
|
||||
#define APP_FW_VERSION_NUM 0x00010014u
|
||||
#define APP_FW_VERSION_NUM 0x00010015u
|
||||
#endif
|
||||
|
||||
#endif /* __APP_VERSION_H__ */
|
||||
|
||||
@ -26,8 +26,10 @@
|
||||
#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
|
||||
* Auto-exit after this much inactivity (ms). Must exceed the host's worst-
|
||||
* case retry window (ble_ota_update.py: 3 tries x 2 s) so a lost frame on a
|
||||
* flaky link does not drop the session before the host resends. */
|
||||
#define OTA_MODE_IDLE_MS 10000u
|
||||
#define OTA_MODE_POLL_MS 100u
|
||||
|
||||
static uint8_t s_otaMode;
|
||||
@ -224,7 +226,7 @@ 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
|
||||
* raw - no echo/editing; the mode exits on 10s of inactivity (or on
|
||||
* device reset after a successful OTA_END).
|
||||
*/
|
||||
static void CliTask(void* argument)
|
||||
@ -243,16 +245,29 @@ static void CliTask(void* argument)
|
||||
|
||||
for (;;)
|
||||
{
|
||||
/* OTA binary frame mode: raw bytes to the OTA channel */
|
||||
/* OTA binary frame mode: raw bytes to the OTA channel.
|
||||
* otaIdleMs is (re)armed on every entry - a leftover count from a
|
||||
* previous session must not trip an instant idle timeout. */
|
||||
otaIdleMs = 0;
|
||||
while (s_otaMode)
|
||||
{
|
||||
if (bsp_usart_read_byte(&ch, OTA_MODE_POLL_MS) != 0)
|
||||
{
|
||||
app_ota_chan_rx_uart(ch);
|
||||
if (app_ota_chan_rx_uart(ch) != 0)
|
||||
{
|
||||
/* OTA_ABORT: explicit exit back to the text CLI */
|
||||
s_otaMode = 0;
|
||||
bsp_usart_tx_lock();
|
||||
cli_uart_write("\r\n[ota] aborted, back to CLI\r\n" CLI_PROMPT);
|
||||
bsp_usart_tx_unlock();
|
||||
}
|
||||
otaIdleMs = 0;
|
||||
continue;
|
||||
}
|
||||
otaIdleMs += OTA_MODE_POLL_MS;
|
||||
/* A byte gap this long mid-frame means the rest was lost on the
|
||||
* wire: drop the partial frame so a host resend re-syncs */
|
||||
app_ota_chan_idle();
|
||||
if (otaIdleMs >= OTA_MODE_IDLE_MS)
|
||||
{
|
||||
s_otaMode = 0;
|
||||
|
||||
@ -212,6 +212,14 @@ static void ota_on_begin(uint8_t seq, const uint8_t* p, uint16_t len)
|
||||
crc = ota_rd32(p + 4);
|
||||
version = ota_rd32(p + 8);
|
||||
|
||||
/* One session at a time across all channels: a second BEGIN (e.g. BLE
|
||||
* while a UART session runs) is refused instead of corrupting state */
|
||||
if (s_active)
|
||||
{
|
||||
ota_respond(seq, BLE_FRAME_OTA_BEGIN, BLE_OTA_ST_BAD_STATE, s_offset);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Pick the opposite bank as the update target */
|
||||
self = app_ota_current_bank_base();
|
||||
if (self == CAIIC_APP1_BASE)
|
||||
@ -485,6 +493,10 @@ void app_ota_reset_poll(void)
|
||||
}
|
||||
}
|
||||
|
||||
static void ota_uart_sink(const uint8_t* frame, uint16_t len);
|
||||
|
||||
static uint8_t s_uart_exit_req; /* OTA_ABORT on UART: drop back to CLI now */
|
||||
|
||||
void app_ota_handle_frame(uint8_t type, uint8_t seq, const uint8_t* payload, uint16_t len)
|
||||
{
|
||||
switch (type)
|
||||
@ -501,6 +513,13 @@ void app_ota_handle_frame(uint8_t type, uint8_t seq, const uint8_t* payload, uin
|
||||
case BLE_FRAME_OTA_ABORT:
|
||||
app_ota_abort();
|
||||
ota_respond(seq, BLE_FRAME_OTA_ABORT, BLE_OTA_ST_OK, s_offset);
|
||||
/* Explicit exit from UART binary mode (ack already on the wire):
|
||||
* the CLI frontend returns to text mode immediately instead of
|
||||
* waiting out the 3 s idle timeout */
|
||||
if (s_rsp_sink == ota_uart_sink)
|
||||
{
|
||||
s_uart_exit_req = 1;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@ -594,6 +613,9 @@ void app_ota_chan_rx(const uint8_t* data, uint16_t len, app_ota_rsp_sink_fn sink
|
||||
app_ota_handle_frame(s_chanBuf[1], s_chanBuf[2],
|
||||
&s_chanBuf[5], payLen);
|
||||
}
|
||||
/* CRC mismatch: silently drop; the host's lockstep
|
||||
* resend recovers (its resync relies on our offset
|
||||
* report in the next ack) */
|
||||
s_chanState = CHAN_RX_WAIT_SOF;
|
||||
s_chanHave = 0;
|
||||
}
|
||||
@ -650,9 +672,14 @@ static void ota_uart_sink(const uint8_t* frame, uint16_t len)
|
||||
s_rsp_consumed = 1;
|
||||
}
|
||||
|
||||
void app_ota_chan_rx_uart(uint8_t ch)
|
||||
uint8_t app_ota_chan_rx_uart(uint8_t ch)
|
||||
{
|
||||
uint8_t exit_req;
|
||||
|
||||
app_ota_chan_rx(&ch, 1, ota_uart_sink);
|
||||
exit_req = s_uart_exit_req;
|
||||
s_uart_exit_req = 0;
|
||||
return exit_req;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -307,17 +307,20 @@ static void CmdUartInfo(int argc, char* argv[])
|
||||
|
||||
/**
|
||||
* @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.
|
||||
* (0xCA OTA frames on the raw stream, framed RSP on TX).
|
||||
* Handshake: the "[ota] binary mode ON" marker line is the go-ahead -
|
||||
* the host must wait for it before sending frames. Exits: OTA_ABORT
|
||||
* frame (immediate), 3 s idle timeout (fallback), or device reset
|
||||
* after a successful OTA_END. 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");
|
||||
cli_write("[ota] binary mode ON - send 0xCA OTA frames now\r\n"
|
||||
"(ble_protocol.md section 6.6; OTA_ABORT or 3s idle exits)\r\n");
|
||||
app_cli_ota_mode_enter();
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,8 @@ Two channels:
|
||||
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.
|
||||
serial link to binary frame mode (marker-confirmed
|
||||
handshake; OTA_ABORT exits); same frames on the wire.
|
||||
|
||||
Both channels select the payload by CUR_BANK (info item 0x07, read-only
|
||||
characteristic ...e0004 / CLI "devinfo"): OTA always writes the INACTIVE
|
||||
@ -28,6 +29,7 @@ import asyncio
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
import zlib
|
||||
|
||||
NAME_PREFIX = "CAIIC-MCM-20260902"
|
||||
@ -43,9 +45,13 @@ TYPE_OTA_RSP = 0x1F
|
||||
COMBO_MAGIC = 0xCA10BA11
|
||||
COMBO_HDR_LEN = 52
|
||||
|
||||
RSP_TIMEOUT_S = 5.0
|
||||
RSP_TIMEOUT_S = 2.0 # one lockstep round trip (flash erase adds ~50ms)
|
||||
XFER_TRIES = 3 # resend a frame whose ack was lost (flaky links)
|
||||
REBOOT_WAIT_S = 20.0
|
||||
|
||||
ST_OK = 0
|
||||
ST_BAD_STATE = 2
|
||||
|
||||
_seq = [0]
|
||||
|
||||
|
||||
@ -137,33 +143,84 @@ class BleTransport:
|
||||
|
||||
|
||||
class UartTransport:
|
||||
"""UART binary mode: frames on the wire, RSP frames come back on RX."""
|
||||
"""UART binary mode: frames on the wire, RSP frames come back on RX.
|
||||
|
||||
Mode switching handshake (firmware V1.00.21+):
|
||||
text CLI --"ota"--> binary frame mode; the device prints the marker
|
||||
"[ota] binary mode ON" as the go-ahead. Exits: OTA_ABORT frame
|
||||
(immediate), 3 s idle timeout (fallback), reset after OTA_END.
|
||||
Re-sync: a bare "\\r" gets an immediate "caiic->" prompt in CLI mode;
|
||||
in binary mode it is silently dropped and the 3 s idle timeout brings
|
||||
the CLI back on its own."""
|
||||
|
||||
MARKER_ON = b"[ota] binary mode ON"
|
||||
PROMPT = b"caiic->"
|
||||
|
||||
def __init__(self, port, baud=115200):
|
||||
import serial
|
||||
self.ser = serial.Serial(port, baud, timeout=0.1)
|
||||
self.dec = FrameDecoder()
|
||||
|
||||
def _read_until(self, markers, timeout_s):
|
||||
"""Read text until any marker appears or the timeout expires.
|
||||
Runs in a worker thread - no asyncio loop time here."""
|
||||
buf = b""
|
||||
deadline = time.monotonic() + timeout_s
|
||||
while time.monotonic() < deadline:
|
||||
buf += self.ser.read(self.ser.in_waiting or 1)
|
||||
if any(m in buf for m in markers):
|
||||
break
|
||||
return buf
|
||||
|
||||
async def _resync_cli(self):
|
||||
"""Make sure the device sits at the text CLI (probe with CR; if a
|
||||
previous run left it in binary mode, wait out the 3 s idle exit)."""
|
||||
self.ser.reset_input_buffer()
|
||||
self.ser.write(b"\r")
|
||||
text = await asyncio.to_thread(self._read_until, [self.PROMPT], 0.8)
|
||||
if self.PROMPT in text:
|
||||
return True
|
||||
# maybe stuck in binary mode: idle timeout exits within ~3.5 s
|
||||
text = await asyncio.to_thread(self._read_until, [self.PROMPT], 3.5)
|
||||
return self.PROMPT in text
|
||||
|
||||
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."""
|
||||
APPn"); then "ota" switches the frontend to binary frame mode,
|
||||
confirmed by the marker line (no blind sleeps)."""
|
||||
if not await self._resync_cli():
|
||||
print("warning: no CLI prompt on UART (check wiring/baud)")
|
||||
self.ser.write(b"devinfo\r")
|
||||
await asyncio.sleep(0.5)
|
||||
text = self.ser.read(self.ser.in_waiting or 1).decode("ascii", "replace")
|
||||
text = await asyncio.to_thread(self._read_until, [b"cur bank:", self.PROMPT], 1.5)
|
||||
bank = 0
|
||||
for line in text.splitlines():
|
||||
for line in text.decode("ascii", "replace").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
|
||||
text = await asyncio.to_thread(self._read_until, [self.MARKER_ON], 2.0)
|
||||
if self.MARKER_ON not in text:
|
||||
raise RuntimeError("ota mode handshake failed (marker not seen; "
|
||||
"firmware >= V1.00.21 required; got: %r)"
|
||||
% text[-120:])
|
||||
self.ser.reset_input_buffer() # drop trailing notice text
|
||||
return bank
|
||||
|
||||
async def leave_ota_mode(self):
|
||||
"""Explicit exit: OTA_ABORT makes the device drop back to the CLI
|
||||
immediately (also the failure-path cleanup)."""
|
||||
try:
|
||||
await self.xfer(encode_frame(TYPE_OTA_ABORT, b""))
|
||||
except Exception:
|
||||
pass
|
||||
await asyncio.to_thread(self._read_until, [self.PROMPT], 1.0)
|
||||
|
||||
async def xfer(self, frame):
|
||||
self.ser.write(frame)
|
||||
raw = bytearray()
|
||||
deadline = asyncio.get_event_loop().time() + RSP_TIMEOUT_S
|
||||
while True:
|
||||
data = await asyncio.to_thread(self.ser.read, 256)
|
||||
raw += data
|
||||
for ftype, seq, payload in self.dec.feed(data):
|
||||
if ftype == TYPE_OTA_RSP:
|
||||
# hand back a raw re-encoded frame (uniform with BLE)
|
||||
@ -172,7 +229,8 @@ class UartTransport:
|
||||
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")
|
||||
raise asyncio.TimeoutError("no OTA_RSP on UART (raw rx: %s)"
|
||||
% bytes(raw).hex(" "))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@ -227,19 +285,36 @@ def decode_tlv(data):
|
||||
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):
|
||||
async def call(ftype, payload, ack_off=None):
|
||||
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
|
||||
# A lost frame (flaky wiring) is recovered by resending: the device
|
||||
# re-acks a duplicate BEGIN as BAD_STATE (session already open) and a
|
||||
# duplicate DATA as BAD_STATE carrying the offset it already expects.
|
||||
for attempt in range(1, XFER_TRIES + 1):
|
||||
try:
|
||||
rsp = await xfer(encode_frame(ftype, payload))
|
||||
except asyncio.TimeoutError:
|
||||
if attempt < XFER_TRIES:
|
||||
print(" %s: ack timeout, resending (attempt %d/%d)"
|
||||
% (stage, attempt + 1, XFER_TRIES))
|
||||
continue
|
||||
raise asyncio.TimeoutError("%s: no OTA_RSP after %d tries"
|
||||
% (stage, XFER_TRIES))
|
||||
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))
|
||||
if ftype == TYPE_OTA_BEGIN and status == ST_BAD_STATE:
|
||||
print(" BEGIN: session already open on device, resynced")
|
||||
return ST_OK, offset
|
||||
if ftype == TYPE_OTA_DATA and status == ST_BAD_STATE \
|
||||
and ack_off is not None and offset == ack_off:
|
||||
print(" DATA: chunk already stored (ack was lost), resynced")
|
||||
return ST_OK, offset
|
||||
return status, offset
|
||||
|
||||
status, _ = await call(TYPE_OTA_BEGIN,
|
||||
struct.pack("<III", len(blob), crc32(blob), version))
|
||||
@ -251,7 +326,8 @@ async def ota_session(xfer, chunk, blob, version, target):
|
||||
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])
|
||||
struct.pack("<I", sent) + blob[sent:sent + n],
|
||||
ack_off=sent + n)
|
||||
if status != 0:
|
||||
raise RuntimeError("OTA_DATA rejected at %d, status=%d (device "
|
||||
"expects offset %d)" % (sent, status, next_off))
|
||||
@ -380,10 +456,7 @@ async def run_uart(port, pkg_path):
|
||||
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
|
||||
await transport.leave_ota_mode() # ABORT: device returns to the CLI
|
||||
return 1
|
||||
print("device resets into the new bank; rerun with the serial CLI "
|
||||
"'devinfo' to confirm (cur bank / version).")
|
||||
|
||||
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",
|
||||
"target_bank": 1,
|
||||
"size": 55264,
|
||||
"crc32": "0x5A24EBF6",
|
||||
"version": 65556
|
||||
"size": 55408,
|
||||
"crc32": "0x71598E94",
|
||||
"version": 65557
|
||||
}
|
||||
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
{
|
||||
"file": "caiic_ble_dual_ota_app2.bin",
|
||||
"target_bank": 2,
|
||||
"size": 55564,
|
||||
"crc32": "0x97ED5D57",
|
||||
"version": 65556
|
||||
"size": 55708,
|
||||
"crc32": "0xDAAEED74",
|
||||
"version": 65557
|
||||
}
|
||||
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",
|
||||
"target_bank": 1,
|
||||
"size": 55264,
|
||||
"crc32": "0x5A24EBF6",
|
||||
"version": 65556
|
||||
"size": 55408,
|
||||
"crc32": "0x71598E94",
|
||||
"version": 65557
|
||||
}
|
||||
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
{
|
||||
"file": "mothercup_ble_prod_ota_app2.bin",
|
||||
"target_bank": 2,
|
||||
"size": 55564,
|
||||
"crc32": "0x97ED5D57",
|
||||
"version": 65556
|
||||
"size": 55708,
|
||||
"crc32": "0xDAAEED74",
|
||||
"version": 65557
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user