feat(vd960DBN): Loop MCU 远程 OTA 固件实现 (MQTT V1.08, ROADMAP P1.4 ①)
先存后刷: MQTT 分片下载 → W25Qxx OTA 暂存区 (0x010000 512KB) → 全镜像 CRC32 复核 → 本地 0x9F ISP 透传刷写 Loop MCU (AT32F421) - 新增 ota_srv.c/h: CRC32(ISO-HDLC 逐位零表)/OtaMeta 双备份(节流 4KB flush)/ 会话状态机(begin/data/end/abort/flash)/本地刷写状态机(非阻塞 tick: A5→A6→A7 停等 ACK 1s×3)/安全窗口(有车拒绝 force 跳过)/失败 event_report ota_error - iot_mqtt_srv.c: 6 个 ota_* 命令分发 + 会话静默(event_report 积压/offlog·快照 落盘暂停, 结束补发) + IOT_EVT_OTA_ERROR + iot_any_car - usart_biz.c: uart_srv OTA 分支 ACK 分发 (本地刷写→ota_flash_feed_ack, BLE 透传不变) - peripheral_main.c: ota_init + ota_poll 挂主循环 - offlog: OTA_START 0x60/OTA_RESULT 0x61 审计事件 - tests/test_ota_srv.c: gcc 隔离单测 9 组全过 (mock NOR/UART2/时钟, 嵌入真实实现) 抓到 2 个真 bug: ①A7 序号未递减(bootloader 等不到末包) ②重发时 retry 清零(超时永不失败) - 语法检查: ota_srv/iot_mqtt_srv/usart_biz/offlog 0 新增错误 (基线 interrupt 假阳性除外) - devlog 置顶条目 + 待板上验证清单
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
#include "offlog.h"
|
||||
#include "snapshot.h"
|
||||
#include "fault_diag.h"
|
||||
#include "ota_srv.h"
|
||||
#include "ch32v20x_iwdg.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
@@ -73,7 +74,8 @@ static uint32_t _last_publish_ms; // 上次上报时刻 (ms)
|
||||
#define IOT_EVT_ACK_TIMEOUT_MS 5000 // 确认超时 (协议 V1.04)
|
||||
#define IOT_EVT_MAX_RETRY 3 // 最大重发次数 (含首发共4次)
|
||||
|
||||
enum { IOT_EVT_CAR_ENTER = 0, IOT_EVT_CAR_LEAVE, IOT_EVT_LOOP_CUT, IOT_EVT_LOOP_RESTORE };
|
||||
enum { IOT_EVT_CAR_ENTER = 0, IOT_EVT_CAR_LEAVE, IOT_EVT_LOOP_CUT, IOT_EVT_LOOP_RESTORE,
|
||||
IOT_EVT_OTA_ERROR }; /* V1.08: OTA 刷写失败告警 (value=OTA_ERR_*) */
|
||||
|
||||
typedef struct {
|
||||
uint8_t type; // IOT_EVT_xxx
|
||||
@@ -112,8 +114,10 @@ static void iot_evt_enqueue(uint8_t type, uint8_t ch, uint32_t value) {
|
||||
IotEvent *e = &_evt_queue[(_evt_head + _evt_count) % IOT_EVT_QUEUE_DEPTH];
|
||||
e->type = type; e->ch = ch; e->value = value;
|
||||
_evt_count++;
|
||||
/* 事件日志: 线圈事件 (主循环上下文) */
|
||||
offlog_coil((uint8_t)type + 1, ch, value); /* sub: 1=进 2=出 3=断开 4=恢复 */
|
||||
/* 事件日志: 线圈事件 (主循环上下文); OTA 会话期间暂停落盘 (V1.08) */
|
||||
if (!ota_silent_active()) {
|
||||
offlog_coil((uint8_t)type + 1, ch, value); /* sub: 1=进 2=出 3=断开 4=恢复 */
|
||||
}
|
||||
}
|
||||
_evt_gaveup = 0; // 新事件到达 → 解除挂起, 触发合并上报
|
||||
PRINT("EVT: enqueue type=%d ch=%d val=%lu (cnt=%d)\n", type, ch, value, _evt_count);
|
||||
@@ -170,7 +174,9 @@ static void iot_sensor_ingest(const LUP_SensorReport *sr) {
|
||||
iot_evt_feed(sr); // 事件沿检测
|
||||
memcpy(&_cached_sr, sr, sizeof(LUP_SensorReport)); // 刷新 loop_data 快照
|
||||
_cached_sr_valid = 1;
|
||||
snap_enqueue(sr); /* 快照入队: 中断安全, 主循环 snap_flush 落盘 */
|
||||
if (!ota_silent_active()) {
|
||||
snap_enqueue(sr); /* 快照入队: 中断安全, 主循环 snap_flush 落盘; OTA 会话暂停 (V1.08) */
|
||||
}
|
||||
FAULT_MARKER(MK_INGEST_OUT);
|
||||
}
|
||||
|
||||
@@ -187,12 +193,18 @@ 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"};
|
||||
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 *p = payload;
|
||||
uint8_t i;
|
||||
|
||||
/* OTA 会话期间静默 (V1.08): 暂停发送, 事件保留队列, 结束后补发 */
|
||||
if (ota_silent_active()) {
|
||||
PRINT("EVT: silent (OTA), skip publish\n");
|
||||
return;
|
||||
}
|
||||
|
||||
w = snprintf(p, rem, "{\"msg_id\":%lu,\"cmd\":\"event_report\",\"ts\":%lu,"
|
||||
"\"data\":{\"events\":[", id, ts);
|
||||
if (w < 0 || w >= rem) return;
|
||||
@@ -201,7 +213,7 @@ static void iot_evt_send(uint32_t id, uint32_t ts, uint8_t n) {
|
||||
for (i = 0; i < n; i++) {
|
||||
const IotEvent *e = &_evt_queue[(_evt_head + i) % IOT_EVT_QUEUE_DEPTH];
|
||||
w = snprintf(p, rem, "%s{\"type\":\"%s\",\"ch\":%d,\"value\":%lu}",
|
||||
(i > 0) ? "," : "", evt_names[e->type & 0x03], e->ch, e->value);
|
||||
(i > 0) ? "," : "", evt_names[(e->type < 5) ? e->type : 0], e->ch, e->value);
|
||||
if (w < 0 || w >= rem) return;
|
||||
p += w; rem -= w;
|
||||
}
|
||||
@@ -230,6 +242,25 @@ void iot_evt_handle_ack(uint32_t msg_id, int code) {
|
||||
_evt_retry = 0;
|
||||
}
|
||||
|
||||
/* OTA 刷写失败告警入队 (V1.08): 走 event_report ACK 闭环, 平台必答
|
||||
(ota_srv.c ota_flash_fail 调用; OTA_ERR_* 码见 ota_srv.h) */
|
||||
void iot_evt_report_ota_error(uint32_t err)
|
||||
{
|
||||
iot_evt_enqueue(IOT_EVT_OTA_ERROR, 0, err);
|
||||
PRINT("EVT: ota_error enqueue err=0x%lX\n", (unsigned long)err);
|
||||
}
|
||||
|
||||
/* 任一通道有车 (ota_flash 安全窗口检查用, 基于最新 0xC0 缓存) */
|
||||
uint8_t iot_any_car(void)
|
||||
{
|
||||
uint8_t i;
|
||||
if (!_cached_sr_valid) return 0;
|
||||
for (i = 0; i < _cached_sr.coil_count && i < LUP_COIL_COUNT; i++) {
|
||||
if (_cached_sr.coils[i].car_state) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 事件发送状态机: 主循环每轮调用 (置于 READY 检查之前, 以感知重连沿) */
|
||||
static void iot_evt_process(void) {
|
||||
static uint8_t _was_ready = 0;
|
||||
@@ -677,6 +708,182 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_
|
||||
msg_id, dev_time_now());
|
||||
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
|
||||
|
||||
} else if (strcmp(cmd_str, "ota_begin") == 0) {
|
||||
/* V1.08: 开启 OTA 会话 / 断点续传定位 (Loop MCU 远程 OTA, 先存后刷) */
|
||||
char resp[400];
|
||||
uint32_t size = 0, crc32 = 0;
|
||||
char version[16] = {0};
|
||||
uint8_t force = 0;
|
||||
int rc;
|
||||
memset(tmp, 0, sizeof(tmp));
|
||||
simple_parse_json(json, "\"size\"", tmp);
|
||||
if (strlen(tmp) > 0) size = (uint32_t)strtoul(tmp, NULL, 10);
|
||||
memset(tmp, 0, sizeof(tmp));
|
||||
simple_parse_json(json, "\"crc32\"", tmp);
|
||||
if (strlen(tmp) > 0) crc32 = (uint32_t)strtoul(tmp, NULL, 10);
|
||||
memset(tmp, 0, sizeof(tmp));
|
||||
simple_parse_json(json, "\"version\"", tmp);
|
||||
strncpy(version, tmp, sizeof(version) - 1);
|
||||
memset(tmp, 0, sizeof(tmp));
|
||||
simple_parse_json(json, "\"force\"", tmp);
|
||||
if (strlen(tmp) > 0) force = (strcmp(tmp, "true") == 0);
|
||||
rc = ota_cmd_begin(size, crc32, version, force);
|
||||
if (rc == 0) {
|
||||
const OtaMeta *m = ota_meta();
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_begin\",\"ts\":%lu,\"code\":0,\"msg\":\"success\","
|
||||
"\"data\":{\"target\":\"loop\",\"slot\":\"%s\",\"offset\":%lu,\"received\":%lu,"
|
||||
"\"size\":%lu,\"crc32\":%lu,\"state\":\"%s\"}}",
|
||||
msg_id, dev_time_now(),
|
||||
(m->slot == 0) ? "a" : "b",
|
||||
(unsigned long)m->received, (unsigned long)m->received,
|
||||
(unsigned long)m->size, (unsigned long)m->crc32,
|
||||
ota_state_str((uint8_t)m->state));
|
||||
} else {
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_begin\",\"ts\":%lu,\"code\":1,\"msg\":\"param error\","
|
||||
"\"data\":{\"target\":\"loop\",\"err_code\":%d}}",
|
||||
msg_id, dev_time_now(), (rc == 3) ? 2 : 4);
|
||||
}
|
||||
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
|
||||
PRINT("IOT: ota_begin size=%lu rc=%d\n", (unsigned long)size, rc);
|
||||
|
||||
} else if (strcmp(cmd_str, "ota_data") == 0) {
|
||||
/* V1.08: 分片下发 (256B/片, 单片 CRC32; 重复片幂等, 乱序拒收指示续传) */
|
||||
char resp[400];
|
||||
char hexbuf[512 + 1];
|
||||
const char *hp;
|
||||
uint32_t offset = 0, crc32 = 0;
|
||||
int rc;
|
||||
memset(tmp, 0, sizeof(tmp));
|
||||
simple_parse_json(json, "\"offset\"", tmp);
|
||||
if (strlen(tmp) > 0) offset = (uint32_t)strtoul(tmp, NULL, 10);
|
||||
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(hexbuf, 0, sizeof(hexbuf));
|
||||
if (hp) {
|
||||
hp += 8; /* 跳过 "data":" */
|
||||
strncpy(hexbuf, hp, sizeof(hexbuf) - 1);
|
||||
{ char *q = strchr(hexbuf, '"'); if (q) *q = '\0'; }
|
||||
}
|
||||
rc = ota_cmd_data(offset, crc32, hexbuf);
|
||||
if (rc == 0) {
|
||||
const OtaMeta *m = ota_meta();
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_data\",\"ts\":%lu,\"code\":0,\"msg\":\"success\","
|
||||
"\"data\":{\"offset\":%lu,\"received\":%lu}}",
|
||||
msg_id, dev_time_now(), (unsigned long)offset, (unsigned long)m->received);
|
||||
} else if (rc == 1) {
|
||||
const OtaMeta *m = ota_meta();
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_data\",\"ts\":%lu,\"code\":1,\"msg\":\"crc/gap error\","
|
||||
"\"data\":{\"err_code\":1,\"offset\":%lu,\"received\":%lu}}",
|
||||
msg_id, dev_time_now(), (unsigned long)m->received, (unsigned long)m->received);
|
||||
} else {
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_data\",\"ts\":%lu,\"code\":3,\"msg\":\"busy\","
|
||||
"\"data\":{\"err_code\":2}}",
|
||||
msg_id, dev_time_now());
|
||||
}
|
||||
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
|
||||
PRINT("IOT: ota_data offset=%lu rc=%d\n", (unsigned long)offset, rc);
|
||||
|
||||
} else if (strcmp(cmd_str, "ota_end") == 0) {
|
||||
/* V1.08: 结束下载, 全镜像 CRC32 复核 */
|
||||
char resp[300];
|
||||
uint32_t crc32 = 0;
|
||||
int rc;
|
||||
memset(tmp, 0, sizeof(tmp));
|
||||
simple_parse_json(json, "\"crc32\"", tmp);
|
||||
if (strlen(tmp) > 0) crc32 = (uint32_t)strtoul(tmp, NULL, 10);
|
||||
rc = ota_cmd_end(crc32);
|
||||
if (rc == 0) {
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_end\",\"ts\":%lu,\"code\":0,\"msg\":\"success\","
|
||||
"\"data\":{\"crc_ok\":true}}",
|
||||
msg_id, dev_time_now());
|
||||
} else if (rc == 5) {
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_end\",\"ts\":%lu,\"code\":5,\"msg\":\"crc mismatch\","
|
||||
"\"data\":{\"err_code\":5,\"crc_ok\":false}}",
|
||||
msg_id, dev_time_now());
|
||||
} else if (rc == 3) {
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_end\",\"ts\":%lu,\"code\":3,\"msg\":\"busy\","
|
||||
"\"data\":{\"err_code\":2}}",
|
||||
msg_id, dev_time_now());
|
||||
} else {
|
||||
const OtaMeta *m = ota_meta();
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_end\",\"ts\":%lu,\"code\":1,\"msg\":\"incomplete\","
|
||||
"\"data\":{\"offset\":%lu,\"received\":%lu}}",
|
||||
msg_id, dev_time_now(), (unsigned long)m->received, (unsigned long)m->received);
|
||||
}
|
||||
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
|
||||
PRINT("IOT: ota_end rc=%d\n", rc);
|
||||
|
||||
} else if (strcmp(cmd_str, "ota_abort") == 0) {
|
||||
/* V1.08: 中止会话, 释放暂存 */
|
||||
char resp[256];
|
||||
int rc = ota_cmd_abort();
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_abort\",\"ts\":%lu,\"code\":%d,\"msg\":\"%s\"}",
|
||||
msg_id, dev_time_now(), (rc == 0) ? 0 : 1, (rc == 0) ? "success" : "internal error");
|
||||
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
|
||||
PRINT("IOT: ota_abort rc=%d\n", rc);
|
||||
|
||||
} else if (strcmp(cmd_str, "ota_flash") == 0) {
|
||||
/* V1.08: 触发本地 ISP 刷写 (同步安全检查 → 异步执行) */
|
||||
char resp[300];
|
||||
char slot_buf[8] = {0};
|
||||
uint8_t slot = 0, force = 0;
|
||||
int rc;
|
||||
memset(tmp, 0, sizeof(tmp));
|
||||
simple_parse_json(json, "\"slot\"", tmp);
|
||||
strncpy(slot_buf, tmp, sizeof(slot_buf) - 1);
|
||||
if (strcmp(slot_buf, "b") == 0) slot = 1;
|
||||
memset(tmp, 0, sizeof(tmp));
|
||||
simple_parse_json(json, "\"force\"", tmp);
|
||||
if (strlen(tmp) > 0) force = (strcmp(tmp, "true") == 0);
|
||||
rc = ota_cmd_flash(slot, force);
|
||||
if (rc == 0) {
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_flash\",\"ts\":%lu,\"code\":0,\"msg\":\"success, flashing async\"}",
|
||||
msg_id, dev_time_now());
|
||||
} else if (rc == 3) {
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_flash\",\"ts\":%lu,\"code\":3,\"msg\":\"busy\","
|
||||
"\"data\":{\"err_code\":3}}",
|
||||
msg_id, dev_time_now());
|
||||
} else {
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_flash\",\"ts\":%lu,\"code\":1,\"msg\":\"param error\"}",
|
||||
msg_id, dev_time_now());
|
||||
}
|
||||
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
|
||||
PRINT("IOT: ota_flash slot=%d force=%d rc=%d\n", slot, force, rc);
|
||||
|
||||
} else if (strcmp(cmd_str, "ota_status") == 0) {
|
||||
/* V1.08: 查询 OTA 状态 (含刷写进度) */
|
||||
char resp[400];
|
||||
const OtaMeta *m = ota_meta();
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_status\",\"ts\":%lu,\"code\":0,\"msg\":\"success\","
|
||||
"\"data\":{\"target\":\"loop\",\"state\":\"%s\",\"slot\":\"%s\",\"size\":%lu,"
|
||||
"\"received\":%lu,\"crc32\":%lu,\"version\":\"%s\","
|
||||
"\"progress\":{\"sent\":%lu,\"total\":%lu},\"last_result\":%lu,\"last_error\":%lu}}",
|
||||
msg_id, dev_time_now(),
|
||||
ota_state_str((uint8_t)m->state), (m->slot == 0) ? "a" : "b",
|
||||
(unsigned long)m->size, (unsigned long)m->received, (unsigned long)m->crc32,
|
||||
m->version,
|
||||
(unsigned long)ota_flash_sent(), (unsigned long)ota_flash_total(),
|
||||
(unsigned long)m->last_result, (unsigned long)m->last_result);
|
||||
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
|
||||
PRINT("IOT: ota_status state=%s\n", ota_state_str((uint8_t)m->state));
|
||||
|
||||
} else if (strcmp(cmd_str, "ssc_net_query") == 0) {
|
||||
/* SSC 网络配置查询 (只读全局, 协议 V1.02) */
|
||||
char resp[400];
|
||||
|
||||
Reference in New Issue
Block a user