From 181467b9e6024384351f075ef88a593c61be8858 Mon Sep 17 00:00:00 2001 From: wangfq Date: Fri, 21 Aug 2026 08:26:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(vd960DBN):=20MQTT=20=E5=8D=8F=E8=AE=AE=20V?= =?UTF-8?q?1.10=20=E5=9B=BA=E4=BB=B6=E5=AE=9E=E7=8E=B0=20=E2=80=94=20?= =?UTF-8?q?=E7=BD=91=E7=BB=9C=E4=B8=8A=E6=8A=A5=E6=90=BA=E5=B8=A6=E5=9C=B0?= =?UTF-8?q?=E6=84=9F=E7=89=88=E6=9C=AC=20(2026-08-21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 协议 V1.10 (3be9e50) 三处新增落地: 1. dev_info_query 响应 data 补 loop_ver/loop_hw_ver (0x4A 缓存值, 未完成/失败为空串) 2. initialize 上报同样携带 (尽力, 可为空) 3. 新增 §4.25 loop_version_query: 0x4A 异步实时查询, 响应在 iot_verq_poll() 回包 固件: - loop_uart_proto.h/c: 新增 LoopVerCache + g_lup_ver_cache (0x4A 缓存); lup_cache_from_info() 写缓存, lup_refresh_version_cache() 后台刷新 (通道空闲才发) - iot_mqtt_srv.c: dev_info_query/initialize 加字段; loop_version_query 分支 (暂存 msg_id/deadline 300ms, busy 拒绝重复查询); iot_verq_poll() 异步回包 (RESPONSE_READY -> code=0 + 三字段 + 同步刷缓存; TIMEOUT -> code=5 保缓存值); 后台刷新: 上电 iot_mqtt_init + OTA done (iot_ota_report_result ok=1) 置 dirty 关键设计: 版本来源=0x4A 实时查询 (非 OTA 元数据 version=目标版本); 缓存值语义 dev_info_query/initialize 同步回包不等异步; 与 TCP 共用 g_lup_cmd 单命令通道 (后发覆盖, 先发者超时 — 低频命令, 待板上验证时序) 单测: tests/test_iot_loop_ver.c — gcc 隔离 (extract+embed 6 函数 + mock) 44 断言全过: 版本帧解析(正常+4 错误路径) / 缓存写入+格式化(空串/NULL 防护) / verq_poll 成功回包(code=0+三字段+topic+缓存刷新+通道释放) / 超时回包(code=5+缓存值) / 后台刷新(发起/忙不消费/响应消费/残留超时清理) 验证: check_c_balance.py 3 文件 OK; 待板上验证 UART2 0x4A 与 loop_data 共用总线时序 --- .../APP/include/loop_uart_proto.h | 16 + .../APP/iot_mqtt_srv.c | 148 +++++- .../APP/loop_uart_proto.c | 25 + vd960DBN/docs/devlog.md | 40 ++ vd960DBN/tests/test_iot_loop_ver.c | 463 ++++++++++++++++++ 5 files changed, 690 insertions(+), 2 deletions(-) create mode 100644 vd960DBN/tests/test_iot_loop_ver.c diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/loop_uart_proto.h b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/loop_uart_proto.h index 24ff326..c3ec0a9 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/loop_uart_proto.h +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/loop_uart_proto.h @@ -137,6 +137,17 @@ typedef struct { char version_str[32]; // 格式化字符串 } LUP_VersionInfo; +/* 版本缓存 (V1.10) — 0x4A 查询结果缓存, 供 MQTT dev_info_query/initialize 带缓存值 */ +typedef struct { + uint8_t valid; /* 1=缓存有效 (查询成功过) */ + uint8_t hard_main; + uint8_t hard_sub; + uint8_t hard_ssub; + uint8_t soft_main; + uint8_t soft_sub; + uint8_t soft_ssub; +} LoopVerCache; + /* 命令状态机 */ typedef enum { LUP_STATE_IDLE = 0, // 空闲 @@ -160,6 +171,7 @@ typedef struct { * Global State *===========================================================================*/ extern LUP_CmdTracker g_lup_cmd; +extern LoopVerCache g_lup_ver_cache; /* V1.10: 0x4A 版本缓存 */ /*=========================================================================== * Public API @@ -194,6 +206,10 @@ void lup_cmd_done(void); int lup_cmd_check_timeout(void); void lup_cmd_on_response(const uint8_t *pkg, uint16_t len); +/* --- 版本缓存 (V1.10) --- */ +void lup_cache_from_info(const LUP_VersionInfo *info); /* 解析结果写入缓存 */ +void lup_refresh_version_cache(void); /* 后台刷新: 命令通道空闲时发 0x4A */ + /* --- High-level Commands (send + wait handled by state machine) --- */ void lup_send_get_version(void); void lup_send_reset(void); diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c index 3b41935..0999ec4 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c @@ -273,6 +273,27 @@ static uint32_t _ota_rep_ts; /* 固定 ts (重发不变) */ static uint8_t _ota_rep_times; /* 已发次数 (首发=1) */ static uint32_t _ota_rep_sent_ms; /* 上次发送时刻 */ +/* V1.10: loop_version_query 异步查询 (0x4A) 状态 — 与 TCP 共用 g_lup_cmd 单命令通道 */ +static uint8_t _iot_verq_pending; /* 1=有查询在途 (等 0x4A 响应回包) */ +static uint32_t _iot_verq_msg_id; /* 命令 msg_id (回包回显) */ +static uint32_t _iot_verq_deadline; /* 回包超时时刻 (ms), 与 TCP json_check_pending 一致 300ms */ +static uint8_t _iot_ver_cache_dirty; /* 1=需后台刷新版本缓存 (上电 / OTA done 后置位) */ + +/* 版本缓存 → "x.y.z" 字符串 (缓存无效则空串) */ +static void iot_loop_ver_str(char *out, uint16_t out_len) +{ + if (!g_lup_ver_cache.valid || !out || out_len == 0) { if (out && out_len) out[0] = '\0'; return; } + snprintf(out, out_len, "%d.%d.%d", + g_lup_ver_cache.soft_main, g_lup_ver_cache.soft_sub, g_lup_ver_cache.soft_ssub); +} + +static void iot_loop_hw_ver_str(char *out, uint16_t out_len) +{ + if (!g_lup_ver_cache.valid || !out || out_len == 0) { if (out && out_len) out[0] = '\0'; return; } + snprintf(out, out_len, "%d.%d.%d", + g_lup_ver_cache.hard_main, g_lup_ver_cache.hard_sub, g_lup_ver_cache.hard_ssub); +} + /* 序列化并发布 ota_report (首发与重发共用: 同 id/ts 同内容) */ static void iot_ota_report_send(void) { @@ -308,6 +329,7 @@ void iot_ota_report_result(uint8_t ok, uint32_t err) _ota_rep_id = ++_evt_msg_id; /* 与 event_report 同源递增 (唯一性) */ _ota_rep_ts = dev_time_now(); _ota_rep_times = 0; + if (ok) _iot_ver_cache_dirty = 1; /* V1.10: 刷写成功 → Loop 跑新固件, 后台刷新版本缓存 */ iot_ota_report_send(); } @@ -579,17 +601,24 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_ /* 目前仅实现基本响应框架, 后续逐步对接 Loop MCU 命令 */ if (strcmp(cmd_str, "dev_info_query") == 0) { + /* V1.10: data 补 loop_ver/loop_hw_ver (0x4A 缓存值, 查询未完成/失败为空串) */ + char loop_ver_str[16], loop_hw_ver_str[16]; char data_json[512]; + iot_loop_ver_str(loop_ver_str, sizeof(loop_ver_str)); + iot_loop_hw_ver_str(loop_hw_ver_str, sizeof(loop_hw_ver_str)); snprintf(data_json, sizeof(data_json), "{\"msg_id\":%lu,\"cmd\":\"dev_info_query\"," "\"ts\":%lu,\"code\":0,\"msg\":\"success\"," "\"data\":{" "\"dev_serial\":\"%s\",\"hard_ver\":\"%s\",\"soft_ver\":\"%s\"," + "\"loop_ver\":\"%s\",\"loop_hw_ver\":\"%s\"," "\"model\":\"%s\",\"product_code\":\"960001\"," "\"sub_code\":{\"net\":%s,\"iot\":%s}," "\"bus\":{\"bus1\":0,\"bus2\":0,\"bus3\":0,\"bus4\":0}}}", msg_id, dev_time_now(), - g_dev_number_str, HARDWARE_VER, FIRMWARE_VER, PRODUCT_MODEL, + g_dev_number_str, HARDWARE_VER, FIRMWARE_VER, + loop_ver_str, loop_hw_ver_str, + PRODUCT_MODEL, g_sub_code_enable.net_enable ? "true" : "false", g_sub_code_enable.iot_enable ? "true" : "false"); iot_mqtt_publish(resp_topic, data_json, strlen(data_json), 1); @@ -964,6 +993,24 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_ iot_mqtt_publish(resp_topic, _ota_io.resp, strlen(_ota_io.resp), 1); PRINT("IOT: ota_status state=%s\n", ota_state_str((uint8_t)m->state)); + } else if (strcmp(cmd_str, "loop_version_query") == 0) { + /* V1.10 §4.25: 实时查询地感 Loop 版本 — 0x4A 异步, 响应在 iot_verq_poll() 回包 + (与 TCP json_check_pending 同模式: 暂存 msg_id, 等 g_lup_cmd 状态推进) */ + if (_iot_verq_pending) { + /* 已有查询在途, 拒绝重复查询 */ + snprintf(_ota_io.resp, sizeof(_ota_io.resp), + "{\"msg_id\":%lu,\"cmd\":\"loop_version_query\",\"ts\":%lu," + "\"code\":3,\"msg\":\"busy\"}", + msg_id, dev_time_now()); + iot_mqtt_publish(resp_topic, _ota_io.resp, strlen(_ota_io.resp), 1); + } else { + lup_send_get_version(); + _iot_verq_pending = 1; + _iot_verq_msg_id = msg_id; + _iot_verq_deadline = mstick() + 300; /* 与 TCP json_check_pending 一致 */ + PRINT("IOT: loop_version_query sent (async)\n"); + } + } else if (strcmp(cmd_str, "ssc_net_query") == 0) { /* SSC 网络配置查询 (只读全局, 协议 V1.02) */ char resp[400]; @@ -1027,15 +1074,21 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_ /* 发送 initialize 上线消息 (IoT 路径) */ static void iot_send_initialize(void) { + /* V1.10: data 补 loop_ver/loop_hw_ver (0x4A 缓存值, 尽力携带可为空) */ + char loop_ver_str[16], loop_hw_ver_str[16]; char payload[512]; char topic[IOT_MQTT_TOPIC_MAX_LEN]; + iot_loop_ver_str(loop_ver_str, sizeof(loop_ver_str)); + iot_loop_hw_ver_str(loop_hw_ver_str, sizeof(loop_hw_ver_str)); snprintf(payload, sizeof(payload), "{\"msg_id\":%lu,\"cmd\":\"initialize\",\"ts\":%lu," "\"data\":{\"dev_serial\":\"%s\",\"model\":\"%s\"," "\"hard_ver\":\"%s\",\"soft_ver\":\"%s\"," + "\"loop_ver\":\"%s\",\"loop_hw_ver\":\"%s\"," "\"extra_info\":{\"code\":\"0\",\"csq\":\"0\",\"location\":\"\"}}}", (unsigned long)(g_iot_msg_id + 1), dev_time_now(), - g_iot_dev_serial, PRODUCT_MODEL, HARDWARE_VER, FIRMWARE_VER); + g_iot_dev_serial, PRODUCT_MODEL, HARDWARE_VER, FIRMWARE_VER, + loop_ver_str, loop_hw_ver_str); snprintf(topic, sizeof(topic), "dld960/%s/dev", g_iot_dev_serial); iot_mqtt_publish(topic, payload, strlen(payload), 0); } @@ -1395,9 +1448,97 @@ void iot_watchdog_kick(void) { /*=========================================================================== * 主循环轮询 *===========================================================================*/ +/* V1.10: Loop 版本查询回包 (loop_version_query 异步) + 版本缓存后台刷新 + 与 TCP 共用 g_lup_cmd 单命令通道 (后发覆盖, 先发者超时 — 低频命令可接受) */ +static void iot_verq_publish(const char *payload, uint16_t len) +{ + char topic[IOT_MQTT_TOPIC_MAX_LEN]; + snprintf(topic, sizeof(topic), "dld960/%s/dev", g_iot_dev_serial); + iot_mqtt_publish(topic, payload, len, 1); +} + +static void iot_verq_poll(void) +{ + if (_iot_verq_pending) { + /* 命令查询在途: 等 0x4A 响应或超时 */ + if (g_lup_cmd.state == LUP_STATE_RESPONSE_READY) { + LUP_VersionInfo info; + char resp[320]; + char ver_str[16], hw_str[16], vs_str[64]; + int ok = 0; + memset(&info, 0, sizeof(info)); + if (lup_parse_version(g_lup_cmd.resp_buf, g_lup_cmd.resp_len, &info) == 0) { + lup_cache_from_info(&info); /* 同步刷新缓存 */ + snprintf(ver_str, sizeof(ver_str), "%d.%d.%d", + info.soft_main, info.soft_sub, info.soft_ssub); + snprintf(hw_str, sizeof(hw_str), "%d.%d.%d", + info.hard_main, info.hard_sub, info.hard_ssub); + snprintf(vs_str, sizeof(vs_str), "V%s (HW:%s)", ver_str, hw_str); + snprintf(resp, sizeof(resp), + "{\"msg_id\":%lu,\"cmd\":\"loop_version_query\",\"ts\":%lu,\"code\":0," + "\"msg\":\"success\"," + "\"data\":{\"loop_ver\":\"%s\",\"loop_hw_ver\":\"%s\",\"version_str\":\"%s\"}}", + _iot_verq_msg_id, dev_time_now(), ver_str, hw_str, vs_str); + ok = 1; + } else { + char lv[16], lhv[16]; + iot_loop_ver_str(lv, sizeof(lv)); + iot_loop_hw_ver_str(lhv, sizeof(lhv)); + snprintf(resp, sizeof(resp), + "{\"msg_id\":%lu,\"cmd\":\"loop_version_query\",\"ts\":%lu,\"code\":5," + "\"msg\":\"parse error\"," + "\"data\":{\"loop_ver\":\"%s\",\"loop_hw_ver\":\"%s\",\"version_str\":\"\"}}", + _iot_verq_msg_id, dev_time_now(), lv, lhv); + } + lup_cmd_done(); + _iot_verq_pending = 0; + iot_verq_publish(resp, strlen(resp)); + PRINT("IOT: loop_version_query resp %s\n", ok ? "ok" : "parse error"); + } else if (g_lup_cmd.state == LUP_STATE_TIMEOUT || mstick() > _iot_verq_deadline) { + char resp[320]; + char lv[16], lhv[16]; + iot_loop_ver_str(lv, sizeof(lv)); + iot_loop_hw_ver_str(lhv, sizeof(lhv)); + snprintf(resp, sizeof(resp), + "{\"msg_id\":%lu,\"cmd\":\"loop_version_query\",\"ts\":%lu,\"code\":5," + "\"msg\":\"no response\"," + "\"data\":{\"loop_ver\":\"%s\",\"loop_hw_ver\":\"%s\",\"version_str\":\"\"}}", + _iot_verq_msg_id, dev_time_now(), lv, lhv); + lup_cmd_done(); + _iot_verq_pending = 0; + iot_verq_publish(resp, strlen(resp)); + PRINT("IOT: loop_version_query timeout\n"); + } + /* 其它 state (WAIT_RESPONSE) 继续等 */ + } else { + /* 后台缓存刷新: 上电 / OTA done 后置 dirty, 命令通道空闲时发 0x4A */ + if (_iot_ver_cache_dirty && g_lup_cmd.state == LUP_STATE_IDLE) { + lup_send_get_version(); + _iot_ver_cache_dirty = 0; + PRINT("IOT: loop version cache refresh query\n"); + } else if (!_iot_ver_cache_dirty && g_lup_cmd.state == LUP_STATE_RESPONSE_READY) { + /* 后台查询响应 (无命令 pending) → 更新缓存 + 释放通道 */ + LUP_VersionInfo info; + memset(&info, 0, sizeof(info)); + if (lup_parse_version(g_lup_cmd.resp_buf, g_lup_cmd.resp_len, &info) == 0) { + lup_cache_from_info(&info); + PRINT("IOT: loop version cache refreshed %d.%d.%d (HW:%d.%d.%d)\n", + info.soft_main, info.soft_sub, info.soft_ssub, + info.hard_main, info.hard_sub, info.hard_ssub); + } + lup_cmd_done(); + } else if (g_lup_cmd.state == LUP_STATE_TIMEOUT) { + lup_cmd_done(); /* 清残留超时态 (后台查询失败或被其它命令覆盖) */ + } + } +} + void iot_mqtt_poll(void) { iot_watchdog_kick(); // 喂硬件 IWDG + /* V1.10: Loop 版本查询回包 + 缓存后台刷新 (放最前: 断连提前 return 前也要推进状态机) */ + iot_verq_poll(); + /* 事件日志: MQTT 状态沿检测 (主循环上下文, 不在 socket 中断里写 SPI) 真复位 → 会有 BOOT 事件 + boot_seq 递增; 仅断连重连 → 只有以下网络事件 */ if (g_iot_state != _prev_iot_state) { @@ -1484,6 +1625,9 @@ void iot_mqtt_init(void) { _iot_reconnect_deadline = mstick() + 2000; // 启动后 2s 开始首次连接 g_iot_state = IOT_STATE_DISCONNECTED; + /* V1.10: 上电后台查询一次 Loop 版本 (0x4A), 缓存供 dev_info_query/initialize */ + _iot_ver_cache_dirty = 1; + /* 初始化硬件看门狗 IWDG (LSI≈40kHz, 4s 超时, 主循环喂狗) */ iot_watchdog_init(); } diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/loop_uart_proto.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/loop_uart_proto.c index f609740..8f793d0 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/loop_uart_proto.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/loop_uart_proto.c @@ -509,6 +509,31 @@ void lup_send_get_version(void) lup_cmd_begin(LUP_CMD_GET_VERSION, 200); } +/*=========================================================================== + * 版本缓存 (V1.10) — 0x4A 查询结果缓存, 供 MQTT dev_info_query/initialize + *===========================================================================*/ +LoopVerCache g_lup_ver_cache; /* BSS 清零: valid=0 */ + +void lup_cache_from_info(const LUP_VersionInfo *info) +{ + if (!info) return; + g_lup_ver_cache.valid = 1; + g_lup_ver_cache.hard_main = info->hard_main; + g_lup_ver_cache.hard_sub = info->hard_sub; + g_lup_ver_cache.hard_ssub = info->hard_ssub; + g_lup_ver_cache.soft_main = info->soft_main; + g_lup_ver_cache.soft_sub = info->soft_sub; + g_lup_ver_cache.soft_ssub = info->soft_ssub; +} + +/* 后台刷新: 仅命令通道空闲时发起 (g_lup_cmd 全局唯一, 与 TCP/MQTT 命令共用 + 单通道; 有命令在途则跳过本轮, 下次空闲再发) */ +void lup_refresh_version_cache(void) +{ + if (g_lup_cmd.state != LUP_STATE_IDLE) return; + lup_send_get_version(); +} + void lup_send_reset(void) { uint8_t buf[LUP_MAX_PKG_LEN]; diff --git a/vd960DBN/docs/devlog.md b/vd960DBN/docs/devlog.md index 59f6822..e59665b 100644 --- a/vd960DBN/docs/devlog.md +++ b/vd960DBN/docs/devlog.md @@ -4,6 +4,45 @@ > > 项目定位: DLD960 通信板 — BLE 配网、TCP JSON 协议服务、Loop MCU 串口桥接 +## 2026-08-21 — MQTT 协议 V1.10 代码落地:网络上报携带地感版本(loop_ver/loop_hw_ver) + +### 背景 + +协议 V1.10(`DLD960_IoT_MQTT协议.md`,commit 3be9e50)三处新增,本次为固件实现: +1. `dev_info_query` 响应 data 补 `loop_ver` / `loop_hw_ver`(地感 Loop 版本,0x4A 缓存值) +2. `initialize` 上报同样携带(尽力,可为空) +3. 新增 §4.25 `loop_version_query` 命令:实时查询 Loop 版本,配合远程 OTA 升级前后核对 + +### 改动 + +| 位置 | 内容 | +|------|------| +| `loop_uart_proto.h/c` | 新增 `LoopVerCache` 结构 + 全局 `g_lup_ver_cache`(0x4A 查询结果缓存);`lup_cache_from_info()` 解析结果写缓存、`lup_refresh_version_cache()` 后台刷新(仅命令通道空闲时发 0x4A) | +| `iot_mqtt_srv.c` dev_info_query | data 补 `loop_ver` / `loop_hw_ver`(缓存格式化字符串,查询未完成/失败为空串) | +| `iot_mqtt_srv.c` initialize | data 补 `loop_ver` / `loop_hw_ver`(尽力携带,可为空) | +| `iot_mqtt_srv.c` 新命令 `loop_version_query` | 0x4A 异步发起 + 暂存 msg_id / deadline(300ms);`iot_verq_poll()` 在 `g_lup_cmd.state == RESPONSE_READY` 时解析回包(code=0 + loop_ver/loop_hw_ver/version_str)并同步刷新缓存;TIMEOUT/超时回 code=5(loop_ver 保持缓存值) | +| `iot_mqtt_srv.c` 后台刷新 | 上电 `iot_mqtt_init()` 置 dirty + OTA done(`iot_ota_report_result ok=1`)置 dirty;poll 在命令通道空闲时自动发 0x4A 刷新缓存 | + +### 关键设计 + +- **版本来源 = 0x4A 实时查询**(`lup_parse_version`),不是 OTA 元数据 `version`——那是"目标版本"(审计用),0x4A 才是 Loop 当前真实固件版本 +- **缓存值语义**:`dev_info_query`/`initialize` 带缓存值(上电自动查一次 + OTA done 后刷新);MQTT 命令处理是同步回包,不能干等 0x4A 异步响应,故查询未完成/失败回空串;平台要最新版本走 `loop_version_query` 实时查询 +- **异步回包**:与 TCP `json_check_pending` 同模式——暂存 msg_id(回包 topic 固定 `dld960/{sn}/dev`,无需存 topic),等 `g_lup_cmd.state` 状态机推进 +- **单命令通道互斥**:`g_lup_cmd` 全局唯一,TCP/MQTT/后台刷新共用;后发覆盖、先发者超时(低频命令,理论无冲突) + +### 验证 + +- gcc 隔离单测 `tests/test_iot_loop_ver.c` **44 断言全过**: + - 版本帧解析:正常帧(Status/Hard=1.0.0/Soft=1.2.3)+ 错误路径(len<10 / bad magic / bad cmd / LEN<7) + - 缓存:`lup_cache_from_info` 写入、`iot_loop_ver_str`/`iot_loop_hw_ver_str` 格式化(含无效缓存空串、NULL 防护) + - verq_poll 命令成功回包:code=0 + 三字段 + msg_id 回显 + topic 正确 + 缓存同步刷新 + 通道释放 + - 超时回包:code=5 + `no response` + 缓存值保留 + - 后台刷新:dirty=1+IDLE 发起查询(不设 pending)、通道忙不消费、响应消费更新缓存、残留超时清理 +- `check_c_balance.py` 三个改动文件语法平衡 OK +- **待板上验证**:0x4A 在 MQTT 工作期间与 `loop_data`/事件上报共用 UART2 总线的时序(查询是低频命令,理论无冲突);OTA done 后缓存刷新(Loop 复位时间窗内查询可能超时,可接受——平台可用 `loop_version_query` 主动核对) + +--- + ## 2026-08-20 — 联网 SocketSend 0x11 死循环修复:发送退避 + SocketCreat 失败中止 ### 现象(板级) @@ -1605,6 +1644,7 @@ TCP 超时要等 ~2 分钟才触发 `SINT_STAT_TIM_OUT`, | 版本 | 时间 | 说明 | |------|------|------| +| V4.8 | 2026-08-21 | MQTT 协议 V1.10 固件实现: dev_info_query/initialize 补 loop_ver/loop_hw_ver + 新命令 loop_version_query 异步回包 + 版本缓存(上电/OTA done 刷新), 单测 44 例 | | V4.7 | 2026-08-19 | 固件版本 1.02.03→1.02.04 (SPI 存储适配: 去厂商识别 + 恢复 factory 写入, 板级验证通过) | | V4.6 | 2026-08-19 | 恢复 factory 配置写入: 解除 8-13 止血, 空片首启写 magic+默认参数 (换 W25Q128 后配置永久丢失修复) | | V4.5 | 2026-08-19 | SPI Flash 识别去厂商代码: W25Qxx 宏去 0XEF 前缀, ReadJEDEC_ID 删厂商校验, storage_init 低字节匹配 (兼容多厂家) | diff --git a/vd960DBN/tests/test_iot_loop_ver.c b/vd960DBN/tests/test_iot_loop_ver.c new file mode 100644 index 0000000..995f3ea --- /dev/null +++ b/vd960DBN/tests/test_iot_loop_ver.c @@ -0,0 +1,463 @@ +/* + * test_iot_loop_ver.c — V1.10 验证: 0x4A 版本解析 + 版本缓存 + MQTT 异步回包时序 + * + * 覆盖: + * 1. lup_parse_version: 0x4A 响应帧解析 (字段/错误路径) + * 2. lup_cache_from_info / iot_loop_ver_str / iot_loop_hw_ver_str: 缓存写入与格式化 + * 3. iot_verq_poll 异步回包时序: + * - 命令 pending + RESPONSE_READY → code=0 回包 + 缓存刷新 + 通道释放 + * - 命令 pending + TIMEOUT → code=5 回包 + * - 后台刷新: dirty=1 + IDLE → 发起 0x4A (不设 pending) + * - 后台响应消费: !dirty + RESPONSE_READY → 更新缓存 + 释放 + * - 残留超时清理: TIMEOUT → 归位 IDLE + * + * 编译: gcc -o test_iot_loop_ver test_iot_loop_ver.c && ./test_iot_loop_ver + */ +#include +#include +#include +#include + +/* ---- mock: 最小依赖 ---- */ +#define LUP_MAGIC 0x7F +#define LUP_CMD_GET_VERSION 0x4A +#define LUP_MAX_PKG_LEN 70 +#define IOT_MQTT_TOPIC_MAX_LEN 64 + +typedef struct { + uint8_t status; + uint8_t hard_main, hard_sub, hard_ssub; + uint8_t soft_main, soft_sub, soft_ssub; + char version_str[32]; +} LUP_VersionInfo; + +typedef struct { + uint8_t valid; + uint8_t hard_main, hard_sub, hard_ssub; + uint8_t soft_main, soft_sub, soft_ssub; +} LoopVerCache; + +typedef enum { + LUP_STATE_IDLE = 0, + LUP_STATE_WAIT_RESPONSE, + LUP_STATE_RESPONSE_READY, + LUP_STATE_TIMEOUT +} LUP_CmdState; + +typedef struct { + uint8_t pending_cmd; + uint32_t send_tick; + uint32_t timeout_ms; + LUP_CmdState state; + uint8_t resp_buf[LUP_MAX_PKG_LEN]; + uint16_t resp_len; +} LUP_CmdTracker; + +/* ---- mock 全局 ---- */ +static LUP_CmdTracker g_lup_cmd; +static LoopVerCache g_lup_ver_cache; +static char g_iot_dev_serial[16] = "A1B2C3D4E5F6"; + +/* ---- mock 时间 ---- */ +static uint32_t g_mock_ms = 100000; +static uint32_t mstick(void) { return g_mock_ms; } +static uint32_t dev_time_now(void) { return 1719000008UL; } + +/* ---- mock 发布捕获 ---- */ +static char g_pub_topic[64]; +static char g_pub_payload[512]; +static uint16_t g_pub_len; +static int g_pub_count; +static int g_getver_calls; + +static int iot_mqtt_publish(const char *topic, const char *payload, + uint16_t payload_len, uint8_t qos) +{ + (void)qos; + g_pub_count++; + snprintf(g_pub_topic, sizeof(g_pub_topic), "%s", topic ? topic : ""); + if (payload_len < sizeof(g_pub_payload)) { + memcpy(g_pub_payload, payload, payload_len); + g_pub_payload[payload_len] = '\0'; + } else { + memcpy(g_pub_payload, payload, sizeof(g_pub_payload) - 1); + g_pub_payload[sizeof(g_pub_payload) - 1] = '\0'; + } + g_pub_len = payload_len; + return 0; +} + +/* mock lup_send_get_version: 模拟 lup_cmd_begin 的状态推进 */ +static void lup_send_get_version(void) +{ + g_getver_calls++; + g_lup_cmd.pending_cmd = LUP_CMD_GET_VERSION; + g_lup_cmd.state = LUP_STATE_WAIT_RESPONSE; + g_lup_cmd.resp_len = 0; + memset(g_lup_cmd.resp_buf, 0, sizeof(g_lup_cmd.resp_buf)); +} + +/* mock lup_cmd_done: 释放命令通道 */ +static void lup_cmd_done(void) +{ + g_lup_cmd.pending_cmd = 0; + g_lup_cmd.state = LUP_STATE_IDLE; + g_lup_cmd.resp_len = 0; +} + +#define PRINT(...) printf(__VA_ARGS__) + +/* ---- 被测静态状态 (从 iot_mqtt_srv.c 提取的变量, 单测转为可访问全局) ---- */ +static uint8_t _iot_verq_pending; +static uint32_t _iot_verq_msg_id; +static uint32_t _iot_verq_deadline; +static uint8_t _iot_ver_cache_dirty; + + +/* ================= 嵌入被测函数 (从真实源码提取) ================= */ + +int lup_parse_version(const uint8_t *pkg, uint16_t len, LUP_VersionInfo *info) +{ + if (len < 10) return -1; // 1+3+7+2 = 13 min + + // pkg layout: [7F][Addr=0][LEN=8][CMD=0x4A][Status...7bytes][XOR][SUM] + if (pkg[0] != LUP_MAGIC || pkg[3] != LUP_CMD_GET_VERSION) return -2; + if (pkg[2] < 7) return -3; // LEN should be >= 7 + + const uint8_t *val = pkg + 4; // skip magic + header(3) + info->status = val[0]; + info->hard_main = val[1]; + info->hard_sub = val[2]; + info->hard_ssub = val[3]; + info->soft_main = val[4]; + info->soft_sub = val[5]; + info->soft_ssub = val[6]; + + return 0; +} + +void lup_cache_from_info(const LUP_VersionInfo *info) +{ + if (!info) return; + g_lup_ver_cache.valid = 1; + g_lup_ver_cache.hard_main = info->hard_main; + g_lup_ver_cache.hard_sub = info->hard_sub; + g_lup_ver_cache.hard_ssub = info->hard_ssub; + g_lup_ver_cache.soft_main = info->soft_main; + g_lup_ver_cache.soft_sub = info->soft_sub; + g_lup_ver_cache.soft_ssub = info->soft_ssub; +} + +static void iot_loop_ver_str(char *out, uint16_t out_len) +{ + if (!g_lup_ver_cache.valid || !out || out_len == 0) { if (out && out_len) out[0] = '\0'; return; } + snprintf(out, out_len, "%d.%d.%d", + g_lup_ver_cache.soft_main, g_lup_ver_cache.soft_sub, g_lup_ver_cache.soft_ssub); +} + +static void iot_loop_hw_ver_str(char *out, uint16_t out_len) +{ + if (!g_lup_ver_cache.valid || !out || out_len == 0) { if (out && out_len) out[0] = '\0'; return; } + snprintf(out, out_len, "%d.%d.%d", + g_lup_ver_cache.hard_main, g_lup_ver_cache.hard_sub, g_lup_ver_cache.hard_ssub); +} + +static void iot_verq_publish(const char *payload, uint16_t len) +{ + char topic[IOT_MQTT_TOPIC_MAX_LEN]; + snprintf(topic, sizeof(topic), "dld960/%s/dev", g_iot_dev_serial); + iot_mqtt_publish(topic, payload, len, 1); +} + +static void iot_verq_poll(void) +{ + if (_iot_verq_pending) { + /* 命令查询在途: 等 0x4A 响应或超时 */ + if (g_lup_cmd.state == LUP_STATE_RESPONSE_READY) { + LUP_VersionInfo info; + char resp[320]; + char ver_str[16], hw_str[16], vs_str[64]; + int ok = 0; + memset(&info, 0, sizeof(info)); + if (lup_parse_version(g_lup_cmd.resp_buf, g_lup_cmd.resp_len, &info) == 0) { + lup_cache_from_info(&info); /* 同步刷新缓存 */ + snprintf(ver_str, sizeof(ver_str), "%d.%d.%d", + info.soft_main, info.soft_sub, info.soft_ssub); + snprintf(hw_str, sizeof(hw_str), "%d.%d.%d", + info.hard_main, info.hard_sub, info.hard_ssub); + snprintf(vs_str, sizeof(vs_str), "V%s (HW:%s)", ver_str, hw_str); + snprintf(resp, sizeof(resp), + "{\"msg_id\":%lu,\"cmd\":\"loop_version_query\",\"ts\":%lu,\"code\":0," + "\"msg\":\"success\"," + "\"data\":{\"loop_ver\":\"%s\",\"loop_hw_ver\":\"%s\",\"version_str\":\"%s\"}}", + _iot_verq_msg_id, dev_time_now(), ver_str, hw_str, vs_str); + ok = 1; + } else { + char lv[16], lhv[16]; + iot_loop_ver_str(lv, sizeof(lv)); + iot_loop_hw_ver_str(lhv, sizeof(lhv)); + snprintf(resp, sizeof(resp), + "{\"msg_id\":%lu,\"cmd\":\"loop_version_query\",\"ts\":%lu,\"code\":5," + "\"msg\":\"parse error\"," + "\"data\":{\"loop_ver\":\"%s\",\"loop_hw_ver\":\"%s\",\"version_str\":\"\"}}", + _iot_verq_msg_id, dev_time_now(), lv, lhv); + } + lup_cmd_done(); + _iot_verq_pending = 0; + iot_verq_publish(resp, strlen(resp)); + PRINT("IOT: loop_version_query resp %s\n", ok ? "ok" : "parse error"); + } else if (g_lup_cmd.state == LUP_STATE_TIMEOUT || mstick() > _iot_verq_deadline) { + char resp[320]; + char lv[16], lhv[16]; + iot_loop_ver_str(lv, sizeof(lv)); + iot_loop_hw_ver_str(lhv, sizeof(lhv)); + snprintf(resp, sizeof(resp), + "{\"msg_id\":%lu,\"cmd\":\"loop_version_query\",\"ts\":%lu,\"code\":5," + "\"msg\":\"no response\"," + "\"data\":{\"loop_ver\":\"%s\",\"loop_hw_ver\":\"%s\",\"version_str\":\"\"}}", + _iot_verq_msg_id, dev_time_now(), lv, lhv); + lup_cmd_done(); + _iot_verq_pending = 0; + iot_verq_publish(resp, strlen(resp)); + PRINT("IOT: loop_version_query timeout\n"); + } + /* 其它 state (WAIT_RESPONSE) 继续等 */ + } else { + /* 后台缓存刷新: 上电 / OTA done 后置 dirty, 命令通道空闲时发 0x4A */ + if (_iot_ver_cache_dirty && g_lup_cmd.state == LUP_STATE_IDLE) { + lup_send_get_version(); + _iot_ver_cache_dirty = 0; + PRINT("IOT: loop version cache refresh query\n"); + } else if (!_iot_ver_cache_dirty && g_lup_cmd.state == LUP_STATE_RESPONSE_READY) { + /* 后台查询响应 (无命令 pending) → 更新缓存 + 释放通道 */ + LUP_VersionInfo info; + memset(&info, 0, sizeof(info)); + if (lup_parse_version(g_lup_cmd.resp_buf, g_lup_cmd.resp_len, &info) == 0) { + lup_cache_from_info(&info); + PRINT("IOT: loop version cache refreshed %d.%d.%d (HW:%d.%d.%d)\n", + info.soft_main, info.soft_sub, info.soft_ssub, + info.hard_main, info.hard_sub, info.hard_ssub); + } + lup_cmd_done(); + } else if (g_lup_cmd.state == LUP_STATE_TIMEOUT) { + lup_cmd_done(); /* 清残留超时态 (后台查询失败或被其它命令覆盖) */ + } + } +} + +/* ================= 测试工具 ================= */ +static int g_pass = 0, g_fail = 0; +#define CHECK(cond, msg) do { \ + if (cond) { g_pass++; printf(" PASS: %s\n", msg); } \ + else { g_fail++; printf(" FAIL: %s (line %d)\n", msg, __LINE__); } \ +} while (0) + +static void reset_state(void) +{ + memset(&g_lup_cmd, 0, sizeof(g_lup_cmd)); + memset(&g_lup_ver_cache, 0, sizeof(g_lup_ver_cache)); + memset(&g_pub_payload, 0, sizeof(g_pub_payload)); + g_pub_count = 0; + g_pub_len = 0; + g_getver_calls = 0; + _iot_verq_pending = 0; + _iot_verq_msg_id = 0; + _iot_verq_deadline = 0; + _iot_ver_cache_dirty = 0; + g_mock_ms = 100000; +} + +/* 构造 0x4A 响应帧: 7F 00 LEN 4A [status][hard_m][hard_s][hard_ss][soft_m][soft_s][soft_ss] [xor][sum] + (checksum 字节 lup_parse_version 不校验, 填 0) */ +static void build_version_frame(uint8_t *pkg, uint16_t *len, + uint8_t status, uint8_t hm, uint8_t hs, uint8_t hss, + uint8_t sm, uint8_t ss, uint8_t sss) +{ + pkg[0] = LUP_MAGIC; + pkg[1] = 0x00; + pkg[2] = 0x08; + pkg[3] = LUP_CMD_GET_VERSION; + pkg[4] = status; + pkg[5] = hm; pkg[6] = hs; pkg[7] = hss; + pkg[8] = sm; pkg[9] = ss; pkg[10] = sss; + pkg[11] = 0; pkg[12] = 0; /* XOR/SUM 不校验 */ + *len = 13; +} + +static void test_parse_version(void) +{ + printf("[1] lup_parse_version\n"); + uint8_t pkg[16]; + uint16_t len; + LUP_VersionInfo info; + + build_version_frame(pkg, &len, 0x00, 1,0,0, 1,2,3); + CHECK(lup_parse_version(pkg, len, &info) == 0, "valid frame returns 0"); + CHECK(info.status == 0x00, "status=0"); + CHECK(info.hard_main == 1 && info.hard_sub == 0 && info.hard_ssub == 0, "hard=1.0.0"); + CHECK(info.soft_main == 1 && info.soft_sub == 2 && info.soft_ssub == 3, "soft=1.2.3"); + + /* 错误路径 */ + CHECK(lup_parse_version(pkg, 9, &info) == -1, "len<10 -> -1"); + pkg[0] = 0x00; + CHECK(lup_parse_version(pkg, len, &info) == -2, "bad magic -> -2"); + pkg[0] = LUP_MAGIC; + pkg[3] = 0x55; + CHECK(lup_parse_version(pkg, len, &info) == -2, "bad cmd -> -2"); + pkg[3] = LUP_CMD_GET_VERSION; + pkg[2] = 0x05; + CHECK(lup_parse_version(pkg, len, &info) == -3, "LEN<7 -> -3"); +} + +static void test_cache(void) +{ + printf("[2] 版本缓存\n"); + LUP_VersionInfo info; + char out[24]; + + memset(&info, 0, sizeof(info)); + info.hard_main = 1; info.hard_sub = 0; info.hard_ssub = 0; + info.soft_main = 1; info.soft_sub = 2; info.soft_ssub = 3; + lup_cache_from_info(&info); + CHECK(g_lup_ver_cache.valid == 1, "cache valid after write"); + CHECK(g_lup_ver_cache.soft_main == 1 && g_lup_ver_cache.soft_ssub == 3, "cache soft fields"); + + iot_loop_ver_str(out, sizeof(out)); + CHECK(strcmp(out, "1.2.3") == 0, "loop_ver str = 1.2.3"); + iot_loop_hw_ver_str(out, sizeof(out)); + CHECK(strcmp(out, "1.0.0") == 0, "loop_hw_ver str = 1.0.0"); + + /* 无效缓存 → 空串 */ + memset(&g_lup_ver_cache, 0, sizeof(g_lup_ver_cache)); + iot_loop_ver_str(out, sizeof(out)); + CHECK(strcmp(out, "") == 0, "invalid cache -> empty str"); + iot_loop_hw_ver_str(out, sizeof(out)); + CHECK(strcmp(out, "") == 0, "invalid hw cache -> empty str"); + + /* NULL/0 长度防护 */ + iot_loop_ver_str(NULL, 0); + CHECK(1, "NULL guard no crash"); +} + +static void test_verq_success(void) +{ + printf("[3] verq_poll: 命令成功回包\n"); + reset_state(); + uint8_t pkg[16]; + uint16_t len; + build_version_frame(pkg, &len, 0x00, 1,0,0, 1,2,3); + memcpy(g_lup_cmd.resp_buf, pkg, len); + g_lup_cmd.resp_len = len; + g_lup_cmd.state = LUP_STATE_RESPONSE_READY; + + _iot_verq_pending = 1; + _iot_verq_msg_id = 407; + _iot_verq_deadline = g_mock_ms + 300; + + iot_verq_poll(); + + CHECK(g_pub_count == 1, "publish once"); + CHECK(strstr(g_pub_payload, "\"msg_id\":407") != NULL, "msg_id echoed"); + CHECK(strstr(g_pub_payload, "\"code\":0") != NULL, "code=0"); + CHECK(strstr(g_pub_payload, "\"loop_ver\":\"1.2.3\"") != NULL, "loop_ver=1.2.3"); + CHECK(strstr(g_pub_payload, "\"loop_hw_ver\":\"1.0.0\"") != NULL, "loop_hw_ver=1.0.0"); + CHECK(strstr(g_pub_payload, "\"version_str\":\"V1.2.3 (HW:1.0.0)\"") != NULL, "version_str format"); + CHECK(strcmp(g_pub_topic, "dld960/A1B2C3D4E5F6/dev") == 0, "resp topic"); + CHECK(_iot_verq_pending == 0, "pending cleared"); + CHECK(g_lup_cmd.state == LUP_STATE_IDLE, "cmd channel released"); + CHECK(g_lup_ver_cache.valid == 1, "cache refreshed by command resp"); +} + +static void test_verq_timeout(void) +{ + printf("[4] verq_poll: 命令超时\n"); + reset_state(); + /* 缓存先有效, 超时回包应携带缓存值 */ + g_lup_ver_cache.valid = 1; + g_lup_ver_cache.soft_main = 9; g_lup_ver_cache.soft_sub = 9; g_lup_ver_cache.soft_ssub = 9; + g_lup_ver_cache.hard_main = 8; g_lup_ver_cache.hard_sub = 8; g_lup_ver_cache.hard_ssub = 8; + + g_lup_cmd.state = LUP_STATE_WAIT_RESPONSE; /* 还挂着 */ + _iot_verq_pending = 1; + _iot_verq_msg_id = 408; + _iot_verq_deadline = g_mock_ms + 300; + g_mock_ms += 400; /* 超过 deadline */ + + iot_verq_poll(); + + CHECK(g_pub_count == 1, "timeout publish once"); + CHECK(strstr(g_pub_payload, "\"code\":5") != NULL, "code=5"); + CHECK(strstr(g_pub_payload, "\"msg\":\"no response\"") != NULL, "msg=no response"); + CHECK(strstr(g_pub_payload, "\"loop_ver\":\"9.9.9\"") != NULL, "cache value kept in resp"); + CHECK(_iot_verq_pending == 0, "pending cleared"); + CHECK(g_lup_cmd.state == LUP_STATE_IDLE, "cmd released after timeout"); + + /* 直接 TIMEOUT 态 */ + reset_state(); + g_lup_cmd.state = LUP_STATE_TIMEOUT; + _iot_verq_pending = 1; + _iot_verq_msg_id = 409; + _iot_verq_deadline = g_mock_ms + 300; + iot_verq_poll(); + CHECK(g_pub_count == 1, "TIMEOUT state publish once"); + CHECK(strstr(g_pub_payload, "\"code\":5") != NULL, "TIMEOUT state code=5"); +} + +static void test_bg_refresh(void) +{ + printf("[5] verq_poll: 后台缓存刷新\n"); + reset_state(); + + /* 5a. dirty=1 + IDLE → 发起查询, 不设 pending */ + _iot_ver_cache_dirty = 1; + g_lup_cmd.state = LUP_STATE_IDLE; + iot_verq_poll(); + CHECK(g_getver_calls == 1, "bg query issued"); + CHECK(_iot_ver_cache_dirty == 0, "dirty cleared"); + CHECK(_iot_verq_pending == 0, "no command pending set"); + CHECK(g_pub_count == 0, "no publish for bg query"); + + /* 5b. 通道忙 (WAIT_RESPONSE) 时 dirty 不消费 */ + { + int base = g_getver_calls; + _iot_ver_cache_dirty = 1; + g_lup_cmd.state = LUP_STATE_WAIT_RESPONSE; + iot_verq_poll(); + CHECK(g_getver_calls == base, "busy channel -> no query"); + CHECK(_iot_ver_cache_dirty == 1, "dirty kept while busy"); + } + + /* 5c. 后台响应消费: !dirty + RESPONSE_READY → 更新缓存 + 释放, 不回包 */ + reset_state(); + uint8_t pkg[16]; + uint16_t len; + build_version_frame(pkg, &len, 0x00, 2,0,0, 3,4,5); + memcpy(g_lup_cmd.resp_buf, pkg, len); + g_lup_cmd.resp_len = len; + g_lup_cmd.state = LUP_STATE_RESPONSE_READY; + _iot_ver_cache_dirty = 0; + iot_verq_poll(); + CHECK(g_pub_count == 0, "bg resp no publish"); + CHECK(g_lup_ver_cache.valid == 1, "bg resp cache valid"); + CHECK(g_lup_ver_cache.soft_main == 3 && g_lup_ver_cache.soft_ssub == 5, "bg resp soft=3.4.5"); + CHECK(g_lup_cmd.state == LUP_STATE_IDLE, "bg resp channel released"); + + /* 5d. 残留超时清理 */ + reset_state(); + g_lup_cmd.state = LUP_STATE_TIMEOUT; + iot_verq_poll(); + CHECK(g_lup_cmd.state == LUP_STATE_IDLE, "stale timeout cleaned"); +} + +int main(void) +{ + printf("=== test_iot_loop_ver: V1.10 版本缓存 + 异步回包 ===\n"); + reset_state(); + test_parse_version(); + test_cache(); + test_verq_success(); + test_verq_timeout(); + test_bg_refresh(); + printf("\nRESULT: %d passed, %d failed\n", g_pass, g_fail); + return g_fail ? 1 : 0; +}