From 1a01316c7f19e22cbcf1f194903d395b6c90bd53 Mon Sep 17 00:00:00 2001 From: wangfq Date: Wed, 5 Aug 2026 08:53:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(vd960DBN):=20offlog=20=E5=8D=8F=E8=AE=AE?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=E5=91=BD=E4=BB=A4=E5=88=86=E5=8F=91=20?= =?UTF-8?q?=E2=80=94=20log=5Fstat/log=5Fquery/log=5Fclear=20=E8=90=BD?= =?UTF-8?q?=E5=9C=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 协议先行 (08893d5, MQTT V1.06 + TCP JSON V1.02) 的固件侧实现: - offlog 新增导出 API: enabled/seq_last/type_str/evt_to_json (seq_first=seq_last-count+1, idx=start_seq-seq_first, count≤4) - MQTT iot_handle_publish 三分支: log_stat/log_query/log_clear (log_query 响应 static buf 防大栈; log_clear 阻塞~2.8s 主循环可接受) - TCP JSON 三 handler + cmd 表 (需鉴权) - 单测新增 test_export_json (8 例全过): boot rst 大端/coil sub/evt_retry/data:null/seq 公式 - devlog V3.9 --- .../APP/include/offlog.h | 9 ++ .../APP/iot_mqtt_srv.c | 67 +++++++++++++ .../BLE/OnlyUpdateApp_Peripheral/APP/offlog.c | 98 +++++++++++++++++++ .../APP/tcp_json_srv.c | 61 +++++++++++- vd960DBN/docs/devlog.md | 38 +++++++ vd960DBN/tests/test_offlog.c | 59 +++++++++++ 6 files changed, 331 insertions(+), 1 deletion(-) diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/offlog.h b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/offlog.h index e70050c..e9db222 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/offlog.h +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/offlog.h @@ -133,4 +133,13 @@ uint16_t offlog_count(void); /* 有效记录数 */ int offlog_read_idx(uint16_t idx, OfflogEvt *out); /* 按逻辑序号读, 0=成功 -1=越界 */ void offlog_clear(void); /* 清空事件区 (清空后写 LOG_CLEAR 审计) */ +/*=========================================================================== + * 协议导出 (MQTT V1.06 / TCP JSON V1.02) + *===========================================================================*/ +uint8_t offlog_enabled(void); /* 日志功能是否启用 (Flash 初始化成功) */ +uint32_t offlog_seq_last(void); /* 最新一条记录全局序号 (0=空) */ +const char *offlog_type_str(uint8_t type); /* 事件类型数字 → 协议字符串名 */ +int offlog_evt_to_json(const OfflogEvt *e, char *buf, int buf_len); /* 记录 → JSON 对象 */ +#define OFFLOG_MAX_QUERY_RECORDS 4 /* log_query 分页上限 (MQTT ≤500B 发布限制) */ + #endif /* _OFFLOG_H__ */ diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c index faae5cf..93e9fc7 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c @@ -538,6 +538,73 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_ iot_mqtt_publish(resp_topic, resp, strlen(resp), 1); } + } 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; + 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); + 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 发布限制) */ + static char resp[IOT_MQTT_SEND_BUF_LEN]; /* static: 避免大栈开销 */ + 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; + + memset(tmp, 0, sizeof(tmp)); + simple_parse_json(json, "\"start_seq\"", tmp); + if (strlen(tmp) > 0) start_seq = (uint32_t)strtoul(tmp, NULL, 10); + 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; + + 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++; + } + } + 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(); + char resp[256]; + 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 { // 暂不支持的命令, 返回错误 char resp[256]; diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/offlog.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/offlog.c index 477d321..ac0ab02 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/offlog.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/offlog.c @@ -344,6 +344,104 @@ int offlog_read_idx(uint16_t idx, OfflogEvt *out) return offlog_evt_read_at(read_off, out); } +/*=========================================================================== + * 协议导出 (MQTT V1.06 / TCP JSON V1.02 共用) + *===========================================================================*/ +uint8_t offlog_enabled(void) +{ + return _ready; +} + +uint32_t offlog_seq_last(void) +{ + return (_wr_seq > 0) ? (_wr_seq - 1) : 0; +} + +const char *offlog_type_str(uint8_t type) +{ + switch (type) { + case OFFLOG_EVT_BOOT: return "boot"; + case OFFLOG_EVT_IOT_CONNECT: return "iot_connect"; + case OFFLOG_EVT_IOT_READY: return "iot_ready"; + case OFFLOG_EVT_IOT_DISCONN: return "iot_disconnect"; + case OFFLOG_EVT_IOT_RECONN: return "iot_reconn"; + case OFFLOG_EVT_EVT_RETRY: return "evt_retry"; + case OFFLOG_EVT_EVT_GIVEUP: return "evt_giveup"; + case OFFLOG_EVT_COIL: return "coil"; + case OFFLOG_EVT_TIME_ANCHOR: return "time_anchor"; + case OFFLOG_EVT_LOG_CLEAR: return "log_clear"; + default: return "unknown"; + } +} + +/* 线圈事件子类型: 1=进 2=出 3=断开 4=恢复 + (与 iot_mqtt_srv.c offlog_coil 调用一致) */ +static const char *offlog_coil_sub_str(uint8_t sub) +{ + switch (sub) { + case 1: return "car_enter"; + case 2: return "car_leave"; + case 3: return "loop_cut"; + case 4: return "loop_restore"; + default: return "unknown"; + } +} + +/* payload 大端 uint32 (与 offlog_boot/offlog_coil 写入一致) */ +static uint32_t offlog_payload_u32(const uint8_t *p) +{ + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3]; +} + +/* 记录 → JSON 对象 (data 按事件类型组装; 无参数事件 data=null) + 回返 snprintf 写入长度; 调用方须保证 buf_len 充足 (一条最大约130B) */ +int offlog_evt_to_json(const OfflogEvt *e, char *buf, int buf_len) +{ + const char *type = offlog_type_str(e->type); + char data[96]; + + switch (e->type) { + case OFFLOG_EVT_BOOT: + snprintf(data, sizeof(data), "{\"rst\":%lu}", + (unsigned long)offlog_payload_u32(e->payload)); + break; + case OFFLOG_EVT_IOT_DISCONN: + snprintf(data, sizeof(data), "{\"reason\":%u}", e->payload[0]); + break; + case OFFLOG_EVT_IOT_RECONN: + snprintf(data, sizeof(data), "{\"backoff_ms\":%lu}", + (unsigned long)offlog_payload_u32(e->payload)); + break; + case OFFLOG_EVT_EVT_RETRY: + snprintf(data, sizeof(data), "{\"msg_id\":%lu,\"retry\":%u}", + (unsigned long)offlog_payload_u32(e->payload), e->payload[4]); + break; + case OFFLOG_EVT_EVT_GIVEUP: + snprintf(data, sizeof(data), "{\"msg_id\":%lu}", + (unsigned long)offlog_payload_u32(e->payload)); + break; + case OFFLOG_EVT_COIL: + snprintf(data, sizeof(data), "{\"sub\":\"%s\",\"ch\":%u,\"value\":%lu}", + offlog_coil_sub_str(e->payload[0]), e->payload[1], + (unsigned long)offlog_payload_u32(e->payload + 2)); + break; + default: + data[0] = '\0'; /* iot_connect/iot_ready/time_anchor/log_clear → null */ + break; + } + + if (data[0] == '\0') { + return snprintf(buf, buf_len, + "{\"seq\":%lu,\"boot_seq\":%u,\"ts_ms\":%lu,\"unix_ts\":%lu,\"type\":\"%s\",\"data\":null}", + (unsigned long)e->seq, (unsigned)e->boot_seq, + (unsigned long)e->ts_ms, (unsigned long)e->unix_ts, type); + } + return snprintf(buf, buf_len, + "{\"seq\":%lu,\"boot_seq\":%u,\"ts_ms\":%lu,\"unix_ts\":%lu,\"type\":\"%s\",\"data\":%s}", + (unsigned long)e->seq, (unsigned)e->boot_seq, + (unsigned long)e->ts_ms, (unsigned long)e->unix_ts, type, data); +} + /*=========================================================================== * 清空 (审计: 清空后写一条 LOG_CLEAR) *===========================================================================*/ diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/tcp_json_srv.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/tcp_json_srv.c index af26a5b..1774eaa 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/tcp_json_srv.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/tcp_json_srv.c @@ -18,6 +18,7 @@ #include "loop_uart_proto.h" #include "simple_json.h" #include "storage.h" +#include "offlog.h" #include #include #include @@ -636,7 +637,62 @@ 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 loop_version_query — 获取地感MCU版本号 (CMD 0x4A) */ +/* 4.16 log_stat — 查询脱机事件日志统计 */ +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); + 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) */ +static void handle_log_query(uint8_t socket, uint32_t msg_id, const char *json) { + char data_json[TCP_JSON_DATA_BUF_LEN]; + 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 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++; + } + } + 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) */ +static void handle_log_clear(uint8_t socket, uint32_t msg_id, const char *json) { + offlog_clear(); + json_send_ok(socket, msg_id, "log_clear", NULL); + PRINT("JSON: log_clear done\n"); +} + +/* 4.19 loop_version_query — 获取地感MCU版本号 (CMD 0x4A) */ static void handle_loop_version_query(uint8_t socket, uint32_t msg_id, const char *json) { lup_send_get_version(); g_json_pending.active = 1; @@ -754,6 +810,9 @@ static const JsonCmdEntry g_cmd_table[] = { {"loop_sens_read", handle_loop_sens_read, 1}, {"loop_sens_write", handle_loop_sens_write, 1}, {"report_config", handle_report_config, 1}, + {"log_stat", handle_log_stat, 1}, + {"log_query", handle_log_query, 1}, + {"log_clear", handle_log_clear, 1}, }; #define JSON_CMD_COUNT (sizeof(g_cmd_table) / sizeof(g_cmd_table[0])) diff --git a/vd960DBN/docs/devlog.md b/vd960DBN/docs/devlog.md index 66b4d79..f4cc4a5 100644 --- a/vd960DBN/docs/devlog.md +++ b/vd960DBN/docs/devlog.md @@ -6,6 +6,43 @@ --- +## 2026-08-05 — offlog 协议导出命令分发落地 (MQTT V1.06 + TCP JSON V1.02) + +### 背景 + +协议文档先行(MQTT V1.06 / TCP JSON V1.02,commit 08893d5)新增 `log_stat` / `log_query` / `log_clear` 三命令规范,固件命令分发未接。本次补齐 P1.3:offlog 底层 API 已就绪(count/read_idx/clear),差协议层接线。 + +### offlog 导出 API(两侧共用,放 offlog 模块避免两协议层重复) + +| API | 作用 | +|------|------| +| `offlog_enabled()` | 日志功能是否启用(Flash 初始化成功) | +| `offlog_seq_last()` | 最新一条记录全局序号(`_wr_seq - 1`,0=空) | +| `offlog_type_str(type)` | 事件类型数字 → 协议字符串名(10 类 + unknown) | +| `offlog_evt_to_json()` | 单条记录 → JSON 对象(data 按事件类型组装,无参数事件 data=null) | +| `OFFLOG_MAX_QUERY_RECORDS` | 分页上限 4(MQTT ≤500B 发布限制) | + +payload 大端解析(`offlog_payload_u32`)与写入侧 `offlog_boot/offlog_coil` 完全一致;coil sub 映射 1=car_enter 2=car_leave 3=loop_cut 4=loop_restore。 + +### 命令分发 + +- **MQTT**(`iot_handle_publish` 三分支):`log_stat`(enabled/boot_seq/count/capacity/seq_first/seq_last)、`log_query`(按全局序号分页,`idx = start_seq - seq_first` 映射 offlog_read_idx,越界返回空 records,响应用 `static resp[IOT_MQTT_SEND_BUF_LEN]` 防大栈)、`log_clear` +- **TCP JSON**(三 handler + cmd 表 3 条目,全部需鉴权):同上,`data_json[TCP_JSON_DATA_BUF_LEN]` 组装后 `json_send_ok` + +### 关键决策与坑 + +1. **`log_clear` 阻塞 ~2.8s**(63 扇区擦除 × ~45ms):SPI 擦除只能在主循环上下文(中断写 SPI 会炸栈),三命令 handler 均在主循环 poll 链上调用,可接受;MQTT 侧 60s keepalive、TCP 有状态重传均能吸收。注释已标注。 +2. **`seq_first = seq_last - count + 1`**(count=0 时置 0):环形覆盖后 count < capacity 是正确语义,分页定位按全局序号不按时间(未同步段时间不可靠)。 +3. **CRLF 二进制编辑**:四个 C 源文件 UTF-8+CRLF,用 Python rb/wb 精确替换,每处校验 count==1;gcc 全文件括号/引号平衡检查通过。 + +### 验证 + +- `test_offlog.c` 新增测试8 `test_export_json`:enabled/seq_last/type_str/evt_to_json(boot rst 大端、coil sub/ch/value、evt_retry msg_id+retry、无参数 data:null、seq_first 公式) +- gcc 隔离单测 **8 例全过**(含既有 7 例回归) +- CRLF/UTF-8 编码零漂移(file 复核) + +--- + ## 2026-07-15 — 🔴 loop_data 陈旧快照: 事件与缓存不同源 (帧竞争) ### 现象 (现场测试: 长车过双线圈) @@ -719,6 +756,7 @@ TCP 超时要等 ~2 分钟才触发 `SINT_STAT_TIM_OUT`, | 版本 | 时间 | 说明 | |------|------|------| +| 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 例 | | V3.7 | 2026-08-04 | tcp_json_srv 缓冲宏化: data_json[2048]→TCP_JSON_DATA_BUF_LEN(1024), 省栈1KB×3 | | V3.6 | 2026-08-04 | 脱机事件日志系统: W25Q32 256KB 环形(8064条) + BOOT/网络/事件/线圈/时钟锚点 8 类事件 + 掉电恢复 + gcc 单测 6 例 | diff --git a/vd960DBN/tests/test_offlog.c b/vd960DBN/tests/test_offlog.c index ea03b99..97ae907 100644 --- a/vd960DBN/tests/test_offlog.c +++ b/vd960DBN/tests/test_offlog.c @@ -232,6 +232,64 @@ static void test_time_anchor_exact(void) printf("PASS test_time_anchor_exact\n"); } +/* 测试8: 协议导出 — enabled/seq_last/type_str/evt_to_json (log_stat/log_query 数据源) */ +static void test_export_json(void) +{ + OfflogEvt e; + char buf[256]; + + reset_flash(); + offlog_init(); + CHECK(offlog_enabled() == 1); + CHECK(offlog_seq_last() == 0); /* 尚无记录 */ + + offlog_boot(0x80000000UL); /* IWDG 复位 → rst=2147483648 */ + CHECK(offlog_count() == 1); + CHECK(offlog_seq_last() == 1); + CHECK(offlog_read_idx(0, &e) == 0); + CHECK(strcmp(offlog_type_str(e.type), "boot") == 0); + offlog_evt_to_json(&e, buf, sizeof(buf)); + CHECK(strstr(buf, "\"type\":\"boot\"") != NULL); + CHECK(strstr(buf, "\"data\":{\"rst\":2147483648}") != NULL); + + /* coil: payload = sub(1=进) ch(2) value(97, 大端) */ + { + uint8_t p[6] = {1, 2, 0, 0, 0, 97}; + offlog_evt(OFFLOG_EVT_COIL, p, 6); + } + CHECK(offlog_read_idx(1, &e) == 0); + CHECK(strcmp(offlog_type_str(e.type), "coil") == 0); + offlog_evt_to_json(&e, buf, sizeof(buf)); + CHECK(strstr(buf, "\"type\":\"coil\"") != NULL); + CHECK(strstr(buf, "\"sub\":\"car_enter\"") != NULL); + CHECK(strstr(buf, "\"ch\":2") != NULL); + CHECK(strstr(buf, "\"value\":97") != NULL); + + /* evt_retry: msg_id=5 retry=2 */ + { + uint8_t p[5] = {0, 0, 0, 5, 2}; + offlog_evt(OFFLOG_EVT_EVT_RETRY, p, 5); + } + CHECK(offlog_read_idx(2, &e) == 0); + offlog_evt_to_json(&e, buf, sizeof(buf)); + CHECK(strstr(buf, "\"msg_id\":5") != NULL); + CHECK(strstr(buf, "\"retry\":2") != NULL); + + /* 无 payload 事件 → data:null */ + offlog_iot_ready(); + CHECK(offlog_read_idx(3, &e) == 0); + offlog_evt_to_json(&e, buf, sizeof(buf)); + CHECK(strstr(buf, "\"type\":\"iot_ready\"") != NULL); + CHECK(strstr(buf, "\"data\":null") != NULL); + + /* seq_first 公式: seq_last - count + 1 (4 条: seq 1..4) */ + CHECK(offlog_seq_last() == 4); + CHECK(offlog_count() == 4); + printf("PASS test_export_json (seq_first=%lu seq_last=%lu)\n", + (unsigned long)(offlog_seq_last() - offlog_count() + 1), + (unsigned long)offlog_seq_last()); +} + int main(void) { test_fresh_init(); @@ -241,6 +299,7 @@ int main(void) test_clear(); test_power_loss_mid_sector(); test_time_anchor_exact(); + test_export_json(); if (failures) { printf("\n%d FAILURE(S)\n", failures);