diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dc76120..f706db2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. - **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. - **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`. + - **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. - **Real-time CSI introspection / low-latency tap on `wifi-densepose-sensing-server` (ADR-099).** New `wifi_densepose_sensing_server::introspection` module wires [midstream](https://github.com/ruvnet/midstream)'s `temporal-attractor` (Lyapunov + diff --git a/docs/WITNESS-LOG-110.md b/docs/WITNESS-LOG-110.md index 51ef30a2..85834794 100644 --- a/docs/WITNESS-LOG-110.md +++ b/docs/WITNESS-LOG-110.md @@ -30,6 +30,7 @@ This witness separates what was **empirically observed on real silicon today** f | **A0.8** | **4-minute mesh soak — quantified offset stability + clock skew** | Same default-v0.6.7 dual-board setup, 240 s parallel capture in `dist/firmware-v0.6.7/iter4-{COM9,COM12}-soak240s.log`. Sampled the structured `c6_espnow` counter line every 100 beacons; 43 samples on each board over the converged window.

**Beacon throughput (both boards):**
• Beacon rate: **10.00 /s** exactly on each board (FreeRTOS timer is rock-solid).
• COM12 (leader, lowest EUI): tx=2101, rx=2101, match=**2101 / 2101 (100.00 %)**, 0 TX failures, leader throughout.
• COM9 (follower): tx=2101, rx=2089, match=**2089 / 2101 (99.43 %)** vs the leader's TX, 0 TX failures, stepped down at +27336 ms.
• 12 missed beacons over 210 s ≈ 1 miss / 17.5 s — well within the `VALID_WINDOW_MS=3000` freshness gate.

**Sync offset profile (COM9 follower, 37 samples after a 5-sample warmup):**
• Mean: **−1 163 123 µs** (this is the boot-time delta; the absolute value depends on which board reset first).
• Standard deviation: **540 µs**.
• Range: 2 994 µs over the soak (sample-to-sample noise dominated by 100 ms beacon period + WiFi MAC TX jitter).
• Drift first-quartile vs last-quartile means: **−84.2 µs/min** over 3 minutes of stable follower state — this is the *measured relative clock skew* between the two specific C6 boards' crystals, ≈ **1.4 ppm** (within ESP32 ±10 ppm spec).

**SOTA reading**: at 10 Hz beacons with measured 1.4 ppm clock skew, two-node multistatic alignment maintains ≤100 µs accuracy over any beacon interval — easily meeting ADR-110 §2.4's stated ±100 µs target. Adding a simple linear or Kalman fit on the offset trajectory (host-side, no firmware change) would reduce per-frame alignment error to **<50 µs**. The hardware substrate is ready; downstream ADR-029/030 multistatic CSI fusion can rely on this number. | | **A0.9** | **EMA offset smoother shipped in firmware (in-line, not host-side)** | Moved the iter-4 recommendation into the firmware itself: `c6_sync_espnow.c` now maintains an exponential-moving-average of the raw beacon-derived offset (α = 1/8, fixed-point shift = 3, ≈ 8-sample effective window at the 10 Hz beacon rate). New getter `c6_sync_espnow_get_offset_us_smoothed()` exposes it; `c6_sync_espnow_get_epoch_us()` now prefers the smoothed value once the follower has heard a leader beacon (otherwise falls back to raw=0). `s_offset_us` (raw) stays unchanged for diagnostics. The diag log line now prints both: `offset_us=… smoothed=…`.

**Live verification (90 s soak)**: `dist/firmware-v0.6.7/iter5-COM9-ema-90s.log`. 12 follower-mode samples, 7 after the warmup window:

`I (52236) ... offset_us=-1163104 smoothed=-1163294`
`I (57236) ... offset_us=-1163115 smoothed=-1163163`
`I (62236) ... offset_us=-1163117 smoothed=-1163150`
`I (67236) ... offset_us=-1163114 smoothed=-1163171`
`I (72236) ... offset_us=-1163094 smoothed=-1163222`
`I (77236) ... offset_us=-1163090 smoothed=-1163320`
`I (82236) ... offset_us=-1163088 smoothed=-1163114`

**Methodology caveat**: in a short 60-second window the raw stdev is small (12.5 µs, basically just per-beacon WiFi-MAC jitter — the drift hasn't accumulated yet) and the smoothed stdev appears larger (69 µs) because the EMA still carries memory of older follower-mode samples that were further from steady state. The smoothing's actual benefit emerges over windows long enough for the raw signal to accumulate drift on top of per-beacon noise (≥5 min, matching §A0.8's regime). The next long-soak iteration will quantify the suppression ratio properly.

**Why it's the right place anyway**: the smoothed value is what `get_epoch_us()` returns — meaning every CSI frame downstream consumer (host aggregator, ADR-029/030 fusion) sees a *bounded-jitter* timestamp without having to re-implement the filter. Per-frame stamping fidelity is what matters for multistatic fusion, not the diagnostic counter. Build: C6 image grew by 32 bytes (≈ the new static state + getter), 45 % partition slack unchanged. | | **A0.10** | **EMA suppression ratio quantified — 3.95× over 5-min soak, ≤100 µs target met by smoothed value alone** | Re-ran the parallel two-board soak with the iter-5 EMA firmware for **300 s** to land in §A0.8's regime where the smoothing benefit actually shows. Raw captures: `dist/firmware-v0.6.7/iter6-{COM9,COM12}-ema-300s.log`. **55 follower-mode samples, 46 after an 8-sample EMA warmup window** (the EMA needs ≈8 samples = ~0.8 s to fully converge from seed).

**Over the 225 s converged window:**

| Stream | stdev (µs) | range (µs) | drift Q1→Q4 (µs/min) |
|---|---|---|---|
| Raw `offset_us` | **411.5** | 2245 | +30.1 |
| EMA `smoothed` | **104.1** | 478 | +27.8 |

**Suppression ratio: 3.95×** on stdev, **4.70×** on peak-to-peak range. Crucially, drift is **preserved** — the smoothed value tracks the true 30 µs/min clock skew (within 2 µs/min of the raw measurement), so multistatic alignment doesn't lag behind reality. The ADR-110 §2.4 ≤100 µs alignment target is now *empirically met by the smoothed offset alone*, no host-side post-processing required.

**Drift note vs §A0.8**: iter 4 saw −84 µs/min, iter 6 sees +30 µs/min between the same two boards. Drift sign + magnitude vary with thermal state and recent activity (boards had been powered ~20 min more by iter 6 — settled to a different equilibrium). Both values are within ESP32's ±10 ppm crystal spec; the EMA tracks whichever value applies in the moment.

**Throughput unchanged** by the smoothing path: tx=2701, rx=2689, match=2689 → **99.56 % cross-board match** over 5 min (vs §A0.8's 99.43 % — within noise). Zero TX failures either board.

**ADR-110 §B substrate status now**: ≤100 µs multistatic alignment is **measured and shipped**, not just designed. The downstream multistatic CSI fusion (ADR-029/030) can rely on this as a black-box timestamp source. | +| **A0.11** | **Wiring gap identified: CSI frames don't yet carry the synced timestamp (deferred)** | `csi_serialize_frame()` in `main/csi_collector.c` builds the ADR-018 frame from `info->rx_ctrl` and the I/Q payload; it does NOT include a timestamp field at all. The ADR-018 wire format reserves bytes [0..19] for the fixed header (magic / node_id / antennas / subcarriers / freq / sequence / RSSI / noise / ADR-110 PPDU+flags), then I/Q from byte 20. Host-side timestamping happens on UDP packet arrival, not from in-frame data.

The §A0.10 mesh sync infrastructure (`c6_sync_espnow_get_epoch_us()`) returns a bounded-jitter clock value, but **no current code path writes that value into a frame the host can read**. Closing the gap is non-trivial — three options, each with trade-offs:

1. **ADR-018 v2 with an 8-byte timestamp field** — cleanest end-state but a breaking change. Old aggregators see a magic mismatch and reject. Needs a new ADR + host-decoder update on both Rust and Python paths.

2. **Separate per-node UDP sync packet** — periodically broadcast `(node_id, sequence_high_water, epoch_us, smoothed_offset)` from each node; host joins by `(node_id, sequence)` to interpolate. Backwards-compatible with the existing ADR-018 frame; requires new aggregator-side join logic.

3. **Repurpose byte 19 flag bit 4** ("802.15.4 time-sync valid") as a "sync-attached-out-of-band" hint, then expose the current offset on the existing HTTP `/api/v1/status` endpoint. Lightest firmware change but lossy (host has to poll, not stream).

Documented here so it's not lost between iters. Likely path: option 2, which keeps the v0.6.x ADR-018 contract stable while ADR-029/030 multistatic fusion lights up. Not in scope for v0.6.8 — that release just ships the mesh substrate + smoother that option 2 will consume. | ## A. Empirically verified (real silicon, today) diff --git a/firmware/esp32-csi-node/version.txt b/firmware/esp32-csi-node/version.txt index 2228cad4..fae59cac 100644 --- a/firmware/esp32-csi-node/version.txt +++ b/firmware/esp32-csi-node/version.txt @@ -1 +1 @@ -0.6.7 +0.6.8