From 4112b7cbbf380f2a0e6f8aafe1c4299aafa0e694 Mon Sep 17 00:00:00 2001 From: wangfq Date: Wed, 15 Jul 2026 12:27:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(iot=5Fmqtt):=20=E5=AE=9E=E7=8E=B0=20event?= =?UTF-8?q?=5Freport=20=E5=B9=B3=E5=8F=B0=E5=BF=85=E7=AD=94+=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E9=87=8D=E5=8F=91=20(=E5=8D=8F=E8=AE=AE=20V1.04);=20?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E5=8D=95=E4=BD=8D=E7=BB=9F=E4=B8=80=2050ms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 时间单位: - 串口协议 V1.06 / TCP JSON 协议: 杂项时间量、事件 value 由 5ms 修正为 50ms, 对齐 Loop 固件 50ms tick 实现 (TMR15 5ms×10) event_report (iot_mqtt_srv.c + net_srv.c): - 沿检测双路汇聚: Step1 消费路径 + lup 回调覆盖 uart_srv 路径, 不漏帧 - 16 深环形队列, 事件仅 ACK(code=0) 后出队; 溢出丢最旧 - 5s 超时重发同 msg_id/原始 ts ×3 次, 耗尽挂起, 新事件/重连沿合并补报 - 帧消费提前至 READY/enable 之前: 断网期间事件入队, 重连补报 - 线圈断开期间屏蔽 car 沿; loop_restore value=本地计时/50 - msg_id 独立 uint32 计数 (g_iot_msg_id 为 uint8 混用会破坏去重窗口) - net_srv.c ACK 路由: cmd=event_report 回显帧确认出队, 不回 unsupported - gcc 隔离单测 8 组全过 (tests/test_event_report.c), 单包 6 条实测 317B --- docs/DLD960Loop_串口通信协议.md | 5 +- docs/DLD960_TCP_JSON协议.md | 4 +- .../APP/include/iot_mqtt_srv.h | 1 + .../APP/iot_mqtt_srv.c | 209 ++++++++++++++- .../OnlyUpdateApp_Peripheral/APP/net_srv.c | 10 + vd960DBN/docs/devlog.md | 33 +++ vd960DBN/tests/test_event_report.c | 240 ++++++++++++++++++ 7 files changed, 492 insertions(+), 10 deletions(-) create mode 100644 vd960DBN/tests/test_event_report.c diff --git a/docs/DLD960Loop_串口通信协议.md b/docs/DLD960Loop_串口通信协议.md index 8c63bfd..8d97c65 100644 --- a/docs/DLD960Loop_串口通信协议.md +++ b/docs/DLD960Loop_串口通信协议.md @@ -216,7 +216,7 @@ SensData:传感数据 | 线圈评估条件 | 1 | condition(4bit), loop\_state(1bit),
car\_state(1bit),
report\_msic(2bit) | loop\_condition 正常(0)有效,环境状态条件(环境状态评估值,值越大,干扰越大),高四位有效。低四位中 ,其中第3位表示线圈状态,0 表示正常,1 表示线圈断开;第 2 位表示有无车,0 表示无车,1 表示有车;低两位表示**杂项类型**,0b00表示时间量,0b01表示线圈断开次数;0b10表示车流量数;0b11表示继电器输出次数 | | 频率 1 | 3 | frequent | 低字节在前,无符号 | | 变化量 1 | 3 | variation | **有符号** 3 字节,低字节在前(小端),补码表示。定义 `variation = loop_Origin − loop_CAPVD`(基线 − 当前值)。**正值**表示当前值低于基线(车辆/金属进入,检测裕量方向);**负值**表示当前值高于基线(反向漂移/异常抬升)。值域 ±8388607(±2²³−1),发送端已做饱和限幅,不会回绕。
**接收端解析**:读入 3 字节后须做符号扩展——`v = b0\|(b1<<8)\|(b2<<16); if (v & 0x800000) v \|= 0xFF000000;` | -| 杂项 | 4 | in\_out\_passtime/cut\_amount/flow\_amount | **杂项类型**,可能是时间量,也可能是线圈断开次数、车流量数,说明:
1、时间量有两种类型,通过时间或车间距,5ms为单位。通过时间是从有车开始到车辆离开的时间(上报时car\_state 为0);车间距是 从上一次车辆离开到有车时候的时间(上报时car\_state 为1)。非0有效。
2、线圈断开次数。
3、车流量数。
4、继电器输出次数。 | +| 杂项 | 4 | in\_out\_passtime/cut\_amount/flow\_amount | **杂项类型**,可能是时间量,也可能是线圈断开次数、车流量数,说明:
1、时间量有两种类型,通过时间或车间距,**50ms 为单位**(固件 50ms tick,TMR15 5ms×10 分频)。通过时间是从有车开始到车辆离开的时间(上报时car\_state 为0);车间距是 从上一次车辆离开到有车时候的时间(上报时car\_state 为1)。非0有效。
2、线圈断开次数。
3、车流量数。
4、继电器输出次数。 | eg: @@ -259,4 +259,5 @@ FE 38 校验字节(XOR=0xFE, SUM=0x38) | V1.02 | 2026-07-02 | 查缺补漏,补充例程说明。 | wangfq | | V1.03 | 2026-07-02 | 0x8A 响应格式 + LEN 计算修正 | wangfq | | V1.04 | 2026-07-02 | 主动上报增加继电器输出次数 类型 | wangfq | -| V1.05 | 2026-07-14 | 多线圈上报(0x0C)变化量 variation 由 2 字节无符号扩展为 **3 字节有符号**(补码, 小端),定义 `variation = Origin − CAPVD`(正=车/裕量, 负=反向漂移);每单元 11→12 字节,Len 47→51。接收端须做符号扩展。**Loop 固件与 DBN 固件须同版本发布。** | wangfq | \ No newline at end of file +| V1.05 | 2026-07-14 | 多线圈上报(0x0C)变化量 variation 由 2 字节无符号扩展为 **3 字节有符号**(补码, 小端),定义 `variation = Origin − CAPVD`(正=车/裕量, 负=反向漂移);每单元 11→12 字节,Len 47→51。接收端须做符号扩展。**Loop 固件与 DBN 固件须同版本发布。** | wangfq | +| V1.06 | 2026-07-15 | 修正杂项时间量单位说明:5ms → **50ms**(对齐固件 50ms tick 实现,TMR15 5ms×10 分频) | wangfq | \ No newline at end of file diff --git a/docs/DLD960_TCP_JSON协议.md b/docs/DLD960_TCP_JSON协议.md index 7822ead..5d26b17 100644 --- a/docs/DLD960_TCP_JSON协议.md +++ b/docs/DLD960_TCP_JSON协议.md @@ -380,9 +380,9 @@ Client Device (DLD960) | 事件类型 `type` | 说明 | `value` | |----------------|------|---------| | `car_enter` | 车辆进入 | 0 | -| `car_leave` | 车辆离开 | 通过时间 (×5ms) | +| `car_leave` | 车辆离开 | 通过时间 (×50ms) | | `loop_cut` | 线圈断开 | 0 | -| `loop_restore` | 线圈恢复 | 断开持续时长 (×5ms) | +| `loop_restore` | 线圈恢复 | 断开持续时长 (×50ms) | **客户端应答(收到后必须立即回复):** diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/iot_mqtt_srv.h b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/iot_mqtt_srv.h index d89bd16..105334c 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/iot_mqtt_srv.h +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/iot_mqtt_srv.h @@ -51,5 +51,6 @@ void iot_mqtt_init(void); // 初始化 MQTT 连接 (TCP→CONNECT void iot_mqtt_poll(void); // 主循环轮询 (状态机 + 心跳 + 重连) void iot_mqtt_handle_sock_int(uint8_t socketid, uint8_t intstat); // Socket 中断处理 void iot_mqtt_publish_sensor(void); // 推送传感器数据到 MQTT +void iot_evt_handle_ack(uint32_t msg_id, int code); // event_report 平台应答入口 (V1.04) #endif /* __IOT_MQTT_SRV_H__ */ diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c index ea3c070..c33d713 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c @@ -53,6 +53,196 @@ static LUP_SensorReport _cached_sr; // 最新传感器数据缓存 static uint8_t _cached_sr_valid; // 缓存有效标志 static uint32_t _last_publish_ms; // 上次上报时刻 (ms) +/*=========================================================================== + * 事件上报 event_report — 协议 V1.04: 平台必答 + 设备重发 + * + * 检测: car_state/loop_state 翻转沿 (每帧比对, 经 lup 回调 + Step1 双路汇聚) + * 队列: 16 深环形队列, 溢出丢最旧; 事件仅在收到 ACK 后出队 + * 重发: 5s 超时, 同 msg_id/原始 ts, 最多 3 次; 耗尽后挂起, + * 待 MQTT 重连或新事件到达时以新 msg_id 合并重报 + * 应答: 平台回 {cmd:"event_report", msg_id(回显), code}, 由 + * manage_mqtt_recv_message (net_srv.c) 路由到 iot_evt_handle_ack() + *===========================================================================*/ +#define IOT_EVT_QUEUE_DEPTH 16 // 待发队列深度 (协议建议 >=16) +#define IOT_EVT_MAX_PER_PKT 6 // 单包最多事件数 (6条约358B < 500B发布上限) +#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 }; + +typedef struct { + uint8_t type; // IOT_EVT_xxx + uint8_t ch; // 通道号 1-based + uint32_t value; // car_leave=通过时间(50ms), loop_restore=断开时长(50ms) +} IotEvent; + +static IotEvent _evt_queue[IOT_EVT_QUEUE_DEPTH]; // 环形队列 +static uint8_t _evt_head, _evt_count; + +static uint8_t _evt_prev_car[LUP_COIL_COUNT]; // 上一帧 car_state +static uint8_t _evt_prev_loop[LUP_COIL_COUNT]; // 上一帧 loop_state (1=断开) +static uint32_t _evt_cut_ms[LUP_COIL_COUNT]; // 断开起始时刻 (mstick) +static uint8_t _evt_prev_valid; // 首帧只建快照不出事件 + +static uint32_t _evt_msg_id; // event_report 独立 msg_id (uint32 递增) +static uint32_t _evt_pend_id; // 待确认包 msg_id, 0=无 +static uint32_t _evt_pend_ts; // 待确认包 ts (首发时刻, 重发不刷新) +static uint8_t _evt_pend_n; // 待确认包内事件数 (= 队列头 N 条) +static uint32_t _evt_sent_ms; // 本次发送时刻 (超时计时) +static uint8_t _evt_retry; // 已重发次数 +static uint8_t _evt_gaveup; // 重试耗尽挂起标志 (新事件/重连时解除) + +/* 入队: 满则丢最旧; 若丢掉的事件属于待确认包, 作废该包 (事件仍按新包重报) */ +static void iot_evt_enqueue(uint8_t type, uint8_t ch, uint32_t value) { + if (_evt_count >= IOT_EVT_QUEUE_DEPTH) { + _evt_head = (_evt_head + 1) % IOT_EVT_QUEUE_DEPTH; + _evt_count--; + if (_evt_pend_id) { // 待确认包引用队列头, 头被丢弃则作废重来 + _evt_pend_id = 0; + if (_evt_pend_n) _evt_pend_n = 0; + } + PRINT("EVT: queue overflow, drop oldest\n"); + } + { + IotEvent *e = &_evt_queue[(_evt_head + _evt_count) % IOT_EVT_QUEUE_DEPTH]; + e->type = type; e->ch = ch; e->value = value; + _evt_count++; + } + _evt_gaveup = 0; // 新事件到达 → 解除挂起, 触发合并上报 + PRINT("EVT: enqueue type=%d ch=%d val=%lu (cnt=%d)\n", type, ch, value, _evt_count); +} + +/* 沿检测: 每收到一帧有效 0xC0 调用一次 (两条消费路径都要喂) */ +static void iot_evt_feed(const LUP_SensorReport *sr) { + uint8_t i; + if (!_evt_prev_valid) { // 首帧: 只建快照 (上电已有车不算进入沿) + for (i = 0; i < LUP_COIL_COUNT; i++) { + _evt_prev_car[i] = (i < sr->coil_count) ? sr->coils[i].car_state : 0; + _evt_prev_loop[i] = (i < sr->coil_count) ? sr->coils[i].loop_state : 0; + } + _evt_prev_valid = 1; + return; + } + for (i = 0; i < sr->coil_count && i < LUP_COIL_COUNT; i++) { + const LUP_CoilSensor *cs = &sr->coils[i]; + + /* 线圈断开期间 car_state 不可信: 仅前后两帧线圈均正常才判进出车沿 */ + if (!_evt_prev_loop[i] && !cs->loop_state) { + if (!_evt_prev_car[i] && cs->car_state) { + iot_evt_enqueue(IOT_EVT_CAR_ENTER, i + 1, 0); + } else if (_evt_prev_car[i] && !cs->car_state) { + /* 离开帧 misc_type=时间量 时携带通过时间 (50ms 单位) */ + uint32_t v = (cs->misc_type == 0) ? cs->misc.passtime_ms : 0; + iot_evt_enqueue(IOT_EVT_CAR_LEAVE, i + 1, v); + } + } + /* 线圈断开/恢复沿 */ + if (!_evt_prev_loop[i] && cs->loop_state) { + _evt_cut_ms[i] = mstick(); + iot_evt_enqueue(IOT_EVT_LOOP_CUT, i + 1, 0); + } else if (_evt_prev_loop[i] && !cs->loop_state) { + iot_evt_enqueue(IOT_EVT_LOOP_RESTORE, i + 1, + (mstick() - _evt_cut_ms[i]) / 50); // 断开时长, 50ms 单位 + } + + _evt_prev_car[i] = cs->car_state; + _evt_prev_loop[i] = cs->loop_state; + } +} + +/* lup 传感回调: uart_srv 消费路径的帧从这里喂入 (已过校验) */ +static void iot_evt_sensor_cb(const uint8_t *pkg, uint16_t len) { + LUP_SensorReport sr; + memset(&sr, 0, sizeof(sr)); + if (lup_parse_sensor_report(pkg, len, &sr) == 0) { + iot_evt_feed(&sr); + } +} + +/* 序列化并发布队列头 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 char payload[512]; // static: 避免 2KB 栈溢出 + int w, rem = sizeof(payload); + char *p = payload; + uint8_t i; + + w = snprintf(p, rem, "{\"msg_id\":%lu,\"cmd\":\"event_report\",\"ts\":%lu," + "\"data\":{\"events\":[", id, ts); + if (w < 0 || w >= rem) return; + p += w; rem -= w; + + 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); + if (w < 0 || w >= rem) return; + p += w; rem -= w; + } + snprintf(p, rem, "]}}"); + + mqtt_publish((char *)g_iot_topic.topic_pub, payload, 1); + PRINT("EVT: publish msg_id=%lu n=%d retry=%d\n", id, n, _evt_retry); +} + +/* 平台应答入口 — manage_mqtt_recv_message (net_srv.c) 收到 cmd=event_report 时调用 */ +void iot_evt_handle_ack(uint32_t msg_id, int code) { + if (_evt_pend_id == 0 || msg_id != _evt_pend_id) { + PRINT("EVT: ack msg_id=%lu ignored (pend=%lu)\n", msg_id, _evt_pend_id); + return; + } + if (code != 0) { // 平台处理失败 → 视同未确认, 等超时重发 + PRINT("EVT: ack code=%d, keep pending\n", code); + return; + } + /* 确认成功 → 弹出本包事件 */ + _evt_head = (_evt_head + _evt_pend_n) % IOT_EVT_QUEUE_DEPTH; + _evt_count -= _evt_pend_n; + PRINT("EVT: ack ok, %d events dequeued (left=%d)\n", _evt_pend_n, _evt_count); + _evt_pend_id = 0; + _evt_pend_n = 0; + _evt_retry = 0; +} + +/* 事件发送状态机: 主循环每轮调用 (置于 READY 检查之前, 以感知重连沿) */ +static void iot_evt_process(void) { + static uint8_t _was_ready = 0; + uint8_t ready = (g_iot_state == IOT_STATE_READY); + uint32_t now = mstick(); + + if (ready && !_was_ready) { // 重连沿: 作废未决包, 解除挂起, 全量重报 + _evt_pend_id = 0; + _evt_gaveup = 0; + } + _was_ready = ready; + if (!ready) return; + + if (_evt_pend_id) { // 有未决包 → 超时重发 + if (now - _evt_sent_ms < IOT_EVT_ACK_TIMEOUT_MS) return; + if (_evt_retry >= IOT_EVT_MAX_RETRY) { + PRINT("EVT: retry exhausted msg_id=%lu, hold %d events\n", + _evt_pend_id, _evt_count); + _evt_pend_id = 0; + _evt_gaveup = 1; // 挂起: 新事件或重连时再触发 + return; + } + _evt_retry++; + iot_evt_send(_evt_pend_id, _evt_pend_ts, _evt_pend_n); // 同 id/ts 重发 + _evt_sent_ms = now; + return; + } + + if (_evt_count == 0 || _evt_gaveup) return; + + /* 发新包: 队列头 N 条合并 */ + _evt_pend_n = (_evt_count > IOT_EVT_MAX_PER_PKT) ? IOT_EVT_MAX_PER_PKT : _evt_count; + _evt_pend_id = ++_evt_msg_id; + _evt_pend_ts = now / 1000; + _evt_retry = 0; + iot_evt_send(_evt_pend_id, _evt_pend_ts, _evt_pend_n); + _evt_sent_ms = now; +} + /*=========================================================================== * Buffers *===========================================================================*/ @@ -382,10 +572,10 @@ static void iot_process_recv(void) { *===========================================================================*/ void iot_mqtt_publish_sensor(void) { static uint8_t _last_car_state[4] = {0}; - if (g_iot_state != IOT_STATE_READY) return; - if (!g_report_cfg.enable) return; - /*--- Step 1: 消费 0xC0 帧 → 更新缓存 ---*/ + /*--- Step 1: 消费 0xC0 帧 → 事件沿检测 + 更新缓存 ---*/ + /* 注意: 置于 READY/enable 检查之前 —— 断网/未使能期间事件照样入队, + 重连后由 iot_evt_process() 补报 (协议 V1.04) */ if (g_pkg_uart_2.flag != 0 && g_pkg_uart_2.pkg[0] == 0x7F && g_pkg_uart_2.pkg[3] == 0xC0) { @@ -396,6 +586,7 @@ void iot_mqtt_publish_sensor(void) { InitPkgUart(&g_pkg_uart_2); // 读完即清, 不持有 if (ret == 0) { + iot_evt_feed(&sr); // 事件沿检测 (Step1 消费路径) memcpy(&_cached_sr, &sr, sizeof(sr)); _cached_sr_valid = 1; } else { @@ -403,6 +594,12 @@ void iot_mqtt_publish_sensor(void) { } } + /*--- 事件上报状态机: 发送/重发/重连补报 ---*/ + iot_evt_process(); + + if (g_iot_state != IOT_STATE_READY) return; + if (!g_report_cfg.enable) return; + /* 尚无缓存数据 → 不发 */ if (!_cached_sr_valid) return; @@ -416,7 +613,7 @@ void iot_mqtt_publish_sensor(void) { // variation 为有符号(V1.05): 正=车/裕量, 负=反向漂移, 两向显著变化都加速 int32_t v = _cached_sr.coils[i].variation; int32_t av = (v >= 0) ? v : -v; - if (av >= IOT_MQTT_VARIATION_THRESHOLD) { + if (av > IOT_MQTT_VARIATION_THRESHOLD) { fast_mode = 1; break; } @@ -666,8 +863,8 @@ void iot_mqtt_init(void) { PRINT("IOT: dev_serial=%s\n", g_iot_dev_serial); - /* 初始化传感器回调 — 用于 MQTT 上报 */ - lup_set_sensor_callback(NULL); // MQTT 模式不需要 JSON sensor_cb + /* 初始化传感器回调 — uart_srv 消费路径的 0xC0 帧喂入事件沿检测 (V1.04) */ + lup_set_sensor_callback(iot_evt_sensor_cb); _iot_reconnect_backoff = 0; _iot_reconnect_deadline = mstick() + 2000; // 启动后 2s 开始首次连接 diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/net_srv.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/net_srv.c index e4e3a88..c173656 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/net_srv.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/net_srv.c @@ -1522,6 +1522,16 @@ void manage_mqtt_recv_message(char * msg, int length) return; } + // --- event_report 平台应答 (V1.04): 回显帧, 确认后出队, 不再回复 --- + if(strcmp(cmd_str, "event_report") == 0) { + int code = -1; + memset(tmp, 0, sizeof(tmp)); + simple_parse_json(msg, "\"code\"", tmp); + if(strlen(tmp) > 0) code = (int)strtol(tmp, NULL, 10); + iot_evt_handle_ack(msg_id, code); + return; // 应答帧终止于此, 严禁再回 unsupported (否则与平台互相打乒乓) + } + // --- 未支持的命令 --- { char resp[256]; diff --git a/vd960DBN/docs/devlog.md b/vd960DBN/docs/devlog.md index 7370101..41e3274 100644 --- a/vd960DBN/docs/devlog.md +++ b/vd960DBN/docs/devlog.md @@ -6,6 +6,39 @@ --- +## 2026-07-15 — event_report 实现: 平台必答 + 设备重发 (协议 V1.04) + +### 实现 (iot_mqtt_srv.c 事件模块 + net_srv.c ACK 路由) + +| 组件 | 说明 | +|------|------| +| 沿检测 `iot_evt_feed` | 每帧比对 car_state/loop_state 快照。**双路汇聚**: Step1 消费路径直接喂 + `lup_set_sensor_callback(iot_evt_sensor_cb)` 覆盖 uart_srv 消费路径(两路都存在帧竞争, 单独任一路都会漏帧漏沿) | +| 事件队列 | 16 深环形, 溢出丢最旧; **事件仅在 ACK(code=0) 后出队** | +| 发送状态机 `iot_evt_process` | 每轮主循环调用(挂在 iot_mqtt_publish_sensor 内, 置于 READY/enable 检查之前)。5s 超时重发, 同 msg_id/原始 ts, 最多 3 次; 耗尽→挂起, 新事件或重连沿解除并以**新 msg_id** 合并重报 | +| ACK 入口 `iot_evt_handle_ack` | net_srv.c `manage_mqtt_recv_message` 收到 `cmd=event_report` 的回显帧时调用, **必须 return 不回 unsupported**(否则与平台互打乒乓) | +| msg_id | 事件独立 uint32 计数器(`g_iot_msg_id` 是 uint8 且与 MQTT packet id 混用, 255 回绕会破坏平台去重窗口) | + +### 关键决策 + +1. **帧消费提前到 READY/enable 之前**: 断网/未使能期间事件照样检测入队, 重连后补报。loop_data 周期上报仍受 READY+enable 门控。 +2. **event_report 不受 report_config.enable 限制**(事件为关键不可再生数据; 若需要开关另加字段)— ⚠️ 待老大确认。 +3. **线圈断开期间屏蔽 car 沿**: 断开时 car_state 不可信, 仅前后两帧 loop 均正常才判进出车; loop_restore 的 value = DBN 本地计时(mstick 差)/50。 +4. car_leave 的 value 直接取离开帧 misc(时间量) = 通过时间(50ms 单位)。 +5. 首帧只建快照: 上电线圈上已有车不算进入沿。 + +### 已知边界 + +- 队列溢出且恰有未决包时: 作废未决包重发新 msg_id → 平台 (sn,msg_id) 去重失效, **可能重复入库一包**。触发条件: broker 不应答的 20s 窗口内涌入 >16 条事件, 现场概率极低, 平台可按事件内容+ts 二次去重兜底。 +- BSS 增量 ~700B(队列128B + 发送缓冲 512B + 状态), **编译后查 .map 确认 RAM 余量**。 + +### 验证 + +gcc 隔离单测 (/tmp/test_event_report.c) 8 组全过: 首帧快照/进出车沿/5s×3 重发同 id 同 ts/挂起与新事件解除/错 msg_id 及 code≠0 不出队/断开恢复时长+断开期屏蔽 car 沿/单包 6 条上限(实测 317B<500B)/重连沿补报。 + +⚠️ 平台端注意: 收到 event_report **先落库后应答**, 按 (dev_serial,msg_id) 10 分钟窗口去重, 重复包直接答 code=0。 + +--- + ## 2026-07-15 — MQTT 快速上报增加 car_state 翻转沿触发 ### 背景 diff --git a/vd960DBN/tests/test_event_report.c b/vd960DBN/tests/test_event_report.c new file mode 100644 index 0000000..96b4336 --- /dev/null +++ b/vd960DBN/tests/test_event_report.c @@ -0,0 +1,240 @@ +/* 隔离单测: iot_mqtt_srv.c event_report 模块 (协议 V1.04) + * 覆盖: 沿检测 / 队列 / ACK出队 / 5s重发×3 / 挂起与解除 / 重连补报 / 包长上限 / 溢出 + * 被测代码与固件同构 (mock: mstick, mqtt_publish, PRINT, LUP 类型) + */ +#include +#include +#include +#include + +/* ---------- mocks ---------- */ +#define PRINT(...) do{}while(0) +#define LUP_COIL_COUNT 4 +typedef struct { uint8_t car_state, loop_state, misc_type; struct { uint32_t passtime_ms; } misc; } LUP_CoilSensor; +typedef struct { uint8_t coil_count; LUP_CoilSensor coils[LUP_COIL_COUNT]; } LUP_SensorReport; +typedef enum { IOT_STATE_DISCONNECTED=0, IOT_STATE_READY=5 } IotMqttState; +static IotMqttState g_iot_state = IOT_STATE_READY; +static uint32_t _mock_ms = 0; +static uint32_t mstick(void) { return _mock_ms; } +static struct { uint8_t topic_pub[64]; } g_iot_topic = {"dld960/AA/dev"}; +/* 捕获发布 */ +static char last_payload[600]; static int pub_count = 0; +static void mqtt_publish(char *topic, char *payload, int qos) { + (void)topic; (void)qos; + strncpy(last_payload, payload, sizeof(last_payload)-1); + pub_count++; +} + +/* ---------- 被测模块 (与固件同构) ---------- */ +#define IOT_EVT_QUEUE_DEPTH 16 +#define IOT_EVT_MAX_PER_PKT 6 +#define IOT_EVT_ACK_TIMEOUT_MS 5000 +#define IOT_EVT_MAX_RETRY 3 +enum { IOT_EVT_CAR_ENTER = 0, IOT_EVT_CAR_LEAVE, IOT_EVT_LOOP_CUT, IOT_EVT_LOOP_RESTORE }; +typedef struct { uint8_t type, ch; uint32_t value; } IotEvent; +static IotEvent _evt_queue[IOT_EVT_QUEUE_DEPTH]; +static uint8_t _evt_head, _evt_count; +static uint8_t _evt_prev_car[LUP_COIL_COUNT], _evt_prev_loop[LUP_COIL_COUNT]; +static uint32_t _evt_cut_ms[LUP_COIL_COUNT]; +static uint8_t _evt_prev_valid; +static uint32_t _evt_msg_id, _evt_pend_id, _evt_pend_ts; +static uint8_t _evt_pend_n; +static uint32_t _evt_sent_ms; +static uint8_t _evt_retry, _evt_gaveup; + +static void iot_evt_enqueue(uint8_t type, uint8_t ch, uint32_t value) { + if (_evt_count >= IOT_EVT_QUEUE_DEPTH) { + _evt_head = (_evt_head + 1) % IOT_EVT_QUEUE_DEPTH; + _evt_count--; + if (_evt_pend_id) { _evt_pend_id = 0; if (_evt_pend_n) _evt_pend_n = 0; } + } + { IotEvent *e = &_evt_queue[(_evt_head + _evt_count) % IOT_EVT_QUEUE_DEPTH]; + e->type = type; e->ch = ch; e->value = value; _evt_count++; } + _evt_gaveup = 0; +} +static void iot_evt_feed(const LUP_SensorReport *sr) { + uint8_t i; + if (!_evt_prev_valid) { + for (i = 0; i < LUP_COIL_COUNT; i++) { + _evt_prev_car[i] = (i < sr->coil_count) ? sr->coils[i].car_state : 0; + _evt_prev_loop[i] = (i < sr->coil_count) ? sr->coils[i].loop_state : 0; + } + _evt_prev_valid = 1; return; + } + for (i = 0; i < sr->coil_count && i < LUP_COIL_COUNT; i++) { + const LUP_CoilSensor *cs = &sr->coils[i]; + if (!_evt_prev_loop[i] && !cs->loop_state) { + if (!_evt_prev_car[i] && cs->car_state) iot_evt_enqueue(IOT_EVT_CAR_ENTER, i+1, 0); + else if (_evt_prev_car[i] && !cs->car_state) { + uint32_t v = (cs->misc_type == 0) ? cs->misc.passtime_ms : 0; + iot_evt_enqueue(IOT_EVT_CAR_LEAVE, i+1, v); + } + } + if (!_evt_prev_loop[i] && cs->loop_state) { + _evt_cut_ms[i] = mstick(); iot_evt_enqueue(IOT_EVT_LOOP_CUT, i+1, 0); + } else if (_evt_prev_loop[i] && !cs->loop_state) { + iot_evt_enqueue(IOT_EVT_LOOP_RESTORE, i+1, (mstick() - _evt_cut_ms[i]) / 50); + } + _evt_prev_car[i] = cs->car_state; _evt_prev_loop[i] = cs->loop_state; + } +} +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 char payload[512]; + int w, rem = sizeof(payload); char *p = payload; uint8_t i; + w = snprintf(p, rem, "{\"msg_id\":%u,\"cmd\":\"event_report\",\"ts\":%u," + "\"data\":{\"events\":[", id, ts); + if (w < 0 || w >= rem) return; + p += w; rem -= w; + 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\":%u}", + (i>0)?",":"", evt_names[e->type & 0x03], e->ch, e->value); + if (w < 0 || w >= rem) return; + p += w; rem -= w; + } + snprintf(p, rem, "]}}"); + mqtt_publish((char *)g_iot_topic.topic_pub, payload, 1); +} +void iot_evt_handle_ack(uint32_t msg_id, int code) { + if (_evt_pend_id == 0 || msg_id != _evt_pend_id) return; + if (code != 0) return; + _evt_head = (_evt_head + _evt_pend_n) % IOT_EVT_QUEUE_DEPTH; + _evt_count -= _evt_pend_n; + _evt_pend_id = 0; _evt_pend_n = 0; _evt_retry = 0; +} +static void iot_evt_process(void) { + static uint8_t _was_ready = 0; + uint8_t ready = (g_iot_state == IOT_STATE_READY); + uint32_t now = mstick(); + if (ready && !_was_ready) { _evt_pend_id = 0; _evt_gaveup = 0; } + _was_ready = ready; + if (!ready) return; + if (_evt_pend_id) { + if (now - _evt_sent_ms < IOT_EVT_ACK_TIMEOUT_MS) return; + if (_evt_retry >= IOT_EVT_MAX_RETRY) { _evt_pend_id = 0; _evt_gaveup = 1; return; } + _evt_retry++; + iot_evt_send(_evt_pend_id, _evt_pend_ts, _evt_pend_n); + _evt_sent_ms = now; + return; + } + if (_evt_count == 0 || _evt_gaveup) return; + _evt_pend_n = (_evt_count > IOT_EVT_MAX_PER_PKT) ? IOT_EVT_MAX_PER_PKT : _evt_count; + _evt_pend_id = ++_evt_msg_id; + _evt_pend_ts = now / 1000; + _evt_retry = 0; + iot_evt_send(_evt_pend_id, _evt_pend_ts, _evt_pend_n); + _evt_sent_ms = now; +} + +/* ---------- 测试 ---------- */ +static int fails = 0; +#define CHECK(cond, msg) do{ if(!(cond)){ printf("FAIL: %s\n", msg); fails++; } }while(0) + +static LUP_SensorReport mkframe(uint8_t c1,uint8_t c2,uint8_t c3,uint8_t c4, + uint8_t l1,uint8_t l2,uint8_t l3,uint8_t l4, uint32_t pass) { + LUP_SensorReport sr; memset(&sr,0,sizeof(sr)); sr.coil_count=4; + uint8_t cars[4]={c1,c2,c3,c4}, loops[4]={l1,l2,l3,l4}; + for(int i=0;i<4;i++){ sr.coils[i].car_state=cars[i]; sr.coils[i].loop_state=loops[i]; + sr.coils[i].misc_type=0; sr.coils[i].misc.passtime_ms=pass; } + return sr; +} + +int main(void) { + LUP_SensorReport sr; + + /* T1: 首帧已有车 → 不产生事件 (只建快照) */ + sr = mkframe(1,0,0,0, 0,0,0,0, 0); + iot_evt_feed(&sr); + CHECK(_evt_count == 0, "T1 首帧不出事件"); + + /* T2: ch2 进车沿 */ + sr = mkframe(1,1,0,0, 0,0,0,0, 0); + iot_evt_feed(&sr); + CHECK(_evt_count == 1 && _evt_queue[0].type == IOT_EVT_CAR_ENTER + && _evt_queue[0].ch == 2, "T2 car_enter ch2"); + + /* T3: 发送 → 5s 无应答重发×3 → 挂起 */ + _mock_ms = 1000; + iot_evt_process(); /* 首发 msg_id=1 */ + CHECK(pub_count == 1 && _evt_pend_id == 1, "T3 首发"); + CHECK(strstr(last_payload, "\"msg_id\":1") && strstr(last_payload, "car_enter"), + "T3 payload 含 msg_id=1 + car_enter"); + uint32_t first_ts = _evt_pend_ts; + for (int r = 1; r <= 3; r++) { + _mock_ms += 5100; iot_evt_process(); /* 重发 r */ + CHECK(pub_count == 1 + r, "T3 重发次数"); + CHECK(_evt_pend_ts == first_ts, "T3 重发 ts 不刷新"); + CHECK(strstr(last_payload, "\"msg_id\":1") != NULL, "T3 重发同 msg_id"); + } + _mock_ms += 5100; iot_evt_process(); /* 第4次超时 → 挂起 */ + CHECK(pub_count == 4 && _evt_pend_id == 0 && _evt_gaveup == 1 && _evt_count == 1, + "T3 重试耗尽挂起, 事件保留"); + _mock_ms += 5100; iot_evt_process(); + CHECK(pub_count == 4, "T3 挂起期间不再发送"); + + /* T4: ch2 出车沿 (新事件) → 解除挂起, 合并重报 (含旧 car_enter) */ + sr = mkframe(1,0,0,0, 0,0,0,0, 350); + iot_evt_feed(&sr); + CHECK(_evt_count == 2, "T4 car_leave 入队"); + CHECK(_evt_queue[1].value == 350, "T4 car_leave 通过时间=350"); + _mock_ms += 100; iot_evt_process(); /* 新包 msg_id=2, 含2条 */ + CHECK(pub_count == 5 && _evt_pend_id == 2 && _evt_pend_n == 2, "T4 合并重报"); + CHECK(strstr(last_payload, "car_enter") && strstr(last_payload, "car_leave"), + "T4 新包含新旧事件"); + + /* T5: ACK code!=0 → 不出队; ACK code=0 → 出队 */ + iot_evt_handle_ack(2, 5); + CHECK(_evt_pend_id == 2 && _evt_count == 2, "T5 code!=0 不出队"); + iot_evt_handle_ack(999, 0); + CHECK(_evt_pend_id == 2, "T5 错 msg_id 忽略"); + iot_evt_handle_ack(2, 0); + CHECK(_evt_pend_id == 0 && _evt_count == 0, "T5 ACK 出队"); + + /* T6: 线圈断开→恢复, value=断开时长(50ms 单位); 断开期间屏蔽 car 沿 */ + _mock_ms = 90000; + sr = mkframe(0,0,0,0, 0,0,0,0, 0); /* 归一化: 清 T4 残留的 prev_car(ch1=1) */ + iot_evt_feed(&sr); /* 产生 ch1 car_leave, 随即排干 */ + while (_evt_count) { iot_evt_process(); iot_evt_handle_ack(_evt_pend_id, 0); _mock_ms += 100; } + _mock_ms = 100000; + sr = mkframe(0,0,0,0, 0,0,1,0, 0); /* ch3 断开 */ + iot_evt_feed(&sr); + CHECK(_evt_count == 1 && _evt_queue[_evt_head].type == IOT_EVT_LOOP_CUT, "T6 loop_cut"); + _mock_ms = 103000; /* 3s 后 */ + sr = mkframe(0,0,1,0, 0,0,0,0, 0); /* ch3 恢复且 car=1: 断开→正常帧不判 car 沿 */ + iot_evt_feed(&sr); + CHECK(_evt_count == 2, "T6 恢复帧只出 loop_restore, car 沿被屏蔽"); + { IotEvent *e = &_evt_queue[(_evt_head+1)%IOT_EVT_QUEUE_DEPTH]; + CHECK(e->type == IOT_EVT_LOOP_RESTORE && e->value == 60, "T6 断开时长 3000ms/50=60"); } + iot_evt_process(); iot_evt_handle_ack(_evt_pend_id, 0); /* 清场 */ + + /* T7: 单包上限 6 条 + 包长 < 500B */ + for (int k = 0; k < 10; k++) { + sr = mkframe(1,0,0,0, 0,0,0,0, 0); iot_evt_feed(&sr); /* enter */ + sr = mkframe(0,0,0,0, 0,0,0,0, 4294967295u); iot_evt_feed(&sr); /* leave, value 最大 */ + } + CHECK(_evt_count == 16, "T7 队列封顶16 (20条丢4)"); + iot_evt_process(); + CHECK(_evt_pend_n == 6, "T7 单包6条"); + CHECK(strlen(last_payload) < 500, "T7 包长<500B"); + printf(" T7 包长实测: %zu B (6条, value含4294967295)\n", strlen(last_payload)); + iot_evt_handle_ack(_evt_pend_id, 0); + CHECK(_evt_count == 10, "T7 ACK 后出队6条"); + + /* T8: 断线重连沿 → 未决包作废 + 挂起解除, 剩余事件补报 */ + iot_evt_process(); /* 发下一包 (4条? 6条上限内=6... 剩10条→6) */ + uint32_t pend_before = _evt_pend_id; + CHECK(pend_before != 0, "T8 有未决包"); + g_iot_state = IOT_STATE_DISCONNECTED; + iot_evt_process(); /* 断线 */ + g_iot_state = IOT_STATE_READY; + _mock_ms += 100; + iot_evt_process(); /* 重连沿: 作废旧包, 立即新包 */ + CHECK(_evt_pend_id == pend_before + 1, "T8 重连后新 msg_id 补报"); + /* 全部确认清空 */ + while (_evt_count) { iot_evt_handle_ack(_evt_pend_id, 0); _mock_ms += 100; iot_evt_process(); } + CHECK(_evt_count == 0, "T8 清空"); + + printf(fails ? "\n== %d FAIL ==\n" : "\n== ALL PASS ==\n", fails); + return fails; +}