diff --git a/DBNMQTTool/dbn_mqtt_tool/protocol.py b/DBNMQTTool/dbn_mqtt_tool/protocol.py index fa57202..04ddcac 100644 --- a/DBNMQTTool/dbn_mqtt_tool/protocol.py +++ b/DBNMQTTool/dbn_mqtt_tool/protocol.py @@ -294,7 +294,7 @@ OTA_SLOT_A = "a" OTA_SLOT_B = "b" OTA_STATE_DESC = { - "idle": "空闲", + "idle": "空闲(镜像保留,已刷写完成则 idle+size>0)", "downloading": "下载中", "ready": "校验通过(可刷写)", "flashing": "刷写中", diff --git a/README.md b/README.md index 9e70308..4d5500f 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ DLD960 是一款基于环形线圈检测原理的四通道车辆检测器,支 | [DLD960Loop_串口通信协议.md](docs/DLD960Loop_串口通信协议.md) | V1.05 | Loop MCU ↔ DBN MCU | 0x7F 帧、0xC0 传感主动上报、variation 3B 有符号 | | [DLD960_串口通信协议.md](docs/DLD960_串口通信协议.md) | V1.01 | 整机 TTL 串口 | 设备管理、参数配置、数据上报 | | [DLD960_TCP_JSON协议.md](docs/DLD960_TCP_JSON协议.md) | V1.03 | 以太网 TCP :5960 | 鉴权 + 命令 + event_report 客户端必答 + 脱机日志(事件/快照流) | -| [DLD960_IoT_MQTT协议.md](docs/DLD960_IoT_MQTT协议.md) | V1.08 | 云平台 MQTT | 双主题 `{sn}/srv`+`{sn}/dev`、initialize、event_report 平台必答、设备时钟同步、脱机日志(事件/快照流)、Loop 远程 OTA(ota_* 命令,先存后刷) | +| [DLD960_IoT_MQTT协议.md](docs/DLD960_IoT_MQTT协议.md) | V1.09 | 云平台 MQTT | 双主题 `{sn}/srv`+`{sn}/dev`、initialize、event_report 平台必答、设备时钟同步、脱机日志(事件/快照流)、Loop 远程 OTA(ota_* 命令,先存后刷) | | [DLD960_BLE协议.md](docs/DLD960_BLE协议.md) | V1.02 | 蓝牙 BLE | 帧格式 + 分包 + 脱机日志(OFFLOG_STAT/QUERY/CLEAR)+ 传感快照(SNAP_STAT/QUERY/CLEAR,0x28/0x29/0x2A),与 MQTT/TCP 同语义 | | [DLD960硬件资源.md](docs/DLD960硬件资源.md) | — | 硬件 | 双 MCU IO 分配、继电器、指示灯、拨码 | diff --git a/docs/DLD960_IoT_MQTT协议.md b/docs/DLD960_IoT_MQTT协议.md index 81dd5b6..b7236cc 100644 --- a/docs/DLD960_IoT_MQTT协议.md +++ b/docs/DLD960_IoT_MQTT协议.md @@ -1004,6 +1004,10 @@ dld960/{dev_serial}/{direction} - 从 W25Qxx 暂存读镜像,按 ≤254B/块发 A7(**非阻塞 tick 驱动**:每轮主循环发送 1~2 块并检查 ACK,绝不阻塞主循环,保证刷写窗口内 MQTT PINGREQ/心跳/IWDG 喂狗正常);停等 ACK,1s 超时重发 ×3 - 末块(sub_amount=1)→ bootloader 写剩余 → 清 flag → 复位跑新 APP 4. 进度经 `ota_report` 上行(§5.5);失败重试 ×3 仍失败 → 元数据 `state=flash_failed` + `event_report{type:ota_error}` 告警(平台必答) +5. **刷写结果(设备侧,V1.09 明确)**: + - 成功(末块 ACK)→ 元数据 `state=idle`(**镜像保留**:`size`/`crc32`/`version`/`slot` 不变,`last_result=0`,`flash_cnt+1`)→ 立即上报 `ota_report{stage:done}`(重发 3 次×5s,同 `msg_id`/`ts`)→ 恢复 `event_report` 发送与 offlog/快照落盘 + - 失败 → 元数据 `state=flash_failed` + `ota_report{stage:failed}` + `event_report{type:ota_error}`(done/failed 同重发策略) + - **状态语义**:刷写完成后 DBN 侧状态回 `idle`("本轮刷写已结束"),与"下载完成待刷(`ready`)"严格区分——**平台不得把 `state=ready` 判为"刷写未启动"**:本地刷写 70 块(17224B)实测 <1s,平台轮询间隔可能错过 `flashing` 中间态 **响应:** `code=0` 仅表示已启动,不代表刷写成功——结果以 `ota_report` / `ota_status` 为准。 @@ -1038,7 +1042,7 @@ dld960/{dev_serial}/{direction} | data 字段 | 类型 | 说明 | |-----------|------|------| -| `state` | string | `idle` / `downloading` / `ready` / `flashing` / `flash_failed` / `aborted` | +| `state` | string | `idle` / `downloading` / `ready` / `flashing` / `flash_failed` / `aborted`;**`idle` 且 `size>0` = 已刷写完成(镜像保留可重刷)** | | `progress.sent` | uint32 | 刷写阶段已送 Loop 的字节数 | | `last_result` | uint32 | 上次刷写结果:0=无/成功,非 0=错误码 | | `last_error` | uint32 | 上次失败细分错误码 | @@ -1051,11 +1055,16 @@ dld960/{dev_serial}/{direction} ▲ │ ▲ │ │ ota_abort │ │ ota_end(CRC✗) │ ota_flash(安全检查✓) │ / flash_failed │ └──────────────┐ ▼ - └────────────────────────┴─────────────────┴───── FLASHING ──成功──▶ (Loop 重启) ──▶ IDLE(清槽/保留) + └────────────────────────┴─────────────────┴───── FLASHING ──成功──▶ IDLE(镜像保留, last_result=0) │ └──失败×3──▶ FLASH_FAILED ──ota_abort/ota_begin──▶ IDLE ``` +**平台判定指引(V1.09):** +- **主依据**:`ota_report` 主动上报(`stage=done` 成功 / `stage=failed` 失败,§5.5)——`ota_flash` 后平台应优先等该信号 +- **兜底**:`ota_status` 查询——`state=idle 且 size>0 且 last_result=0` = 刷写成功(镜像保留,可重刷);`state=flash_failed` = 失败;`state=ready` = 下载完成待刷(**不是**"刷写未启动") +- 刷写窗口极短(<1s),轮询可能捕捉不到 `flashing` 中间态,不得以此判失败 + --- # 5 设备主动上报 @@ -1332,6 +1341,8 @@ dld960/{dev_serial}/{direction} **上报节奏**:`begin`/`ready`/`done`/`failed` 各 1 次;`flashing` 阶段按进度节流(建议每 64 块或每 8KB 一次,避免刷写期间消息风暴)。`done`/`failed` 设备侧重发 3 次(间隔 5s,同 `msg_id`/`ts`),平台去重窗口建议 10 分钟(与 `event_report` 同策略)。 +**平台判定(V1.09 明确)**:`ota_report` 是刷写结果的**主依据**——收到 `stage=done` 判成功、`stage=failed` 判失败,无需再轮询 `ota_status`。设备 `ota_flash` 响应 `code=0` 仅表示已启动(异步),**不得以"轮询 `ota_status` 未见 `flashing`"或"状态持续为 `ready`"判"刷写未启动"**——本地刷写 <1s 完成,轮询大概率错过中间态;兜底判定见 §4.24。 + **会话期间静默**(V1.08):OTA 会话期间(`ota_begin` ~ 结束)设备暂停 `event_report` 发送(入队积压,结束后补发)与 offlog/快照落盘("升级开始"日志暂停前写入、"升级结果"恢复后补记),保证刷写窗口内 MQTT 保活与 IWDG 喂狗不受影响。 --- @@ -1348,5 +1359,6 @@ dld960/{dev_serial}/{direction} | V1.05 | 2026-07-15 | 增加**设备时钟同步**(§2.3,方案B):设备无 RTC,`initialize` 上线后平台经 `report_config` 命令下发 Unix `ts`,设备据此校准,之后上行 `ts` 为真实 Unix 时间;校准前为上电秒数 | wangfq | | V1.06 | 2026-08-04 | 增加**脱机事件日志**命令:`log_stat`(统计/分页定位)、`log_query`(按全局序号分页,count≤4)、`log_clear`(清除+审计留痕);§2.3 补充日志双时间戳语义(`ts_ms` 相对 + `unix_ts` 已同步,0=未同步,锚点回算规则) | wangfq | | V1.07 | 2026-08-18 | `log_stat` / `log_query` / `log_clear` 增加**快照流**支持(`stream=snapshot`,与 BLE 0x28/0x29/0x2A 同语义):快照统计 capacity 随芯片动态(48064~449472)、快照分页 count≤1(4 通道记录 JSON ~810B 超发送缓冲,实测修正;BLE 原始通道仍 ≤2)、快照清除审计留痕;`capacity`/`count` 类型修正为 uint32(W25Q256 事件流 130944 超 16bit) | wangfq | +| V1.09 | 2026-08-20 | **OTA 刷写结果判定修复**(现场:刷写物理成功但平台误判"刷写未启动"):① 刷写成功后台侧状态回 `idle`(镜像保留:`size`/`crc32`/`version` 不变,`last_result=0`,可重刷),与"下载完成待刷 `ready`"严格区分;② `ota_report` 升级为刷写结果**主依据**(`stage=done`/`failed` 设备必报,重发 3 次×5s),`ota_status` 仅兜底;③ 明确平台判定指引——`idle+size>0+last_result=0`=成功,不得以轮询未见 `flashing` 或状态持续 `ready` 判"刷写未启动"(本地刷写 <1s,轮询大概率错过中间态);④ `ota_begin` 兼容 `idle+size/crc32 一致` → 免下载直接可刷(重刷) | wangfq | | V1.08 | 2026-08-20 | 增加 **Loop MCU 远程 OTA**(ROADMAP P1.4 ①,先存后刷):命令 `ota_begin` / `ota_data` / `ota_end` / `ota_abort` / `ota_flash` / `ota_status`(srv→dev)+ `ota_report`(dev→srv);单片 256B + 单片/全镜像 CRC32(ISO-HDLC,§4.19.1);断点续传(`ota_begin` 返回 offset);Slot A/B 双槽回滚(镜像 ≤96KB);`ota_flash` 安全窗口检查 + 非阻塞 tick 驱动刷写(刷写窗口内 MQTT 保活/IWDG 不受影响);会话期间暂停 `event_report` 发送与脱机日志落盘;`event_report` 扩展 `type=ota_error` 失败告警;§2.2 补 OTA 细分错误码(err_code);老固件兼容(`ota_*` 回 code=4) | wangfq | diff --git a/docs/DLD960_MQTT_OTA协议.md b/docs/DLD960_MQTT_OTA协议.md index e5e07ff..3c2bc0d 100644 --- a/docs/DLD960_MQTT_OTA协议.md +++ b/docs/DLD960_MQTT_OTA协议.md @@ -324,6 +324,11 @@ typedef struct { /* 64B */ **响应:** 标准成功/失败(`code=0` 仅表示已启动,不代表刷写成功——结果以 `ota_report`/`ota_status` 为准)。 +**刷写结果(V1.09 明确):** +- 成功(末块 ACK)→ 元数据 `state=idle`(**镜像保留**:`size`/`crc32`/`version`/`slot` 不变,`last_result=0`,`flash_cnt+1`)→ 上报 `ota_report{stage:done}`(重发 3 次×5s)→ 恢复 event_report/offlog 落盘 +- 失败 → `state=flash_failed` + `ota_report{stage:failed}` + `event_report{type:ota_error}` +- **状态语义**:刷写完成后回 `idle`("本轮刷写已结束"),与"下载完成待刷 `ready`"严格区分——平台**不得把 `state=ready` 判为"刷写未启动"**:本地刷写 <1s,轮询大概率错过 `flashing` 中间态;平台判定以 `ota_report done/failed` 为主依据,`ota_status` 兜底(`idle+size>0+last_result=0`=成功) + ## 5.6 查询状态 `ota_status` > Topic: `dld960/{sn}/srv` @@ -366,11 +371,13 @@ typedef struct { /* 64B */ ▲ │ ▲ │ │ ota_abort │ │ ota_end(CRC✗) │ ota_flash(安全检查✓) │ / flash_failed │ └──────────────┐ ▼ - └────────────────────────┴─────────────────┴───── FLASHING ──成功──▶ (Loop 重启) ──▶ IDLE(清槽/保留) + └────────────────────────┴─────────────────┴───── FLASHING ──成功──▶ IDLE(镜像保留, last_result=0) │ └──失败×3──▶ FLASH_FAILED ──ota_abort/ota_begin──▶ IDLE ``` +**平台判定(V1.09)**:刷写结果以 `ota_report`(`stage=done`/`failed`)为主依据;`ota_status` 兜底——`state=idle 且 size>0 且 last_result=0` = 成功(镜像保留可重刷);`state=ready` = 待刷(**不是**"刷写未启动");轮询可能错过 `flashing` 中间态,不得以此判失败。 + --- # 6 主动上报 `ota_report` @@ -403,6 +410,8 @@ typedef struct { /* 64B */ **上报节奏**:`begin`/`ready`/`done`/`failed` 各 1 次;`flashing` 阶段按块进度节流(建议每 64 块或每 8KB 一次,避免刷写期间消息风暴)。`done`/`failed` 设备侧重发 3 次(间隔 5s,同 msg_id/ts),平台去重窗口建议 10 分钟(与 event_report 同策略)。 +**平台判定(V1.09 明确)**:`ota_report` 是刷写结果**主依据**——`stage=done` 判成功、`stage=failed` 判失败,无需轮询 `ota_status`。`ota_flash` 响应 `code=0` 仅表示已启动(异步),**不得以"轮询未见 `flashing`"或"状态持续 `ready`"判"刷写未启动"**(本地刷写 <1s,轮询大概率错过中间态);兜底判定见 §5.6。 + ## 6.1 失败告警(扩展 event_report) 刷写整体失败(重试 ×3 仍失败)时,设备经 `event_report` 上报(**平台必答**,复用 §5.3 ACK + 重发机制): diff --git a/docs/DLD960_技术规格书.md b/docs/DLD960_技术规格书.md index 8f9a670..0c04e1c 100644 --- a/docs/DLD960_技术规格书.md +++ b/docs/DLD960_技术规格书.md @@ -106,7 +106,7 @@ DLD960 是一款基于环形线圈(LC 振荡)检测原理的四通道车辆 |------|------|------|------| | DLD960 串口通信协议 | V1.01 | TTL | 设备管理、参数配置、数据上报 | | DLD960 TCP JSON 协议 | V1.03 | ETH :5960 | 密码鉴权 + 18 条命令;event_report 客户端必答(5s×3 重发);脱机日志 log_stat/log_query/log_clear(事件/快照流,stream 区分) | -| DLD960 IoT MQTT 协议 | V1.08 | ETH → Broker | 双主题 `dld960/{sn}/srv`+`/dev`;initialize 上线、loop_data 三档调度、event_report 平台必答、设备时钟同步、脱机日志 log_stat/log_query/log_clear(事件/快照流,stream 区分);Loop 远程 OTA(ota_begin/ota_data/ota_end/ota_abort/ota_flash/ota_status + ota_report,256B/片 CRC32,先存后刷断点续传,Slot A/B 双槽回滚) | +| DLD960 IoT MQTT 协议 | V1.09 | ETH → Broker | 双主题 `dld960/{sn}/srv`+`/dev`;initialize 上线、loop_data 三档调度、event_report 平台必答、设备时钟同步、脱机日志 log_stat/log_query/log_clear(事件/快照流,stream 区分);Loop 远程 OTA(ota_begin/ota_data/ota_end/ota_abort/ota_flash/ota_status + ota_report,256B/片 CRC32,先存后刷断点续传,Slot A/B 双槽回滚) | | DLD960Loop 串口协议 | V1.05 | MCU 间(内部) | 0x7F 帧、0xC0 传感上报(variation 3B 有符号) | | DLD960 BLE 协议 | V1.02 | 蓝牙 BLE | 帧格式 + 分包;脱机日志 OFFLOG_STAT/QUERY/CLEAR + 传感快照 SNAP_STAT/QUERY/CLEAR(0x28/0x29/0x2A),与 MQTT/TCP 同语义 | diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/iot_mqtt_srv.h b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/iot_mqtt_srv.h index 387805a..cad3ff1 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/iot_mqtt_srv.h +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/iot_mqtt_srv.h @@ -53,6 +53,7 @@ void iot_mqtt_handle_sock_int(uint8_t socketid, uint8_t intstat); // Socket void iot_mqtt_publish_sensor(void); // 推送传感器数据到 MQTT void iot_evt_handle_ack(uint32_t msg_id, int code); // event_report 平台应答入口 (V1.04) void iot_evt_report_ota_error(uint32_t err); // OTA 刷写失败告警入队 (V1.08, 走 event_report 闭环) +void iot_ota_report_result(uint8_t ok, uint32_t err); // OTA 刷写结果上报 ota_report (V1.09: ok=1 done / ok=0 failed) uint8_t iot_any_car(void); // 任一通道有车 (ota_flash 安全窗口检查) void iot_watchdog_init(void); // 初始化硬件 IWDG (无条件, 主循环卡死兜底) void iot_watchdog_kick(void); // 喂硬件 IWDG (主循环调用) diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c index 05c91fa..3b41935 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c @@ -59,6 +59,10 @@ static LUP_SensorReport _cached_sr; // 最新传感器数据缓存 static uint8_t _cached_sr_valid; // 缓存有效标志 static uint32_t _last_publish_ms; // 上次上报时刻 (ms) +/* 上行发布共享缓冲 (V1.09): event_report / ota_report 复用, 互斥调用 + (RAM 90% 环境: 独立数组挤栈, 文件级共享零新增) */ +static char _iot_pub_payload[512]; + /*=========================================================================== * 事件上报 event_report — 协议 V1.04: 平台必答 + 设备重发 * @@ -194,8 +198,8 @@ static void iot_evt_sensor_cb(const uint8_t *pkg, uint16_t len) { /* 序列化并发布队列头 n 条事件 (首发与重发共用: 同 id/ts 同内容) */ static void iot_evt_send(uint32_t id, uint32_t ts, uint8_t n) { static const char *evt_names[] = {"car_enter", "car_leave", "loop_cut", "loop_restore", "ota_error"}; - static char payload[512]; // static: 避免 2KB 栈溢出 - int w, rem = sizeof(payload); + char *payload = _iot_pub_payload; // V1.09: 文件级共享缓冲 + int w, rem = sizeof(_iot_pub_payload); char *p = payload; uint8_t i; @@ -250,6 +254,77 @@ void iot_evt_report_ota_error(uint32_t err) PRINT("EVT: ota_error enqueue err=0x%lX\n", (unsigned long)err); } +/*=========================================================================== + * OTA 结果上报 ota_report — 协议 V1.08 §5.5 / V1.09 平台判定主依据 + * + * done/failed 主动推送, 无 ACK 闭环 → 定时重发 3 次×5s (同 msg_id/ts), + * 平台按 (sn,msg_id) 去重 (窗口建议 10min)。共享 _iot_pub_payload 缓冲。 + * iot_ota_report_result(): 记录结果 + 立即首发; iot_ota_report_poll(): + * 主循环每轮调用, 到 5s 且未超 3 次 → 重发。 + *===========================================================================*/ +#define IOT_OTA_REP_MAX_RETRY 3 /* 重发次数 (含首发共 4 次) */ +#define IOT_OTA_REP_INTERVAL_MS 5000 + +static uint8_t _ota_rep_pend; /* 1=有未完成的 ota_report */ +static uint8_t _ota_rep_ok; /* 1=done 0=failed */ +static uint32_t _ota_rep_err; /* failed 的错误码 (OTA_ERR_*) */ +static uint32_t _ota_rep_id; /* 固定 msg_id (重发不变) */ +static uint32_t _ota_rep_ts; /* 固定 ts (重发不变) */ +static uint8_t _ota_rep_times; /* 已发次数 (首发=1) */ +static uint32_t _ota_rep_sent_ms; /* 上次发送时刻 */ + +/* 序列化并发布 ota_report (首发与重发共用: 同 id/ts 同内容) */ +static void iot_ota_report_send(void) +{ + char *p = _iot_pub_payload; + int w, rem = sizeof(_iot_pub_payload); + + w = snprintf(p, rem, + "{\"msg_id\":%lu,\"cmd\":\"ota_report\",\"ts\":%lu," + "\"data\":{\"target\":\"loop\",\"stage\":\"%s\"," + "\"progress\":{\"sent\":%lu,\"total\":%lu},\"code\":%lu,\"msg\":\"\"}}", + _ota_rep_id, _ota_rep_ts, + _ota_rep_ok ? "done" : "failed", + (unsigned long)ota_flash_sent(), (unsigned long)ota_flash_total(), + _ota_rep_ok ? 0UL : (unsigned long)_ota_rep_err); + if (w < 0 || w >= rem) return; + + mqtt_publish((char *)g_iot_topic.topic_pub, p, 1); + _ota_rep_times++; + _ota_rep_sent_ms = mstick(); + PRINT("IOT: ota_report %s msg_id=%lu sent=%lu/%lu times=%u\n", + _ota_rep_ok ? "done" : "failed", _ota_rep_id, + (unsigned long)ota_flash_sent(), (unsigned long)ota_flash_total(), + _ota_rep_times); +} + +/* OTA 刷写结果上报入口 (ota_srv.c ota_flash_done/fail 调用): + ok=1 → stage=done, ok=0 → stage=failed */ +void iot_ota_report_result(uint8_t ok, uint32_t err) +{ + _ota_rep_pend = 1; + _ota_rep_ok = ok ? 1 : 0; + _ota_rep_err = err; + _ota_rep_id = ++_evt_msg_id; /* 与 event_report 同源递增 (唯一性) */ + _ota_rep_ts = dev_time_now(); + _ota_rep_times = 0; + iot_ota_report_send(); +} + +/* 主循环轮询: 5s×3 定时重发 (无 ACK, 纯推送 + 平台去重) */ +static void iot_ota_report_poll(void) +{ + if (!_ota_rep_pend) return; + if (g_iot_state != IOT_STATE_READY) return; /* 断网不消耗次数 */ + if (_ota_rep_times > IOT_OTA_REP_MAX_RETRY) { + _ota_rep_pend = 0; /* 重发耗尽 (共 4 次) */ + PRINT("IOT: ota_report retry exhausted\n"); + return; + } + if (mstick() - _ota_rep_sent_ms < IOT_OTA_REP_INTERVAL_MS) return; + iot_ota_report_send(); +} + /* 任一通道有车 (ota_flash 安全窗口检查用, 基于最新 0xC0 缓存) */ uint8_t iot_any_car(void) { @@ -1111,6 +1186,9 @@ void iot_mqtt_publish_sensor(void) { /*--- 事件上报状态机: 发送/重发/重连补报 ---*/ iot_evt_process(); + /*--- OTA 结果上报重发 (V1.09): done/failed 5s×3, 同 msg_id/ts ---*/ + iot_ota_report_poll(); + if (g_iot_state != IOT_STATE_READY) return; if (!g_report_cfg.enable) return; diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/ota_srv.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/ota_srv.c index 43f810a..073297b 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/ota_srv.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/ota_srv.c @@ -248,6 +248,13 @@ int ota_cmd_begin(uint32_t size, uint32_t crc32, const char *version, uint8_t fo /* 镜像已就绪, 平台可直接 ota_flash */ return 0; /* data.offset = size */ } + if (_meta.state == OTA_STATE_IDLE) { + /* V1.09: 刷写完成后的 idle 且镜像一致 → 直接回 ready, 免下载可重刷 */ + _meta.state = OTA_STATE_READY; + ota_meta_write(&_meta); + PRINT("OTA: begin idle+same image → ready (reflash)\n"); + return 0; /* data.offset = size */ + } if (_meta.state == OTA_STATE_DOWNLOADING) { _meta.state = OTA_STATE_DOWNLOADING; PRINT("OTA: begin resume received=%lu\n", (unsigned long)_meta.received); @@ -454,7 +461,7 @@ static void ota_send_9f(uint8_t sub_l, uint8_t sub_h, uint8_t cmd, UART2_SendString(_ota_buf.frame, (uint16_t)(5 + datalen + 1)); } -/* 刷写失败收尾: 状态落盘 + event_report 告警 + 恢复 0x7F 模式 */ +/* 刷写失败收尾 (V1.09): 状态落盘 + ota_report failed + event_report 告警 + 恢复 0x7F 模式 */ static void ota_flash_fail(uint32_t err) { PRINT("OTA: flash FAILED err=0x%lX\n", (unsigned long)err); @@ -467,16 +474,20 @@ static void ota_flash_fail(uint32_t err) g_flag_counter_ota.flag = 0; lup_frame_reset(); _fs = OTA_FLASH_IDLE; + /* ota_report stage=failed (平台判定主依据, V1.09) */ + extern void iot_ota_report_result(uint8_t ok, uint32_t err); + iot_ota_report_result(0, err); /* event_report ota_error 告警 (平台必答, 由 iot_mqtt_srv 发送) */ extern void iot_evt_report_ota_error(uint32_t err); iot_evt_report_ota_error(err); } -/* 刷写成功收尾: 保留镜像 (state=ready 可重刷), 恢复 0x7F 模式 */ +/* 刷写成功收尾 (V1.09): 状态回 idle 但镜像保留 (可重刷, last_result=0), + 上报 ota_report stage=done (平台判定主依据), 恢复 0x7F 模式 */ static void ota_flash_done(void) { PRINT("OTA: flash DONE sent=%lu\n", (unsigned long)_fs_sent); - _meta.state = OTA_STATE_READY; + _meta.state = OTA_STATE_IDLE; /* V1.09: idle+size>0 = 已刷写完成 (区别于 ready=待刷) */ _meta.last_result = 0; ota_meta_write(&_meta); offlog_ota_result(0); @@ -484,6 +495,9 @@ static void ota_flash_done(void) g_flag_counter_ota.flag = 0; lup_frame_reset(); _fs = OTA_FLASH_IDLE; + /* ota_report stage=done (平台判定主依据, V1.09) */ + extern void iot_ota_report_result(uint8_t ok, uint32_t err); + iot_ota_report_result(1, 0); } /* UART2 收到 bootloader 0x9F ACK 帧 (uart_srv OTA 分支调用, 主循环上下文) diff --git a/vd960DBN/tests/test_ota_srv.c b/vd960DBN/tests/test_ota_srv.c index 253e711..caf72a7 100644 --- a/vd960DBN/tests/test_ota_srv.c +++ b/vd960DBN/tests/test_ota_srv.c @@ -107,8 +107,12 @@ void lup_frame_reset(void) { } /* ---- mock iot_mqtt_srv ---- */ static uint8_t _mock_any_car = 0; static uint32_t _ota_err_report = 0, _ota_err_cnt = 0; +static uint8_t _ota_rep_ok = 0xFF; +static uint32_t _ota_rep_err = 0, _ota_rep_cnt = 0; uint8_t iot_any_car(void) { return _mock_any_car; } void iot_evt_report_ota_error(uint32_t err) { _ota_err_report = err; _ota_err_cnt++; } +/* V1.09: ota_report 结果上报 mock (ok=1 done, ok=0 failed) */ +void iot_ota_report_result(uint8_t ok, uint32_t err) { _ota_rep_ok = ok; _ota_rep_err = err; _ota_rep_cnt++; } /* ============================================================================ * 嵌入真实 ota_srv.c (同一编译单元, static 可见) @@ -353,6 +357,8 @@ static int test_flash_flow(void) flash_init(); tx_reset(); _ota_err_cnt = 0; + _ota_rep_cnt = 0; + _ota_rep_ok = 0xFF; ota_init(); /* 下载 → ready */ @@ -407,13 +413,18 @@ static int test_flash_flow(void) /* 逐块 A7 + ACK 直到完成 */ flash_step_until_ack(&(uint32_t){0}, 21); - /* 完成后: 恢复 0x7F 模式, 元数据 ready (镜像保留可重刷), 结果已记 */ - CHK(_meta.state == OTA_STATE_READY); + /* 完成后 (V1.09): 状态回 idle (镜像保留可重刷), 恢复 0x7F 模式, 结果已记, + ota_report stage=done 已上报 */ + CHK(_meta.state == OTA_STATE_IDLE); + CHK(_meta.last_result == 0); CHK(g_ota_flash_active == 0); CHK(g_flag_counter_ota.flag == 0); CHK(_ota_result_cnt >= 1); CHK(_last_ota_result == 0); CHK(_ota_err_cnt == 0); /* 无失败告警 */ + CHK(_ota_rep_cnt == 1); /* ota_report done 上报一次 */ + CHK(_ota_rep_ok == 1); + CHK(_ota_rep_err == 0); printf("PASS test_flash_flow (21 blocks)\n"); return 0; } @@ -440,6 +451,8 @@ static int test_flash_ready_timeout(void) flash_init(); tx_reset(); _ota_err_cnt = 0; + _ota_rep_cnt = 0; + _ota_rep_ok = 0xFF; ota_init(); _meta.state = OTA_STATE_READY; _meta.size = 512; @@ -461,10 +474,56 @@ static int test_flash_ready_timeout(void) CHK(g_flag_counter_ota.flag == 0); CHK(_ota_result_cnt >= 1); CHK(_last_ota_result == OTA_ERR_READY_TIMEOUT); + CHK(_ota_rep_cnt == 1); /* ota_report failed 上报一次 */ + CHK(_ota_rep_ok == 0); + CHK(_ota_rep_err == OTA_ERR_READY_TIMEOUT); printf("PASS test_flash_ready_timeout\n"); return 0; } +/* V1.09: 刷写完成后 idle+镜像一致 → ota_begin 直接回 ready (免下载重刷) */ +static int test_begin_reflash(void) +{ + uint8_t img[5120]; + make_image(img, sizeof(img)); + flash_init(); + tx_reset(); + ota_init(); + + /* 下载 → ready */ + CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "1.1.0", 0) == 0); + { + uint32_t i; + for (i = 0; i < sizeof(img); i += 256) { + uint32_t clen = (sizeof(img) - i > 256) ? 256 : (sizeof(img) - i); + uint8_t chunk[256]; + char hex[513]; + uint32_t j; + memcpy(chunk, &img[i], clen); + for (j = 0; j < clen; j++) sprintf(hex + j * 2, "%02x", chunk[j]); + CHK(ota_cmd_data(i, ota_crc32(chunk, clen), hex) == 0); + } + } + CHK(ota_cmd_end(ota_crc32(img, sizeof(img))) == 0); + CHK(_meta.state == OTA_STATE_READY); + + /* 模拟刷写完成 → idle (镜像保留) */ + _meta.state = OTA_STATE_IDLE; + CHK(_meta.size == sizeof(img)); + + /* 重刷: begin 同镜像 → 直接 ready, offset=size (免下载) */ + CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "1.1.0", 0) == 0); + CHK(_meta.state == OTA_STATE_READY); + CHK(_meta.received == sizeof(img)); + + /* 不同镜像 → 新会话重下 */ + CHK(ota_cmd_begin(sizeof(img) + 256, ota_crc32(img, sizeof(img)), "1.2.0", 0) == 0); + CHK(_meta.state == OTA_STATE_DOWNLOADING); + CHK(_meta.received == 0); + printf("PASS test_begin_reflash\n"); + return 0; +} + int main(void) { int fails = 0; @@ -478,6 +537,7 @@ int main(void) fails += test_flash_flow(); fails += test_flash_reject_car(); fails += test_flash_ready_timeout(); + fails += test_begin_reflash(); if (fails == 0) { printf("\nALL PASS\n"); return 0;