fix(vd960DBN): OTA 刷写成功误判未启动 — 状态回 idle + 补 ota_report 主动上报

现场: 远程平台发起 OTA, 设备物理刷写成功 (70块全ACK, flash DONE),
但平台 ota_status 轮询只见 state=ready → 误判'刷写未启动'。

根因:
1. ota_flash_done 刷写成功后台侧状态置 READY (与协议状态机
   'FLASHING──成功──▶IDLE(清槽/保留)' 不符), 平台无法区分
   '待刷 ready' 与 '已刷完' → 持续 ready 判未启动
2. 固件漏实现协议 §5.5 ota_report 主动上报 (done/failed 重发 3次×5s),
   平台收不到成功信号, 只能靠 ota_status 轮询兜底

修复 (协议 V1.09 + 固件):
- 协议: 刷写成功状态明确回 idle (镜像保留, last_result=0, 可重刷);
  ota_report 升级为刷写结果主依据; 平台判定指引 (idle+size>0+last_result=0
  =成功; 不得以轮询未见 flashing 或持续 ready 判未启动)
- ota_srv.c: ota_flash_done → OTA_STATE_IDLE (镜像保留) + 上报 done;
  ota_flash_fail → 补 ota_report failed (保留 event_report 告警);
  ota_cmd_begin 兼容 idle+size/crc32 一致 → 直接回 ready (免下载重刷)
- iot_mqtt_srv.c: 新增 ota_report 上报状态机 (立即首发 + 5s×3 重发,
  同 msg_id/ts), 共享 _iot_pub_payload (event_report 复用, RAM 零新增)
- 单测: test_flash_flow/test_flash_ready_timeout 断言 ota_report 上报,
  state 断言 READY→IDLE; 新增 test_begin_reflash; 10/10 全过 + 22 py 断言
This commit is contained in:
wangfq
2026-08-20 18:56:21 +08:00
parent 1badba3893
commit 51298693da
9 changed files with 187 additions and 13 deletions
+1 -1
View File
@@ -294,7 +294,7 @@ OTA_SLOT_A = "a"
OTA_SLOT_B = "b"
OTA_STATE_DESC = {
"idle": "空闲",
"idle": "空闲(镜像保留,已刷写完成则 idle+size>0)",
"downloading": "下载中",
"ready": "校验通过(可刷写)",
"flashing": "刷写中",
+1 -1
View File
@@ -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/CLEAR0x28/0x29/0x2A),与 MQTT/TCP 同语义 |
| [DLD960硬件资源.md](docs/DLD960硬件资源.md) | — | 硬件 | 双 MCU IO 分配、继电器、指示灯、拨码 |
+14 -2
View File
@@ -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≤14 通道记录 JSON ~810B 超发送缓冲,实测修正;BLE 原始通道仍 ≤2)、快照清除审计留痕;`capacity`/`count` 类型修正为 uint32W25Q256 事件流 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 + 单片/全镜像 CRC32ISO-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 |
+10 -1
View File
@@ -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 + 重发机制):
+1 -1
View File
@@ -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 远程 OTAota_begin/ota_data/ota_end/ota_abort/ota_flash/ota_status + ota_report256B/片 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 远程 OTAota_begin/ota_data/ota_end/ota_abort/ota_flash/ota_status + ota_report256B/片 CRC32,先存后刷断点续传,Slot A/B 双槽回滚) |
| DLD960Loop 串口协议 | V1.05 | MCU 间(内部) | 0x7F 帧、0xC0 传感上报(variation 3B 有符号) |
| DLD960 BLE 协议 | V1.02 | 蓝牙 BLE | 帧格式 + 分包;脱机日志 OFFLOG_STAT/QUERY/CLEAR + 传感快照 SNAP_STAT/QUERY/CLEAR0x28/0x29/0x2A),与 MQTT/TCP 同语义 |
@@ -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 (主循环调用)
@@ -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;
@@ -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 分支调用, 主循环上下文)
+62 -2
View File
@@ -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;