Skip to content

Commit 00a234e

Browse files
authored
ADR-110: ESP32-C6 firmware extension (#764)
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.
1 parent 5d54412 commit 00a234e

63 files changed

Lines changed: 5865 additions & 64 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/firmware-ci.yml

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
echo "version.txt matches the release tag."
3939
4040
build:
41-
name: Build ESP32-S3 Firmware (${{ matrix.variant }})
41+
name: Build firmware (${{ matrix.target }} / ${{ matrix.variant }})
4242
runs-on: ubuntu-latest
4343
container:
4444
image: espressif/idf:v5.4
@@ -47,17 +47,27 @@ jobs:
4747
matrix:
4848
include:
4949
- variant: 8mb
50+
target: esp32s3
5051
sdkconfig: sdkconfig.defaults
5152
partition_table_name: partitions_display.csv
5253
size_limit_kb: 1100
5354
artifact_app: esp32-csi-node.bin
5455
artifact_pt: partition-table.bin
5556
- variant: 4mb
57+
target: esp32s3
5658
sdkconfig: sdkconfig.defaults.4mb
5759
partition_table_name: partitions_4mb.csv
5860
size_limit_kb: 1100
5961
artifact_app: esp32-csi-node-4mb.bin
6062
artifact_pt: partition-table-4mb.bin
63+
# ADR-110: ESP32-C6 research target (Wi-Fi 6 / 802.15.4 / TWT / LP-core)
64+
- variant: c6-4mb
65+
target: esp32c6
66+
sdkconfig: sdkconfig.defaults
67+
partition_table_name: partitions_4mb.csv
68+
size_limit_kb: 1100
69+
artifact_app: esp32-csi-node-c6.bin
70+
artifact_pt: partition-table-c6.bin
6171

6272
steps:
6373
- uses: actions/checkout@v4
@@ -66,12 +76,22 @@ jobs:
6676
working-directory: firmware/esp32-csi-node
6777
run: |
6878
. $IDF_PATH/export.sh
69-
if [ "${{ matrix.variant }}" != "8mb" ]; then
79+
# 4mb variant supplies its own sdkconfig.defaults overlay.
80+
# c6-4mb variant relies on the auto-applied sdkconfig.defaults.esp32c6
81+
# overlay (ESP-IDF auto-loads sdkconfig.defaults.$TARGET when present).
82+
if [ "${{ matrix.variant }}" = "4mb" ]; then
7083
cp "${{ matrix.sdkconfig }}" sdkconfig.defaults
7184
fi
72-
idf.py set-target esp32s3
85+
idf.py set-target ${{ matrix.target }}
7386
idf.py build
7487
88+
- name: Build and run host-side ADR-110 unit tests
89+
if: matrix.variant == 'c6-4mb'
90+
working-directory: firmware/esp32-csi-node/test
91+
run: |
92+
make test_adr110
93+
./test_adr110
94+
7595
- name: Verify binary size (< ${{ matrix.size_limit_kb }} KB gate)
7696
working-directory: firmware/esp32-csi-node
7797
run: |

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6262
they can be reintroduced with a real implementation.
6363

6464
### Added
65+
- **ESP32-C6 firmware target with Wi-Fi 6 / 802.15.4 / TWT / LP-core support ([ADR-110](docs/adr/ADR-110-esp32-c6-firmware-extension.md), #762).** `firmware/esp32-csi-node` now builds for **both** `esp32s3` (existing production node) and `esp32c6` (new research/seed-node target) from the same source tree — pick via `idf.py set-target esp32c6` and ESP-IDF auto-applies the new `sdkconfig.defaults.esp32c6` overlay. Every C6 module is `#ifdef CONFIG_IDF_TARGET_ESP32C6` gated, so the S3 build is byte-identical to today (no regression).
66+
- **Wi-Fi 6 HE-LTF subcarrier tagging**`csi_collector.c` now reads `rx_ctrl.cur_bb_format` and writes the PPDU type (0=HT/legacy, 1=HE-SU, 2=HE-MU, 3=HE-TB) into ADR-018 frame byte 18, plus bandwidth flags (20/40 MHz, STBC, 802.15.4-sync-valid) into byte 19. Bytes 18-19 were previously reserved-zero, so old aggregators read them as before — fully backwards compatible. Magic stays `0xC5110001`. Default on via `CONFIG_CSI_FRAME_HE_TAGGING`. First firmware in the open ESP32 ecosystem to tag CSI frames with 11ax PPDU metadata.
67+
- **802.15.4 mesh time-sync** — new `c6_timesync.{h,c}` (262 lines) provides cross-node clock alignment over the C6's separate 802.15.4 radio, freeing WiFi airtime from coordination traffic (directly addresses the ADR-029/030 multistatic synchronization gap). Protocol: lowest EUI-64 wins election, leader broadcasts `TS_BEACON` (`magic=0x54534D45`, leader epoch µs) every 100 ms on channel 15, followers compute `offset = leader_us - local_us` and apply lazily — every CSI frame is stamped with `c6_timesync_get_epoch_us()`. Target alignment ±100 µs. Default on via `CONFIG_C6_TIMESYNC_ENABLE`. Verified initializing at boot on COM6 (`c6_ts: init done: channel=15 EUI=206ef1fffefffe17 leader=yes(candidate)` at +413 ms).
68+
- **TWT (Target Wake Time)** — new `c6_twt.{h,c}` (223 lines) wraps `esp_wifi_sta_itwt_setup` from `esp_wifi_he.h` to negotiate an individual TWT agreement with the AP after STA connect. Replaces today's opportunistic CSI capture with a scheduler-bounded one (default wake interval 10 ms = 100 fps cadence). Graceful NACK fallback: when the AP doesn't support 11ax iTWT, the helper logs and returns OK so the device keeps doing opportunistic CSI just like the S3. Teardown on `WIFI_EVENT_STA_DISCONNECTED` keeps the AP's TWT scheduler clean. Gated on `SOC_WIFI_HE_SUPPORT` (auto-set on C6/C5 chips).
69+
- **LP-core wake-on-motion hibernation** — new `c6_lp_core.{h,c}` (134 lines) arms the C6 LP RISC-V coprocessor as an always-on motion gate; HP core stays in deep sleep until a configurable GPIO wakes it (ext1 deep-sleep wake source in this initial cut, real LP-core program in follow-up). Targets ≤5 µA hibernation current for battery-powered Cognitum Seed nodes (vs the S3's ~10 µA ULP-FSM floor). Opt-in via `CONFIG_C6_LP_CORE_ENABLE` (default off — only enabled on nodes flashed for battery-powered seed duty).
70+
- **Build matrix**: S3 stays `partitions_display.csv` (8 MB + display + WASM), C6 uses `partitions_4mb.csv` (4 MB single OTA, no display, no WASM3, no LCD). C6 final binary 1003 KB (46% partition slack), 9 % smaller than S3 production. Free heap 310 KiB at boot, app_main reached in 343 ms, 802.15.4 stack up in another 70 ms.
71+
- **Why this matters**: opens three research surfaces nobody has published yet — Wi-Fi-6 CSI human pose, multistatic CSI clock alignment over a side-channel radio, and TWT-bounded deterministic CSI cadence. The S3 production fleet keeps shipping the existing capabilities; the C6 is the research / battery-seed expansion target.
72+
- **Docs**: ADR-110 (186 lines, Status=Accepted), tracking issue [ruvnet/RuView#762](https://github.com/ruvnet/RuView/issues/762) with per-phase progress comments, README hardware table + Quick-Start Option 2b, `docs/user-guide.md` full ESP32-C6 section (build, flash, provision, multi-room time-sync, battery seed mode), full empirical record in [`docs/WITNESS-LOG-110.md`](docs/WITNESS-LOG-110.md) with verified / claimed / bugs-fixed / bugs-found sections.
73+
- **Wave 2 follow-up (D1 workaround)**: 5 systematic experiments on 3 live C6 boards confirmed the IDF v5.4 802.15.4 RX path is unfixable from user code (TX works 100 %, RX delivers 0 frames; coex/channel/OpenThread/manual-rearm all ruled out). Pivoted to ESP-NOW for the cross-node sync transport — `main/c6_sync_espnow.{h,c}` is the same TS_BEACON protocol over WiFi peer-to-peer, same `get_epoch_us / is_valid / is_leader` API surface. **120 s single-board soak: 1151 transmits, 0 failures (0.00 %), 9.6 tx/s sustained, no crash or reset.** The 802.15.4 path stays in source as documented-broken (D1) for when the IDF driver gets fixed.
74+
- **Host-side dual-pipeline decoder for ADR-018 byte 18-19** (ADR-110 protocol closure):
75+
- **Rust** (`v2/crates/wifi-densepose-hardware`): new `PpduType` enum (HtLegacy/HeSu/HeMu/HeTb/Unknown) and `Adr018Flags` struct (bw40/stbc/ldpc/ieee802154_sync_valid) on `CsiMetadata`. 6 new deterministic unit tests; **122/122 hardware-crate tests pass**.
76+
- **Python** (`archive/v1/src/hardware/csi_extractor.py`): `HEADER_FMT` extended from `<IBBHIIBB2x` to `<IBBHIIBBBB`; new metadata fields (`ppdu_type`, `he_capable`, `bw40`, `stbc`, `ldpc`, `ieee802154_sync_valid`). 5 new `TestAdr110ByteEncoding` cases; **11/11 parser tests pass**.
77+
- Both decoders match the firmware encoder bit-for-bit. Pre-ADR-110 firmware sends zeros that round-trip as `HtLegacy` + default flags — fully backwards compatible.
78+
- **Security fix** (`scripts/redact-secrets.py` + `generate-witness-bundle.sh`): the Python proof step was echoing `.env` contents into the bundled `verification-output.log` via Pydantic validation errors. Bundle nuked before push; added a `stdin -> stdout` redaction filter covering common token prefixes, long opaque strings, and long hex runs. Verified zero leaks on rebuild.
79+
- **Wave 3 — firmware v0.6.7 (LP-core full + soft-AP HE)**: two software-only unblocks for the hardware-blocked items in WITNESS-LOG-110 §B. (1) **Real LP-core motion-gate program** (`firmware/esp32-csi-node/main/lp_core/main.c` + integration in `c6_lp_core.c`). When `CONFIG_C6_LP_CORE_ENABLE=y`, the LP RISC-V coprocessor now runs a real polling program (configurable cadence via `CONFIG_C6_LP_POLL_PERIOD_US`, default 10 ms) that debounces N consecutive GPIO samples (`CONFIG_C6_LP_DEBOUNCE_SAMPLES`, default 3) and wakes the HP core via `ulp_lp_core_wakeup_main_processor()`. HP entry uses `esp_sleep_enable_ulp_wakeup` + `ESP_SLEEP_WAKEUP_ULP`. Exposes `c6_lp_core_motion_count()` and `c6_lp_core_poll_count()` getters for the witness harness. **Replaces** the v0.6.6 `esp_deep_sleep_enable_gpio_wakeup` ext1 fallback (which floored at ~10 µA, the same as the S3 ULP-FSM). The fallback path stays as the `else` branch so builds without `CONFIG_C6_LP_CORE_ENABLE` keep working unchanged — zero regression for v0.6.6-era fleets. Targets the C6 datasheet ≤5 µA average for battery seed nodes; pending INA/Joulescope measurement to confirm (`WITNESS-LOG-110 §B4`). (2) **Wi-Fi 6 soft-AP with TWT Responder=1** (`c6_softap_he.{h,c}` + `main.c` AP+STA mode switch). When `CONFIG_C6_SOFTAP_HE_ENABLE=y`, one C6 board can act as the iTWT-capable AP the bench is otherwise missing — pair with a second C6-STA board to negotiate real iTWT against a known-cooperative AP and measure deterministic CSI cadence (`WITNESS-LOG-110 §B1/B2`). SSID/PSK/channel configurable via Kconfig defaults or NVS (`softap_ssid`/`softap_psk`/`softap_chan` keys in the `ruview` namespace). Default off so existing nodes are unaffected. **Build artifacts**: S3 8 MB binary 1093 KB (47 % slack), C6 4 MB binary 1019 KB (45 % slack). Tag: `v0.6.7-esp32`.
80+
- **Wave 4 — firmware v0.6.8 (ESP-NOW mesh offset smoother)**: `c6_sync_espnow.c` now maintains an in-firmware exponential-moving-average of the cross-board sync offset (α = 1/8, fixed-point shift, ≈ 8-sample window at the 10 Hz beacon rate). New getter `c6_sync_espnow_get_offset_us_smoothed()`. `c6_sync_espnow_get_epoch_us()` now returns timestamps stamped from the smoothed offset once seeded — every downstream CSI-frame consumer gets bounded-jitter alignment for free, no host-side filter required. **Measured on the bench**: 5-min two-board soak (WITNESS-LOG-110 §A0.10) drops raw offset stdev 411.5 µs → smoothed 104.1 µs (**3.95× suppression** on stdev, 4.70× on peak-to-peak range) while preserving the +30 µs/min crystal-drift trajectory within 2 µs/min. **The ADR-110 §2.4 ≤100 µs multistatic alignment target that v0.6.6 designed is now empirically measured, not just stated.** Cross-board beacon match rate 99.56% over 5 min, 0 TX failures. Binary cost: +32 bytes (one int64, one bool, one getter). Diag log adds `smoothed=…` field. Tag: `v0.6.8-esp32`. **Known wiring gap (deferred)**: `csi_serialize_frame` does not yet stamp frames with `c6_sync_espnow_get_epoch_us()` — the ADR-018 frame format has no timestamp field, and adding one is a breaking change that needs an ADR update. Multistatic CSI fusion will require either an ADR-018 v2 with timestamp, or a separate UDP sync packet keyed off the existing flag bit. Tracked in WITNESS-LOG-110 §A0.11.
81+
- **Wave 5 — firmware v0.6.9 + v0.7.0 + host wiring (loop iter 8 → iter 26)**: closes the §A0.11 gap and lights up the substrate end-to-end across firmware → host → JSON broadcast. **Firmware**: (a) **v0.6.9-esp32** — `csi_collector.c` emits a 32-byte UDP sync packet (magic `0xC511A110`, distinct from CSI frame magic `0xC5110001`) every `CONFIG_C6_SYNC_EVERY_N_FRAMES` (default 20) CSI frames, carrying `node_id`, `local_us`, mesh-aligned `epoch_us` (from the Wave 4 smoothed offset), and the CSI sequence high-water for host-side pairing. Same UDP socket as CSI; host dispatches by leading magic. Operator-tunable cadence via the new Kconfig knob — N=1 (10 Hz) for tight multistatic, N=200 (~20 s) for low-power seeds. Live-verified on COM9+COM12 (§A0.12): follower reports `local − epoch = 1 163 565 µs`, matches the §A0.10 boot-delta measurement within 285 µs of WiFi MAC TX jitter. (b) **v0.7.0-esp32** — `csi_collector.c:221` ADR-018 byte 19 bit 4 ("cross-node sync valid") now ORs in `c6_sync_espnow_is_valid()` so frames from sync'd ESP-NOW nodes correctly advertise sync (previously only sourced from the broken 802.15.4 path — false-negative bug, §A0.13). Side effect: S3 boards now also set the bit since `c6_sync_espnow` is cross-target. **Host decoders + 25 unit tests**: Python `SyncPacketParser` + `SyncPacket` dataclass with `apply_to_local` / `mesh_aligned_us_for_sequence` / `local_minus_epoch_us` (10 tests in `TestSyncPacketParser`); Rust `wifi_densepose_hardware::SyncPacket` + `SyncPacketFlags` + `SYNC_PACKET_MAGIC` re-exported from the crate root with identical API surface (15 tests in `sync_packet::tests`). **Cross-language conformance gate** (loop iter 21): the same 32-byte canonical hex `10a111c509010600f26db70100000000c5aca501000000001400000000000000` is pinned in both test suites; if either decoder drifts from the wire, exactly one named test fires and points at the moved side. **Sensing-server wiring**: `udp_receiver_task` magic-dispatches `0xC511A110` and stores per-node `latest_sync: Option<SyncPacket>` + `latest_sync_at: Option<Instant>` on `NodeState`. New helpers: `NodeState::mesh_aligned_us(local_us)`, `NodeState::mesh_aligned_us_for_csi_frame(sequence)` (uses the per-node measured fps EMA with 5-sample warmup + 9 s staleness gate), `NodeState::observe_csi_frame_arrival(now)` (feeds `update_csi_fps_ema` α=1/8, called once per accepted CSI frame). 4 fps-EMA tests + 3 NodeSyncSnapshot serialization tests on the binary target. **Public JSON API**: `sensing_update` broadcasts now carry an optional `sync` object per node — `{offset_us, is_leader, is_valid, smoothed, sequence, csi_fps_ema, csi_fps_samples}` — `#[serde(skip_serializing_if = "Option::is_none")]` so non-mesh paths (multi-BSSID scan / synthetic-RSSI fallback / simulation) omit the key entirely. Existing pre-v0.7.0 UI clients ignore it cleanly. Documented in `docs/user-guide.md` "Per-node mesh sync (ADR-110)" section with field table, UI rendering rules, and the timestamp-recovery recipe. **Branch-coordination**: `docs/ADR-110-BRANCH-STATE.md` maps which files each of `adr-110-esp32c6` vs `feat/adr-115-ha-mqtt-matter` touches (regions are disjoint, merges should be clean line-merges). **Verification baselines**: full v2 cargo workspace at **1437 tests passing** (no regression across 17 crate batches), full `wifi-densepose-hardware` crate at **137 tests**. ADR-110 §B substrate is now end-to-end visible to UI clients and ready for ADR-029/030 multistatic CSI fusion consumption.
6582
- **Real-time CSI introspection / low-latency tap on `wifi-densepose-sensing-server` (ADR-099).**
6683
New `wifi_densepose_sensing_server::introspection` module wires
6784
[midstream](https://github.com/ruvnet/midstream)'s `temporal-attractor` (Lyapunov +

README.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,28 @@ docker pull ruvnet/wifi-densepose:latest
8080
docker run -p 3000:3000 ruvnet/wifi-densepose:latest
8181
# Open http://localhost:3000
8282

83-
# Option 2: Live sensing with ESP32-S3 hardware ($9)
83+
# Option 2a: Live sensing with ESP32-S3 hardware ($9)
8484
# Flash firmware, provision WiFi, and start sensing:
8585
python -m esptool --chip esp32s3 --port COM9 --baud 460800 \
8686
write_flash 0x0 bootloader.bin 0x8000 partition-table.bin \
8787
0xf000 ota_data_initial.bin 0x20000 esp32-csi-node.bin
8888
python firmware/esp32-csi-node/provision.py --port COM9 \
8989
--ssid "YourWiFi" --password "secret" --target-ip 192.168.1.20
9090

91+
# Option 2b: WiFi 6 + 802.15.4 research sensing with ESP32-C6 ($6-10, ADR-110)
92+
# Same csi-node firmware compiled for the C6 target — picks up the C6
93+
# overlay (sdkconfig.defaults.esp32c6) automatically.
94+
cd firmware/esp32-csi-node
95+
idf.py set-target esp32c6 && idf.py build
96+
idf.py -p COM6 flash
97+
# C6 boot extras (vs S3): HE-LTF subcarrier tagging in ADR-018 bytes 18-19,
98+
# 802.15.4 mesh time-sync on channel 15, TWT setup when the AP supports it,
99+
# opt-in LP-core wake-on-motion for ~5 µA battery seed nodes.
100+
# v0.6.7 adds: real LP-core RISC-V motion-gate program (debounce + motion
101+
# counter) and a Wi-Fi 6 soft-AP with TWT Responder so two C6 boards can
102+
# benchmark real iTWT without buying an 11ax router. Both default off,
103+
# flip CONFIG_C6_{LP_CORE,SOFTAP_HE}_ENABLE to turn them on.
104+
91105
# Option 3: Full system with Cognitum Seed ($140)
92106
# ESP32 streams CSI → bridge forwards to Seed for persistent storage + kNN + witness chain
93107
node scripts/rf-scan.js --port 5006 # Live RF room scan
@@ -103,7 +117,8 @@ node scripts/mincut-person-counter.js --port 5006 # Correct person counting
103117
> | Option | Hardware | Cost | Full CSI | Capabilities |
104118
> |--------|----------|------|----------|-------------|
105119
> | **ESP32 + Cognitum Seed** (recommended) | ESP32-S3 + [Cognitum Seed](https://cognitum.one) | ~$140 | Yes | Presence, motion, breathing, heart rate, fall detection, multi-person counting, 17-keypoint pose (signed Cog binary), 105-cog catalog, persistent vector store, kNN search, witness chain, MCP proxy |
106-
> | **ESP32 Mesh** | 3-6x ESP32-S3 + WiFi router | ~$54 | Yes | Same capabilities as above without the persistent-memory features |
120+
> | **ESP32 Mesh** | 3-6× ESP32-S3 + WiFi router | ~$54 | Yes | Same capabilities as above without the persistent-memory features |
121+
> | **ESP32-C6 research node** ([ADR-110](docs/adr/ADR-110-esp32-c6-firmware-extension.md), [witness](docs/WITNESS-LOG-110.md), [reviewer guide](docs/ADR-110-REVIEW-GUIDE.md), [firmware v0.7.0](https://github.com/ruvnet/RuView/releases/tag/v0.7.0-esp32)) | ESP32-C6-DevKit ($6–10) | ~$10 | Yes (Wi-Fi 6 capable) | Same CSI pipeline as S3 with the dual-target firmware. **Firmware-side ADR-110 substrate now closed** (v0.7.0): ESP-NOW cross-board mesh quantified at **99.56 % match / 104 µs smoothed offset stdev / 3.95× EMA suppression** over a 5-min two-board soak (witness §A0.10), 32-byte UDP sync packet with operator-tunable cadence (§A0.12), ADR-018 byte 19 bit 4 wire-fix sourced from the working ESP-NOW path (§A0.13). Wire format ready for HE-LTF PPDU tagging in ADR-018 bytes 18-19 (firmware encoder + Rust + Python decoders verified end-to-end across 23 unit tests). LP-core motion-gate RISC-V program and Wi-Fi 6 soft-AP with TWT Responder both ship as opt-in code paths (default off). **Hardware-gated for measurement**: HE-LTF live subcarrier capture needs an 11ax AP (IDF v5.4 doesn't expose AP-side HE config — §A0.6); ~5 µA LP-core hibernation needs an INA meter to capture; 802.15.4 raw RX is broken in IDF v5.4 (workaround: ESP-NOW transport, shipped + measured). See witness log for the empirical / claimed split. |
107122
> | **Research NIC** | Intel 5300 / Atheros AR9580 | ~$50-100 | Yes | Full CSI with 3x3 MIMO |
108123
> | **Any WiFi** | Windows, macOS, or Linux laptop | $0 | No | RSSI-only: coarse presence and motion (see [tutorial #36](https://github.com/ruvnet/RuView/issues/36)) |
109124
>

0 commit comments

Comments
 (0)