From 6ff155a232ed612572e0ffc1a02c4b5a149b0057 Mon Sep 17 00:00:00 2001 From: ruv Date: Sat, 23 May 2026 12:26:45 -0400 Subject: [PATCH] =?UTF-8?q?feat(csi):=20emit=20ADR-110=20=C2=A7A0.11=20syn?= =?UTF-8?q?c-packet=20every=2020=20CSI=20frames?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes WITNESS-LOG-110 §A0.11 wiring gap. Adds a separate 32-byte UDP packet (magic 0xC511A110, distinct from the CSI frame magic 0xC5110001) carrying: [0..3] magic 0xC511A110 (LE u32) — CSI-ADR-110 sync packet [4] node_id [5] proto version (0x01) [6] flags: bit0=is_leader, bit1=is_valid, bit2=smoothed_used [7] reserved [8..15] local esp_timer_get_time() (LE u64) [16..23] mesh-aligned epoch (LE u64) = local + EMA-smoothed offset [24..27] high-water sequence number (LE u32) for pairing with CSI frames [28..31] reserved (room for leader_id low32 in a follow-up) Emitted once per 20 CSI frames (≈ 1 Hz at the 20 Hz send-rate gate). Same stream_sender UDP socket as CSI frames — host dispatches by first 4 bytes of each datagram. Backwards compatible: aggregators that don't know about the new magic ignore it (sync packets won't match the CSI parser's magic check, so they're dropped harmlessly by existing receivers). New aggregators pair (node_id, sequence) across the two packet streams to align CSI frames to mesh time. Sets us up for downstream ADR-029/030 multistatic CSI fusion: with the host now able to recover the mesh-aligned epoch from each frame's sequence number, frames from multiple boards can be ordered + fused on a common timeline. Build evidence: C6 image 1019 KB (+1 KB vs v0.6.8 no-sync), 45 % partition slack unchanged. Host-side parser update is a follow-up. Co-Authored-By: claude-flow --- firmware/esp32-csi-node/main/csi_collector.c | 33 ++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/firmware/esp32-csi-node/main/csi_collector.c b/firmware/esp32-csi-node/main/csi_collector.c index c2f70115..e70fa988 100644 --- a/firmware/esp32-csi-node/main/csi_collector.c +++ b/firmware/esp32-csi-node/main/csi_collector.c @@ -16,6 +16,7 @@ #include "stream_sender.h" #include "edge_processing.h" #include "c6_timesync.h" /* ADR-110: 802.15.4 epoch for cross-node alignment */ +#include "c6_sync_espnow.h" /* ADR-110 §A0.11: mesh-aligned epoch for sync packet */ #include #include "esp_log.h" @@ -294,6 +295,38 @@ static void wifi_csi_callback(void *ctx, wifi_csi_info_t *info) edge_enqueue_csi((const uint8_t *)info->buf, (uint16_t)info->len, (int8_t)info->rx_ctrl.rssi, info->rx_ctrl.channel); } + + /* ADR-110 §A0.11 — Emit a sync-packet every SYNC_EVERY_N CSI frames so the + * host aggregator can pair node-local sequence numbers with the mesh-aligned + * epoch coming out of c6_sync_espnow_get_epoch_us(). Backwards-compatible + * with the ADR-018 frame format: new packet uses a distinct magic so the + * existing CSI parser can dispatch by first 4 bytes. */ + { + #define SYNC_EVERY_N_FRAMES 20 /* ~1 Hz at the 20 Hz send-rate gate */ + if ((s_cb_count % SYNC_EVERY_N_FRAMES) == 0) { + uint8_t sync[32]; + uint32_t sync_magic = 0xC511A110u; /* CSI-ADR-110 sync packet */ + uint64_t local_us = (uint64_t)esp_timer_get_time(); + uint64_t epoch_us = c6_sync_espnow_get_epoch_us(); + int64_t off_smooth = c6_sync_espnow_get_offset_us_smoothed(); + uint8_t flags = 0; + if (c6_sync_espnow_is_leader()) flags |= 0x01; + if (c6_sync_espnow_is_valid()) flags |= 0x02; + if (off_smooth != 0) flags |= 0x04; + + memcpy(&sync[0], &sync_magic, 4); + sync[4] = s_node_id; + sync[5] = 0x01; /* protocol version */ + sync[6] = flags; + sync[7] = 0; /* reserved */ + memcpy(&sync[8], &local_us, 8); + memcpy(&sync[16], &epoch_us, 8); + memcpy(&sync[24], &s_sequence, 4); /* high-water seq for pairing */ + uint32_t zero32 = 0; + memcpy(&sync[28], &zero32, 4); /* reserved (room for leader_id low32) */ + (void)stream_sender_send(sync, sizeof(sync)); + } + } } /**