feat(vd960DBN): MQTT 协议 V1.10 固件实现 — 网络上报携带地感版本 (2026-08-21)
协议 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 共用总线时序
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user