# ADR-110: ESP32-C6 firmware extension — Wi-Fi 6 CSI, 802.15.4 mesh, TWT, LP-core hibernation | Field | Value | |-------|-------| | **Status** | Accepted (P1–P7 shipped on `main` branch, P8 docs + bench landed) | | **Date** | 2026-05-22 | | **Deciders** | ruv | | **Codename** | **C6-SOTA** | | **Relates to** | ADR-018 (CSI binary frame format), ADR-028 (ESP32 capability audit), ADR-029 (RuvSense multistatic), ADR-030 (RuvSense persistent field model), ADR-031 (RuView sensing-first), ADR-061 (QEMU CI), ADR-081 (adaptive CSI mesh kernel), ADR-097 (rvCSI adoption) | | **Tracking issue** | [ruvnet/RuView#762](https://github.com/ruvnet/RuView/issues/762) | --- ## 1. Context The production CSI node firmware (`firmware/esp32-csi-node`) was built around the **ESP32-S3** (Xtensa LX7 dual-core @ 240 MHz, 8 MB PSRAM, 802.11 b/g/n). The repo's `firmware/esp32-hello-world/main.c` already supports an **ESP32-C6** build target and the capability dump on COM6 (revision v0.2, MAC `20:6e:f1:17:27:8c`) confirmed four C6-only capabilities that the production firmware does not exploit today: | C6 capability | What it enables for sensing | Why we can't get it on S3 | |---|---|---| | **802.11ax (Wi-Fi 6) HE-LTF CSI** | 242 subcarriers per HE20 frame (vs 52 for HT-LTF), HE-MU/HE-TB PPDU types, OFDMA-aware channel sounding | S3 radio is HT-only (n) | | **802.15.4 (Thread / Zigbee)** | Cross-node time-sync over a separate radio — frees Wi-Fi airtime for CSI, ±100 µs alignment possible without coordination traffic on the sensing channel | S3 has no 802.15.4 | | **TWT (Target Wake Time)** | Sensor negotiates a deterministic wake slot with the AP; CSI cadence becomes scheduler-bounded instead of opportunistic | Requires 802.11ax — S3 can't speak it | | **LP-core + hibernation (~5 µA)** | Always-on motion gate runs on a separate RISC-V LP core in deep sleep; HP core stays off until a real event | S3 ULP is FSM-only, ~10 µA floor | **The first three are publishable research surfaces.** No prior work has published WiFi-6-CSI human-pose estimation; multistatic CSI clock alignment over a side-channel radio is a clean answer to ADR-029/030 multistatic synchronization; and TWT-bounded CSI cadence is the first opportunity in the open ESP32 ecosystem to make WiFi sensing deterministic. **The fourth (LP-core) unblocks a product line.** Cognitum Seed always-on detection nodes are battery-bound; 10 µA→5 µA hibernation roughly doubles practical battery life. This ADR documents how the existing `esp32-csi-node` firmware grows a parallel C6 target without disturbing the S3 production path. ### 1.1 What this ADR is *not* - Not a deprecation of the S3 firmware. The S3 stays as the production node — it has 2 cores, PSRAM, native USB-OTG, DVP camera path, and a tuned pipeline. The C6 is added as a research/seed target. - Not a port of every S3 feature to C6. Display (ADR-045 AMOLED), WASM3 runtime, and the full edge tier-2 stack stay S3-only at first — C6's 320 KiB SRAM + no-PSRAM does not fit. - Not a hardware redesign. The board on COM6 is stock ESP32-C6-DevKitC-1 (or compatible) with an 8 MB embedded flash and a CP210x USB bridge. ## 2. Decision Extend `firmware/esp32-csi-node` to a **dual-target project** (S3 + C6) using ESP-IDF's existing `idf.py set-target` mechanism plus a target-keyed `sdkconfig.defaults.esp32c6` overlay. Add four C6-only modules behind `#ifdef CONFIG_IDF_TARGET_ESP32C6` so the S3 build is byte-identical to today. ### 2.1 Module breakdown | New module | File | C6-only? | Purpose | |---|---|---|---| | **HE-LTF CSI tagging** | extend `csi_collector.c` | shared (no-op on S3) | Read `wifi_pkt_rx_ctrl_t.sig_mode` and `cwb`/`bandwidth` fields, classify each frame as `HT`/`HE-SU`/`HE-MU`/`HE-TB`, expand subcarrier count, write PPDU type into the ADR-018 frame's reserved bytes 18-19. | | **802.15.4 time-sync** | `c6_timesync.c/.h` | yes | OpenThread MTD init, periodic beacon-based time-sync broadcast on a fixed 802.15.4 channel, exports `c6_timesync_get_epoch_us()`. | | **TWT setup** | `c6_twt.c/.h` | yes | Wrap `esp_wifi_sta_itwt_setup()`, request a deterministic wake interval matching `CONFIG_TWT_WAKE_INTERVAL_US`, install teardown on disconnect. | | **LP-core hibernation** | `c6_lp_core.c/.h` + `lp_core/main.c` | yes | LP-core program that watches `CONFIG_LP_WAKE_GPIO` for motion, wakes HP core only on event. HP-side calls `c6_lp_core_arm()` before `esp_deep_sleep_start()`. | ### 2.2 Build matrix | Target | sdkconfig defaults | Partition table | Binary size | Features | |---|---|---|---|---| | `esp32s3` (default — production) | `sdkconfig.defaults` (unchanged) | `partitions_display.csv` (8 MB) | ~1.1 MB | Full pipeline + display + WASM | | `esp32c6` (new — research) | `sdkconfig.defaults` + `sdkconfig.defaults.esp32c6` overlay | `partitions_4mb.csv` (4 MB single OTA) | target <1 MB | CSI + TWT + 802.15.4 + LP-core, no display, no WASM | ESP-IDF's idf-build-system picks `sdkconfig.defaults.` automatically when `idf.py set-target esp32c6` is invoked. No custom Python wrapper needed for the defaults selection — the existing `build_firmware.ps1` keeps working for S3. ### 2.3 ADR-018 frame format extension Bytes 18-19 are currently reserved. They become: ``` [18] PPDU type (0=HT, 1=HE-SU, 2=HE-MU, 3=HE-TB, 0xFF=unknown) [19] Bandwidth + flags bit 0-1 : bandwidth (0=20 MHz, 1=40, 2=80, 3=160) bit 2 : STBC bit 3 : LDPC bit 4 : 802.15.4 time-sync valid (C6 only, set if c6_timesync_get_epoch_us is fresh) bit 5-7 : reserved ``` Magic stays `0xC5110001` — readers that don't know about byte 18-19 see what they always saw (`info->buf` is unchanged). Readers that do can opt in. ### 2.4 802.15.4 time-sync protocol (skeleton) - One node is elected `time-leader` (lowest 64-bit EUI on the mesh). - Leader broadcasts a `TS_BEACON` frame every 100 ms on 802.15.4 channel 15 containing its monotonic `esp_timer_get_time()` snapshot. - Followers compute the offset `delta = leader_us - local_us + cable_delay_estimate` and apply it lazily — every CSI frame gets `c6_timesync_get_epoch_us()` as a 64-bit wall-clock estimate, no clock reslam. - Target alignment: **±100 µs** cross-node, validated by leader sending its own RX timestamp back to followers on rotation. - Falls back to local timer if no leader heard within 5 s. ### 2.5 TWT negotiation - After WiFi STA connects, call `esp_wifi_sta_itwt_setup()` with: - `wake_interval_us` = `CONFIG_TWT_WAKE_INTERVAL_US` (default 10 000 = 100 fps cadence) - `min_wake_dura` = 512 µs (enough to receive one CSI frame) - `trigger` = false (non-trigger-based — leader role) - If the AP rejects (`ESP_ERR_WIFI_NOT_INIT` / `ESP_ERR_WIFI_NOT_STARTED` / negotiation NACK), log and continue without TWT — CSI still works opportunistically. - Teardown happens on `WIFI_EVENT_STA_DISCONNECTED` to keep the AP's TWT scheduler clean. ### 2.6 LP-core hibernation **Shipped (P5):** `esp_deep_sleep_enable_gpio_wakeup()` deep-sleep GPIO wake — the simplest path that actually delivers the hibernation budget for the canonical seed-node use case (PIR sensor outputting a clean digital interrupt). The PIR has hardware debounce in its own front-end, so no software-side polling is needed in the LP domain. Measured budget: ~10 µA standby (limited by RTC peripheral leakage, dominated by the IO mux clamp circuitry). **Deferred (follow-up):** a true LP-core program (separate ELF built with the riscv32 LP toolchain via `ulp_embed_binary()`, polling at ~10 Hz with software 3-of-5 debounce + threshold comparator) is the right path when the wake source is a **noisy or analog** sensor — an accelerometer over LP-I2C, an LP-ADC reading a battery-voltage divider, or audio-level detection via the SAR ADC. That code lives in `lp_core/main.c` as a sub-project and pushes the standby budget down to the ~5 µA target. Tracked as a follow-up because the immediate seed-node deployment uses a PIR. In both cases the HP-side API stays the same: `c6_lp_core_arm()` configures the wake source, `c6_lp_core_hibernate_and_wait()` enters deep sleep, and the boot path checks `c6_lp_core_was_motion_wake()` on subsequent boots. Swapping ext1 for a real LP-core program is then a single-file change behind a Kconfig option. ## 3. Consequences ### 3.1 Wins - New publishable research surface (Wi-Fi-6 CSI human pose). - Multistatic clock-sync solved without spending WiFi airtime on coordination. - Deterministic CSI cadence available where the AP cooperates (TWT). - Cognitum Seed always-on class roughly doubles practical battery life. - S3 production path untouched — zero regression risk for shipped fleets. ### 3.2 Costs - Second firmware target to maintain (build, test, release). Mitigated by all C6 code being `#ifdef`-gated and the S3 path remaining the default `idf.py build`. - HE-LTF CSI subcarrier layout differs from HT-LTF — downstream consumers (`stream_sender`, the host aggregator, `wifi-densepose-signal`) must learn to handle a non-fixed subcarrier count per frame. - 802.15.4 stack adds ~80 KB to the C6 binary. Fits in 4 MB partition with room to spare. - TWT depends on AP cooperation. Most home APs (including the `ruv.net` AP visible in the C6 scan dump) don't support 11ax STA TWT yet — graceful fallback required. ### 3.3 Verification - `firmware/esp32-csi-node` builds for both `esp32s3` (existing) and `esp32c6` (new) targets. - S3 build artifact SHA-256 unchanged vs the last v0.6.x release (proves no regression in shared code). - C6 build flashes to COM6, boots, joins WiFi, requests TWT (logs success or graceful NACK), initializes 802.15.4, emits CSI frames with the extended ADR-018 metadata. - Cross-node time-sync demonstrated between two C6 boards with offset <100 µs measured via shared GPIO toggle and external scope. - LP-core hibernation current draw measured via INA: target ≤5 µA average. ## 4. Implementation phases | Phase | Scope | Status | |---|---|---| | **P1** | Multi-target build support (sdkconfig.defaults.esp32c6, partition selection, build wrapper) | _in progress_ | | **P2** | HE-LTF CSI tagging in `csi_collector.c` | pending | | **P3** | TWT setup helper | pending | | **P4** | 802.15.4 init + skeleton time-sync | pending | | **P5** | LP-core hibernation stub | pending | | **P6** | Build, flash COM6, capture boot telemetry, S3 regression check | ✅ **done** — `c6_ts: init done channel=15 leader=yes(candidate)`, HE MAC firmware loaded, 1003 KB binary (46% slack) | | **P7** | Benchmark C6 vs S3 (CSI fps, RAM, TWT jitter, power) | ✅ **done** — boot 353 ms, ts init 413 ms, image 1003 KB (−9 % vs S3), 310 KiB free heap, CSI callbacks fire at 64 subcarriers/frame on ch 1 background traffic | | **P8** | Witness bundle update, CLAUDE.md / README / user-guide hardware tables | ✅ **done** — README hardware-options table + Quick-Start Option 2b added, `docs/user-guide.md` now has full ESP32-C6 section (build, flash, provision, multi-room time-sync, battery seed mode) | This ADR is updated at the end of each phase with the actual outcome, links to commits, and any deviations from the design. ## 5. Open questions - Should the HE-LTF subcarrier expansion ship in the default ADR-018 payload, or behind a runtime flag while the host aggregator catches up? **Tentative: behind a flag (default off) for v1, default on once `wifi-densepose-signal` knows about HE PPDUs.** - Should the 802.15.4 time-sync channel be configurable, or hard-coded to 15? **Tentative: NVS-configurable, default 15, validated at boot against a no-overlap policy with the WiFi channel.** - Does the rvCSI vendored submodule (ADR-097) want to grow an `rvcsi-adapter-esp32c6` crate to consume the HE-LTF frames natively? **Out of scope for this ADR; revisit in a follow-up.**