The libFuzzer harness was compiled without CONFIG_CSI_FRAME_HE_TAGGING,
so the new byte 18/19 path in csi_collector.c was zero-filled at compile
time and never fuzzed. Three changes to fix that:
1. test/stubs/esp_stubs.h: wifi_pkt_rx_ctrl_t gains both branch families
- HE branch (CONFIG_SOC_WIFI_HE_SUPPORT path): cur_bb_format, second
- Legacy branch (S3 / pre-HE chips): sig_mode, cwb, stbc
A single stub compiles for either branch; the Makefile picks which
one is active via -D flags. Both sets are declared so a build for
the unselected branch still compiles cleanly.
2. test/Makefile: CFLAGS now defines CONFIG_CSI_FRAME_HE_TAGGING=1 so
the new code path is reachable. CONFIG_SOC_WIFI_HE_SUPPORT stays
UNSET (default — exercises the legacy S3 branch). Add it to CFLAGS
for a parallel HE-stub run if you want coverage of the C6 branch.
3. test/fuzz_csi_serialize.c: parses 3 more control bytes from fuzz
input (he_inputs[2] + legacy_inputs) and writes them through
info.rx_ctrl.{cur_bb_format,second,sig_mode,cwb,stbc} so the
serializer's PpduType switch and Adr018Flags computation are
reached on every iteration.
Result: the existing libFuzzer corpus + ASAN/UBSAN now covers the
ADR-110 wire encoding paths on every run. No more zero-fill silent skip.
Co-Authored-By: claude-flow <ruv@ruv.net>
After 5 systematic experiments confirmed the 802.15.4 RX path is
unfixable from user code in this IDF v5.4 + C6 combination (D1), add a
parallel sync transport over ESP-NOW. Same TS_BEACON protocol, same
public API (c6_sync_espnow_get_epoch_us / is_valid / is_leader), but
runs on the WiFi MAC layer that ESP-IDF fully supports across every
ESP32 family.
The 802.15.4 code stays in source for when the IDF driver is fixed.
ESP-NOW is the working primary today.
Empirical (single-board COM9 — other 3 boards dropped off USB during
the experiment):
- c6_sync_espnow_init() succeeds: "init done local_id=… leader=
yes(candidate) period=100ms"
- TX path 100% reliable: tx#101 fail=0 over ~15s at 100ms cadence
- RX awaiting cross-board test once USB-enumeration is restored
Trade vs. 802.15.4 design:
- Loses: "frees WiFi airtime for CSI" property
- Gains: known-working RX path, cross-target (S3 and C6 both)
- Same API surface — consumers swap transports without code change
Files:
- main/c6_sync_espnow.{h,c} — new module, ~210 lines
- main/CMakeLists.txt — add to SRCS (always built, used on any target)
- main/main.c — init after WiFi STA up, skip on QEMU mock
- test/capture-3board-experiment.py — surface c6_espnow log lines
- docs/WITNESS-LOG-110.md — new §D-workaround documenting the pivot
Ref: ruvnet/RuView#762 / D1 known-issue / draft PR #764
Co-Authored-By: claude-flow <ruv@ruv.net>
`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>
* fix: bug triage from issues #559, #561, #588
- verify: point at archive/v1/ proof paths (v1/ was removed) (#559)
- firmware README: app flash offset 0x10000 -> 0x20000, include
ota_data_initial.bin at 0xf000, correct provision.py path from
scripts/ to firmware/esp32-csi-node/ (#561)
- provision.py: drop password-length leak in console output; print
(set)/(empty) instead of len(password) asterisks (#588)
Co-Authored-By: claude-flow <ruv@ruv.net>
* ci: fix Fuzz Testing + Swarm Test (ADR-062) workflow regressions
Both have been red on main for ~5 weeks; root-causing them so PR #590
can land green rather than merging on top of pre-existing breakage.
- esp_stubs.h: add wifi_ps_type_t enum (WIFI_PS_NONE/MIN/MAX) and
esp_wifi_set_ps() stub. csi_collector.c:346 added a real
esp_wifi_set_ps(WIFI_PS_NONE) call to disable modem sleep
(RuView#521 fix); the host-native fuzz target couldn't link.
- scripts/qemu_swarm.py: pass --force-partial to provision.py.
The per-node TDM/channel overlay intentionally omits WiFi
credentials (those live in the base flash image), but the
issue #391 wifi-trio guard now rejects calls missing the
--ssid/--password trio. --force-partial is exactly the opt-in
for this case.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(firmware): fall detection false positives + 4MB flash support (#263, #265)
Issue #263: Default fall_thresh raised from 2.0 to 15.0 rad/s² — normal
walking produces accelerations of 2.5-5.0 which triggered constant false
"Fall Detected" alerts. Added consecutive-frame requirement (3 frames)
and 5-second cooldown debounce to prevent alert storms.
Issue #265: Added partitions_4mb.csv and sdkconfig.defaults.4mb for
ESP32-S3 boards with 4MB flash (e.g. SuperMini). OTA slots are 1.856MB
each, fitting the ~978KB firmware binary with room to spare.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(ci): repair all 3 QEMU workflow job failures
1. Fuzz Tests: add esp_timer_create_args_t, esp_timer_create(),
esp_timer_start_periodic(), esp_timer_delete() stubs to
esp_stubs.h — csi_collector.c uses these for channel hop timer.
2. QEMU Build: add libgcrypt20-dev to apt dependencies —
Espressif QEMU's esp32_flash_enc.c includes <gcrypt.h>.
Bump cache key v4→v5 to force rebuild with new dep.
3. NVS Matrix: switch to subprocess-first invocation of
nvs_partition_gen to avoid 'str' has no attribute 'size' error
from esp_idf_nvs_partition_gen API change. Falls back to
direct import with both int and hex size args.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(ci): pip3 in IDF container + fix swarm QEMU artifact path
QEMU Test jobs: espressif/idf:v5.4 container has pip3, not pip.
Swarm Test: use /opt/qemu-esp32 (fixed path) instead of
${{ github.workspace }}/qemu-build which resolves incorrectly
inside Docker containers.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(ci): source IDF export.sh before pip install in container
espressif/idf:v5.4 container doesn't have pip/pip3 on PATH — it
lives inside the IDF Python venv which is only activated after
sourcing $IDF_PATH/export.sh.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(ci): pad QEMU flash image to 8MB with --fill-flash-size
QEMU rejects flash images that aren't exactly 2/4/8/16 MB.
esptool merge_bin produces a sparse image (~1.1 MB) by default.
Add --fill-flash-size 8MB to pad with 0xFF to the full 8 MB.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(ci): source IDF export before NVS matrix generation in QEMU tests
The generate_nvs_matrix.py script needs the IDF venv's python
(which has esp_idf_nvs_partition_gen installed) rather than the
system /usr/bin/python3 which doesn't have the package.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(ci): QEMU validation treats WARNs as OK + swarm IDF export
1. validate_qemu_output.py: WARNs exit 0 by default (no real WiFi
hardware in QEMU = no CSI data = expected WARNs for frame/vitals
checks). Add --strict flag to fail on warnings when needed.
2. Swarm Test: source IDF export.sh before running qemu_swarm.py
so pip-installed pyyaml is on the Python path.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(ci): provision.py subprocess-first NVS gen + swarm IDF venv
provision.py had same 'str' has no attribute 'size' bug as the
NVS matrix generator — switch to subprocess-first approach.
Swarm test also needs IDF export for the swarm smoke test step.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(ci): handle missing 'ip' command in QEMU swarm orchestrator
The IDF container doesn't have iproute2 installed, so 'ip' binary
is missing. Add shutil.which() check to can_tap guard and catch
FileNotFoundError in _run_ip() for robustness.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(ci): skip Rust aggregator when cargo not available in swarm test
The IDF container doesn't have Rust installed. Check for cargo
with shutil.which() before attempting to spawn the aggregator,
falling back to aggregator-less mode (QEMU nodes still boot and
exercise the firmware pipeline).
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(ci): treat swarm test WARNs as acceptable in CI
The max_boot_time_s assertion WARNs because QEMU doesn't produce
parseable boot time data. Exit code 1 (WARN) is acceptable in CI
without real hardware; only exit code 2+ (FAIL/FATAL) should fail.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(firmware): Kconfig EDGE_FALL_THRESH default 2000→15000
The nvs_config.c fallback (15.0f) was never reached because
Kconfig always defines CONFIG_EDGE_FALL_THRESH. The Kconfig
default was still 2000 (=2.0 rad/s²), causing false fall alerts
on real WiFi CSI data (7 alerts in 45s).
Fixed to 15000 (=15.0 rad/s²). Verified on real ESP32-S3 hardware
with live WiFi CSI: 0 false fall alerts in 60s / 1300+ frames.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs: update README, CHANGELOG, user guide for v0.4.3-esp32
- README: add v0.4.3 to release table, 4MB flash instructions,
fix fall-thresh example (5000→15000)
- CHANGELOG: v0.4.3-esp32 entry with all fixes and additions
- User guide: 4MB flash section with esptool commands
Co-Authored-By: claude-flow <ruv@ruv.net>