mirror of
https://github.com/ruvnet/RuView
synced 2026-07-25 17:51:48 +00:00
f23e34ee5c
`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>
126 lines
4.0 KiB
Python
126 lines
4.0 KiB
Python
"""ADR-110 multi-board live capture — 802.15.4 sync + TWT + HE-LTF.
|
|
|
|
Captures from up to 3 ESP32-C6 boards simultaneously, resets them
|
|
together so the leader election starts from a clean slate, then
|
|
records 35 s of serial output to per-port log files and prints
|
|
a summary of the time-sync state machine, TWT events, and CSI
|
|
metadata at the end.
|
|
"""
|
|
import serial
|
|
import threading
|
|
import time
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
PORTS = ['COM6', 'COM9', 'COM12']
|
|
DURATION_SECONDS = 35
|
|
OUTPUT_DIR = Path(__file__).parent / 'witness-3board'
|
|
OUTPUT_DIR.mkdir(exist_ok=True)
|
|
|
|
|
|
def capture(port: str, results: dict):
|
|
"""Reset and capture from one port for DURATION_SECONDS."""
|
|
try:
|
|
ser = serial.Serial(port, 115200, timeout=1)
|
|
# Hard reset via DTR/RTS pulse.
|
|
ser.setDTR(False); ser.setRTS(True); time.sleep(0.05)
|
|
ser.setDTR(False); ser.setRTS(False)
|
|
ser.reset_input_buffer()
|
|
buf = bytearray()
|
|
start = time.time()
|
|
while time.time() - start < DURATION_SECONDS:
|
|
data = ser.read(4096)
|
|
if data:
|
|
buf.extend(data)
|
|
ser.close()
|
|
log_path = OUTPUT_DIR / f'{port}.log'
|
|
log_path.write_bytes(bytes(buf))
|
|
text = bytes(buf).decode('utf-8', errors='replace')
|
|
results[port] = text
|
|
print(f'[{port}] {len(buf)} bytes captured -> {log_path}')
|
|
except Exception as e:
|
|
print(f'[{port}] ERROR: {e}')
|
|
results[port] = None
|
|
|
|
|
|
# Launch 3 capture threads — actual concurrent reset + capture.
|
|
results = {}
|
|
threads = [threading.Thread(target=capture, args=(p, results)) for p in PORTS]
|
|
for t in threads:
|
|
t.start()
|
|
for t in threads:
|
|
t.join()
|
|
|
|
|
|
# ── Analyze ────────────────────────────────────────────────────────────
|
|
|
|
def grep_pattern(text: str, pattern: str, n: int = 8):
|
|
rx = re.compile(pattern)
|
|
return [L.strip() for L in (text or '').split('\n') if rx.search(L)][:n]
|
|
|
|
|
|
print('\n' + '='*78)
|
|
print('ADR-110 multi-board capture summary')
|
|
print('='*78)
|
|
|
|
|
|
for port in PORTS:
|
|
text = results.get(port)
|
|
if not text:
|
|
print(f'\n--- {port}: NO DATA ---')
|
|
continue
|
|
print(f'\n--- {port} ---')
|
|
|
|
# Boot banner
|
|
for L in grep_pattern(text, r'main: ESP32-C6.*Node ID', 2):
|
|
print(f' banner : {L}')
|
|
|
|
# Time-sync init
|
|
for L in grep_pattern(text, r'c6_ts:.*(init done|promot|stepping down|tx fail)', 4):
|
|
print(f' c6_ts : {L}')
|
|
|
|
# WiFi mode + connect status
|
|
for L in grep_pattern(text, r'(wifi:mode|wifi:state|Retrying WiFi|got ip|Connected to WiFi)', 6):
|
|
print(f' wifi : {L}')
|
|
|
|
# TWT events
|
|
for L in grep_pattern(text, r'c6_twt|itwt|TWT', 5):
|
|
print(f' twt : {L}')
|
|
|
|
# CSI callbacks
|
|
for L in grep_pattern(text, r'CSI cb #\d+.*len=', 5):
|
|
print(f' csi_cb : {L}')
|
|
|
|
# 11ax MAC firmware
|
|
for L in grep_pattern(text, r'mac_version:HAL_MAC_ESP32AX', 2):
|
|
print(f' he-mac : {L}')
|
|
|
|
|
|
# Cross-board leader election summary
|
|
print('\n' + '='*78)
|
|
print('Leader election analysis')
|
|
print('='*78)
|
|
eui_re = re.compile(r'EUI=([0-9a-fA-F]+)')
|
|
euis = {}
|
|
for port in PORTS:
|
|
text = results.get(port) or ''
|
|
m = eui_re.search(text)
|
|
if m:
|
|
euis[port] = int(m.group(1), 16)
|
|
print(f' {port} EUI=0x{m.group(1).lower()} -> {"LEADER" if False else "candidate"}')
|
|
|
|
if len(euis) >= 2:
|
|
lowest_port = min(euis, key=euis.get)
|
|
print(f'\n lowest EUI -> expected leader: {lowest_port} (0x{euis[lowest_port]:016x})')
|
|
|
|
# Did a "stepping down" log appear on the non-lowest boards?
|
|
for port in PORTS:
|
|
if port == lowest_port:
|
|
continue
|
|
text = results.get(port) or ''
|
|
if 'stepping down' in text:
|
|
print(f' {port}: [OK] stepped down (heard leader beacon)')
|
|
elif port in euis:
|
|
print(f' {port}: [FAIL] did NOT step down — investigate (own EUI=0x{euis[port]:016x}, expected leader=0x{euis[lowest_port]:016x})')
|