feat(firmware): ESP32-C6 target — Wi-Fi 6 / 802.15.4 / TWT / LP-core (ADR-110)

`firmware/esp32-csi-node` now builds for both `esp32s3` (existing
production) and `esp32c6` (new research / battery-seed target) from
the same source tree. ESP-IDF auto-applies `sdkconfig.defaults.esp32c6`
when the target is set to esp32c6; every C6 module is gated on
CONFIG_IDF_TARGET_ESP32C6 (or the SOC_WIFI_HE_SUPPORT capability) so
the S3 build path is byte-identical to today.

New modules (all #ifdef-gated, no-op stubs on S3):
- c6_twt.{h,c}      — iTWT wrapper, graceful AP-NACK fallback
- c6_timesync.{h,c} — 802.15.4 beacon-based mesh time-sync, EUI-64
                      leader election, c6_timesync_get_epoch_us()
- c6_lp_core.{h,c}  — wake-on-motion deep-sleep helper (ext1 path
                      this cut; real LP-core polling deferred)

ADR-018 frame extension:
- byte 18: PPDU type (0=HT/legacy, 1=HE-SU, 2=HE-MU, 3=HE-TB)
- byte 19: bandwidth + STBC + 802.15.4-sync-valid flags
- Magic 0xC5110001 unchanged — backwards compatible
- Dual-branch encoding handles both struct variants of
  wifi_pkt_rx_ctrl_t (legacy S3 / HE C6) per CONFIG_SOC_WIFI_HE_SUPPORT

Critical bug fixed during live witness collection (verified across 3
boards on COM6/COM9/COM12):
- c6_timesync.c read MAC into a 6-byte buffer and ran MAC-48->EUI-64
  conversion. But esp_read_mac(ESP_MAC_IEEE802154) returns 8 bytes
  already in EUI-64 form on C6 — code was double-inserting FFFE.
  Boot log was 206ef1fffefffe17, fix yields 206ef1fffe17278c which
  matches esptool's eFuse reading exactly.

Tooling:
- CI workflow (firmware-ci.yml) extended with c6-4mb matrix row +
  ADR-110 host-unit-test step
- Host unit tests for pure functions (mac48_to_eui64,
  eui64_bytes_to_u64, PPDU encoding both branches) — runs on Ubuntu CI
- Multi-board live-capture harness (test/capture-3board-experiment.py)
- Witness bundle script records SHA-256s for s3-adr110, c6-adr110, and
  s3-fair-adr110 (apples-to-apples) binary archives

Honest empirical findings (full report in docs/WITNESS-LOG-110.md):
- Verified live on 3 C6 boards: boot, 802.15.4 init w/ correct EUIs,
  WiFi STA reaching assoc->run on ruv.net, TWT setup attempted +
  gracefully NACKed (AP is 11n-only, TWT Responder:0), HE-MAC firmware
  loaded
- NOT verified (need 11ax AP / second-channel exp / INA meter):
  HE-LTF subcarrier expansion, TWT cadence determinism, ±100 µs sync
  alignment, 5 µA hibernation
- Bug found: leader election doesn't step down under live WiFi load —
  likely 2.4 GHz radio coex preemption (WiFi ch 5 vs 15.4 ch 15);
  follow-up task #30
- Apples-to-apples size: S3-no-display = 886 KB, C6 = 1003 KB
  (C6 is 13% LARGER for equivalent CSI features; the extra is the
  802.15.4 + OpenThread stack that S3 lacks)

Tracking: ruvnet/RuView#762

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv
2026-05-22 20:10:30 -04:00
parent 68abb385ae
commit f23e34ee5c
35 changed files with 1720 additions and 13 deletions
+59
View File
@@ -1094,6 +1094,15 @@ An RVF file contains: model weights, HNSW vector index, quantization codebooks,
## Hardware Setup
### Supported targets
| Target | Use case | Source target flag | Notes |
|---|---|---|---|
| **ESP32-S3** (default) | Production CSI mesh, 17-keypoint pose | `idf.py set-target esp32s3` | Dual-core 240 MHz, PSRAM, native USB-OTG, DVP camera path |
| **ESP32-C6** ([ADR-110](adr/ADR-110-esp32-c6-firmware-extension.md)) | Wi-Fi 6 / 802.15.4 research, battery seed nodes | `idf.py set-target esp32c6` | Single-core 160 MHz, no PSRAM, 802.11ax HE PHY, 802.15.4 (Thread/Zigbee), LP-core hibernation ~5 µA |
The same `firmware/esp32-csi-node` source tree builds for both. ESP-IDF picks up `sdkconfig.defaults.esp32c6` automatically when the target is set to `esp32c6`; otherwise it uses `sdkconfig.defaults` (S3). All C6-only modules are `#ifdef`-gated, so the S3 build is byte-identical to today.
### ESP32-S3 Mesh
A 3-6 node ESP32-S3 mesh provides full CSI at 20 Hz. Total cost: ~$54 for a 3-node setup.
@@ -1155,6 +1164,56 @@ python firmware/esp32-csi-node/provision.py --port COM7 \
All nodes in a mesh must share the same 256-bit mesh key for HMAC-SHA256 beacon authentication. The key is stored in ESP32 NVS flash and zeroed on firmware erase.
### ESP32-C6 (Wi-Fi 6 + 802.15.4 research target — ADR-110)
The C6 build adds four capabilities to the existing csi-node firmware, all opt-in via `idf.py menuconfig → ESP32-C6 capabilities (ADR-110)`:
| Capability | Kconfig | What it does |
|---|---|---|
| **Wi-Fi 6 HE-LTF tagging** | `CSI_FRAME_HE_TAGGING` (default on) | Each ADR-018 frame's previously-reserved bytes 18-19 now carry PPDU type (HT / HE-SU / HE-MU / HE-TB) + bandwidth flags. Magic stays `0xC5110001` — old aggregators see zeros and ignore. |
| **802.15.4 mesh time-sync** | `C6_TIMESYNC_ENABLE` (default on, channel 15) | Beacon-based cross-node clock alignment over the 802.15.4 radio. Frees the WiFi channel from coordination traffic — solves the ADR-029/030 multistatic clock-sync problem. |
| **TWT (Target Wake Time)** | `C6_TWT_ENABLE` (default on, 10 ms wake interval) | After WiFi connect, negotiates an individual TWT agreement with the AP for deterministic CSI cadence. Graceful NACK fallback if the AP doesn't support 11ax TWT. |
| **LP-core wake-on-motion hibernation** | `C6_LP_CORE_ENABLE` (default off) | Always-on motion gate on the LP RISC-V core; HP core stays in deep sleep until the configured GPIO wakes it. Targets ~5 µA for battery-powered Cognitum Seed nodes. |
**Build + flash:**
```bash
cd firmware/esp32-csi-node
idf.py set-target esp32c6
idf.py build # ~1.0 MB binary, 46% partition slack on 4 MB flash
idf.py -p COM6 flash
# Then provision the same way as S3 (provision.py works for both targets):
python provision.py --port COM6 --ssid "YourWiFi" --password "secret" --target-ip 192.168.1.20
```
**Verifying the C6 modules came up**`idf.py -p COM6 monitor` should show:
```
I (353) main: ESP32-C6 CSI Node (ADR-018 / ADR-110) — v0.6.6 — Node ID: 1
I (413) c6_ts: init done: channel=15 EUI=<your-EUI64> leader=yes(candidate)
I (463) wifi: mac_version:HAL_MAC_ESP32AX_761 ← 802.11ax MAC firmware loaded
```
The `c6_ts: init done` line confirms the 802.15.4 stack is up; if TWT succeeds you'll also see an `iTWT setup event received from AP` line after the WiFi connect completes.
**Multi-room time-aligned multistatic capture (preview):**
Flash two or more C6 boards, leave them on the same 802.15.4 channel (default 15). One will elect itself leader (lowest EUI-64) and broadcast `TS_BEACON` frames every 100 ms; the others compute and apply offsets. Each CSI frame from a follower carries a `c6_timesync_get_epoch_us()` wall-clock estimate aligned to within ±100 µs of the leader's monotonic time. Target use case: ADR-029/030 multistatic fusion without burning WiFi airtime on coordination.
**Battery seed-node mode:**
```bash
# Enable LP-core hibernation in menuconfig:
# ESP32-C6 capabilities (ADR-110) → Enable LP-core wake-on-motion hibernation
# → LP-core wake GPIO (default 4 — connect a PIR or accelerometer INT line here)
idf.py menuconfig
idf.py build flash
```
When enabled, the C6 boots, takes one CSI burst, then enters deep sleep with the LP-core armed. Target standby current ~5 µA.
**What's NOT on the C6 build** (vs S3 production): no AMOLED display (ADR-045 needs 8 MB + LCD touch driver), no WASM3 (ADR-040 needs PSRAM), no Seeed mmWave fusion (separate board). The C6 is a research/seed target, not a drop-in replacement for the S3 production node.
**TDM slot assignment:**
Each node in a multistatic mesh needs a unique TDM slot ID (0-based):