From f1c9358aaddc374a6552142a9e2a1bf34fe43940 Mon Sep 17 00:00:00 2001 From: wangfq Date: Tue, 18 Aug 2026 09:01:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(vd960DBN):=20TCP/MQTT=20log=5F*=20?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E6=94=AF=E6=8C=81=E5=BF=AB=E7=85=A7=E6=B5=81?= =?UTF-8?q?=20stream=3Dsnapshot=20(2026-08-18)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - snapshot.c/h: 新增 snap_rec_to_json() — SnapRec 64B → JSON (channels 对齐 0xC0, variation 3B 符号扩展, misc_type 全枚举) - tcp_json_srv.c: handle_log_stat/query/clear 加 stream 解析 + snapshot 分支 - iot_mqtt_srv.c: log_stat/query/clear 加 stream 解析 + snapshot 分支 - 事件流 count=0 按上限处理 (与 BLE 对齐); 快照 QUERY count≤2, CLEAR ~45ms - 新增 gcc 隔离单测 tests/test_snap_to_json.c 34 断言全过 - devlog V4.1 --- .../APP/include/snapshot.h | 1 + .../APP/iot_mqtt_srv.c | 124 +++++++++---- .../OnlyUpdateApp_Peripheral/APP/snapshot.c | 59 +++++++ .../APP/tcp_json_srv.c | 124 +++++++++---- vd960DBN/docs/devlog.md | 37 ++++ vd960DBN/tests/test_snap_to_json.c | 164 ++++++++++++++++++ 6 files changed, 444 insertions(+), 65 deletions(-) create mode 100644 vd960DBN/tests/test_snap_to_json.c diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/snapshot.h b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/snapshot.h index 3db41ed..055ccd4 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/snapshot.h +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/snapshot.h @@ -133,5 +133,6 @@ uint32_t snap_seq_last(void); /* 最新一条记录全局序 uint32_t snap_boot_seq(void); /* 当前 boot 序号 */ int snap_read_idx(uint32_t idx, SnapRec *out); /* 按逻辑序号读, 0=成功 -1=越界 */ void snap_clear(void); /* 清空快照区 (审计写入事件流) */ +int snap_rec_to_json(const SnapRec *r, char *buf, int buf_len); /* 记录 -> JSON (TCP/MQTT log_query) */ #endif /* _SNAPSHOT_H__ */ diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c index 6a4a115..a0b9bab 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c @@ -549,32 +549,52 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_ } } else if (strcmp(cmd_str, "log_stat") == 0) { - /* 日志统计/分页定位: seq_first = seq_last - count + 1 */ - uint32_t total = offlog_count(); - uint32_t seq_last = offlog_seq_last(); - uint32_t seq_first = (total > 0) ? (seq_last - total + 1) : 0; + /* 日志统计/分页定位: event/snapshot 流 (协议 V1.07) */ + char stream_buf[16]; + memset(stream_buf, 0, sizeof(stream_buf)); + simple_parse_json(json, "\"stream\"", stream_buf); char resp[300]; - snprintf(resp, sizeof(resp), - "{\"msg_id\":%lu,\"cmd\":\"log_stat\"," - "\"ts\":%lu,\"code\":0,\"msg\":\"success\"," - "\"data\":{\"stream\":\"event\",\"enabled\":%s,\"boot_seq\":%lu," - "\"count\":%lu,\"capacity\":%u,\"seq_first\":%lu,\"seq_last\":%lu}}", - msg_id, dev_time_now(), - offlog_enabled() ? "true" : "false", - (unsigned long)offlog_boot_seq(), - (unsigned long)total, (unsigned)OFFLOG_MAX_RECORDS, - (unsigned long)seq_first, (unsigned long)seq_last); + if (strcmp(stream_buf, "snapshot") == 0) { + uint32_t total = snap_count(); + uint32_t seq_last = snap_seq_last(); + uint32_t seq_first = (total > 0) ? (seq_last - total + 1) : 0; + snprintf(resp, sizeof(resp), + "{\"msg_id\":%lu,\"cmd\":\"log_stat\"," + "\"ts\":%lu,\"code\":0,\"msg\":\"success\"," + "\"data\":{\"stream\":\"snapshot\",\"enabled\":%s,\"boot_seq\":%lu," + "\"count\":%lu,\"capacity\":%lu,\"seq_first\":%lu,\"seq_last\":%lu}}", + msg_id, dev_time_now(), + snap_enabled() ? "true" : "false", + (unsigned long)snap_boot_seq(), + (unsigned long)total, (unsigned long)SNAP_MAX_RECORDS, + (unsigned long)seq_first, (unsigned long)seq_last); + PRINT("IOT: log_stat(snapshot) count=%lu seq_first=%lu seq_last=%lu\n", + (unsigned long)total, (unsigned long)seq_first, (unsigned long)seq_last); + } else { + uint32_t total = offlog_count(); + uint32_t seq_last = offlog_seq_last(); + uint32_t seq_first = (total > 0) ? (seq_last - total + 1) : 0; + snprintf(resp, sizeof(resp), + "{\"msg_id\":%lu,\"cmd\":\"log_stat\"," + "\"ts\":%lu,\"code\":0,\"msg\":\"success\"," + "\"data\":{\"stream\":\"event\",\"enabled\":%s,\"boot_seq\":%lu," + "\"count\":%lu,\"capacity\":%lu,\"seq_first\":%lu,\"seq_last\":%lu}}", + msg_id, dev_time_now(), + offlog_enabled() ? "true" : "false", + (unsigned long)offlog_boot_seq(), + (unsigned long)total, (unsigned long)OFFLOG_MAX_RECORDS, + (unsigned long)seq_first, (unsigned long)seq_last); + PRINT("IOT: log_stat count=%lu seq_first=%lu seq_last=%lu\n", + (unsigned long)total, (unsigned long)seq_first, (unsigned long)seq_last); + } iot_mqtt_publish(resp_topic, resp, strlen(resp), 1); - PRINT("IOT: log_stat count=%lu seq_first=%lu seq_last=%lu\n", - (unsigned long)total, (unsigned long)seq_first, (unsigned long)seq_last); } else if (strcmp(cmd_str, "log_query") == 0) { - /* 分页拉取脱机日志: 按全局序号, count≤4 (500B 发布限制) */ + /* 分页拉取脱机日志: 按全局序号, event/snapshot 流 (协议 V1.07) */ static char resp[IOT_MQTT_SEND_BUF_LEN]; /* static: 避免大栈开销 */ + char stream_buf[16]; uint32_t start_seq = 0, req_count = 0; - uint32_t total = offlog_count(); - uint32_t seq_last = offlog_seq_last(); - uint32_t seq_first = (total > 0) ? (seq_last - total + 1) : 0; + int is_snap = 0; memset(tmp, 0, sizeof(tmp)); simple_parse_json(json, "\"start_seq\"", tmp); @@ -582,38 +602,74 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_ memset(tmp, 0, sizeof(tmp)); simple_parse_json(json, "\"count\"", tmp); if (strlen(tmp) > 0) req_count = (uint32_t)strtoul(tmp, NULL, 10); - if (req_count > OFFLOG_MAX_QUERY_RECORDS) req_count = OFFLOG_MAX_QUERY_RECORDS; + memset(stream_buf, 0, sizeof(stream_buf)); + simple_parse_json(json, "\"stream\"", stream_buf); + if (strcmp(stream_buf, "snapshot") == 0) is_snap = 1; int pos = snprintf(resp, sizeof(resp), "{\"msg_id\":%lu,\"cmd\":\"log_query\",\"ts\":%lu,\"code\":0," "\"msg\":\"success\",\"data\":{\"start_seq\":%lu,\"records\":[", msg_id, dev_time_now(), (unsigned long)start_seq); uint32_t fetched = 0; - if (total > 0 && start_seq >= seq_first && start_seq <= seq_last) { - uint32_t idx = start_seq - seq_first; /* 逻辑索引 = 全局序号 - seq_first */ - while (fetched < req_count && (idx + fetched) < total) { - OfflogEvt evt; - if (offlog_read_idx((uint16_t)(idx + fetched), &evt) != 0) break; - if (fetched > 0) pos += snprintf(resp + pos, sizeof(resp) - pos, ","); - pos += offlog_evt_to_json(&evt, resp + pos, sizeof(resp) - pos); - fetched++; + + if (is_snap) { + uint32_t total = snap_count(); + uint32_t seq_last = snap_seq_last(); + uint32_t seq_first = (total > 0) ? (seq_last - total + 1) : 0; + if (req_count > SNAP_MAX_QUERY_RECORDS) req_count = SNAP_MAX_QUERY_RECORDS; + if (req_count == 0) req_count = SNAP_MAX_QUERY_RECORDS; + if (snap_enabled() && total > 0 && start_seq >= seq_first && start_seq <= seq_last) { + uint32_t idx = start_seq - seq_first; /* 逻辑索引 = 全局序号 - seq_first */ + while (fetched < req_count && (idx + fetched) < total) { + SnapRec rec; + if (snap_read_idx(idx + fetched, &rec) != 0) break; + if (fetched > 0) pos += snprintf(resp + pos, sizeof(resp) - pos, ","); + pos += snap_rec_to_json(&rec, resp + pos, sizeof(resp) - pos); + fetched++; + } } + PRINT("IOT: log_query(snapshot) start_seq=%lu req=%lu fetched=%lu\n", + (unsigned long)start_seq, (unsigned long)req_count, (unsigned long)fetched); + } else { + uint32_t total = offlog_count(); + uint32_t seq_last = offlog_seq_last(); + uint32_t seq_first = (total > 0) ? (seq_last - total + 1) : 0; + if (req_count > OFFLOG_MAX_QUERY_RECORDS) req_count = OFFLOG_MAX_QUERY_RECORDS; + if (req_count == 0) req_count = OFFLOG_MAX_QUERY_RECORDS; + if (total > 0 && start_seq >= seq_first && start_seq <= seq_last) { + uint32_t idx = start_seq - seq_first; /* 逻辑索引 = 全局序号 - seq_first */ + while (fetched < req_count && (idx + fetched) < total) { + OfflogEvt evt; + if (offlog_read_idx((uint16_t)(idx + fetched), &evt) != 0) break; + if (fetched > 0) pos += snprintf(resp + pos, sizeof(resp) - pos, ","); + pos += offlog_evt_to_json(&evt, resp + pos, sizeof(resp) - pos); + fetched++; + } + } + PRINT("IOT: log_query start_seq=%lu req=%lu fetched=%lu\n", + (unsigned long)start_seq, (unsigned long)req_count, (unsigned long)fetched); } snprintf(resp + pos, sizeof(resp) - pos, "]}}"); - PRINT("IOT: log_query start_seq=%lu req=%lu fetched=%lu\n", - (unsigned long)start_seq, (unsigned long)req_count, (unsigned long)fetched); iot_mqtt_publish(resp_topic, resp, strlen(resp), 1); } else if (strcmp(cmd_str, "log_clear") == 0) { - /* 清除日志 (审计留痕). 阻塞 ~2.8s (63 扇区擦除), 主循环上下文可接受 */ - offlog_clear(); + /* 清除日志 (审计留痕). event 阻塞 ~2.8s (63 扇区), snapshot 阻塞 ~45ms (逻辑清除+当前扇区) */ + char stream_buf[16]; + memset(stream_buf, 0, sizeof(stream_buf)); + simple_parse_json(json, "\"stream\"", stream_buf); char resp[256]; + if (strcmp(stream_buf, "snapshot") == 0) { + snap_clear(); + PRINT("IOT: log_clear(snapshot) done\n"); + } else { + offlog_clear(); + PRINT("IOT: log_clear done\n"); + } snprintf(resp, sizeof(resp), "{\"msg_id\":%lu,\"cmd\":\"log_clear\"," "\"ts\":%lu,\"code\":0,\"msg\":\"success\"}", msg_id, dev_time_now()); iot_mqtt_publish(resp_topic, resp, strlen(resp), 1); - PRINT("IOT: log_clear done\n"); } else { // 暂不支持的命令, 返回错误 diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/snapshot.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/snapshot.c index d8c4df5..816c52d 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/snapshot.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/snapshot.c @@ -16,6 +16,7 @@ #include "storage.h" #include "cmcng.h" #include +#include /* 编译期断言: 记录必须是 64B (定长环形索引依赖) */ typedef char snap_rec_size_must_be_64[(sizeof(SnapRec) == 64) ? 1 : -1]; @@ -421,3 +422,61 @@ void snap_clear(void) } snap_flush_head(); } + +/* SnapRec -> JSON object (channels aligned with 0xC0 wire format). + Used by TCP JSON / MQTT log_query stream=snapshot (protocol V1.03/V1.07). + Returns snprintf written length; caller must ensure buf_len enough. */ +int snap_rec_to_json(const SnapRec *r, char *buf, int buf_len) +{ + int coil_count = (r->len > (SNAP_COILS_MAX * SNAP_COIL_BYTES)) + ? SNAP_COILS_MAX : (r->len / SNAP_COIL_BYTES); + if (coil_count < 0) coil_count = 0; + + int pos = snprintf(buf, buf_len, + "{\"seq\":%lu,\"boot_seq\":%u,\"ts_ms\":%lu,\"coil_count\":%u,\"channels\":[", + (unsigned long)r->seq, (unsigned)r->boot_seq, + (unsigned long)r->ts_ms, (unsigned)coil_count); + + int c; + for (c = 0; c < coil_count; c++) { + const uint8_t *p = &r->coils[c * SNAP_COIL_BYTES]; + uint8_t cfg = p[0]; + uint8_t cond = p[1]; + uint32_t freq = (uint32_t)p[2] | ((uint32_t)p[3] << 8) | ((uint32_t)p[4] << 16); + int32_t variation = (int32_t)((uint32_t)p[5] | ((uint32_t)p[6] << 8) | ((uint32_t)p[7] << 16)); + uint32_t misc = (uint32_t)p[8] | ((uint32_t)p[9] << 8) + | ((uint32_t)p[10] << 16) | ((uint32_t)p[11] << 24); + uint8_t freq_level = (cfg >> 6) & 0x03; + uint8_t direction = (cfg >> 5) & 0x01; + uint8_t freq_type = (cfg >> 4) & 0x01; + uint8_t sensitivity = cfg & 0x0F; + uint8_t condition = (cond >> 4) & 0x0F; + uint8_t loop_state = (cond >> 3) & 0x01; /* 0=normal 1=cut */ + uint8_t car_state = (cond >> 2) & 0x01; /* 0=nocar 1=car */ + uint8_t misc_type = cond & 0x03; + const char *fl = "high"; + const char *mt = "time"; + + if (variation & 0x800000) variation |= (int32_t)0xFF000000; /* 3B sign extend */ + + if (freq_level == 1) fl = "mid_high"; + else if (freq_level == 2) fl = "mid_low"; + else if (freq_level == 3) fl = "low"; + + if (misc_type == 1) mt = "cut_count"; + else if (misc_type == 2) mt = "flow_count"; + else if (misc_type == 3) mt = "relay_count"; + + if (c > 0) pos += snprintf(buf + pos, buf_len - pos, ","); + pos += snprintf(buf + pos, buf_len - pos, + "{\"ch\":%u,\"freq_level\":\"%s\",\"direction\":%u,\"freq_type\":%u," + "\"sensitivity\":%u,\"condition\":%u,\"loop_ok\":%s,\"has_car\":%s," + "\"misc_type\":\"%s\",\"freq\":%lu,\"variation\":%ld,\"misc\":%lu}", + (unsigned)(c + 1), fl, (unsigned)direction, (unsigned)freq_type, + (unsigned)sensitivity, (unsigned)condition, + loop_state ? "false" : "true", car_state ? "true" : "false", + mt, (unsigned long)freq, (long)variation, (unsigned long)misc); + } + pos += snprintf(buf + pos, buf_len - pos, "]}"); + return pos; +} diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/tcp_json_srv.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/tcp_json_srv.c index 75ee361..0bbf245 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/tcp_json_srv.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/tcp_json_srv.c @@ -19,6 +19,7 @@ #include "simple_json.h" #include "storage.h" #include "offlog.h" +#include "snapshot.h" #include "fault_diag.h" #include #include @@ -638,59 +639,120 @@ static void handle_report_config(uint8_t socket, uint32_t msg_id, const char *js g_report_cfg.enable, g_report_cfg.interval, g_report_cfg.sensor_type); } -/* 4.16 log_stat — 查询脱机事件日志统计 */ +/* 4.16 log_stat — 查询脱机日志统计 (event/snapshot 流, 协议 V1.03) */ static void handle_log_stat(uint8_t socket, uint32_t msg_id, const char *json) { char data_json[256]; - uint32_t count = offlog_count(); - uint32_t seq_last = offlog_seq_last(); - uint32_t seq_first = (count > 0) ? (seq_last - count + 1) : 0; - snprintf(data_json, sizeof(data_json), - "{\"stream\":\"event\",\"enabled\":%s,\"boot_seq\":%lu," - "\"count\":%lu,\"capacity\":%u,\"seq_first\":%lu,\"seq_last\":%lu}", - offlog_enabled() ? "true" : "false", - (unsigned long)offlog_boot_seq(), - (unsigned long)count, (unsigned)OFFLOG_MAX_RECORDS, - (unsigned long)seq_first, (unsigned long)seq_last); + char stream_buf[16]; + uint32_t count, seq_last, seq_first, capacity; + const char *stream = "event"; + + memset(stream_buf, 0, sizeof(stream_buf)); + simple_parse_json(json, "\"stream\"", stream_buf); + if (strcmp(stream_buf, "snapshot") == 0) stream = "snapshot"; + + if (strcmp(stream, "snapshot") == 0) { + count = snap_count(); + seq_last = snap_seq_last(); + seq_first = (count > 0) ? (seq_last - count + 1) : 0; + capacity = SNAP_MAX_RECORDS; + snprintf(data_json, sizeof(data_json), + "{\"stream\":\"snapshot\",\"enabled\":%s,\"boot_seq\":%lu," + "\"count\":%lu,\"capacity\":%lu,\"seq_first\":%lu,\"seq_last\":%lu}", + snap_enabled() ? "true" : "false", + (unsigned long)snap_boot_seq(), + (unsigned long)count, (unsigned long)capacity, + (unsigned long)seq_first, (unsigned long)seq_last); + PRINT("JSON: log_stat(snapshot) count=%lu seq_first=%lu seq_last=%lu\n", + (unsigned long)count, (unsigned long)seq_first, (unsigned long)seq_last); + } else { + count = offlog_count(); + seq_last = offlog_seq_last(); + seq_first = (count > 0) ? (seq_last - count + 1) : 0; + capacity = OFFLOG_MAX_RECORDS; + snprintf(data_json, sizeof(data_json), + "{\"stream\":\"event\",\"enabled\":%s,\"boot_seq\":%lu," + "\"count\":%lu,\"capacity\":%lu,\"seq_first\":%lu,\"seq_last\":%lu}", + offlog_enabled() ? "true" : "false", + (unsigned long)offlog_boot_seq(), + (unsigned long)count, (unsigned long)capacity, + (unsigned long)seq_first, (unsigned long)seq_last); + PRINT("JSON: log_stat count=%lu seq_first=%lu seq_last=%lu\n", + (unsigned long)count, (unsigned long)seq_first, (unsigned long)seq_last); + } json_send_ok(socket, msg_id, "log_stat", data_json); - PRINT("JSON: log_stat count=%lu seq_first=%lu seq_last=%lu\n", - (unsigned long)count, (unsigned long)seq_first, (unsigned long)seq_last); } -/* 4.17 log_query — 分页拉取脱机事件日志 (按全局序号, count≤4) */ +/* 4.17 log_query — 分页拉取脱机日志 (event/snapshot 流; 事件≤4, 快照≤2, 协议 V1.03) */ static void handle_log_query(uint8_t socket, uint32_t msg_id, const char *json) { char data_json[TCP_JSON_DATA_BUF_LEN]; + char stream_buf[16]; uint32_t start_seq = json_get_uint_field(json, "\"start_seq\""); uint32_t req_count = json_get_uint_field(json, "\"count\""); - uint32_t total = offlog_count(); - uint32_t seq_last = offlog_seq_last(); - uint32_t seq_first = (total > 0) ? (seq_last - total + 1) : 0; - if (req_count > OFFLOG_MAX_QUERY_RECORDS) req_count = OFFLOG_MAX_QUERY_RECORDS; + int is_snap = 0; + + memset(stream_buf, 0, sizeof(stream_buf)); + simple_parse_json(json, "\"stream\"", stream_buf); + if (strcmp(stream_buf, "snapshot") == 0) is_snap = 1; int pos = snprintf(data_json, sizeof(data_json), "{\"start_seq\":%lu,\"records\":[", (unsigned long)start_seq); uint32_t fetched = 0; - if (total > 0 && start_seq >= seq_first && start_seq <= seq_last) { - uint32_t idx = start_seq - seq_first; /* 逻辑索引 = 全局序号 - seq_first */ - while (fetched < req_count && (idx + fetched) < total) { - OfflogEvt evt; - if (offlog_read_idx((uint16_t)(idx + fetched), &evt) != 0) break; - if (fetched > 0) pos += snprintf(data_json + pos, sizeof(data_json) - pos, ","); - pos += offlog_evt_to_json(&evt, data_json + pos, sizeof(data_json) - pos); - fetched++; + + if (is_snap) { + uint32_t total = snap_count(); + uint32_t seq_last = snap_seq_last(); + uint32_t seq_first = (total > 0) ? (seq_last - total + 1) : 0; + if (req_count > SNAP_MAX_QUERY_RECORDS) req_count = SNAP_MAX_QUERY_RECORDS; + if (req_count == 0) req_count = SNAP_MAX_QUERY_RECORDS; + if (snap_enabled() && total > 0 && start_seq >= seq_first && start_seq <= seq_last) { + uint32_t idx = start_seq - seq_first; /* 逻辑索引 = 全局序号 - seq_first */ + while (fetched < req_count && (idx + fetched) < total) { + SnapRec rec; + if (snap_read_idx(idx + fetched, &rec) != 0) break; + if (fetched > 0) pos += snprintf(data_json + pos, sizeof(data_json) - pos, ","); + pos += snap_rec_to_json(&rec, data_json + pos, sizeof(data_json) - pos); + fetched++; + } } + PRINT("JSON: log_query(snapshot) start_seq=%lu req=%lu fetched=%lu\n", + (unsigned long)start_seq, (unsigned long)req_count, (unsigned long)fetched); + } else { + uint32_t total = offlog_count(); + uint32_t seq_last = offlog_seq_last(); + uint32_t seq_first = (total > 0) ? (seq_last - total + 1) : 0; + if (req_count > OFFLOG_MAX_QUERY_RECORDS) req_count = OFFLOG_MAX_QUERY_RECORDS; + if (req_count == 0) req_count = OFFLOG_MAX_QUERY_RECORDS; + if (total > 0 && start_seq >= seq_first && start_seq <= seq_last) { + uint32_t idx = start_seq - seq_first; /* 逻辑索引 = 全局序号 - seq_first */ + while (fetched < req_count && (idx + fetched) < total) { + OfflogEvt evt; + if (offlog_read_idx((uint16_t)(idx + fetched), &evt) != 0) break; + if (fetched > 0) pos += snprintf(data_json + pos, sizeof(data_json) - pos, ","); + pos += offlog_evt_to_json(&evt, data_json + pos, sizeof(data_json) - pos); + fetched++; + } + } + PRINT("JSON: log_query start_seq=%lu req=%lu fetched=%lu\n", + (unsigned long)start_seq, (unsigned long)req_count, (unsigned long)fetched); } snprintf(data_json + pos, sizeof(data_json) - pos, "]}"); json_send_ok(socket, msg_id, "log_query", data_json); - PRINT("JSON: log_query start_seq=%lu req=%lu fetched=%lu\n", - (unsigned long)start_seq, (unsigned long)req_count, (unsigned long)fetched); } -/* 4.18 log_clear — 清除脱机事件日志 (审计留痕, 阻塞 ~2.8s) */ +/* 4.18 log_clear — 清除脱机日志 (event/snapshot 流, 审计留痕, 协议 V1.03) */ static void handle_log_clear(uint8_t socket, uint32_t msg_id, const char *json) { - offlog_clear(); + char stream_buf[16]; + memset(stream_buf, 0, sizeof(stream_buf)); + simple_parse_json(json, "\"stream\"", stream_buf); + if (strcmp(stream_buf, "snapshot") == 0) { + snap_clear(); /* 阻塞 ~45ms (逻辑清除 + 当前写扇区) */ + PRINT("JSON: log_clear(snapshot) done\n"); + } else { + offlog_clear(); /* 阻塞 ~2.8s (63 扇区擦除) */ + PRINT("JSON: log_clear done\n"); + } json_send_ok(socket, msg_id, "log_clear", NULL); - PRINT("JSON: log_clear done\n"); } /* 4.19 loop_version_query — 获取地感MCU版本号 (CMD 0x4A) */ diff --git a/vd960DBN/docs/devlog.md b/vd960DBN/docs/devlog.md index 6a69c74..17b0570 100644 --- a/vd960DBN/docs/devlog.md +++ b/vd960DBN/docs/devlog.md @@ -4,6 +4,42 @@ > > 项目定位: DLD960 通信板 — BLE 配网、TCP JSON 协议服务、Loop MCU 串口桥接 +## 2026-08-18 — TCP/MQTT 脱机日志快照流支持(协议 V1.03/V1.07 落地) + +### 背景 + +协议文档先行(TCP JSON V1.03 + IoT MQTT V1.07,2026-08-18):`log_stat` / `log_query` / `log_clear` 通过 `data.stream` 区分 `event` / `snapshot` 流(BLE 0x28/0x29/0x2A 的网络通道对称语义)。固件侧此前只实现事件流(offlog_*),快照流缺失——本次补齐。 + +### 变更 + +| 文件 | 变更 | +|------|------| +| `snapshot.c/h` | 新增 `snap_rec_to_json()`:SnapRec 64B 原始结构 → JSON(channels 对齐 0xC0 传感单元,variation 3B 符号扩展,misc_type 全枚举 time/cut_count/flow_count/relay_count) | +| `tcp_json_srv.c` | `handle_log_stat/log_query/log_clear` 加 `stream` 解析 + snapshot 分支(snap_* API);事件流 count=0 按上限处理(与 BLE 对齐) | +| `iot_mqtt_srv.c` | `log_stat/log_query/log_clear` 加 `stream` 解析 + snapshot 分支;同上 | + +### 关键决策 + +- **复用 log_* 命令 + stream 字段**(协议 V1.00 即预留"快照流预留"),不新增命令码;与 BLE 侧独立命令(SNAP_*)不同,JSON 协议命令面保持 18 条不变 +- 快照 QUERY `count≤2`(`SNAP_MAX_QUERY_RECORDS=2`,64B×2 记录),事件流仍 ≤4 +- 快照 CLEAR 阻塞 ~45ms(逻辑清除+当前写扇区),事件流 ~2.8s 不变 +- `capacity`/`count` JSON 类型 uint32(W25Q256 事件流 130944 超 16bit,原文档 uint16 标注修正) +- `snap_rec_to_json` 放 snapshot.c 公共模块,TCP/MQTT 两路共用(避免重复序列化代码) + +### 验证 + +- gcc 隔离单测 `tests/test_snap_to_json.c`:**34 断言全过**(高频/低频/方向判别/初始频率、负 variation -10 符号扩展、misc_type 全枚举、loop_ok/has_car 位解析、空记录 coil_count=0 边界) +- `check_c_balance.py` 5 文件 OK(tcp_json_srv.c / iot_mqtt_srv.c / snapshot.c / snapshot.h ×2) +- `simple_parse_json` 字符串提取语义确认:去引号取内容,`strcmp(stream,"snapshot")` 成立 +- 待板级验证:MRS 真编译 + 真机 TCP/MQTT `log_query stream=snapshot` + +### 注意 + +- 固件版本未升(仍 1.02.01),随下次发版统一 +- 协议文档已先行(V1.03/V1.07),固件本次同步跟上(README/规格书/产品手册索引已同步) + +--- + ## 2026-08-17 — 频繁"复位"根因闭环:.bss 挤占 RAM → 栈溢出 → PC 跑飞 ### 背景 @@ -1271,6 +1307,7 @@ TCP 超时要等 ~2 分钟才触发 `SINT_STAT_TIM_OUT`, | 版本 | 时间 | 说明 | |------|------|------| +| V4.1 | 2026-08-18 | TCP/MQTT 脱机日志快照流: log_stat/log_query/log_clear 支持 stream=snapshot (协议 V1.03/V1.07), snap_rec_to_json 序列化, 事件流 count=0 按上限 | | V4.0 | 2026-08-10 | BLE 脱机日志接口: OFFLOG_STAT/QUERY/CLEAR (0x25/0x26/0x27), 32B OfflogEvt 二进制直传, 缓冲扩容 132B, 隔离测试 7 例 + 协议文档 V1.00 | | V3.9 | 2026-08-05 | offlog 协议导出命令分发: log_stat/log_query/log_clear 落地 MQTT V1.06 + TCP JSON V1.02 (seq_first/seq_last 计算、idx 映射、OFFLOG_MAX_QUERY_RECORDS=4、log_clear 阻塞~2.8s) | | V3.8 | 2026-08-04 | offlog: TIME_ANCHOR 严格用平台下发 unix_ts (offlog_evt_ts), 单测 7 例 | diff --git a/vd960DBN/tests/test_snap_to_json.c b/vd960DBN/tests/test_snap_to_json.c new file mode 100644 index 0000000..30c1a95 --- /dev/null +++ b/vd960DBN/tests/test_snap_to_json.c @@ -0,0 +1,164 @@ +#include +#include +#include + +/* mock SnapRec (与 snapshot.h 一致, 64B) */ +typedef struct { + uint8_t magic; + uint8_t len; + uint8_t flags; + uint8_t rsvd; + uint32_t seq; + uint32_t ts_ms; + uint16_t boot_seq; + uint16_t rsvd2; + uint8_t coils[48]; +} SnapRec; + +#define SNAP_COILS_MAX 4 +#define SNAP_COIL_BYTES 12 + +int snap_rec_to_json(const SnapRec *r, char *buf, int buf_len) +{ + int coil_count = (r->len > (SNAP_COILS_MAX * SNAP_COIL_BYTES)) + ? SNAP_COILS_MAX : (r->len / SNAP_COIL_BYTES); + if (coil_count < 0) coil_count = 0; + + int pos = snprintf(buf, buf_len, + "{\"seq\":%lu,\"boot_seq\":%u,\"ts_ms\":%lu,\"coil_count\":%u,\"channels\":[", + (unsigned long)r->seq, (unsigned)r->boot_seq, + (unsigned long)r->ts_ms, (unsigned)coil_count); + + int c; + for (c = 0; c < coil_count; c++) { + const uint8_t *p = &r->coils[c * SNAP_COIL_BYTES]; + uint8_t cfg = p[0]; + uint8_t cond = p[1]; + uint32_t freq = (uint32_t)p[2] | ((uint32_t)p[3] << 8) | ((uint32_t)p[4] << 16); + int32_t variation = (int32_t)((uint32_t)p[5] | ((uint32_t)p[6] << 8) | ((uint32_t)p[7] << 16)); + uint32_t misc = (uint32_t)p[8] | ((uint32_t)p[9] << 8) + | ((uint32_t)p[10] << 16) | ((uint32_t)p[11] << 24); + uint8_t freq_level = (cfg >> 6) & 0x03; + uint8_t direction = (cfg >> 5) & 0x01; + uint8_t freq_type = (cfg >> 4) & 0x01; + uint8_t sensitivity = cfg & 0x0F; + uint8_t condition = (cond >> 4) & 0x0F; + uint8_t loop_state = (cond >> 3) & 0x01; /* 0=normal 1=cut */ + uint8_t car_state = (cond >> 2) & 0x01; /* 0=nocar 1=car */ + uint8_t misc_type = cond & 0x03; + const char *fl = "high"; + const char *mt = "time"; + + if (variation & 0x800000) variation |= (int32_t)0xFF000000; /* 3B sign extend */ + + if (freq_level == 1) fl = "mid_high"; + else if (freq_level == 2) fl = "mid_low"; + else if (freq_level == 3) fl = "low"; + + if (misc_type == 1) mt = "cut_count"; + else if (misc_type == 2) mt = "flow_count"; + else if (misc_type == 3) mt = "relay_count"; + + if (c > 0) pos += snprintf(buf + pos, buf_len - pos, ","); + pos += snprintf(buf + pos, buf_len - pos, + "{\"ch\":%u,\"freq_level\":\"%s\",\"direction\":%u,\"freq_type\":%u," + "\"sensitivity\":%u,\"condition\":%u,\"loop_ok\":%s,\"has_car\":%s," + "\"misc_type\":\"%s\",\"freq\":%lu,\"variation\":%ld,\"misc\":%lu}", + (unsigned)(c + 1), fl, (unsigned)direction, (unsigned)freq_type, + (unsigned)sensitivity, (unsigned)condition, + loop_state ? "false" : "true", car_state ? "true" : "false", + mt, (unsigned long)freq, (long)variation, (unsigned long)misc); + } + pos += snprintf(buf + pos, buf_len - pos, "]}"); + return pos; +} + +#define CHECK(cond) do { if (!(cond)) { printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); fails++; } else { passes++; } } while (0) + +static int passes = 0, fails = 0; + +int main(void) +{ + char buf[2048]; + SnapRec r; + + /* 用例1: 单线圈, 高频(00)/触发/实时频率/灵敏度2, 正常无车, 时间量 + freq=0x010F2A=69418, variation=+7, misc=0 */ + memset(&r, 0, sizeof(r)); + r.magic = 0xA6; r.len = 12; r.seq = 12345; r.ts_ms = 456789; r.boot_seq = 2; + r.coils[0] = 0x12; /* freq_level=00 direction=0 freq_type=1 sens=0010 */ + r.coils[1] = 0x00; /* condition=0 loop_state=0 car_state=0 misc_type=00 */ + r.coils[2] = 0x2A; r.coils[3] = 0x0F; r.coils[4] = 0x01; /* 69418 */ + r.coils[5] = 0x07; r.coils[6] = 0x00; r.coils[7] = 0x00; /* +7 */ + r.coils[8] = 0x00; r.coils[9] = 0x00; r.coils[10] = 0x00; r.coils[11] = 0x00; + + snap_rec_to_json(&r, buf, sizeof(buf)); + printf("case1: %s\n", buf); + CHECK(strstr(buf, "\"seq\":12345") != NULL); + CHECK(strstr(buf, "\"boot_seq\":2") != NULL); + CHECK(strstr(buf, "\"ts_ms\":456789") != NULL); + CHECK(strstr(buf, "\"coil_count\":1") != NULL); + CHECK(strstr(buf, "\"ch\":1") != NULL); + CHECK(strstr(buf, "\"freq_level\":\"high\"") != NULL); + CHECK(strstr(buf, "\"direction\":0") != NULL); + CHECK(strstr(buf, "\"freq_type\":1") != NULL); + CHECK(strstr(buf, "\"sensitivity\":2") != NULL); + CHECK(strstr(buf, "\"condition\":0") != NULL); + CHECK(strstr(buf, "\"loop_ok\":true") != NULL); + CHECK(strstr(buf, "\"has_car\":false") != NULL); + CHECK(strstr(buf, "\"misc_type\":\"time\"") != NULL); + CHECK(strstr(buf, "\"freq\":69418") != NULL); + CHECK(strstr(buf, "\"variation\":7") != NULL); + CHECK(strstr(buf, "\"misc\":0") != NULL); + + /* 用例2: 4线圈全量 + 负 variation + 低频 + relay_count + 有车/断开 + ch1: 低频(11)/方向判别(1)/初始频率(0)/灵敏度0, condition=0xF, loop_state=1(断开), car_state=1(有车), misc_type=11(relay) + freq=0x00C383=50051, variation=-10 (F6 FF FF), misc=128 */ + memset(&r, 0, sizeof(r)); + r.magic = 0xA6; r.len = 48; r.seq = 999; r.ts_ms = 1234; r.boot_seq = 3; + r.coils[0] = 0xE8; /* 11 1 0 0000 */ + r.coils[1] = 0xFF; /* 1111 1 1 11 */ + r.coils[2] = 0x83; r.coils[3] = 0xC3; r.coils[4] = 0x00; /* 50051 */ + r.coils[5] = 0xF6; r.coils[6] = 0xFF; r.coils[7] = 0xFF; /* -10 */ + r.coils[8] = 0x80; r.coils[9] = 0x00; r.coils[10] = 0x00; r.coils[11] = 0x00; /* 128 */ + /* ch2: 中高(01), 正常无车, cut_count */ + r.coils[12] = 0x50; /* 01 0 1 0000 */ + r.coils[13] = 0x01; /* 0000 0 0 01 */ + r.coils[14] = 0x00; r.coils[15] = 0x02; r.coils[16] = 0x00; /* 512 */ + r.coils[17] = 0x01; r.coils[18] = 0x00; r.coils[19] = 0x00; /* +1 */ + /* ch3: 中低(10), flow_count */ + r.coils[24] = 0xA0; /* 10 1 0 0000 */ + r.coils[25] = 0x02; /* 0000 0 0 10 */ + r.coils[26] = 0x00; r.coils[27] = 0x01; r.coils[28] = 0x00; /* 256 */ + /* ch4: 高频, 空 */ + + snap_rec_to_json(&r, buf, sizeof(buf)); + printf("case2: %s\n", buf); + CHECK(strstr(buf, "\"coil_count\":4") != NULL); + CHECK(strstr(buf, "\"freq_level\":\"low\"") != NULL); + CHECK(strstr(buf, "\"direction\":1") != NULL); + CHECK(strstr(buf, "\"freq_type\":0") != NULL); + CHECK(strstr(buf, "\"sensitivity\":0") != NULL); + CHECK(strstr(buf, "\"condition\":15") != NULL); + CHECK(strstr(buf, "\"loop_ok\":false") != NULL); + CHECK(strstr(buf, "\"has_car\":true") != NULL); + CHECK(strstr(buf, "\"misc_type\":\"relay_count\"") != NULL); + CHECK(strstr(buf, "\"freq\":50051") != NULL); + CHECK(strstr(buf, "\"variation\":-10") != NULL); + CHECK(strstr(buf, "\"misc\":128") != NULL); + CHECK(strstr(buf, "\"freq_level\":\"mid_high\"") != NULL); + CHECK(strstr(buf, "\"misc_type\":\"cut_count\"") != NULL); + CHECK(strstr(buf, "\"freq_level\":\"mid_low\"") != NULL); + CHECK(strstr(buf, "\"misc_type\":\"flow_count\"") != NULL); + + /* 用例3: len=0 边界 (空记录) */ + memset(&r, 0, sizeof(r)); + r.magic = 0xA6; r.len = 0; + snap_rec_to_json(&r, buf, sizeof(buf)); + printf("case3: %s\n", buf); + CHECK(strstr(buf, "\"coil_count\":0") != NULL); + CHECK(strstr(buf, "\"channels\":[]") != NULL); + + printf("\nPASS=%d FAIL=%d\n", passes, fails); + return fails ? 1 : 0; +}