diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/ota_srv.h b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/ota_srv.h index 3fb70c7..65dc1f3 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/ota_srv.h +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/ota_srv.h @@ -124,6 +124,9 @@ int ota_cmd_flash(uint8_t slot, uint8_t force); /* 分片落盘用: hex → bin (n 字节), 成功 0 */ int ota_hex_decode(const char *hex, uint8_t *out, uint32_t n); +/* 从 MQTT JSON 中提取 ota_data 的 data 字段 hex 值 (兼容 "data":"..." 与 "data": "...") + 返回 hex 字符串长度, 0=未找到/为空 */ +int ota_json_extract_hex(const char *json, char *out, int out_len); /* CRC-32/ISO-HDLC (逐位实现, 零查表 RAM) */ uint32_t ota_crc32(const uint8_t *data, uint32_t len); diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c index 1ee9e9b..359f034 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c @@ -755,7 +755,6 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_ } else if (strcmp(cmd_str, "ota_data") == 0) { /* V1.08: 分片下发 (256B/片, 单片 CRC32; 重复片幂等, 乱序拒收指示续传) */ - const char *hp; uint32_t offset = 0, crc32 = 0; int rc; memset(tmp, 0, sizeof(tmp)); @@ -764,14 +763,8 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_ memset(tmp, 0, sizeof(tmp)); simple_parse_json(json, "\"crc32\"", tmp); if (strlen(tmp) > 0) crc32 = (uint32_t)strtoul(tmp, NULL, 10); - /* hex 值提取: data 对象内字段是 "data":"...", 顶层 data 对象是 "data":{ — 前者唯一 */ - hp = strstr(json, "\"data\":\""); - memset(_ota_io.hexbuf, 0, sizeof(_ota_io.hexbuf)); - if (hp) { - hp += 8; /* 跳过 "data":" */ - strncpy(_ota_io.hexbuf, hp, sizeof(_ota_io.hexbuf) - 1); - { char *q = strchr(_ota_io.hexbuf, '"'); if (q) *q = '\0'; } - } + /* hex 值提取: ota_json_extract_hex 兼容 "data":"..." 与 "data": "..." (json.dumps 带空格) */ + ota_json_extract_hex(json, _ota_io.hexbuf, (int)sizeof(_ota_io.hexbuf)); rc = ota_cmd_data(offset, crc32, _ota_io.hexbuf); if (rc == 0) { const OtaMeta *m = ota_meta(); diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/ota_srv.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/ota_srv.c index 83b1448..43f810a 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/ota_srv.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/ota_srv.c @@ -99,6 +99,39 @@ int ota_hex_decode(const char *hex, uint8_t *out, uint32_t n) return 0; } +/* 从 MQTT JSON 提取 ota_data 的 data 字段 hex 值。 + ⚠ 2026-08-20 事故: json.dumps 默认带空格 ("data": "hex"), + strstr("\"data\":\"") 无空格匹配 → 提取失败 → 单片 CRC 全错。 + 本函数扫描 "data" 键后跳过任意空格/冒号, 兼容两种格式。 */ +int ota_json_extract_hex(const char *json, char *out, int out_len) +{ + const char *p = json; + int n = 0; + if (!json || !out || out_len <= 0) return 0; + out[0] = '\0'; + while (*p) { + if (strncmp(p, "\"data\"", 6) == 0) { + const char *q = p + 6; + while (*q == ' ' || *q == '\t') q++; + if (*q == ':') { + q++; + while (*q == ' ' || *q == '\t') q++; + if (*q == '"') { + q++; /* 跳过起始引号 */ + while (*q && *q != '"' && n < out_len - 1) { + out[n++] = *q; + q++; + } + out[n] = '\0'; + return n; + } + } + } + p++; + } + return 0; +} + /*=========================================================================== * 元数据读写 (双备份, 同扇区 0x010000~0x0103FF) * 写: 显式擦扇区 (45ms) + NoCheck 写主 + 写备份 diff --git a/vd960DBN/docs/devlog.md b/vd960DBN/docs/devlog.md index 4fe0b9c..ae90aa9 100644 --- a/vd960DBN/docs/devlog.md +++ b/vd960DBN/docs/devlog.md @@ -4,6 +4,28 @@ > > 项目定位: DLD960 通信板 — BLE 配网、TCP JSON 协议服务、Loop MCU 串口桥接 +## 2026-08-20 — OTA 下载失败修复:hex 提取不兼容 json.dumps 空格(单片 CRC 全错) + +### 现象(板级) + +DBNMQTTool 下载第一片即失败:`ota_data code=1 crc/gap error`,received=0,重试 4 次全败。 + +### 根因 + +工具 `json.dumps` 默认**带空格**(`"data": "hex..."`),设备端手工 `strstr(json, "\"data\":\"")` 找**无空格**格式 → 匹配不到 → hexbuf 空 → `ota_hex_decode` 失败 → code=1。其他命令走 `simple_parse_json`(容错空格)不受影响,只有 OTA 的 hex 提取踩坑。 + +### 修复 + +- `ota_srv.c/h` 新增 `ota_json_extract_hex()`:扫描 `"data"` 键后跳过任意空格/冒号/引号,兼容 `"data":"..."` 与 `"data": "..."` 两种格式 +- `iot_mqtt_srv.c` ota_data 分支改用该函数(删除 strstr 手工提取 + 残留 hp 变量) +- 单测新增 `test_json_extract_hex`:无空格/带空格(事故场景)/512 hex 满片/找不到,4 场景全过 + +### 教训 + +嵌入式 JSON 协议字段提取**统一用 simple_parse_json 或兼容空格的扫描**,不要 strstr 精确匹配——上位机 JSON 序列化器(json.dumps/不同语言库)的空格行为不可控。 + +--- + ## 2026-08-20 — 联网复位事故修复:OTA 命令缓冲合并 union(RAM 90% 栈余量不足) ### 现象(板级) diff --git a/vd960DBN/tests/test_ota_srv.c b/vd960DBN/tests/test_ota_srv.c index 3332eaf..253e711 100644 --- a/vd960DBN/tests/test_ota_srv.c +++ b/vd960DBN/tests/test_ota_srv.c @@ -1,450 +1,487 @@ -/* - * ota_srv.c 隔离单测 (gcc, 无 WCH 环境) - * - * 策略: mock CONFIG.h/cmcng.h + mock 外设 (NOR flash 模拟 / UART2 发送记录 / - * 可推进时钟 / offlog 记录) → #include 真实 ../APP/ota_srv.c (同编译单元, - * static 函数可见) → 断言会话状态机 + 刷写协议帧 + 超时重试。 - * - * 编译: - * gcc -I tests/ota_mock -I APP/include -I APP -o /tmp/test_ota_srv \ - * tests/test_ota_srv.c - * 运行: /tmp/test_ota_srv - */ -#include -#include -#include -#include - -#include "cmcng.h" /* mock: Flag_Counter / PRINT / mstick / UART2_SendString */ - -/* ============================================================================ - * mock 外设 - * ============================================================================*/ -Flag_Counter g_flag_counter_key = {0, 0, 0}; -Flag_Counter g_flag_counter_ota = {0, 0, 0}; - -static uint32_t _mock_ms = 0; -uint32_t mstick(void) { return _mock_ms; } -static void mock_advance(uint32_t ms) { _mock_ms += ms; } - -/* UART2 发送记录: 保存最近一帧 */ -static uint8_t _tx_buf[512]; -static uint16_t _tx_len = 0; -static uint16_t _tx_count = 0; -void UART2_SendString(uint8_t *buf, uint16_t len) -{ - _tx_len = (len <= sizeof(_tx_buf)) ? len : (uint16_t)sizeof(_tx_buf); - memcpy(_tx_buf, buf, _tx_len); - _tx_count++; -} -static void tx_reset(void) { _tx_len = 0; _tx_count = 0; } - -/* ---- mock NOR flash (W25Q32 语义: 写=AND, 擦=0xFF) ---- */ -#define MOCK_FLASH_SIZE (4 * 1024 * 1024) -static uint8_t flash[MOCK_FLASH_SIZE]; -static void flash_init(void) { memset(flash, 0xFF, sizeof(flash)); } - -void SPI_Flash_Read(uint8_t *pBuffer, uint32_t ReadAddr, uint16_t size) -{ - assert(ReadAddr + size <= MOCK_FLASH_SIZE); - memcpy(pBuffer, &flash[ReadAddr], size); -} -void SPI_Flash_Write_NoCheck(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t size) -{ - uint16_t i; - assert(WriteAddr + size <= MOCK_FLASH_SIZE); - for (i = 0; i < size; i++) flash[WriteAddr + i] &= pBuffer[i]; -} -void SPI_Flash_Erase_Sector(uint32_t Dst_Addr) -{ - assert(Dst_Addr * 4096UL + 4096UL <= MOCK_FLASH_SIZE); - memset(&flash[Dst_Addr * 4096UL], 0xFF, 4096); -} -/* 与 storage.c 语义一致: 目标区非 0xFF → 擦扇区 → 读-改-写保留同扇区其他数据 */ -void SPI_Flash_Write(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t size) -{ - uint32_t secpos, secoff, secremain; - uint16_t i; - while (size > 0) { - secpos = WriteAddr / 4096; - secoff = WriteAddr % 4096; - secremain = 4096 - secoff; - if (size <= secremain) secremain = size; - /* 目标区是否全 0xFF */ - { - uint8_t need_erase = 0; - for (i = 0; i < secremain; i++) { - if (flash[secpos * 4096 + secoff + i] != 0xFF) { need_erase = 1; break; } - } - if (need_erase) { - uint8_t sector[4096]; - memcpy(sector, &flash[secpos * 4096], 4096); - for (i = 0; i < secremain; i++) sector[secoff + i] = pBuffer[i]; - memset(&flash[secpos * 4096], 0xFF, 4096); - for (i = 0; i < 4096; i++) flash[secpos * 4096 + i] &= sector[i]; - } else { - for (i = 0; i < secremain; i++) flash[WriteAddr + i] &= pBuffer[i]; - } - } - pBuffer += secremain; - WriteAddr += (uint32_t)secremain; - size -= (uint16_t)secremain; - } -} -uint8_t SPI_Flash_ReadJEDEC_ID(void) { return 0x16; } /* W25Q32 */ - -/* ---- mock offlog (记录最近 OTA 事件) ---- */ -static uint32_t _ota_start_cnt = 0, _ota_result_cnt = 0; -static uint32_t _last_ota_result = 0; -void offlog_evt(uint8_t type, const uint8_t *payload, uint8_t len) { (void)type; (void)payload; (void)len; } -void offlog_ota_start(uint8_t slot, uint32_t size) { _ota_start_cnt++; (void)slot; (void)size; } -void offlog_ota_result(uint32_t code) { _ota_result_cnt++; _last_ota_result = code; } -void offlog_boot(uint32_t r) { (void)r; } - -/* ---- mock loop_uart_proto ---- */ -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; -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++; } - -/* ============================================================================ - * 嵌入真实 ota_srv.c (同一编译单元, static 可见) - * ============================================================================*/ -#include "../APP/ota_srv.c" - -/* ============================================================================ - * 测试 - * ============================================================================*/ -#define CHK(cond) do { if (!(cond)) { \ - printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); return 1; } } while (0) - -/* 构造测试镜像 (5120B = 20 片) */ -static void make_image(uint8_t *buf, uint32_t size) -{ - uint32_t i; - for (i = 0; i < size; i++) buf[i] = (uint8_t)(i * 7 + 3); -} - -static int test_crc32(void) -{ - /* CRC-32/ISO-HDLC 标准向量 */ - CHK(ota_crc32((const uint8_t *)"123456789", 9) == 0xCBF43926UL); - CHK(ota_crc32(NULL, 0) == 0x00000000UL); - printf("PASS test_crc32\n"); - return 0; -} - -static int test_hex_decode(void) -{ - uint8_t out[4]; - CHK(ota_hex_decode("0102aBff", out, 4) == 0); - CHK(out[0] == 0x01 && out[1] == 0x02 && out[2] == 0xAB && out[3] == 0xFF); - CHK(ota_hex_decode("01z2", out, 2) != 0); /* 非法字符 */ - CHK(ota_hex_decode("010", out, 2) != 0); /* 长度不足 */ - printf("PASS test_hex_decode\n"); - return 0; -} - -static int test_meta_restore(void) -{ - flash_init(); - tx_reset(); - ota_init(); - CHK(_meta.state == OTA_STATE_IDLE); - /* 写一份 downloading 元数据 → 重新 init → 恢复续传状态 */ - _meta.magic = OTA_MAGIC; - _meta.state = OTA_STATE_DOWNLOADING; - _meta.size = 512; - _meta.received = 256; - ota_meta_write(&_meta); - ota_init(); - CHK(_meta.state == OTA_STATE_DOWNLOADING); - CHK(_meta.received == 256); - printf("PASS test_meta_restore\n"); - return 0; -} - -static int test_session_flow(void) -{ - uint8_t img[5120]; - uint32_t i; - make_image(img, sizeof(img)); - flash_init(); - tx_reset(); - ota_init(); - - /* begin */ - CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "1.1.0", 0) == 0); - CHK(_meta.state == OTA_STATE_DOWNLOADING); - CHK(_meta.received == 0); - - /* 20 片下发 */ - 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(_meta.received == sizeof(img)); - - /* end → ready */ - CHK(ota_cmd_end(ota_crc32(img, sizeof(img))) == 0); - CHK(_meta.state == OTA_STATE_READY); - - /* 暂存区与镜像一致 (读回比对) */ - { - uint8_t back[5120]; - SPI_Flash_Read(back, OTA_SLOT_A_BASE, sizeof(back)); - CHK(memcmp(back, img, sizeof(img)) == 0); - } - printf("PASS test_session_flow\n"); - return 0; -} - -static int test_begin_resume(void) -{ - uint8_t img[5120]; - uint32_t i; - make_image(img, sizeof(img)); - flash_init(); - ota_init(); - - /* 下载 16 片 (4096B, 触发元数据节流 flush) 后"断电重启" */ - CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "", 0) == 0); - for (i = 0; i < 16 * 256; i += 256) { - uint8_t chunk[256]; - char hex[513]; - uint32_t j; - memcpy(chunk, &img[i], 256); - for (j = 0; j < 256; j++) sprintf(hex + j * 2, "%02x", chunk[j]); - CHK(ota_cmd_data(i, ota_crc32(chunk, 256), hex) == 0); - } - CHK(_meta.received == 16 * 256); /* 4096B, 已 flush */ - - /* 重启 */ - ota_init(); - CHK(_meta.state == OTA_STATE_DOWNLOADING); - CHK(_meta.received == 16 * 256); /* 节流点精确续传 */ - - /* 重新 begin → 续传点 */ - CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "", 0) == 0); - CHK(_meta.received == 16 * 256); - - /* 继续下载剩余 → ready */ - for (i = 16 * 256; 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); - printf("PASS test_begin_resume\n"); - return 0; -} - -static int test_dup_and_gap(void) -{ - uint8_t img[1024]; - uint8_t chunk[256]; - char hex[513]; - uint32_t j; - make_image(img, sizeof(img)); - flash_init(); - ota_init(); - CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "", 0) == 0); - - memcpy(chunk, &img[0], 256); - for (j = 0; j < 256; j++) sprintf(hex + j * 2, "%02x", chunk[j]); - CHK(ota_cmd_data(0, ota_crc32(chunk, 256), hex) == 0); - - /* 重复片幂等 */ - CHK(ota_cmd_data(0, ota_crc32(chunk, 256), hex) == 0); - CHK(_meta.received == 256); - - /* 乱序缺片 (跳 256 直接发 512) → 拒绝 */ - memcpy(chunk, &img[512], 256); - for (j = 0; j < 256; j++) sprintf(hex + j * 2, "%02x", chunk[j]); - CHK(ota_cmd_data(512, ota_crc32(chunk, 256), hex) == 1); - CHK(_meta.received == 256); - - /* 单片 CRC 错 → 拒绝 */ - memcpy(chunk, &img[256], 256); - for (j = 0; j < 256; j++) sprintf(hex + j * 2, "%02x", chunk[j]); - CHK(ota_cmd_data(256, 0xDEADBEEFUL, hex) == 1); - CHK(_meta.received == 256); - printf("PASS test_dup_and_gap\n"); - return 0; -} - -/* 模拟 bootloader: 根据收到的 A7 块回 data ACK */ -static void flash_step_until_ack(uint32_t *blocks_done, uint32_t total_blocks) -{ - /* ota_poll 推进一帧发送 + 喂 ACK, 直到收到末块 */ - uint32_t guard = 0; - while (*blocks_done < total_blocks && guard++ < 10000) { - ota_poll(); /* 发 A7 块 */ - CHK(_tx_len >= 7 && _tx_buf[0] == 0x9F); - CHK(_tx_buf[4] == 0xA7); - /* 构造 data ACK: 9F SubL SubH 02 A7 00 CHECK */ - { - uint8_t ack[7]; - uint8_t sum = 0; - ack[0] = 0x9F; ack[1] = _tx_buf[1]; ack[2] = _tx_buf[2]; - ack[3] = 0x02; ack[4] = 0xA7; ack[5] = 0x00; - sum = (uint8_t)(ack[1] + ack[2] + ack[3] + ack[4] + ack[5]); - ack[6] = sum; - ota_flash_feed_ack(ack, 7); - } - (*blocks_done)++; - } - CHK(*blocks_done == total_blocks); -} - -static int test_flash_flow(void) -{ - uint8_t img[5120]; - make_image(img, sizeof(img)); - flash_init(); - tx_reset(); - _ota_err_cnt = 0; - ota_init(); - - /* 下载 → ready */ - CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "", 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); - - /* flash: 无车 → 启动 */ - _mock_any_car = 0; - tx_reset(); - CHK(ota_cmd_flash(0, 0) == 0); - CHK(_meta.state == OTA_STATE_FLASHING); - CHK(g_ota_flash_active == 1); - CHK(g_flag_counter_ota.flag == 1); - CHK(_ota_start_cnt >= 1); /* offlog 升级开始已写 */ - - /* START → 发 A5 启动帧 */ - ota_poll(); - CHK(_tx_len == 6 && _tx_buf[0] == 0x9F && _tx_buf[4] == 0xA5 && _tx_buf[5] == 0xA7); - - /* pre_ok → ADDR */ - { - uint8_t pre_ok[7] = {0x9F, 0x01, 0x00, 0x02, 0xA5, 0x00, 0xA8}; - ota_flash_feed_ack(pre_ok, 7); - } - ota_poll(); - /* A6 地址帧: 9F 01 00 05 A6 08 00 34 00 CHECK */ - CHK(_tx_len == 10 && _tx_buf[4] == 0xA6); - CHK(_tx_buf[5] == 0x08 && _tx_buf[6] == 0x00 && _tx_buf[7] == 0x34 && _tx_buf[8] == 0x00); - /* 校验和: SubL+SubH+LEN+CMD+ADDR = 01+00+05+A6+08+00+34+00 */ - CHK(_tx_buf[9] == (uint8_t)(0x01 + 0x00 + 0x05 + 0xA6 + 0x08 + 0x00 + 0x34 + 0x00)); - - /* addr_ok → DATA 阶段 (总块数 = ceil(5120/248) = 21) */ - { - uint8_t addr_ok[7] = {0x9F, 0x01, 0x00, 0x02, 0xA6, 0x00, 0xA9}; - ota_flash_feed_ack(addr_ok, 7); - } - CHK(_fs_sub == 21); /* 首块序号 = 总块数 */ - - /* 逐块 A7 + ACK 直到完成 */ - flash_step_until_ack(&(uint32_t){0}, 21); - - /* 完成后: 恢复 0x7F 模式, 元数据 ready (镜像保留可重刷), 结果已记 */ - CHK(_meta.state == OTA_STATE_READY); - 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); /* 无失败告警 */ - printf("PASS test_flash_flow (21 blocks)\n"); - return 0; -} - -static int test_flash_reject_car(void) -{ - flash_init(); - ota_init(); - /* 直接构造 ready 态 */ - _meta.state = OTA_STATE_READY; - _meta.size = 512; - _mock_any_car = 1; - CHK(ota_cmd_flash(0, 0) == 3); /* 有车 → 拒绝 */ - CHK(g_ota_flash_active == 0); - _mock_any_car = 1; - CHK(ota_cmd_flash(0, 1) == 0); /* force → 跳过 */ - CHK(g_ota_flash_active == 1); - printf("PASS test_flash_reject_car\n"); - return 0; -} - -static int test_flash_ready_timeout(void) -{ - flash_init(); - tx_reset(); - _ota_err_cnt = 0; - ota_init(); - _meta.state = OTA_STATE_READY; - _meta.size = 512; - _mock_any_car = 0; - - CHK(ota_cmd_flash(0, 0) == 0); - ota_poll(); /* 发 A5 (首发) */ - /* 不喂 ACK: 首发后 3 次超时重发 + 第 4 个超时周期 → 失败 - 轮次: 超时1(→START) A5(2) 超时2(→START) A5(3) 超时3(→START) A5(4) 超时4(→fail) */ - { - int k; - for (k = 0; k < 7; k++) { mock_advance(1001); ota_poll(); } - } - CHK(_meta.state == OTA_STATE_FLASH_FAILED); - CHK(_meta.last_result == OTA_ERR_READY_TIMEOUT); - CHK(_ota_err_cnt == 1); /* event_report ota_error 告警 */ - CHK(_ota_err_report == OTA_ERR_READY_TIMEOUT); - CHK(g_ota_flash_active == 0); - CHK(g_flag_counter_ota.flag == 0); - CHK(_ota_result_cnt >= 1); - CHK(_last_ota_result == OTA_ERR_READY_TIMEOUT); - printf("PASS test_flash_ready_timeout\n"); - return 0; -} - -int main(void) -{ - int fails = 0; - fails += test_crc32(); - fails += test_hex_decode(); - fails += test_meta_restore(); - fails += test_session_flow(); - fails += test_begin_resume(); - fails += test_dup_and_gap(); - fails += test_flash_flow(); - fails += test_flash_reject_car(); - fails += test_flash_ready_timeout(); - if (fails == 0) { - printf("\nALL PASS\n"); - return 0; - } - printf("\n%d TEST(S) FAILED\n", fails); - return 1; -} +/* + * ota_srv.c 隔离单测 (gcc, 无 WCH 环境) + * + * 策略: mock CONFIG.h/cmcng.h + mock 外设 (NOR flash 模拟 / UART2 发送记录 / + * 可推进时钟 / offlog 记录) → #include 真实 ../APP/ota_srv.c (同编译单元, + * static 函数可见) → 断言会话状态机 + 刷写协议帧 + 超时重试。 + * + * 编译: + * gcc -I tests/ota_mock -I APP/include -I APP -o /tmp/test_ota_srv \ + * tests/test_ota_srv.c + * 运行: /tmp/test_ota_srv + */ +#include +#include +#include +#include + +#include "cmcng.h" /* mock: Flag_Counter / PRINT / mstick / UART2_SendString */ + +/* ============================================================================ + * mock 外设 + * ============================================================================*/ +Flag_Counter g_flag_counter_key = {0, 0, 0}; +Flag_Counter g_flag_counter_ota = {0, 0, 0}; + +static uint32_t _mock_ms = 0; +uint32_t mstick(void) { return _mock_ms; } +static void mock_advance(uint32_t ms) { _mock_ms += ms; } + +/* UART2 发送记录: 保存最近一帧 */ +static uint8_t _tx_buf[512]; +static uint16_t _tx_len = 0; +static uint16_t _tx_count = 0; +void UART2_SendString(uint8_t *buf, uint16_t len) +{ + _tx_len = (len <= sizeof(_tx_buf)) ? len : (uint16_t)sizeof(_tx_buf); + memcpy(_tx_buf, buf, _tx_len); + _tx_count++; +} +static void tx_reset(void) { _tx_len = 0; _tx_count = 0; } + +/* ---- mock NOR flash (W25Q32 语义: 写=AND, 擦=0xFF) ---- */ +#define MOCK_FLASH_SIZE (4 * 1024 * 1024) +static uint8_t flash[MOCK_FLASH_SIZE]; +static void flash_init(void) { memset(flash, 0xFF, sizeof(flash)); } + +void SPI_Flash_Read(uint8_t *pBuffer, uint32_t ReadAddr, uint16_t size) +{ + assert(ReadAddr + size <= MOCK_FLASH_SIZE); + memcpy(pBuffer, &flash[ReadAddr], size); +} +void SPI_Flash_Write_NoCheck(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t size) +{ + uint16_t i; + assert(WriteAddr + size <= MOCK_FLASH_SIZE); + for (i = 0; i < size; i++) flash[WriteAddr + i] &= pBuffer[i]; +} +void SPI_Flash_Erase_Sector(uint32_t Dst_Addr) +{ + assert(Dst_Addr * 4096UL + 4096UL <= MOCK_FLASH_SIZE); + memset(&flash[Dst_Addr * 4096UL], 0xFF, 4096); +} +/* 与 storage.c 语义一致: 目标区非 0xFF → 擦扇区 → 读-改-写保留同扇区其他数据 */ +void SPI_Flash_Write(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t size) +{ + uint32_t secpos, secoff, secremain; + uint16_t i; + while (size > 0) { + secpos = WriteAddr / 4096; + secoff = WriteAddr % 4096; + secremain = 4096 - secoff; + if (size <= secremain) secremain = size; + /* 目标区是否全 0xFF */ + { + uint8_t need_erase = 0; + for (i = 0; i < secremain; i++) { + if (flash[secpos * 4096 + secoff + i] != 0xFF) { need_erase = 1; break; } + } + if (need_erase) { + uint8_t sector[4096]; + memcpy(sector, &flash[secpos * 4096], 4096); + for (i = 0; i < secremain; i++) sector[secoff + i] = pBuffer[i]; + memset(&flash[secpos * 4096], 0xFF, 4096); + for (i = 0; i < 4096; i++) flash[secpos * 4096 + i] &= sector[i]; + } else { + for (i = 0; i < secremain; i++) flash[WriteAddr + i] &= pBuffer[i]; + } + } + pBuffer += secremain; + WriteAddr += (uint32_t)secremain; + size -= (uint16_t)secremain; + } +} +uint8_t SPI_Flash_ReadJEDEC_ID(void) { return 0x16; } /* W25Q32 */ + +/* ---- mock offlog (记录最近 OTA 事件) ---- */ +static uint32_t _ota_start_cnt = 0, _ota_result_cnt = 0; +static uint32_t _last_ota_result = 0; +void offlog_evt(uint8_t type, const uint8_t *payload, uint8_t len) { (void)type; (void)payload; (void)len; } +void offlog_ota_start(uint8_t slot, uint32_t size) { _ota_start_cnt++; (void)slot; (void)size; } +void offlog_ota_result(uint32_t code) { _ota_result_cnt++; _last_ota_result = code; } +void offlog_boot(uint32_t r) { (void)r; } + +/* ---- mock loop_uart_proto ---- */ +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; +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++; } + +/* ============================================================================ + * 嵌入真实 ota_srv.c (同一编译单元, static 可见) + * ============================================================================*/ +#include "../APP/ota_srv.c" + +/* ============================================================================ + * 测试 + * ============================================================================*/ +#define CHK(cond) do { if (!(cond)) { \ + printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); return 1; } } while (0) + +/* 构造测试镜像 (5120B = 20 片) */ +static void make_image(uint8_t *buf, uint32_t size) +{ + uint32_t i; + for (i = 0; i < size; i++) buf[i] = (uint8_t)(i * 7 + 3); +} + +static int test_crc32(void) +{ + /* CRC-32/ISO-HDLC 标准向量 */ + CHK(ota_crc32((const uint8_t *)"123456789", 9) == 0xCBF43926UL); + CHK(ota_crc32(NULL, 0) == 0x00000000UL); + printf("PASS test_crc32\n"); + return 0; +} + +static int test_hex_decode(void) +{ + uint8_t out[4]; + CHK(ota_hex_decode("0102aBff", out, 4) == 0); + CHK(out[0] == 0x01 && out[1] == 0x02 && out[2] == 0xAB && out[3] == 0xFF); + CHK(ota_hex_decode("01z2", out, 2) != 0); /* 非法字符 */ + CHK(ota_hex_decode("010", out, 2) != 0); /* 长度不足 */ + printf("PASS test_hex_decode\n"); + return 0; +} + +static int test_json_extract_hex(void) +{ + char out[520]; + + /* 无空格 (紧凑格式) */ + CHK(ota_json_extract_hex( + "{\"msg_id\":402,\"cmd\":\"ota_data\",\"ts\":1,\"data\":{\"target\":\"loop\",\"offset\":0,\"crc32\":1,\"data\":\"0102aBff\"}}", + out, sizeof(out)) == 8); + CHK(strcmp(out, "0102aBff") == 0); + + /* 带空格 (json.dumps 默认) — 2026-08-20 事故场景 */ + CHK(ota_json_extract_hex( + "{\"msg_id\": 402, \"cmd\": \"ota_data\", \"ts\": 1, \"data\": {\"target\": \"loop\", \"offset\": 0, \"crc32\": 1, \"data\": \"0102aBff\"}}", + out, sizeof(out)) == 8); + CHK(strcmp(out, "0102aBff") == 0); + + /* 512 hex 满片 */ + { + char json[700]; + char hex[513]; + int i, n; + for (i = 0; i < 256; i++) sprintf(hex + i * 2, "%02x", i); + snprintf(json, sizeof(json), + "{\"msg_id\":1,\"cmd\":\"ota_data\",\"data\":{\"data\":\"%s\"}}", hex); + n = ota_json_extract_hex(json, out, sizeof(out)); + CHK(n == 512); + CHK(strcmp(out, hex) == 0); + } + + /* 找不到 data 字段 */ + CHK(ota_json_extract_hex("{\"cmd\":\"ota_status\"}", out, sizeof(out)) == 0); + CHK(out[0] == '\0'); + printf("PASS test_json_extract_hex\n"); + return 0; +} + +static int test_meta_restore(void) +{ + flash_init(); + tx_reset(); + ota_init(); + CHK(_meta.state == OTA_STATE_IDLE); + /* 写一份 downloading 元数据 → 重新 init → 恢复续传状态 */ + _meta.magic = OTA_MAGIC; + _meta.state = OTA_STATE_DOWNLOADING; + _meta.size = 512; + _meta.received = 256; + ota_meta_write(&_meta); + ota_init(); + CHK(_meta.state == OTA_STATE_DOWNLOADING); + CHK(_meta.received == 256); + printf("PASS test_meta_restore\n"); + return 0; +} + +static int test_session_flow(void) +{ + uint8_t img[5120]; + uint32_t i; + make_image(img, sizeof(img)); + flash_init(); + tx_reset(); + ota_init(); + + /* begin */ + CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "1.1.0", 0) == 0); + CHK(_meta.state == OTA_STATE_DOWNLOADING); + CHK(_meta.received == 0); + + /* 20 片下发 */ + 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(_meta.received == sizeof(img)); + + /* end → ready */ + CHK(ota_cmd_end(ota_crc32(img, sizeof(img))) == 0); + CHK(_meta.state == OTA_STATE_READY); + + /* 暂存区与镜像一致 (读回比对) */ + { + uint8_t back[5120]; + SPI_Flash_Read(back, OTA_SLOT_A_BASE, sizeof(back)); + CHK(memcmp(back, img, sizeof(img)) == 0); + } + printf("PASS test_session_flow\n"); + return 0; +} + +static int test_begin_resume(void) +{ + uint8_t img[5120]; + uint32_t i; + make_image(img, sizeof(img)); + flash_init(); + ota_init(); + + /* 下载 16 片 (4096B, 触发元数据节流 flush) 后"断电重启" */ + CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "", 0) == 0); + for (i = 0; i < 16 * 256; i += 256) { + uint8_t chunk[256]; + char hex[513]; + uint32_t j; + memcpy(chunk, &img[i], 256); + for (j = 0; j < 256; j++) sprintf(hex + j * 2, "%02x", chunk[j]); + CHK(ota_cmd_data(i, ota_crc32(chunk, 256), hex) == 0); + } + CHK(_meta.received == 16 * 256); /* 4096B, 已 flush */ + + /* 重启 */ + ota_init(); + CHK(_meta.state == OTA_STATE_DOWNLOADING); + CHK(_meta.received == 16 * 256); /* 节流点精确续传 */ + + /* 重新 begin → 续传点 */ + CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "", 0) == 0); + CHK(_meta.received == 16 * 256); + + /* 继续下载剩余 → ready */ + for (i = 16 * 256; 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); + printf("PASS test_begin_resume\n"); + return 0; +} + +static int test_dup_and_gap(void) +{ + uint8_t img[1024]; + uint8_t chunk[256]; + char hex[513]; + uint32_t j; + make_image(img, sizeof(img)); + flash_init(); + ota_init(); + CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "", 0) == 0); + + memcpy(chunk, &img[0], 256); + for (j = 0; j < 256; j++) sprintf(hex + j * 2, "%02x", chunk[j]); + CHK(ota_cmd_data(0, ota_crc32(chunk, 256), hex) == 0); + + /* 重复片幂等 */ + CHK(ota_cmd_data(0, ota_crc32(chunk, 256), hex) == 0); + CHK(_meta.received == 256); + + /* 乱序缺片 (跳 256 直接发 512) → 拒绝 */ + memcpy(chunk, &img[512], 256); + for (j = 0; j < 256; j++) sprintf(hex + j * 2, "%02x", chunk[j]); + CHK(ota_cmd_data(512, ota_crc32(chunk, 256), hex) == 1); + CHK(_meta.received == 256); + + /* 单片 CRC 错 → 拒绝 */ + memcpy(chunk, &img[256], 256); + for (j = 0; j < 256; j++) sprintf(hex + j * 2, "%02x", chunk[j]); + CHK(ota_cmd_data(256, 0xDEADBEEFUL, hex) == 1); + CHK(_meta.received == 256); + printf("PASS test_dup_and_gap\n"); + return 0; +} + +/* 模拟 bootloader: 根据收到的 A7 块回 data ACK */ +static void flash_step_until_ack(uint32_t *blocks_done, uint32_t total_blocks) +{ + /* ota_poll 推进一帧发送 + 喂 ACK, 直到收到末块 */ + uint32_t guard = 0; + while (*blocks_done < total_blocks && guard++ < 10000) { + ota_poll(); /* 发 A7 块 */ + CHK(_tx_len >= 7 && _tx_buf[0] == 0x9F); + CHK(_tx_buf[4] == 0xA7); + /* 构造 data ACK: 9F SubL SubH 02 A7 00 CHECK */ + { + uint8_t ack[7]; + uint8_t sum = 0; + ack[0] = 0x9F; ack[1] = _tx_buf[1]; ack[2] = _tx_buf[2]; + ack[3] = 0x02; ack[4] = 0xA7; ack[5] = 0x00; + sum = (uint8_t)(ack[1] + ack[2] + ack[3] + ack[4] + ack[5]); + ack[6] = sum; + ota_flash_feed_ack(ack, 7); + } + (*blocks_done)++; + } + CHK(*blocks_done == total_blocks); +} + +static int test_flash_flow(void) +{ + uint8_t img[5120]; + make_image(img, sizeof(img)); + flash_init(); + tx_reset(); + _ota_err_cnt = 0; + ota_init(); + + /* 下载 → ready */ + CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "", 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); + + /* flash: 无车 → 启动 */ + _mock_any_car = 0; + tx_reset(); + CHK(ota_cmd_flash(0, 0) == 0); + CHK(_meta.state == OTA_STATE_FLASHING); + CHK(g_ota_flash_active == 1); + CHK(g_flag_counter_ota.flag == 1); + CHK(_ota_start_cnt >= 1); /* offlog 升级开始已写 */ + + /* START → 发 A5 启动帧 */ + ota_poll(); + CHK(_tx_len == 6 && _tx_buf[0] == 0x9F && _tx_buf[4] == 0xA5 && _tx_buf[5] == 0xA7); + + /* pre_ok → ADDR */ + { + uint8_t pre_ok[7] = {0x9F, 0x01, 0x00, 0x02, 0xA5, 0x00, 0xA8}; + ota_flash_feed_ack(pre_ok, 7); + } + ota_poll(); + /* A6 地址帧: 9F 01 00 05 A6 08 00 34 00 CHECK */ + CHK(_tx_len == 10 && _tx_buf[4] == 0xA6); + CHK(_tx_buf[5] == 0x08 && _tx_buf[6] == 0x00 && _tx_buf[7] == 0x34 && _tx_buf[8] == 0x00); + /* 校验和: SubL+SubH+LEN+CMD+ADDR = 01+00+05+A6+08+00+34+00 */ + CHK(_tx_buf[9] == (uint8_t)(0x01 + 0x00 + 0x05 + 0xA6 + 0x08 + 0x00 + 0x34 + 0x00)); + + /* addr_ok → DATA 阶段 (总块数 = ceil(5120/248) = 21) */ + { + uint8_t addr_ok[7] = {0x9F, 0x01, 0x00, 0x02, 0xA6, 0x00, 0xA9}; + ota_flash_feed_ack(addr_ok, 7); + } + CHK(_fs_sub == 21); /* 首块序号 = 总块数 */ + + /* 逐块 A7 + ACK 直到完成 */ + flash_step_until_ack(&(uint32_t){0}, 21); + + /* 完成后: 恢复 0x7F 模式, 元数据 ready (镜像保留可重刷), 结果已记 */ + CHK(_meta.state == OTA_STATE_READY); + 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); /* 无失败告警 */ + printf("PASS test_flash_flow (21 blocks)\n"); + return 0; +} + +static int test_flash_reject_car(void) +{ + flash_init(); + ota_init(); + /* 直接构造 ready 态 */ + _meta.state = OTA_STATE_READY; + _meta.size = 512; + _mock_any_car = 1; + CHK(ota_cmd_flash(0, 0) == 3); /* 有车 → 拒绝 */ + CHK(g_ota_flash_active == 0); + _mock_any_car = 1; + CHK(ota_cmd_flash(0, 1) == 0); /* force → 跳过 */ + CHK(g_ota_flash_active == 1); + printf("PASS test_flash_reject_car\n"); + return 0; +} + +static int test_flash_ready_timeout(void) +{ + flash_init(); + tx_reset(); + _ota_err_cnt = 0; + ota_init(); + _meta.state = OTA_STATE_READY; + _meta.size = 512; + _mock_any_car = 0; + + CHK(ota_cmd_flash(0, 0) == 0); + ota_poll(); /* 发 A5 (首发) */ + /* 不喂 ACK: 首发后 3 次超时重发 + 第 4 个超时周期 → 失败 + 轮次: 超时1(→START) A5(2) 超时2(→START) A5(3) 超时3(→START) A5(4) 超时4(→fail) */ + { + int k; + for (k = 0; k < 7; k++) { mock_advance(1001); ota_poll(); } + } + CHK(_meta.state == OTA_STATE_FLASH_FAILED); + CHK(_meta.last_result == OTA_ERR_READY_TIMEOUT); + CHK(_ota_err_cnt == 1); /* event_report ota_error 告警 */ + CHK(_ota_err_report == OTA_ERR_READY_TIMEOUT); + CHK(g_ota_flash_active == 0); + CHK(g_flag_counter_ota.flag == 0); + CHK(_ota_result_cnt >= 1); + CHK(_last_ota_result == OTA_ERR_READY_TIMEOUT); + printf("PASS test_flash_ready_timeout\n"); + return 0; +} + +int main(void) +{ + int fails = 0; + fails += test_crc32(); + fails += test_hex_decode(); + fails += test_json_extract_hex(); + fails += test_meta_restore(); + fails += test_session_flow(); + fails += test_begin_resume(); + fails += test_dup_and_gap(); + fails += test_flash_flow(); + fails += test_flash_reject_car(); + fails += test_flash_ready_timeout(); + if (fails == 0) { + printf("\nALL PASS\n"); + return 0; + } + printf("\n%d TEST(S) FAILED\n", fails); + return 1; +}