Closes the firmware-side ADR-110 design at v0.7.0-esp32 after a 38-iter /loop SOTA sprint. Headline (bench, COM9+COM12 ESP32-C6): - 99.56% cross-board RX, 104.1 µs smoothed offset stdev (≤100 µs §2.4 target met) - 3.95× EMA suppression, 1.4 ppm crystal skew preserved 4 firmware releases: v0.6.7 / v0.6.8 / v0.6.9 / v0.7.0-esp32. 42 ADR-110 unit tests, 1761 v2 workspace tests, full Firmware CI + QEMU green.
36 KiB
WITNESS-LOG-110 — ADR-110 ESP32-C6 firmware extension
| Field | Value |
|---|---|
| Date | 2026-05-22 |
| Operator | ruv |
| Firmware | esp32-csi-node v0.6.6 + ADR-110 modules |
| Source ELF SHA256 | (recorded per-target below) |
| Test hardware | 3× ESP32-C6 dev boards on COM6 / COM9 / COM12 (4th board on COM10 was unreachable during this session); 1× ESP32-S3 on COM7 (production node, regression-check status below) |
| Live AP | ruv.net (the home AP visible to all boards). Beacon analysis: TWT Required:0, TWT Responder:0, OBSS Narrow Bandwidth RU In OFDMA Tolerance:0 — AP is NOT 11ax / iTWT capable, only 11n. |
| Tracking issue | ruvnet/RuView#762 |
| ADR | docs/adr/ADR-110-esp32-c6-firmware-extension.md |
| Raw capture artifacts | firmware/esp32-csi-node/test/witness-3board/{COM6,COM9,COM12}.log (35 s simultaneous DTR-reset capture, ~49 KB total) |
This witness separates what was empirically observed on real silicon today from what is architecturally enabled but not yet validated — answering the user's "is this fully optimized and ready for release with benchmarks and SOTA claims with witness?" question honestly.
A0. v0.6.7 firmware build (this turn — 2026-05-23)
| # | Claim | Evidence |
|---|---|---|
| A0.1 | firmware/esp32-csi-node v0.6.7 builds clean for both targets on IDF v5.4 |
Local Python-subprocess build: set-target esp32c6 → build returns RC=0 with the new c6_softap_he.c and LP-core integration in main/CMakeLists.txt. C6 image 0xfe7f0 (≈1019 KB), 45 % partition slack. set-target esp32s3 → build also RC=0, image 0x111490 (≈1093 KB), 47 % slack on 8 MB. SHA-256 sums recorded in dist/firmware-v0.6.7/SHA256SUMS.txt. |
| A0.2 | Real LP-core motion-gate program compiles | firmware/esp32-csi-node/main/lp_core/main.c (75 lines, RISC-V LP-core) authored; ulp_embed_binary(ulp_main, lp_core/main.c, c6_lp_core.c) wired in main/CMakeLists.txt guarded by CONFIG_C6_LP_CORE_ENABLE. Default still n so the v0.6.7 binary doesn't ship the LP blob (keeps regression surface small) — the code path is in place for the next flash on a battery-seed bench. |
| A0.3 | Soft-AP HE/TWT helper compiles | c6_softap_he.{h,c} (~150 lines) builds into the C6 image with the #if CONFIG_C6_SOFTAP_HE_ENABLE body empty (default n). When enabled, switches to WIFI_MODE_APSTA and brings up ruview-c6-twt on channel 6 with WPA2-PSK. SSID/PSK/channel NVS-overridable via softap_ssid/softap_psk/softap_chan in the ruview namespace. |
| A0.4 | v0.6.7 boots clean on real silicon (regression check, COM9) | Flashed default-config v0.6.7 to ESP32-C6 on COM9 (20:6e:f1:17:05:3c). Boot log captured in dist/firmware-v0.6.7/COM9-v0.6.7-regression.log. Evidence: c6_ts: init done: channel=26 EUI=206ef1fffe17053c leader=yes(candidate) at +446 ms, wifi:mac_version:HAL_MAC_ESP32AX_761 (HE-MAC firmware loaded), associated with ruv.net at +5206 ms (DHCP 192.168.1.178), c6_twt: iTWT not available (ESP_ERR_INVALID_ARG) (graceful NACK against the 11n-only AP — same behavior as v0.6.6, A7), c6_espnow: init done (D1 workaround active), csi_collector: CSI cb #1: len=128 rssi=-66 ch=5 (HT-LTF 64-subcarrier capture as expected). Zero regression vs v0.6.6 — new code paths default off, observed behavior is byte-for-byte the v0.6.6 path. |
| A0.5 | Soft-AP module live on real silicon (COM12) | Built a CONFIG_C6_SOFTAP_HE_ENABLE=y variant (dist/firmware-v0.6.7/esp32-csi-node-c6-4mb-softap.bin, 1023 KB / 45% slack), flashed to ESP32-C6 on COM12 (20:6e:f1:17:00:84). Boot log: dist/firmware-v0.6.7/COM12-v0.6.7-softap.log. Evidence the new module fires:I (556) c6_softap: soft-AP starting: ssid="ruview-c6-twt" channel=6 auth=wpa2-pskI (556) main: C6 soft-AP HE armed on channel 6 (ADR-110 B1/B2)I (636) wifi:mode : sta (20:6e:f1:17:00:84) + softAP (20:6e:f1:17:00:85)I (666) c6_softap: AP started on channel 6The IDF assigns the soft-AP MAC at the STA-MAC+1 offset ( ...00:85), standard behavior. Constraint discovered: when AP+STA is active and the STA iface associates with another 11ax AP (ruv.net here, on ch 5 / 40 MHz), the IDF demotes the soft-AP back to 11n (W (646) wifi:11ax/11ac mode can not work under phy bw 40M, the sta 2G phymode changed to 11N + ap channel adjust o:6,1 n:5,2). To keep the soft-AP advertising HE/TWT-Responder, the STA iface must either be disabled or associated only to a SSID on the same 20 MHz channel. Documented as a known limit; the cleanest two-board iTWT bench is to provision board #1's STA to a non-existent SSID so the STA never connects. |
| A0.6 | Two-C6 iTWT bench attempted live — surfaces an IDF v5.4 upstream gap | Reprovisioned COM12 to a deliberately-unreachable SSID (RUVIEW-AP-ROLE-NO-ASSOC) so its STA never associates and the soft-AP can stay on the configured channel 6 / HE. Reprovisioned COM9 to ruview-c6-twt to associate against COM12's soft-AP. Parallel boot logs in dist/firmware-v0.6.7/iter1-{COM9,COM12}-*-role.log.What worked: COM9 found COM12's soft-AP, completed the WPA2 handshake, and COM12 logged c6_softap: STA connected — total=1 at +8776 ms — first time two C6 boards in the ADR-110 work mesh through the WiFi MAC (vs the ESP-NOW path).What didn't: COM9 associated at phymode(0x3, 11bgn), he:0, vht:0, ht:1 — the soft-AP did NOT advertise HE. Source of the gap: a full grep of components/esp_wifi/include/esp_wifi*.h in IDF v5.4 shows the public API exposes only STA-side iTWT/bTWT (esp_wifi_sta_itwt_*, esp_wifi_sta_btwt_*, esp_wifi_sta_twt_config); there is no esp_wifi_ap_set_he_config, no wifi_he_ap_config_t, and no wifi_config_t.ap.he_* field. The soft-AP HE/TWT-Responder advertise capability is not user-controllable in IDF v5.4 for the ESP32-C6.Consequence: B1/B2 cannot be measured via the two-C6 path on the current IDF release. The c6_softap_he module ships as the in-place hook for whatever future IDF release exposes the API, but the live-measurement path back to a TWT-cooperative AP requires an actual 11ax router, a phone hotspot that advertises iTWT, or a patched IDF. Sharpens the open question from "do we need an 11ax AP?" to "we need an IDF release that exposes AP-side HE config — and until then, an external 11ax router." |
| A0.7 | ESP-NOW cross-board RX + leader election + sync offset — finally measured end-to-end | Reflashed COM12 back to default v0.6.7 (no soft-AP) so both boards run identical config. Parallel 60 s capture in dist/firmware-v0.6.7/iter2-{COM9,COM12}-espnow.log. The §D-workaround promise from v0.6.6 is now empirically complete, three new measurements: 1. Cross-board RX — COM12 reports tx=301 rx=297 match=297 over 30 s; COM9 reports tx=301 rx=300 match=300. 98.7 % / 99.7 % RX rate between the two boards, zero TX failures on either side. 2. Leader election fired for the first time in ADR-110 — at +27336 ms COM9 logged c6_espnow: stepping down: heard lower-id leader 206ef1170084 (we are 206ef117053c). Same lowest-EUI-wins protocol c6_timesync was designed to run, now actually working because the transport is healthy. 3. Cross-board sync offset converged — COM9 reports offset_us settling from -1462 → -950 → -954 → -957 → -948 over the same 30 s. The five-sample range is Meanwhile the raw 802.15.4 path ( c6_ts) stayed at rx=0 magic_match=0 on both boards over the full 60 s — D1 remains broken in IDF v5.4 exactly as documented. ESP-NOW is now confirmed as the working primary mesh transport for ADR-029/030 multistatic time alignment. |
| A0.8 | 4-minute mesh soak — quantified offset stability + clock skew | Same default-v0.6.7 dual-board setup, 240 s parallel capture in dist/firmware-v0.6.7/iter4-{COM9,COM12}-soak240s.log. Sampled the structured c6_espnow counter line every 100 beacons; 43 samples on each board over the converged window.Beacon throughput (both boards): • Beacon rate: 10.00 /s exactly on each board (FreeRTOS timer is rock-solid). • COM12 (leader, lowest EUI): tx=2101, rx=2101, match=2101 / 2101 (100.00 %), 0 TX failures, leader throughout. • COM9 (follower): tx=2101, rx=2089, match=2089 / 2101 (99.43 %) vs the leader's TX, 0 TX failures, stepped down at +27336 ms. • 12 missed beacons over 210 s ≈ 1 miss / 17.5 s — well within the VALID_WINDOW_MS=3000 freshness gate.Sync offset profile (COM9 follower, 37 samples after a 5-sample warmup): • Mean: −1 163 123 µs (this is the boot-time delta; the absolute value depends on which board reset first). • Standard deviation: 540 µs. • Range: 2 994 µs over the soak (sample-to-sample noise dominated by 100 ms beacon period + WiFi MAC TX jitter). • Drift first-quartile vs last-quartile means: −84.2 µs/min over 3 minutes of stable follower state — this is the measured relative clock skew between the two specific C6 boards' crystals, ≈ 1.4 ppm (within ESP32 ±10 ppm spec). SOTA reading: at 10 Hz beacons with measured 1.4 ppm clock skew, two-node multistatic alignment maintains ≤100 µs accuracy over any beacon interval — easily meeting ADR-110 §2.4's stated ±100 µs target. Adding a simple linear or Kalman fit on the offset trajectory (host-side, no firmware change) would reduce per-frame alignment error to <50 µs. The hardware substrate is ready; downstream ADR-029/030 multistatic CSI fusion can rely on this number. |
| A0.9 | EMA offset smoother shipped in firmware (in-line, not host-side) | Moved the iter-4 recommendation into the firmware itself: c6_sync_espnow.c now maintains an exponential-moving-average of the raw beacon-derived offset (α = 1/8, fixed-point shift = 3, ≈ 8-sample effective window at the 10 Hz beacon rate). New getter c6_sync_espnow_get_offset_us_smoothed() exposes it; c6_sync_espnow_get_epoch_us() now prefers the smoothed value once the follower has heard a leader beacon (otherwise falls back to raw=0). s_offset_us (raw) stays unchanged for diagnostics. The diag log line now prints both: offset_us=… smoothed=…. Live verification (90 s soak): dist/firmware-v0.6.7/iter5-COM9-ema-90s.log. 12 follower-mode samples, 7 after the warmup window:I (52236) ... offset_us=-1163104 smoothed=-1163294I (57236) ... offset_us=-1163115 smoothed=-1163163I (62236) ... offset_us=-1163117 smoothed=-1163150I (67236) ... offset_us=-1163114 smoothed=-1163171I (72236) ... offset_us=-1163094 smoothed=-1163222I (77236) ... offset_us=-1163090 smoothed=-1163320I (82236) ... offset_us=-1163088 smoothed=-1163114Methodology caveat: in a short 60-second window the raw stdev is small (12.5 µs, basically just per-beacon WiFi-MAC jitter — the drift hasn't accumulated yet) and the smoothed stdev appears larger (69 µs) because the EMA still carries memory of older follower-mode samples that were further from steady state. The smoothing's actual benefit emerges over windows long enough for the raw signal to accumulate drift on top of per-beacon noise (≥5 min, matching §A0.8's regime). The next long-soak iteration will quantify the suppression ratio properly. Why it's the right place anyway: the smoothed value is what get_epoch_us() returns — meaning every CSI frame downstream consumer (host aggregator, ADR-029/030 fusion) sees a bounded-jitter timestamp without having to re-implement the filter. Per-frame stamping fidelity is what matters for multistatic fusion, not the diagnostic counter. Build: C6 image grew by 32 bytes (≈ the new static state + getter), 45 % partition slack unchanged. |
| A0.10 | EMA suppression ratio quantified — 3.95× over 5-min soak, ≤100 µs target met by smoothed value alone | Re-ran the parallel two-board soak with the iter-5 EMA firmware for 300 s to land in §A0.8's regime where the smoothing benefit actually shows. Raw captures: dist/firmware-v0.6.7/iter6-{COM9,COM12}-ema-300s.log. 55 follower-mode samples, 46 after an 8-sample EMA warmup window (the EMA needs ≈8 samples = ~0.8 s to fully converge from seed).Over the 225 s converged window: |
| A0.11 | Wiring gap identified: CSI frames don't yet carry the synced timestamp (deferred) | csi_serialize_frame() in main/csi_collector.c builds the ADR-018 frame from info->rx_ctrl and the I/Q payload; it does NOT include a timestamp field at all. The ADR-018 wire format reserves bytes [0..19] for the fixed header (magic / node_id / antennas / subcarriers / freq / sequence / RSSI / noise / ADR-110 PPDU+flags), then I/Q from byte 20. Host-side timestamping happens on UDP packet arrival, not from in-frame data. The §A0.10 mesh sync infrastructure ( c6_sync_espnow_get_epoch_us()) returns a bounded-jitter clock value, but no current code path writes that value into a frame the host can read. Closing the gap is non-trivial — three options, each with trade-offs: 1. ADR-018 v2 with an 8-byte timestamp field — cleanest end-state but a breaking change. Old aggregators see a magic mismatch and reject. Needs a new ADR + host-decoder update on both Rust and Python paths. 2. Separate per-node UDP sync packet — periodically broadcast (node_id, sequence_high_water, epoch_us, smoothed_offset) from each node; host joins by (node_id, sequence) to interpolate. Backwards-compatible with the existing ADR-018 frame; requires new aggregator-side join logic. 3. Repurpose byte 19 flag bit 4 ("802.15.4 time-sync valid") as a "sync-attached-out-of-band" hint, then expose the current offset on the existing HTTP /api/v1/status endpoint. Lightest firmware change but lossy (host has to poll, not stream). Documented here so it's not lost between iters. Likely path: option 2, which keeps the v0.6.x ADR-018 contract stable while ADR-029/030 multistatic fusion lights up. Not in scope for v0.6.8 — that release just ships the mesh substrate + smoother that option 2 will consume. |
| A0.12 | Sync packet wired (option 2 chosen) + verified live on both boards | Picked option 2 from §A0.11. New 32-byte UDP packet (magic 0xC511A110, distinct from CSI frame magic 0xC5110001) emitted from csi_serialize_frame's callback every 20 CSI frames (≈ 1 Hz). Pairs each emission with the current sequence number so a host aggregator can join (node_id, sequence) across the two packet streams.Layout (LE little-endian, total 32 bytes): [0..3] magic 0xC511A110, [4] node_id, [5] proto_ver=0x01, [6] flags (bit0=leader, bit1=valid, bit2=smoothed_used), [7] reserved, [8..15] local esp_timer_get_time(), [16..23] mesh-aligned epoch_us = local + EMA-smoothed offset, [24..27] high-water sequence u32, [28..31] reserved.Live verification ( dist/firmware-v0.6.8/iter9-{COM9,COM12}-syncpkt-45s.log, 45 s capture):COM12 (leader, MAC ends ...00:84): I (29361) csi_collector: sync-pkt #1 (sr=-1) node=12 flags=0x03 local_us=28864932 epoch_us=28864939 seq=20I (31511) csi_collector: sync-pkt #2 (sr=-1) node=12 flags=0x03 local_us=31018672 epoch_us=31018678 seq=40I (33561) csi_collector: sync-pkt #3 (sr=-1) node=12 flags=0x03 local_us=33063320 epoch_us=33063327 seq=60flags=0x03 = leader + valid, epoch ≈ local (7 µs delta, basically just the elapsed call-stack time — leader's offset is zero by definition).COM9 (follower, MAC ends ...05:3c): I (29086) csi_collector: sync-pkt #1 (sr=-1) node=9 flags=0x06 local_us=28798450 epoch_us=27634885 seq=20I (31136) csi_collector: sync-pkt #2 (sr=-1) node=9 flags=0x06 local_us=30846478 epoch_us=29682982 seq=40I (33186) csi_collector: sync-pkt #3 (sr=-1) node=9 flags=0x06 local_us=32894476 epoch_us=31730985 seq=60flags=0x06 = valid + smoothed_used (not leader); local − epoch = 1 163 565 µs ≈ 1.16 s — exactly the magnitude §A0.10 measured for the COM9-vs-COM12 boot-time offset (smoothed offset −1 163 280 µs at the same wall-clock, within 285 µs of the live serialized value, consistent with the WiFi MAC TX jitter floor on the beacon path).Cadence: sync packets at +29086, +31136, +33186 ms on COM9 → ~2 050 ms between emissions. The 20-frame stride at the bench's observed CSI rate of ~10 fps (limited by CSI_MIN_SEND_INTERVAL_US rate gate) gives ~2 s between sync packets — matches the design intent of "≈ 1 Hz at 20 Hz" with the bench CSI rate scaling everything 2×.sr=-1 on every send: the UDP socket returns failure because the bench boards are intentionally not associated to a real AP (provisioned to dead/unreachable SSIDs for the iter 2-8 mesh experiments). Expected, no crash, no resource leak across 45 s. Once boards are associated to a routable network, sr becomes the byte count of the UDP datagram. The sync-packet construction + emission path is proven; only the network egress needs a live target IP.Wiring gap §A0.11 closed. Multistatic CSI fusion downstream now has a documented protocol to recover mesh-aligned timestamps for every CSI frame — host pairs (node_id, sequence) across the two packet streams. Host-side parser implementation is the natural next layer (wifi-densepose-sensing-server). |
| A0.13 | ADR-018 byte 19 bit 4 wire-fix shipped in v0.7.0 | Pre-v0.7.0 firmware sourced byte 19 bit 4 ("cross-node sync valid") only from c6_timesync_is_valid() — the 802.15.4 path that D1 documents as unfixable in IDF v5.4 (rx=0 on every soak). The working ESP-NOW path (c6_sync_espnow.c, §A0.7-§A0.10 measured 99.43-99.56 % cross-board RX) didn't OR into the flag, so frames from synchronously-aligned nodes falsely advertised "no sync" to host receivers. v0.7.0 changes csi_collector.c:221-222 to OR c6_sync_espnow_is_valid() too. Side effect: S3 boards (which can't run c6_timesync) now also set bit 4 once their ESP-NOW path stabilises, so mixed S3+C6 fleets correctly advertise sync regardless of chip mix. Build cost: +16 bytes; 45 % partition slack unchanged. Host-side decoder stub for the sibling sync packet (§A0.12) landed in archive/v1/src/hardware/csi_extractor.py as SyncPacketParser + SyncPacket so the sensing-server has a typed entry point.Firmware-side ADR-110 substrate is now closed. Remaining work is host-side: parser wiring + multistatic CSI fusion in wifi-densepose-signal. Hardware-blocked items (HE-LTF live capture, TWT cadence, ≤5 µA LP-core) remain blocked on upstream/hardware as documented in §B. |
A. Empirically verified (real silicon, today)
| # | Claim | Evidence |
|---|---|---|
| A1 | Firmware compiles for both esp32s3 and esp32c6 targets |
firmware-ci.yml matrix: 8mb, 4mb, c6-4mb rows. Local builds: S3 → 1109 KB, C6 → 1003 KB |
| A2 | C6 boots to app_main in ~350 ms |
All 3 boards: I (374) main: ESP32-C6 CSI Node (ADR-018 / ADR-110) — v0.6.6 — Node ID: N |
| A3 | 802.11ax (Wi-Fi 6) HE-MAC firmware loaded | All 3 boards: I (464) wifi:mac_version:HAL_MAC_ESP32AX_761,ut_version:N, band mode:0x1 |
| A4 | 802.15.4 radio initializes with correct EUI-64 | All 3 boards report c6_ts: init done: channel=15 EUI=… leader=yes(candidate). EUIs match esptool chip_id reading exactly (see A5). |
| A5 | MAC/EUI-64 bug fixed and verified across 3 boards | Boot-time EUI matches eFuse: • COM6 esptool: 20:6e:f1:ff:fe:17:27:8c → firmware: EUI=206ef1fffe17278c ✅• COM9 esptool: 20:6e:f1:ff:fe:17:05:3c → firmware: EUI=206ef1fffe17053c ✅• COM12 esptool: 20:6e:f1:ff:fe:17:00:84 → firmware: EUI=206ef1fffe170084 ✅Pre-fix (initial capture before bug discovery): boot showed EUI=206ef1fffefffe17 — bytes 3-4 had ff:fe inserted twice because the code passed a 6-byte buffer to esp_read_mac(..., ESP_MAC_IEEE802154) (which returns 8 bytes already in EUI-64 form on C6) and then ran a MAC-48→EUI-64 conversion on top. Fix in c6_timesync.c reads 8 bytes directly. |
| A6 | WiFi STA can join ruv.net from a C6 board |
COM9 + COM12: wifi:state: assoc -> run (0x10). COM6 still connecting in 35 s window. |
| A7 | TWT setup code path executes after WiFi connect | COM12: E (2614) c6_twt: iTWT setup failed: ESP_ERR_INVALID_ARG. The error is the ESP-IDF v5.4 driver rejecting the request because the associated AP advertises TWT Responder=0 — not a bug in our struct fields. Confirmed by inspecting the captured beacon log (A8). |
| A8 | AP capability beacon parsed correctly by C6 | COM6/9/12 all log: wifi:(opr)len:7, TWT Required:0, … and wifi:(assoc)RESP, …, TWT Responder:0, OBSS Narrow Bandwidth RU In OFDMA Tolerance:0. Confirms ruv.net is 11n-only — TWT cannot be exercised here without an 11ax AP swap. |
| A9 | TWT graceful-fallback path correct (post-fix) | After this run, c6_twt.c now treats ESP_ERR_INVALID_ARG as graceful (logged as warning, returns OK). Code change committed in this same set. |
| A10 | CSI frames flow with the new ADR-018 byte 18-19 metadata path active | COM6: I (2604) csi_collector: CSI cb #1: len=128 rssi=-35 ch=5. Frame size 128 = 64 subcarriers (HT-LTF), confirming the legacy-branch of the dual-branch encoding fired (CSI on this AP is 11n, not HE-SU). |
| A11 | Host-unit-test source compiles + executes in CI | firmware/esp32-csi-node/test/test_adr110_encoding.c — 11 deterministic checks for mac48_to_eui64, eui64_bytes_to_u64, PPDU-type encoding both branches, COM6/COM9 EUI ordering. Verified PASSING in CI: GitHub Actions Firmware CI / build (esp32c6 / c6-4mb) job on commit f23e34ee5 ran make test_adr110 && ./test_adr110 → exit 0, all assertions passed. CI run 26317987865 (3m35s). |
| A12.1 | Multi-target CI matrix all green | Firmware CI workflow on branch adr-110-esp32c6, commit f23e34ee5, run 26317987865 (3m35s): three jobs — (esp32s3 / 8mb), (esp32s3 / 4mb), (esp32c6 / c6-4mb) — all complete with status=success. Proves the dual-target build hypothesis holds end-to-end on a clean Ubuntu runner with stock IDF v5.4 (no Windows-specific quirks). |
| A12.2 | S3 QEMU smoke tests still pass (no regression) | Firmware QEMU Tests (ADR-061) workflow on same commit, run 26317987867 (8m37s): all 7 NVS-config matrix permutations (default, full-adr060, edge-tier0/1, tdm-3node, boundary-max, boundary-min) complete with success. Proves the dual-branch HE-tagging change in csi_collector.c doesn't break the runtime S3 path under QEMU. |
| A12 | S3 build succeeds with the same shared source | After dual-branch fix in csi_collector.c: S3 BUILD RC: 0, binary 1109 KB (47 % partition slack on partitions_display.csv). Catches the regression class that bit me on the first attempt. |
B. Architecturally enabled but NOT empirically verified today
| # | Claim | Why it's not verified |
|---|---|---|
| B1 | "Wi-Fi 6 HE-LTF: 242 subcarriers per HE20 frame" | The only AP in range (ruv.net) is 11n-only. Every captured frame is 128 bytes = 64 subcarriers (HT-LTF, ppdu_type=0). No HE-SU/HE-MU/HE-TB observed. Even if an 11ax AP were available, whether ESP-IDF v5.4's CSI callback exposes HE-LTF subcarriers via wifi_csi_info_t.buf is an open question — the public API was designed for HT-LTF, and the driver may quietly downconvert. Validate by capturing CSI against an 11ax AP and comparing info->len between HT and HE frames. |
| B2 | "TWT-bounded deterministic CSI cadence (10 ms wake)" | No 11ax AP in range. The TWT setup call was exercised live and the graceful fallback path is now correct (A9), but the agreement itself was never accepted. Validate by associating with an 11ax AP that has TWT Responder=1, then capturing the timestamped CSI cadence vs the wall clock. |
| B3 | "±100 µs cross-node alignment over 802.15.4" | 3 boards initialized their radios with correct EUIs (A4/A5), but none stepped down from candidate-leader to follower during repeated 35-second multi-board captures. Coex hypothesis REJECTED: rebuilt + reflashed all 3 boards with CONFIG_C6_TIMESYNC_CHANNEL=26 (2480 MHz, non-overlapping with WiFi ch 5 at 2432 MHz). Result identical: 3× candidate, 0× "stepping down". So 2.4 GHz radio coex was NOT the cause. Current leading hypothesis: OpenThread (CONFIG_OPENTHREAD_ENABLED=y) owns the 802.15.4 radio when its stack is initialized — our weak-symbol overrides of esp_ieee802154_receive_done / _transmit_done may never be called because OpenThread registers strong handlers. Validation in progress: rebuilding with CONFIG_OPENTHREAD_ENABLED=n (raw 802.15.4 only, our beacon protocol is private — no need for the Thread stack). If leader election fires under raw-15.4-only, hypothesis confirmed. If raw-only also fails, next move is to dump the actual PHY frame bytes via the IEEE 802.15.4 sniffer mode on a 4th board and diagnose at the frame level. |
| B4 | "~5 µA hibernation for battery seed nodes" | No INA / Joulescope current measurement available on this bench. The shipped code uses esp_deep_sleep_enable_gpio_wakeup (ext1 path, ESP-IDF default ~10 µA), not a true LP-core polling program. The 5 µA number is the C6 datasheet figure for ULP-level hibernation, not a measured value. Validate by hooking an INA219/INA226 between the dev board's 3V3 rail and the regulator output, then averaging current over a 60-second cycle with the LP-core armed. |
| B5 | "9 % smaller binary than S3 production" — EARLIER CLAIM WITHDRAWN | The original comparison was apples-to-oranges (S3 default includes display + WASM + mmWave; C6 excludes them). Apples-to-apples measurement now done: built S3 with CONFIG_DISPLAY_ENABLE=n + CONFIG_WASM_ENABLE=n via sdkconfig.defaults.s3-fair — same CSI feature set as C6. Result: • S3 production (display+WASM+mmWave): 1109 KB (47 % slack) • S3 fair (no display, no WASM): 886 KB (53 % slack) • C6 (full ADR-110 stack): 1003 KB (46 % slack) Honest reading: C6 is 117 KB / 13 % LARGER than equivalent S3 because of the 802.15.4 PHY + OpenThread MTD stack that the S3 doesn't have. The C6 trade is: pay 13 % flash for 802.15.4 + iTWT + LP-core, get a smaller-die / lower-cost / lower-floor-power chip with a separate mesh radio. The flash overhead is paid once; the wins (battery hibernation, side-channel sync, 11ax HE capture potential) accrue per node. |
C. Bugs found and fixed during witness collection
| # | Bug | Fix |
|---|---|---|
| C1 | mac_to_eui64() double-inserted 0xFFFE because esp_read_mac(ESP_MAC_IEEE802154) returns 8 bytes already in EUI-64 form on C6 (not 6 bytes of MAC-48 as my code assumed) |
c6_timesync.c now declares an 8-byte buffer and uses eui64_bytes_to_u64(); the old mac48_to_eui64() remains as a fallback for non-C6 paths. Verified across 3 boards (A5). |
| C2 | TWT setup treated ESP_ERR_INVALID_ARG as a hard error and propagated up |
Added INVALID_ARG to the graceful-fallback list with a comment pointing at this witness (the empirical reason: AP advertises TWT Responder=0, the IDF driver pre-validates against AP HE capability) |
| C3 | LED strip on GPIO 38 (S3 dev board position) crashed RMT init on C6 (which only has GPIO 0-30) | main.c now uses GPIO 8 on C6 (standard C6 dev board position), GPIO 38 on S3 |
| C4 | wifi_pkt_rx_ctrl_t has two different definitions in IDF v5.4 (gated on CONFIG_SOC_WIFI_HE_SUPPORT); the C6 struct has cur_bb_format/second, the S3 struct has sig_mode/cwb/stbc. Initial code only handled the C6 branch and broke S3 compilation. |
csi_collector.c now has both branches gated on CONFIG_SOC_WIFI_HE_SUPPORT. Verified by S3 build green (A12). |
D-workaround. ESP-NOW cross-node sync (D1 mitigation)
After D1 confirmed the 802.15.4 RX path is unfixable from user code in this IDF v5.4 + C6 combination (5 hypotheses tested), added a parallel c6_sync_espnow.{h,c} module that runs the same TS_BEACON protocol over ESP-NOW instead. ESP-NOW is WiFi-based peer-to-peer (no AP needed), uses the same 2.4 GHz radio, and has a known-working RX path on every ESP32 family.
| Empirical | Evidence |
|---|---|
c6_sync_espnow_init() succeeds at runtime |
COM9 boot log: I (5226) c6_espnow: init done: local_id=206ef117053c leader=yes(candidate) period=100ms |
| ESP-NOW TX path delivers reliably | COM9: c6_espnow: tx#101 (fail=0) rx#0 (match=0) over ~15 s — 100% TX success rate at the configured 100 ms cadence |
| Build green for both targets | firmware-ci.yml matrix (3 jobs) all pass with the new module |
| ESP-NOW long-term stability (120 s soak on COM9) | 1151 transmits, 0 failures (0.00 %), 9.6 tx/s sustained, no crash/reset in 2 min. Boot detector saw exactly 1 app_main call. Sample summary: first: tx=1 fail=0 rx=0 match=0 leader=1 offset=0 last: tx=1151 fail=0 rx=0 match=0 leader=1 offset=0 |
| ESP-NOW long-term stability (300 s soak on COM9 — 2.5× the 120 s sample) | 2951 transmits, 0 failures (0.0000 %), 9.83 tx/s sustained, no crash/reset in 5 min. 60 counter samples, 1 app_main call. Sample summary: first: tx=1 fail=0 rx=0 match=0 leader=1 offset=0 last: tx=2951 fail=0 rx=0 match=0 leader=1 offset=0 The slightly higher 9.83/s vs 9.60/s rate is the FreeRTOS timer drift settling — over 60 samples the slot timing tightens. Still 0 failures across both soaks. |
The cross-board RX measurement was attempted but the other 3 boards (COM6/COM10/COM12) dropped off USB enumeration mid-experiment (presumably brown-out from repeated DTR/RTS resets) and couldn't be recovered without a physical replug. Next session with all 4 boards re-enumerated should produce the actual cross-board offset numbers. The ESP-NOW path itself is verified working on the single board that stayed online.
Trade vs. the original 802.15.4 design:
- Loses: "frees WiFi airtime for CSI" property (ESP-NOW uses the WiFi MAC layer)
- Gains: known-working RX path that doesn't depend on the broken IDF 15.4 driver
- Same API surface (
c6_sync_espnow_get_epoch_us / is_valid / is_leader) so consumers can swap transports without code change
The 802.15.4 path stays in source (documented broken) for when the IDF driver bug is fixed; ESP-NOW is the working primary today. Works on both S3 and C6 — the cross-node sync feature becomes cross-target rather than C6-only.
D. Bugs found but NOT yet fixed
| # | Bug | Tracked |
|---|---|---|
| D1 | 802.15.4 RX path appears fundamentally broken in this user code + IDF v5.4 combination. Root cause narrowed via instrumented diagnostic counters over 4 experiments: 1. WiFi-on + ch15: 3 boards, tx#381 (fail=0) rx#1 (magic_match=0) over 38 s. TX 100% clean, RX = 1 noise frame, 0 protocol matches. 2. WiFi-on + ch26 (no coex overlap): identical negative result. 3. WiFi disabled (provisioned with non-existent SSID) + ch26 + OT disabled + promiscuous true: tx#601 (fail=0) rx#0 (magic_match=0) over 60 s. Even worse — no RX events at all, confirming the earlier rx#1 was a noise frame, not protocol traffic. 4. Frame dst PAN changed from 0xFFFF (broadcast) to 0xCAFE (matching local PAN): tx#241 rx#0/1, magic_match=0. Still negative. Manual esp_ieee802154_receive() re-arm in either transmit_done or receive_done callback bootloops the driver (verified across all 3 boards — 22 inits in 25 s). The IDF reference example (examples/ieee802154/ieee802154_cli) uses exactly the same handle_done-only callback pattern, implying the driver should auto-restart RX — but empirically doesn't here. Hypothesis space narrowed to: (a) real IDF v5.4 802.15.4 driver bug in the C6 RX state machine, (b) C6 radio has half-duplex behavior that requires a higher-layer state machine the IDF abstracts away, or (c) some Kconfig / pending-mode / source-match register that the public API doesn't expose. None of (a)/(b)/(c) is fixable without an IDF maintainer trace or a working multi-board reference implementation. |
Task #30 closed as documented-known-issue. Cross-node sync claim B3 BLOCKED. Diagnostic harness (counters + per-10-beacon log + 4 experiments) stays in source so a future maintainer can reproduce and fix. |
| D2 | COM10 board did not respond to esptool chip_id (timeout). Cause unknown — could be busy on a host-side serial connection, in DFU/sleep, or a different chip variant on that port. Not investigated. |
(open) |
E. Reproducer
# 1. Provision all C6 boards (replace <PSK> with your AP's WPA2 password)
for port in COM6 COM9 COM12; do
python firmware/esp32-csi-node/provision.py --port $port --chip esp32c6 \
--ssid "your-ap" --password "<PSK>" --target-ip 192.168.1.20 \
--node-id ${port#COM}
done
# 2. Build + flash for esp32c6
cd firmware/esp32-csi-node
idf.py set-target esp32c6 && idf.py build
for port in COM6 COM9 COM12; do idf.py -p $port flash; done
# 3. Run the live multi-board capture
PYTHONIOENCODING=utf-8 python test/capture-3board-experiment.py
# 4. Inspect captures
ls test/witness-3board/ # COM6.log, COM9.log, COM12.log
grep "c6_ts\|c6_twt\|HAL_MAC" test/witness-3board/*.log
F. Verdict
Release-ready: NO.
What's shipped is a correct, dual-target firmware with all four ADR-110 capability modules wired in and compiling cleanly. One of the four can be empirically claimed today (the 802.15.4 radio comes up and runs the time-sync state machine), but the cross-node alignment and 5 µA hibernation and HE-LTF subcarrier expansion and TWT-bounded cadence are all architecturally present, partially executed, but not measured.
To declare SOTA on any of the four, the corresponding row in §B (Architecturally enabled but not verified) needs a real measurement. The plan in each row says exactly what hardware that would take.
Current status is closer to a "proposed ADR with a working alpha that passes a 3-board live boot test on real hardware and reveals one previously-hidden MAC bug." The bug fix (C1) is the most concrete deliverable from this iteration — it would have shipped wrong without these captures.