mirror of
https://github.com/ruvnet/RuView
synced 2026-07-01 13:53:17 +00:00
82be960de5883acd2cd0d94dcde87793ec8087ae
681 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
82be960de5 |
test(adr-110): cross-language wire-format conformance gate
Iter 21 — ultra-opt for protocol correctness across the two production decoders. Pin the same 32-byte canonical hex in both Python and Rust tests; if either decoder drifts from the wire, ONE of the tests starts failing — and it's clear which side moved. Canonical packet: COM9 sync-pkt #1 from §A0.12 live capture, expressed as exact little-endian bytes: 10a111c5 09 01 06 00 magic + node + ver + flags + rsvd f26db70100000000 local_us = 28_798_450 c5aca50100000000 epoch_us = 27_634_885 1400000000000000 sequence = 20 + reserved Python test: archive/v1/tests/unit/test_esp32_binary_parser.py::TestSyncPacketParser ::test_canonical_wire_bytes_match_rust_decoder — decodes the pinned hex, asserts every field including the §A0.10 1,163,565 µs offset. Rust test: v2/crates/wifi-densepose-hardware/src/sync_packet.rs::tests ::canonical_wire_bytes_match_python_decoder — decodes the same bytes, asserts the same fields, then re-encodes via to_bytes() and asserts the round-trip produces the EXACT same 32 bytes. So this also catches drift in the Rust encoder. Test counts after this iter: Rust sync_packet: 15/15 green (was 14) Python SyncPacketParser: 7/7 green (was 6) Branch contract: if a future PR changes the firmware wire format, BOTH tests must be updated atomically with the new canonical hex. CI will gate this naturally. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
40bd6b81b8 |
test(adr-110): end-to-end sync decode → frame mesh recovery integration test
Iter 20 — defensive ultra-opt: one test that exercises the entire iter 14→17 chain in a single assertion, so any future refactor that breaks the contract surfaces as a single, named regression instead of 14 unit-test diffs to triangulate. 1. firmware emits sync packet (bytes built here as a stand-in) 2. host decodes via SyncPacket::from_bytes — assert round-trip 3. a CSI frame arrives 100 sequences later (≈ 5 s @ 20 fps) 4. mesh_aligned_us_for_sequence recovers the mesh timestamp 5. cross-check: same value via raw apply_to_local Asserts mesh_us == sync.epoch_us + 5_000_000 µs exactly, plus both paths (sequence-interpolation + direct local→mesh) agree byte-for-byte. Result: 14/14 sync_packet tests pass, full wifi-densepose-hardware crate at 136/136 (no regression from iter 1-19). Contract for any ADR-029/030 multistatic fusion consumer is now defended by a test that fails loud if either piece of the chain drifts. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
898a2d7d9f |
feat(adr-110): wire observe_csi_frame_arrival into CSI receive path
Iter 19 — without this call, iter 18's EMA fps tracking was dead code because csi_fps_samples stayed 0 forever and mesh_aligned_us_for_csi_frame always fell back to the 20 Hz constant. In udp_receiver_task's parse_esp32_frame branch, replace the bare last_frame_time assignment with NodeState::observe_csi_frame_arrival, which computes dt vs last_frame_time, feeds update_csi_fps_ema (α=1/8), bumps csi_fps_samples, and sets last_frame_time as a side effect (same value the bare assignment did). Effect: after ~5 CSI frames arrive from any node, mesh_aligned_us_for_csi_frame returns interpolated timestamps using the node's actually-observed frame rate instead of the 20 Hz default. Real bench rate was ~10 fps, so this halves the per-frame timestamp error in §A0.12-style multistatic alignment. cargo check -p wifi-densepose-sensing-server --no-default-features → green. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
0dfa3d46aa |
feat(adr-115): P1 — Cargo features + CLI flags for MQTT/Matter/Semantic
Adds `mqtt` and `matter` Cargo features (default off) plus 20+ new CLI
flags wired through cli.rs per ADR-115 §3.8 / §3.10 / §3.11 / §3.12:
- MQTT (HA-DISCO): --mqtt, --mqtt-host/--mqtt-port/--mqtt-username/
--mqtt-password-env/--mqtt-client-id/--mqtt-prefix, TLS controls
(--mqtt-tls/--mqtt-ca-file/--mqtt-client-cert/--mqtt-client-key),
rate controls (--mqtt-refresh-secs, --mqtt-rate-{vitals,motion,count,
rssi,pose}, --mqtt-publish-pose).
- Privacy (ADR-106): --privacy-mode strips HR/BR/pose pre-publish.
- Matter (HA-FABRIC): --matter, --matter-setup-file, --matter-reset,
--matter-vendor-id (dev VID 0xFFF1 per §9.9), --matter-product-id.
- Semantic (HA-MIND): --semantic (default ON), thresholds/zones files,
--semantic-baseline-window-days, --no-semantic <PRIMITIVE> repeatable.
rumqttc 0.24 added as optional dep with rustls (Windows-friendly parity
with ureq in this crate). matter-rs deferred to P7 spike per §9.10.
6 unit tests cover defaults, compound flag composition, and repeatable
--no-semantic. Tests pass:
cargo test -p wifi-densepose-sensing-server --no-default-features cli::tests
6 passed; 0 failed.
Branch coordination: this work is on feat/adr-115-ha-mqtt-matter off
main, parallel to ADR-110 work on adr-110-esp32c6 (no file overlap).
Refs #776 (ADR-115 implementation tracking issue).
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
4705fb5ae8 |
feat(adr-115): ADR + P1 — MQTT/Matter/Semantic CLI plumbing (refs #776)
ADR-115 lands the dual-protocol HA integration design:
- MQTT auto-discovery (HA-DISCO) carrying full RuView telemetry
- Matter Bridge (HA-FABRIC) carrying the standardised subset across
Apple Home / Google Home / Alexa / SmartThings / HA
- Semantic Automation Primitives (HA-MIND) — 10 v1 inferred states
(someone-sleeping, possible-distress, room-active, elderly-anomaly,
meeting-in-progress, bathroom-occupied, fall-risk-elevated, bed-exit,
no-movement, multi-room-transition) that turn raw signals into HA
entities, Matter events, and Apple Home scene triggers — the inference
layer that moves RuView from "RF sensing" to "ambient intelligence
infrastructure". All 13 §9 open questions ACK'd by maintainer.
P1 (this commit) — `mqtt` and `matter` Cargo features (default off) +
20+ new CLI flags wired through cli.rs:
- --mqtt / --mqtt-host / --mqtt-port / --mqtt-username /
--mqtt-password-env / --mqtt-client-id / --mqtt-prefix /
--mqtt-tls / --mqtt-ca-file / --mqtt-client-cert / --mqtt-client-key
- --mqtt-refresh-secs / --mqtt-rate-{vitals,motion,count,rssi,pose} /
--mqtt-publish-pose
- --privacy-mode (ADR-106 primitive-isolation contract)
- --matter / --matter-setup-file / --matter-reset /
--matter-vendor-id (dev VID 0xFFF1 per §9.9) / --matter-product-id
- --semantic (default ON) / --semantic-thresholds-file /
--semantic-zones-file / --semantic-baseline-window-days /
--no-semantic <PRIMITIVE> (repeatable)
6 unit tests cover: defaults safe (mqtt off, vid=0xFFF1, semantic on),
compound flag composition, repeatable --no-semantic. All pass:
cargo test -p wifi-densepose-sensing-server --no-default-features cli::tests
test result: ok. 6 passed; 0 failed.
rumqttc 0.24 added as optional dep (gated behind `mqtt` feature) with
rustls instead of openssl for Windows parity with the rest of the
workspace. matter-rs intentionally absent until P7 spike validates the
SDK choice (§9.10).
Coordinates with ADR-110 work (different branch, different files).
This branch is feat/adr-115-ha-mqtt-matter off main. ADR-110 work
continues on adr-110-esp32c6.
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
ca2059b07f |
fix(branch-coord): revert ADR-115 Cargo.toml/cli.rs that slipped into iter 18
Iter 18's commit
|
||
|
|
2997165bc1 |
feat(adr-110): per-node measured CSI fps + EMA for mesh-time interpolation
Iter 18 (after recovery from a cross-branch slip — see commit-history
context below). Replaces the hardcoded 20 Hz CSI_FPS_HZ constant in
mesh_aligned_us_for_csi_frame with a per-node EMA of observed
inter-frame intervals, falling back to 20 Hz until ≥5 samples land.
Real bench data (§A0.12 captures) showed the actual CSI rate at ~10 fps
because the firmware's CSI_MIN_SEND_INTERVAL_US gate combined with
ruv.net's traffic level paces it to that. Using 20 Hz against actual
10 fps inflates Δus 2× and shifts the recovered mesh timestamp by up
to the inter-sync interval / 2 = ~1 s. Measured fps fixes that.
State on NodeState:
csi_fps_ema: f64 — EMA (seeded at 20.0)
csi_fps_samples: u32 — counts inter-frame deltas observed
API:
NodeState::observe_csi_frame_arrival(now) — call once per CSI frame
from udp_receiver_task
update_csi_fps_ema(prev_fps, dt_sec) -> Option<f64> — free fn,
testable
mesh_aligned_us_for_csi_frame now uses the measured fps when samples ≥ 5,
falls back to 20 Hz otherwise.
4 unit tests (fps_ema_tests module, all passing on the binary):
* steady_10hz_converges_toward_10 — 40 samples at 100 ms converge to ±0.1 Hz
* steady_20hz_stays_near_20 — 20 samples at 50 ms stay within 0.05 Hz
* nonpositive_dt_rejected — dt ≤ 0 returns None
* long_gap_rejected_as_implausible — dt > 1 s rejected (likely a dropout)
Branch-coordination note: this iter's working tree was briefly applied
to feat/adr-115-ha-mqtt-matter by a `git checkout` between iter 17 and
iter 18. Stashed the ADR-115 agent's MQTT/Matter Cargo.toml work
(`stash@adr115-pending-work`) before switching back here. No code lost.
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
0c311a202b |
feat(adr-110): SyncPacket::mesh_aligned_us_for_sequence (interpolation) + NodeState hook
Iter 17 — closes the per-frame mesh-time loop for ADR-018 CSI frames
that carry no per-frame local_us field (the v1 wire format reserves no
slot — see WITNESS-LOG-110 §A0.11).
Math: pair the frame's sequence number against the sync packet's
sequence high-water + an assumed CSI frame rate. Δframes × 1/fps
estimates the node-local delta from the sync, then apply_to_local
recovers the mesh epoch.
SyncPacket::mesh_aligned_us_for_sequence(frame_seq: u32, fps_hz: f64) -> u64
3 new unit tests (13 total in sync_packet::tests, all green):
* mesh_aligned_for_sequence_identity_at_sync_point — at sync.sequence
returns sync.epoch_us exactly
* mesh_aligned_for_sequence_extrapolates_forward — 20 frames @ 20 fps
extrapolates by exactly 1 s
* mesh_aligned_for_sequence_handles_seq_wraparound — u32 sequence
wrap doesn't jump backward by 2^32 (wrapping_sub guards it)
NodeState hook:
NodeState::mesh_aligned_us_for_csi_frame(frame_sequence: u32) -> Option<u64>
Wraps the SyncPacket method, defaults fps_hz=20.0 (matches the
firmware's CSI_MIN_SEND_INTERVAL_US-implied ceiling), enforces the
same 9 s staleness gate as mesh_aligned_us.
cargo check -p wifi-densepose-sensing-server --no-default-features → green.
cargo test -p wifi-densepose-hardware sync_packet → 13/13, 122 filtered.
Downstream ADR-029/030 multistatic fusion code can now do:
if frame.adr018_flags.ieee802154_sync_valid {
if let Some(mesh_us) = ns.mesh_aligned_us_for_csi_frame(frame.sequence) {
// pair this frame with frames from sibling nodes by mesh_us
}
}
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
df95360e52 |
feat(adr-110 P10): apply_to_local + NodeState::mesh_aligned_us + full ADR rewrite
Iter 16 closes the math loop and updates ADR-110 to reflect the full
P1-P10 sprint outcome (per user request).
Code (the math layer that converts the iter 15 stored sync into a
per-frame mesh-aligned timestamp):
wifi-densepose-hardware:
SyncPacket::apply_to_local(local_at_frame_us: u64) -> u64
Pure integer math: offset = epoch - local; mesh = local_at_frame + offset.
3 new unit tests (10 total, all green):
- apply_to_local_recovers_packet_epoch (identity at the packet's local_us)
- apply_to_local_preserves_inter_frame_delta (Δlocal == Δmesh)
- apply_to_local_on_leader_is_near_identity (leader offset ≈ 0)
wifi-densepose-sensing-server:
NodeState::mesh_aligned_us(local_at_frame_us: u64) -> Option<u64>
Returns the recovered mesh timestamp using the most-recent sync
packet, or None if no sync seen or last one older than 9 s
(3× firmware VALID_WINDOW_MS = 9 s staleness gate).
cargo check -p wifi-densepose-sensing-server --no-default-features
→ green
ADR-110 substantial rewrite (per user "update adr 110 with details"):
- Status line: P1-P10 complete, firmware-side substrate closed at v0.7.0.
- Front matter now lists all 4 firmware releases + witness link.
- Phase table grows a P10 row capturing the v0.6.8 / v0.6.9 / v0.7.0
arc (EMA smoother + sync packet + bit-4 wire-fix + host crates).
- New §4.1 — /loop 5m SOTA sprint summary table (iters 1-16, 4 releases,
17 commits, 13 unit tests, what shipped each iter).
- New §4.2 — measured numbers table with 99.56% RX, 104.1 µs smoothed
stdev, 3.95x suppression, 1.4 ppm crystal skew, etc — every cell
backed by a witness §A0.x entry and a preserved bench log.
- New §4.3 — host-side production surface listing (sync_packet.rs +
sensing-server NodeState + Python parser, with file paths).
- §5 open question on 802.15.4 channel resolved (Kconfig, default ch26
not ch15, with the witness §D1 rationale).
- New §6 — explicit scope of what's outside this ADR (multistatic fusion
math in ADR-029/030, hardware-gated measurements needing INA / 11ax AP,
IDF upstream fixes pending).
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
23fd8ac371 |
feat(sensing-server): consume ADR-110 §A0.12 sync packets, store per-node
Iter 15 — converts the iter 14 SyncPacket decoder from "shipped" to
"consumed" by wiring it into the sensing-server UDP receive loop.
Wiring:
- Cargo.toml gains wifi-densepose-hardware = path = "../wifi-densepose-hardware"
to pull in the SyncPacket decoder + SYNC_PACKET_MAGIC dispatch constant.
- NodeState gains two new fields:
latest_sync: Option<SyncPacket> — the parsed packet
latest_sync_at: Option<std::time::Instant> — staleness clock
- udp_receiver_task now magic-dispatches every incoming datagram against
SYNC_PACKET_MAGIC (0xC511A110) before falling through to the existing
ADR-039 vitals / ADR-040 WASM / ADR-018 CSI parsers. Same Option-returning
pattern as the other parsers, so future packet types slot in cleanly.
When a sync packet arrives:
* write-lock state, lookup-or-create NodeState by node_id
* stash the SyncPacket + Instant::now() on the node
* debug-log node, leader/valid/smoothed flags, sequence, offset_us
* continue (don't fall through — we know it's not a CSI frame)
Downstream multistatic CSI fusion now has a documented landing pad: any
CSI frame with byte 19 bit 4 set looks up the matching NodeState, applies
ns.latest_sync.epoch_us + (now_local - ns.latest_sync.local_us) to get a
mesh-aligned timestamp. Implementation of that fusion math is the next
ADR-029/030 layer (wifi-densepose-signal).
Verification:
- cargo check -p wifi-densepose-sensing-server --no-default-features → green
- cargo test -p wifi-densepose-hardware sync_packet → 7/7 pass, 122 filtered
- Zero behavioral change for nodes that don't emit sync packets — the
dispatch only fires on magic match.
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
d72944f887 |
feat(hardware): Rust SyncPacket decoder + 7 unit tests (ADR-110 §A0.12)
Iter 14 — moves the v0.7.0 Python stub into the Rust production tree so the sensing-server can decode incoming UDP datagrams by leading magic and apply mesh-aligned timestamps to in-flight CSI frames. Module: v2/crates/wifi-densepose-hardware/src/sync_packet.rs Public surface (re-exported from the crate root): - SyncPacket — 32-byte decoded packet - SyncPacketFlags — bit0=leader, bit1=valid, bit2=smoothed - SYNC_PACKET_MAGIC = 0xC511A110, SYNC_PACKET_SIZE = 32 Tests (all 7 passing, plus 122 existing hardware-crate tests still pass): * follower_typical_packet_roundtrips — reproduces COM9 sync-pkt #1 from §A0.12, including the 1,163,565 µs offset §A0.10 measured * leader_packet_has_local_close_to_epoch — COM12 leader case (flags=0x03, epoch ≈ local, offset = -7 µs call-stack only) * magic_mismatch_is_typed_error * short_packet_is_typed_error * all_flag_combinations_roundtrip — every (leader,valid,smoothed) triple * sync_and_csi_magics_differ — host can dispatch by leading u32 * wire_size_constant_is_correct Uses the existing ParseError variants (InvalidMagic, InsufficientData) so the sensing-server's dispatch code can treat sync-packet decode failures the same way it treats CSI frame decode failures. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
3a6648c290 |
test+docs(adr-110): 6 SyncPacketParser tests + README/user-guide for v0.7.0
Iter 13 — solidifies v0.7.0 as a real, reviewable release.
Tests (archive/v1/tests/unit/test_esp32_binary_parser.py):
- TestSyncPacketParser (6 tests, all passing in 0.27s):
* test_follower_typical_packet_roundtrips — matches the COM9-witnessed
sync-pkt #1 byte-for-byte, including the 1,163,565 µs offset that
§A0.10 measured for the COM9-vs-COM12 boot-time delta
* test_leader_packet_has_local_close_to_epoch — COM12 leader case,
flags=0x03, epoch ≈ local
* test_magic_mismatch_raises — non-sync datagrams don't silently decode
* test_short_packet_raises — early error vs silent truncation
* test_all_flag_combinations — every (leader, valid, smoothed) triple
round-trips independently
* test_dispatch_distinguishes_csi_from_sync — CSI vs sync magics differ
so a host can dispatch by leading u32
Docs:
- README C6 hardware row now headlines v0.7.0 (was v0.6.7), names the
measured 99.56% match / 104 µs stdev / 3.95× suppression numbers, and
acknowledges the firmware-side ADR-110 substrate closure.
- docs/user-guide.md firmware release table now lists v0.7.0 / v0.6.9 /
v0.6.8 / v0.6.7 chain with one-liner highlights so 4MB-flash users +
multistatic operators know which release brings what.
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
d199279caa |
release(firmware): v0.7.0-esp32 major — ADR-110 firmware-side substrate closed
Marks the end of the firmware-side ADR-110 push. Everything the firmware
can deliver toward §B multistatic alignment without hardware-blocked
dependencies is shipped, measured, and witnessed:
§A0.7–§A0.10 ESP-NOW mesh quantified: 99.43-99.56% cross-board match,
104.1 µs smoothed offset stdev, 1.4 ppm crystal-skew
tracking, ≤100 µs alignment target empirically met.
§A0.12 32-byte UDP sync packet emits with mesh-aligned epoch
+ sequence high-water; verified live both boards.
§A0.13 (new) bit-4 wire-fix: byte 19 bit 4 sourced from
c6_sync_espnow_is_valid() too. Mixed S3+C6 fleets now
correctly advertise mesh-sync.
Host-side enabler (Python):
archive/v1/src/hardware/csi_extractor.py grows SyncPacketParser +
SyncPacket dataclass. ESP32BinaryParser docstring acknowledges the
sibling sync packet. Sets up wifi-densepose-sensing-server to
consume the §A0.12 stream without inventing the parser.
Build artifacts (IDF v5.4, both RC=0):
S3 8 MB: 1094 KB, 47% partition slack
C6 4 MB: 1019 KB, 45% partition slack
Tag v0.7.0-esp32. Branch adr-110-esp32c6. PR #764.
What remains is outside the firmware: host-side parser wiring,
multistatic CSI fusion in wifi-densepose-signal, 11ax-cooperative AP
(or future IDF AP-HE API), INA226 for ≤5 µA LP-core.
Co-Authored-By: claude-flow <ruv@ruv.net>
v0.7.0-esp32
|
||
|
|
e69572ff99 |
fix(csi): ADR-018 byte 19 bit 4 now signals ESP-NOW sync too (not just broken 15.4)
WITNESS-LOG-110 prior state had byte 19 bit 4 (cross-node sync valid) only being set from c6_timesync_is_valid() — but c6_timesync is the 802.15.4 path that D1 documented as unfixable in IDF v5.4 (rx=0 across every soak we've run). The working transport is c6_sync_espnow (§A0.7, §A0.10: 99.43-99.56% RX cross-board, 104 µs smoothed-offset stdev), yet frames from sync'd nodes had bit 4 cleared because the ESP-NOW path didn't OR into the flag. Fix: also set bit 4 when c6_sync_espnow_is_valid() — the OR semantic means a node signals sync from whichever transport is healthy. Host sees bit 4 set, knows to pair the frame against the most recent sync packet (§A0.12) from this node_id. Side effect: this also enables S3 boards to set bit 4 (c6_sync_espnow works on both targets, c6_timesync is C6-only). So a multi-target mesh of S3+C6 boards now correctly signals cross-node alignment regardless of which chips are in the fleet. Build evidence: C6 image 1019 KB (+16 bytes for the new check), 45% slack unchanged. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
4e1b62ab4f |
release(firmware): v0.6.9-esp32 — sync-packet wired, CONFIG_C6_SYNC_EVERY_N_FRAMES tunable
Bundles the iter 8 + iter 9 sync-packet work (§A0.11 + §A0.12) into a shipped release. v0.6.8 didn't carry the sync emission; v0.6.9 closes the loop. What ships: - csi_collector emits a 32-byte UDP sync packet (magic 0xC511A110) every CONFIG_C6_SYNC_EVERY_N_FRAMES CSI callbacks (default 20). - New Kconfig knob lets operators tune cadence from ~0.1 Hz (N=1000) to ~10 Hz (N=1) without rebuilding — sensible defaults for mainstream multistatic at ~2 s sync interval. - Backwards-compatible at the wire level: old aggregators drop the new magic on existing parser mismatch path. Build artifacts (both green on IDF v5.4): - S3 8 MB: 1094 KB, 47% partition slack - C6 4 MB: 1019 KB, 45% partition slack The macro define was renamed from SYNC_EVERY_N_FRAMES to CONFIG_C6_SYNC_EVERY_N_FRAMES so the Kconfig generator wires through. Header guard preserves the default for builds without the kconfig applied. Co-Authored-By: claude-flow <ruv@ruv.net>v0.6.9-esp32 |
||
|
|
d2effcc6f6 |
witness(ADR-110 §A0.12): sync-packet wired + verified live on both boards
SOTA iter 9 — closes the §A0.11 wiring gap with empirical evidence. Added a diagnostic ESP_LOGI in the sync emit path; flashed both C6 boards; captured 45s parallel serial output. Sync packet generation confirmed live: COM12 (leader, ...00:84): sync-pkt #1 ... node=12 flags=0x03 local_us=28864932 epoch_us=28864939 flags=0x03 = leader+valid, epoch ≈ local (7 µs delta = call-stack elapsed only — leader has no offset by definition) COM9 (follower, ...05:3c): sync-pkt #1 ... node=9 flags=0x06 local_us=28798450 epoch_us=27634885 flags=0x06 = valid+smoothed_used, local-epoch = 1,163,565 µs Matches §A0.10's measured -1.16 s mesh-aligned offset within 285 µs (WiFi MAC TX jitter floor between samples). Cadence: 2.05 s between sync packets — 20 CSI frames at the bench's observed 10 fps rate = exactly the design intent. UDP send returns -1 (sr=-1) because the bench boards are intentionally not associated to a real AP (provisioned to dead SSIDs for the iter 2-8 mesh experiments). No crash, no resource leak in 45s. Once boards hit a routable network, sr becomes the byte count. Wiring gap §A0.11 now CLOSED. Multistatic CSI fusion downstream has a documented protocol to recover mesh-aligned timestamps for every CSI frame: host pairs (node_id, sequence) across the two packet streams. Host-side parser is the natural next layer (wifi-densepose-sensing-server). Build evidence: C6 image 1019 KB (+0.5 KB for the diag log line), 45% partition slack unchanged. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
6ff155a232 |
feat(csi): emit ADR-110 §A0.11 sync-packet every 20 CSI frames
Closes WITNESS-LOG-110 §A0.11 wiring gap. Adds a separate 32-byte UDP packet (magic 0xC511A110, distinct from the CSI frame magic 0xC5110001) carrying: [0..3] magic 0xC511A110 (LE u32) — CSI-ADR-110 sync packet [4] node_id [5] proto version (0x01) [6] flags: bit0=is_leader, bit1=is_valid, bit2=smoothed_used [7] reserved [8..15] local esp_timer_get_time() (LE u64) [16..23] mesh-aligned epoch (LE u64) = local + EMA-smoothed offset [24..27] high-water sequence number (LE u32) for pairing with CSI frames [28..31] reserved (room for leader_id low32 in a follow-up) Emitted once per 20 CSI frames (≈ 1 Hz at the 20 Hz send-rate gate). Same stream_sender UDP socket as CSI frames — host dispatches by first 4 bytes of each datagram. Backwards compatible: aggregators that don't know about the new magic ignore it (sync packets won't match the CSI parser's magic check, so they're dropped harmlessly by existing receivers). New aggregators pair (node_id, sequence) across the two packet streams to align CSI frames to mesh time. Sets us up for downstream ADR-029/030 multistatic CSI fusion: with the host now able to recover the mesh-aligned epoch from each frame's sequence number, frames from multiple boards can be ordered + fused on a common timeline. Build evidence: C6 image 1019 KB (+1 KB vs v0.6.8 no-sync), 45 % partition slack unchanged. Host-side parser update is a follow-up. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
503411a8d2 |
release(firmware): bump to v0.6.8-esp32 — ESP-NOW mesh EMA smoother
SOTA iter 7. Tags + ships the firmware that actually has the iter-5/6 EMA path so the GitHub release matches the witness measurements. v0.6.7 binaries on the release predate the EMA work — anyone downloading from the v0.6.7 release would not get the smoothing §A0.10 measured. Build evidence (IDF v5.4, both RC=0): - S3 8 MB: 1093 KB (47 % slack), SHA256 60e3ef907f... - C6 4 MB: 1019 KB (45 % slack), SHA256 feb88d60a0... - Soft-AP and 4 MB S3 variants ship unchanged from v0.6.7; not rebuilt. Wiring gap documented in WITNESS §A0.11: ADR-018 wire format has no timestamp field, so the synced clock value from get_epoch_us() doesn't yet reach CSI frames. Three options outlined (ADR-018 v2 / separate UDP sync packet / out-of-band HTTP probe). Likely landing place is the separate UDP sync packet — keeps the existing ADR-018 contract intact while ADR-029/030 multistatic fusion lights up the substrate. CHANGELOG Wave 4 entry summarises what v0.6.8 ships + the deferred gap so future maintainers don't lose the breadcrumb. Co-Authored-By: claude-flow <ruv@ruv.net>v0.6.8-esp32 |
||
|
|
e5c3b27daa |
witness(ADR-110 §A0.10): EMA suppression quantified — 3.95x, ≤100 µs alignment shipped
SOTA iter 6 — the long-soak iter 5 owed. 300 s parallel two-board capture
with the iter 5 EMA firmware, 46 converged follower-mode samples.
Over the 225 s steady-state window:
stdev range drift Q1->Q4
raw 411.5 µs 2245 µs +30.1 µs/min
smoothed 104.1 µs 478 µs +27.8 µs/min
suppression: 3.95x (stdev), 4.70x (range)
The ADR-110 §2.4 ≤100 µs alignment target is now empirically met by the
smoothed offset alone — no host-side filter required. Drift is preserved
(within 2 µs/min between raw and smoothed), so the EMA tracks real clock
skew, not lag behind it.
Drift sign + magnitude vary with thermal state across runs (-84 µs/min
in §A0.8 iter 4, +30 µs/min here in iter 6 with boards warmer — both
within ESP32 ±10 ppm crystal spec). The EMA tracks whichever value
applies at any given moment.
Throughput: tx=2701, rx=2689, match=2689 → 99.56% cross-board match,
zero TX failures.
ADR-110 §B sync-substrate status: ≤100 µs multistatic alignment is now
*measured and shipped*, not just designed. Downstream multistatic CSI
fusion (ADR-029/030) can treat c6_sync_espnow_get_epoch_us() as a
black-box bounded-jitter timestamp source.
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
f41f5fc85b |
feat(c6_sync_espnow): EMA-smooth cross-board offset, expose via get_epoch_us
SOTA iter 5 — converted the iter 4 ADR-110 §A0.8 closing recommendation
("host-side Kalman / linear fit on the offset trajectory") into a
firmware-side, fixed-point EMA so every downstream consumer of
c6_sync_espnow_get_epoch_us() gets bounded-jitter timestamps for free.
Implementation:
* α = 1/8 (Q3.3 shift = 3), ≈8-sample effective window at the 10 Hz
beacon rate. Tracks the ≈1.4 ppm crystal drift §A0.8 measured while
averaging out per-beacon WiFi-MAC jitter spikes.
* y[n] = y[n-1] + (raw - y[n-1]) >> 3 — integer arithmetic, two cycles
on the RISC-V LP/HP cores, no float dependency.
* Seeded from the first follower-mode sample so we don't bias toward 0.
* New getter: int64_t c6_sync_espnow_get_offset_us_smoothed(void).
* c6_sync_espnow_get_offset_us() (raw) stays for diagnostics, unchanged.
* c6_sync_espnow_get_epoch_us() now prefers the smoothed offset once
s_smoothed_seeded — meaning every CSI frame timestamp ADR-029/030
consumes is already filtered, no host-side rework required.
Diag log line now prints both:
c6_espnow: tx#N ... offset_us=R smoothed=S
90 s bench verification (witness §A0.9 + iter5-COM9-ema-90s.log) shows
both values tracking. Methodology caveat in §A0.9: short windows don't
let the smoothing benefit emerge over the raw noise floor — the
suppression ratio measurement needs ≥5 min, deferred to a long-soak
iteration.
Binary size cost: ~32 bytes (one int64, one bool, one getter). C6 build
still 45% partition slack.
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
676297c48f |
witness(ADR-110 §A0.8): 4-minute mesh soak — quantified stability + measured clock skew
SOTA iter 4 (cron c40dab4a tick 4). Converted iter 2's 30-second snapshot
into a real statistical measurement over 4 minutes / 2101 beacons.
Beacon throughput (both boards):
- Rate: 10.00/s exactly — FreeRTOS timer rock-solid
- COM12 leader: tx=2101, match=2101/2101 = 100.00%, 0 TX fail
- COM9 follower: tx=2101, match=2089/2101 = 99.43%, 0 TX fail
- 12 missed beacons / 210 s ≈ 1 miss / 17.5 s — inside the 3-second
VALID_WINDOW_MS freshness gate, sync remains valid
Sync offset (COM9, 37 follower-mode samples after warmup):
- mean: -1,163,123 µs (boot-time delta, not jitter)
- stdev: 540 µs
- range: 2994 µs over the soak
- drift Q1->Q4: -84.2 µs/min over 3 minutes
= 1.4 ppm relative clock skew between the two specific C6 crystals
(ESP32 spec: typical ±10 ppm — well within tolerance)
ADR-110 §2.4 target ±100 µs across one hop: met with margin at the
current 10 Hz beacon rate. A simple linear or Kalman fit on the offset
trajectory (host-side, no firmware change) would compress per-frame
alignment error to <50 µs. Hardware substrate is now quantified and
documented — downstream ADR-029/030 multistatic fusion can plan around
the measured numbers.
Also corrected §A0.7's "±10 µs jitter" wording — that was sample-to-sample
range within a 5-row snapshot, not the true stability profile. §A0.8
supersedes with the proper soak data.
Raw captures: dist/firmware-v0.6.7/iter4-{COM9,COM12}-soak240s.log
(7400+ lines each, full c6_espnow + c6_ts counter records).
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
d636604330 |
docs(user-guide): point 4MB-flash flow at the v0.6.7 S3 4MB binary
SOTA loop iter 3 added esp32-csi-node-s3-4mb.bin to the v0.6.7-esp32 release (882 KB binary built from sdkconfig.defaults.4mb, 52% partition slack on 4MB single-OTA — vs 47% for the 8MB build, +5pp). v0.6.6 shipped 8MB+4MB parity; v0.6.7 now matches. User-guide previously pointed SuperMini 4MB owners at v0.4.3 (which predates ADR-110 / fall-threshold fix / 4102-tx ESP-NOW soak). Point at v0.6.7 directly so 4MB users get the same firmware as 8MB users. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
572e09ad86 |
witness(ADR-110 §A0.7): ESP-NOW cross-board mesh — leader election + sync offset measured
SOTA iter 2 (cron c40dab4a tick 2). The §D-workaround that v0.6.6 left
on TX-only soak coverage is now empirically complete end-to-end.
Parallel 60 s capture with COM9 (206ef117053c) + COM12 (206ef1170084)
both on default v0.6.7, no WiFi associations needed:
* RX rate cross-board:
- COM12: tx=301 rx=297 match=297 (98.7 %)
- COM9: tx=301 rx=300 match=300 (99.7 %)
- 0 TX failures on either side over 30 s of beacons
* Leader election fired for the first time in ADR-110:
+27336 ms COM9: "stepping down: heard lower-id leader 206ef1170084
(we are 206ef117053c)" — the lowest-EUI-wins protocol the original
c6_timesync was designed to run, now actually working because the
transport is healthy.
* Cross-board sync offset converged and stable:
COM9 offset_us: -1462 -> -950 -> -954 -> -957 -> -948
±10 µs jitter once leader-following stabilises, hitting the ±100 µs
target named in ADR-110 §2.4.
802.15.4 c6_ts path stayed rx=0 across both 60 s captures — D1 still
broken in IDF v5.4, exactly as documented. ESP-NOW is confirmed as the
working multistatic time alignment transport.
Raw captures: dist/firmware-v0.6.7/iter2-{COM9,COM12}-espnow.log.
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
f9aad75413 |
witness+opt: ADR-110 §A0.6 — IDF v5.4 soft-AP HE gap, swarm warnings
Iter 1 finding from /loop 5m SOTA sprint: two C6 boards now mesh through the c6_softap_he soft-AP (COM12 hosts ruview-c6-twt, COM9 associates), but COM9 lands at phymode(0x3, 11bgn), he:0 — the soft-AP doesn't advertise HE. Confirmed by full grep of components/esp_wifi/include/esp_wifi*.h: the public API exposes ONLY STA-side iTWT/bTWT. There is no esp_wifi_ap_set_he_config, no wifi_he_ap_config_t, no wifi_config_t.ap.he_* field — soft-AP HE/TWT-Responder advertise is not user-controllable on ESP32-C6 in IDF v5.4. Consequence: B1/B2 cannot be measured via the two-C6 path on this IDF release. The c6_softap_he module ships as the in-place hook for any future IDF release that exposes the API; until then a real 11ax router or phone hotspot remains the path. Sharpens the open question from "do we need an 11ax AP?" to "we need either a future IDF AP-side HE config API, or an external 11ax AP". WITNESS-LOG-110 §A0.6 records the parallel boot logs from both boards plus the IDF surface grep evidence. c6_softap_he.c gains an ESP_LOGW at AP-up time so operators understand exactly why STAs land at 11bgn (avoids confusion with the v0.6.6 §A8 graceful-TWT-NACK story). While here: cleared the three -Wunused-variable warnings in swarm_bridge.c that fired on every build (fw_ver, free_heap, presence in heartbeat block). fw_ver now feeds an ESP_LOGI so the boot log names the build; free_heap + heartbeat-presence were dead anyway. Pure ultra-opt: smaller .o files, zero warning noise. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
83f20f7c61 |
witness(ADR-110): v0.6.7 live silicon evidence — A0.4 + A0.5
Flashed v0.6.7 to two ESP32-C6 boards (COM9 + COM12, both matching the witness-log MACs from v0.6.6 session). A0.4 — regression check on COM9 (default config): - v0.6.7 boots in 446 ms, c6_ts up on ch 26, HAL_MAC_ESP32AX_761 loaded, ruv.net association at +5206 ms, iTWT graceful NACK, ESP-NOW init OK, CSI flowing at HT-LTF 64 subcarriers. Byte-for-byte same behavior as v0.6.6 confirms the new code paths (LP-core + soft-AP) are correctly default-off — zero behavioral regression for shipped fleets. A0.5 — soft-AP module live on COM12: - Built a CONFIG_C6_SOFTAP_HE_ENABLE=y variant locally, flashed COM12. - AP came up at +666 ms on channel 6 with WPA2-PSK, dual STA+AP iface visible (...00:84 STA / ...00:85 AP — standard +1 MAC offset). - Discovered live IDF constraint: when AP+STA both active and STA associates to an 11ax AP on a different bandwidth, the soft-AP gets demoted from HE to 11n by the radio scheduler. Documented in §A0.5 — the cleanest two-board iTWT bench needs the AP-role board's STA iface not to associate elsewhere (point it at a non-existent SSID). Release v0.6.7-esp32 now also carries: - esp32-csi-node-c6-4mb-softap.bin (the AP-variant binary) - COM9-v0.6.7-regression.log + COM12-v0.6.7-softap.log raw captures - SHA256SUMS.txt updated with the soft-AP variant hash Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
756bfc0a1a |
docs(readme,user-guide): record v0.6.7 LP-core + soft-AP HE/TWT additions
- README C6 hardware row now links the v0.6.7-esp32 release and notes the LP-core RISC-V program (B4 code path) + soft-AP TWT Responder (B1/B2 two-board unblock). - README Option-2b quick-start mentions the new opt-in toggles. - User-guide gets the v0.6.7 boot banner, expanded battery-seed instructions (real LP-core poll period + debounce knobs), and a fresh "Two-board iTWT bench" section covering the soft-AP role (CONFIG_C6_SOFTAP_HE_ENABLE) and the NVS overrides for SSID / PSK / channel. - User-guide firmware release table prepends v0.6.7-esp32 as Latest above v0.5.0 (still recommended for S3-mesh production). Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
948768bdda |
feat(firmware): v0.6.7-esp32 — real LP-core program + C6 soft-AP HE/TWT helper
ADR-110 P9 — software-only unblocks for the WITNESS-LOG-110 §B
hardware-blocked items. Two new modules, both default-off so v0.6.6 fleets
see no behavior change.
LP-core (B4 path):
- New firmware/esp32-csi-node/main/lp_core/main.c: real RISC-V LP-core
motion-gate program with debounce + monotonic motion_count counter.
- c6_lp_core.c rewritten to load + run the LP binary via ulp_lp_core_run
when CONFIG_C6_LP_CORE_ENABLE=y; falls back to the v0.6.6 ext1 GPIO-wake
path otherwise (keeps regression surface small).
- ulp_embed_binary() wired in main/CMakeLists.txt, gated on the Kconfig.
- New Kconfig knobs: C6_LP_POLL_PERIOD_US (default 10 ms),
C6_LP_DEBOUNCE_SAMPLES (default 3).
- Exposes c6_lp_core_motion_count() / c6_lp_core_poll_count() for the
witness harness — once an INA/Joulescope is on the bench, B4 is one
capture away from a measured number.
Soft-AP HE (B1/B2 unblock):
- New c6_softap_he.{h,c}: brings up the C6 in AP+STA mode with WPA2-PSK
+ HE advertisement, so a second C6 in STA mode can negotiate real
iTWT against a known-cooperative AP without buying an 11ax router.
- main.c calls c6_softap_he_start() right before esp_wifi_start() when
CONFIG_C6_SOFTAP_HE_ENABLE=y.
- New Kconfig knobs: C6_SOFTAP_HE_{SSID,PSK,CHANNEL} with NVS overrides
via softap_ssid / softap_psk / softap_chan in the ruview namespace.
Build artifacts (IDF v5.4, both green, RC=0):
- S3 8 MB: 1093 KB (47% partition slack)
- C6 4 MB: 1019 KB (45% partition slack)
- SHA-256 sums in dist/firmware-v0.6.7/SHA256SUMS.txt
Doc updates: CHANGELOG wave-3 entry, ADR-110 phase table gets P5
upgrade note + new P9 row, WITNESS-LOG-110 gets new A0 section
recording the v0.6.7 build evidence.
Co-Authored-By: claude-flow <ruv@ruv.net>
v0.6.7-esp32
|
||
|
|
561647b3af |
docs(readme): link new ADR-110 reviewer guide + update soak total
Two tiny updates to the ESP32-C6 row in the hardware-options table: - Add link to docs/ADR-110-REVIEW-GUIDE.md (the new one-page reviewer on-ramp added in |
||
|
|
3133be6d48 |
docs(adr-110): add reviewer one-page guide
The witness log is comprehensive but ~300 lines. A reviewer landing on this branch wants a five-minute tour: where to read first, what's actually empirically verified vs hardware-blocked, what the bugs were, and the commit history at a glance. docs/ADR-110-REVIEW-GUIDE.md provides that, with explicit links to the canonical witness + ADR. Doesn't duplicate content — points to where the canonical record lives. Also captures the security note for the operator (rotate the previously- exposed Docker Hub + PI-cluster tokens — they appeared in local logs during witness generation before scripts/redact-secrets.py was added). Ref: ruvnet/RuView#762, draft PR #764 Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
9a46fc8aa2 |
witness: ESP-NOW 300 s soak — 2951 tx 0 fail (2.5x sample)
Confirmation run vs the earlier 120 s soak. Same firmware, same board,
longer window:
Captured 67307 bytes over 300 s
ESP-NOW samples: 60
first: tx=1 fail=0 rx=0 match=0 leader=1 offset=0
last: tx=2951 fail=0 rx=0 match=0 leader=1 offset=0
TX rate: 9.83/s (target 10/s)
TX failure rate: 0.0000%
app_main calls (reset detector): 1 -> no crash
2.5x sample size, identical zero-failure rate, marginally higher
sustained rate (9.83 vs 9.60) — FreeRTOS timer settling. Adds a second
data point to WITNESS-LOG-110 §D-workaround.
Ref: ruvnet/RuView#762, draft PR #764
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
e255b7d43a |
docs(firmware): README acknowledges dual S3+C6 target (ADR-110)
After ADR-110 made this the same source tree for both esp32s3 (production) and esp32c6 (research / Wi-Fi-6 / 802.15.4 / LP-core seed nodes), the firmware README header should reflect that. Title, one-liner, and target badge updated; body sections still use S3 examples as the production default. The C6 build path is documented in docs/user-guide.md + sdkconfig.defaults.esp32c6 + Quick-Start Option 2b in the top-level README. Ref: ruvnet/RuView#762, draft PR #764 Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
553b07d04c |
docs(readme): tighten ESP32-C6 row to match empirical witness (ADR-110)
Original row said C6 *has* HE-LTF tagging + multi-node sync + 5µA hibernation as if they were active features. Reality per WITNESS-LOG-110: - Wire format VERIFIED (17 unit tests across firmware/Rust/Python) - ESP-NOW transport VERIFIED on 1 board (1151 tx, 0 fail in 120s soak) - TWT graceful NACK VERIFIED live (AP isn't 11ax → INVALID_ARG handled) - HE-LTF live capture: BLOCKED on 11ax AP availability - 5µA hibernation: datasheet number, not a measurement (no INA) - 802.15.4 RX: known broken in IDF v5.4, ESP-NOW is the workaround New row leads with 'wire format ready' + 'hardware-gated' to set honest expectations, and links to docs/WITNESS-LOG-110.md so readers can see the full empirical/claimed split themselves. Ref: ruvnet/RuView#762, draft PR #764 Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
9de34ba096 |
docs(adr): index ADR-110 in Hardware and firmware section
The ADR index README hadn't been updated past ADR-099. Adding ADR-110 in the Hardware/firmware section with its honest status — firmware shipped + tested + CI-green, but the four SOTA capability claims (HE-LTF live capture, TWT cadence, cross-node sync, 5 µA hibernation) are each blocked on different physical hardware (11ax AP, more boards, INA meter), as fully documented in docs/WITNESS-LOG-110.md. Ref: ruvnet/RuView#762 / draft PR #764 Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
fc75a8a5c8 |
test(fuzz): extend csi_serialize fuzz harness for ADR-110 byte 18-19
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>
|
||
|
|
89972c0917 |
docs(changelog): expand ADR-110 entry with wave 2-5 additions
The original CHANGELOG entry covered the initial firmware ship. Adding sub-bullets for everything that landed after: - D1 workaround: ESP-NOW cross-node sync (TX 0% failure rate over 1151 transmits in 120 s soak), 802.15.4 path documented as broken - Host-side dual-pipeline decoder for ADR-018 byte 18-19 (Rust 122/122, Python 11/11 — protocol path verified end-to-end without 11ax hardware) - Security fix for witness bundle secret leakage via Pydantic error dumps (redact-secrets.py filter) Witness link: docs/WITNESS-LOG-110.md Ref: ruvnet/RuView#762, draft PR #764 Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
b808a6380b |
witness: ESP-NOW 120s soak — 1151 tx 0 fail, 9.6/s, no crash
Real empirical evidence the ESP-NOW sync transport is long-term stable
on the C6 (D-workaround). Single-board capture on COM9, latest firmware
on branch (
|
||
|
|
8eaa92cf21 |
feat(python): host-side decode for ADR-018 byte 18-19 (ADR-110)
Python ESP32BinaryParser was using struct format '<IBBHIIBB2x' — the
'2x' skipped bytes 18-19 as reserved. After the Rust-side decoder was
extended to surface PPDU type + flags, the Python pipeline (which
archive/v1 still uses for testing + the proof verifier) needs the same
update so its consumers see the HE metadata too.
csi_extractor.py:
- HEADER_FMT now '<IBBHIIBBBB' (captures bytes 18-19)
- New metadata fields: ppdu_type ('ht_legacy'|'he_su'|'he_mu'|'he_tb'|'unknown'),
ppdu_type_raw, he_capable, bw40, stbc, ldpc, ieee802154_sync_valid,
adr018_flags_raw
- Class constants PPDU_HT_LEGACY..PPDU_UNKNOWN mirror the firmware
test_esp32_binary_parser.py:
- build_binary_frame() takes optional ppdu_byte + flags_byte (default 0)
- New TestAdr110ByteEncoding class with 5 tests:
- Pre-ADR-110 zeros decode as 'ht_legacy' + all-flags-false
- HE-SU / HE-MU / HE-TB decode correctly
- 0xFF decodes as 'unknown'
- All-flags-set round-trip (0x1D)
11/11 parser tests pass (6 existing + 5 new). Backwards compat verified.
Pairs with the Rust-side decoder in commit
|
||
|
|
3959fabf31 |
feat(rust): host-side decode for ADR-018 byte 18-19 (ADR-110 closure)
Parse the C6 firmware's HE PPDU type + bandwidth/flags from ADR-018 bytes 18-19 (previously discarded as _reserved). Adds two types to CsiMetadata: ppdu_type (HtLegacy/HeSu/HeMu/HeTb/Unknown) and adr018_flags (bw40/stbc/ldpc/ieee802154_sync_valid). Pre-ADR-110 firmware sends zeros which round-trip as HtLegacy + default flags — fully backwards compatible. 6 new deterministic unit tests: - Pre-ADR-110 backwards compat - HE-SU / HE-MU / HE-TB decode - Unknown PPDU byte -> Unknown - All-bits-set flags round-trip - PpduType byte round-trip Result: 122 wifi-densepose-hardware tests pass, 0 fail. Host decoder now matches the firmware encoder bit-for-bit — HE-LTF metadata path works end-to-end the moment an 11ax AP is in range. Ref: ruvnet/RuView#762 Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
88be283ab0 |
feat(c6): ESP-NOW cross-node sync — D1 workaround for broken 15.4 RX
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>
|
||
|
|
f8a2e36958 |
fix(witness): redact secrets from bundled verify.py output (SECURITY)
The Python proof verifier (archive/v1/data/proof/verify.py) imports the project settings, which read the user's .env file. When pydantic validation fails (e.g., extra fields not in the Settings schema), the error dump includes the offending input_value — which means real Docker tokens, GitHub PATs, API keys, etc. were being echoed to stdout and captured into the bundled verification-output.log. Confirmed on this branch's first bundle generation: dckr_pat_, tok_... cluster token, and other long opaque strings leaked into witness-bundle-ADR028-<commit>/proof/verification-output.log inside the .tar.gz. Bundle + tarball nuked from disk before any push. Added: - scripts/redact-secrets.py — stdin->stdout filter with patterns for common token prefixes (dckr_pat_, tok_, sk-, ghp_, gho_, github_pat_, AKIA, hf_, xoxb-, xoxp-, Bearer), `field=secret` assignments, long opaque alphanumeric strings (40+ chars), and long hex runs (20+ chars which catch token suffixes after `...` truncation). - generate-witness-bundle.sh now pipes verify.py stderr through that filter before tee-ing into the bundled log. - Also fixed pre-existing stale `v1/` paths in the witness script (correct path is `archive/v1/`). The user must rotate the leaked credentials regardless (the bundle was never pushed, but they appeared in this local Claude session log). Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
4c39e28bd0 |
fix(c6): PAN-ID match in 15.4 beacon + expanded D1 diagnostic record
Tried 4th hypothesis for the RX-path bug: maybe the IDF v5.4 receiver
strictly requires dst PAN to match the local set_panid() instead of
honoring the 0xFFFF broadcast PAN per 802.15.4 spec. Changed beacon
dst PAN to 0xCAFE (matching set_panid call) to test.
Result: still negative (tx#241 rx#0/1, magic_match=0). PAN was not the
root cause — but the change is technically more correct per the IDF
behavior and is kept.
Also expanded WITNESS-LOG-110 §D1 to record the 4-experiment matrix
that's now been run:
1. WiFi-on + ch15: tx#381 rx#1 magic_match=0
2. WiFi-on + ch26: identical negative
3. WiFi-off + ch26 + OT off + promiscuous true: tx#601 rx#0 — even
the earlier rx#1 was a noise frame, not protocol traffic
4. Dst PAN 0xCAFE: still negative
Hypothesis space narrowed; needs IDF maintainer trace or working
multi-board reference to fix.
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
66523843e6 |
fix(c6): TWT INVALID_ARG graceful + ch26 + diagnostic counters (ADR-110 D1)
After 3 systematic hypotheses tested + rejected (radio coex, OpenThread shadowing, manual RX re-arm), the 802.15.4 leader-election bug is narrowed to: TX path works perfectly (~10/s clean, 0 fail), but the RX path stops after exactly 1 frame. Manual esp_ieee802154_receive() from either callback bootloops the driver (verified across all 3 boards). The IDF reference example uses the same handle_done-only pattern as this code, implying the driver should auto-restart RX — but empirically doesn't here. Either a half-duplex radio state issue or an IDF v5.4 bug. Tracked as known issue D1 in WITNESS-LOG-110. Changes shipped: - c6_twt.c: ESP_ERR_INVALID_ARG added to graceful-fallback list (empirically: ruv.net AP advertises TWT Responder=0, IDF driver validates against AP HE capability and rejects with INVALID_ARG) - c6_timesync.c: diagnostic counters (s_tx_count, s_tx_fail, s_rx_count, s_rx_magic_match) + per-10-beacon log line preserved so future investigation has the diagnostic harness ready - sdkconfig.defaults.esp32c6: 15.4 channel default 15 → 26 (non-overlap with WiFi 2.4 GHz channels), OpenThread disabled (we use raw 15.4) - promiscuous=true on the radio (broadcast frames addressed to 0xFFFF) - WITNESS-LOG-110 §D1 expanded with the full diagnostic trace + 3-hypothesis investigation record Cross-node sync claim (B3) BLOCKED until either an IDF maintainer trace or a working multi-board reference is available. The other three SOTA dimensions (HE-LTF, TWT cadence, 5 µA hibernation) are also still unverified and need different hardware (11ax AP, INA meter) — honestly recorded in §B. Tracking: ruvnet/RuView#762, task #30 closed as known-issue. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
f23e34ee5c |
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>
|
||
|
|
68abb385ae |
docs(readme): swap hero image to ruview-seed.png (#753)
Replaces assets/ruview-small-gemini.jpg with assets/ruview-seed.png as the hero image. Same Cognitum Seed link target.v1136 |
||
|
|
92badd84e6 |
research(sota-loop): final 00-summary.md — loop closes at 12:00 UTC stop (#747)
Closes the autonomous SOTA research loop kicked off 2026-05-21 ~21:00 UTC. ~15 hours, 41 cron-driven research ticks + 3 housekeeping PRs. Output inventory: - 19 research threads (R1, R3, R5-R15, R16, R17, R18, R19, R20, R20.1, R20.2) - 8 exotic verticals - 7 ADRs from loop (105/106/107/108/109/113/114) + bridges with 3 existing - 1 quantum-sensing doc (17) bridging the existing 11-16 series - 22 numpy reference implementations in 9 thematic folders - Production roadmap (6 tiers, ~3,500 LOC, ~25 person-weeks) - 41 per-tick summaries Three kinds of negative result demonstrated: - Missing-tool (revisitable): R12 -> R12 PABS POSITIVE -> R12.1 CLOSED LOOP - Architecture-error (correctable): R3.1 -> R3.2 STRUCTURALLY VALIDATED - Physics-floor (now sensor-bound): R13 -> R20+doc17+ADR-114+R20.1+R20.2 Three multi-tick research arcs: - R12 (3 ticks): structure detection NEG -> POS -> CLOSED - R3 (3 ticks): cross-room re-ID POS -> NEG (arch error) -> STRUCTURALLY VALIDATED - R20 (5 ticks): vision -> bridge -> spec -> demo -> refinement (45 min) R6 placement family (9 ticks) consolidated into ADR-113 4-axis matrix. Ship recipe: 2D chest-centric + multi-subject + N=5 = 100% coverage. Production Tier 1 (Q3 2026): 93x placement lift + 9.36x intruder lift + ADR-029 closed. ~490 LOC, 3-4 person-weeks. Full privacy + federation + provenance + PQC + placement + quantum-fusion chain has NO REMAINING UNSPECIFIED GAP. Cron d6e5c473 deleted at summary write. Autonomous phase ends here.v1125 v1133 v1131 v1129 v1127 v1123 v1121 v1119 v1117 v1115 v1113 v1111 v1109 v1107 v1105 v1103 v1101 v1099 v1097 v1095 v1093 v1091 v1089 v1087 v1085 v1083 v1081 v1079 v1077 v1075 v1073 v1071 v1069 v1067 v1065 v1063 v1061 v1059 v1057 |
||
|
|
fecb1da252 |
research(R20.2): threshold-based hand-off — works at 0.5 m, harmonic gap at 1 m surfaces Pan-Tompkins requirement (#746)
Implements R20.1's catalogued refinement: when NV conf > 60% AND amplitude > 3 pT, trust NV entirely. Mixed result (5 distances): - 0.5 m: NV=72.00 ✓, smart=72.0 (+0.0 error, NV trusted) ✓ - 1.0 m: NV=144 (harmonic!), smart trusts wrong NV (+72 BPM error) - 1.5 m+: falls back to weighted (NV conf below threshold) Production lesson: the threshold-based policy is correct in spirit but incorrect with simple FFT rate estimator (picks harmonics). Production needs: 1. Harmonic rejection (Pan-Tompkins QRS or autocorrelation) 2. Cross-check vs breathing band 3. Per-frame plausibility window R20.1's 'production needs Pan-Tompkins' note is confirmed BINDING, not nice-to-have, before threshold hand-off can ship. ADR-114 implementation budget refined: +30-50 LOC for Pan-Tompkins. Five-step quantum arc: - R20 vision (tick 37) - Doc 17 bridge (tick 38) - ADR-114 spec (tick 39) - R20.1 working demo (tick 40) - R20.2 threshold refinement (this tick) Production ADR-114 cog now has all known refinements catalogued BEFORE any Rust code is written. Honest mixed result — catalogue-then-revisit pattern works: R20.1 flagged production gap; R20.2 attempted fix; fix surfaced deeper gap (harmonic rejection). Three layers of refinement. |
||
|
|
eb88035699 |
docs(examples/research-sota): add main + 9 sub-folder READMEs (follow-up to #744) (#745)
PR #744 moved the files into 9 thematic folders via git mv but missed the READMEs due to a working-directory issue with git add. This PR adds the actual READMEs: - examples/research-sota/README.md (main overview) - examples/research-sota/01-physics-floor/README.md - examples/research-sota/02-placement/README.md - examples/research-sota/03-spatial-intelligence/README.md - examples/research-sota/04-rssi/README.md - examples/research-sota/05-cross-room-reid/README.md - examples/research-sota/06-structure-detection/README.md - examples/research-sota/07-negative-results/README.md - examples/research-sota/08-verticals/README.md - examples/research-sota/09-quantum-fusion/README.md Each sub-README documents: - Scripts + headlines table - Why this folder bounds/composes with others - Sample output / honest scope - Cross-references to related loop notes + ADRs Main README covers: - Folder map with thread numbers - Cross-folder dependency graph - 8-entry headline findings table - Reading order for newcomers (4 scripts in suggested order) - Honest scope (synthetic-physics caveats) |
||
|
|
4e879bf62a |
chore: organise examples/research-sota/ into 9 thematic folders with READMEs (#744)
User request: organise examples/research-sota/ into folders with READMEs and main overview. Moved 46 files into 9 thematic folders by thread family + research category: 01-physics-floor/ (R1, R6, R6.1) — bedrock primitives 02-placement/ (R6.2 family, 7 sub-ticks) — antenna placement 03-spatial-intelligence/ (R5, R7) — saliency + mincut 04-rssi/ (R8, R9) — RSSI-only sensing 05-cross-room-reid/ (R3 arc, 3 ticks) — cross-room identity 06-structure-detection/ (R12 arc, 3 ticks) — PABS + closed loop 07-negative-results/ (R13) — productive failure 08-verticals/ (R10, R11) — wildlife + maritime physics 09-quantum-fusion/ (R20.1) — ADR-114 quantum-classical demo Each folder has its own README.md documenting: - Scripts + headlines table - Why this folder bounds / composes with others - Sample output / honest scope - Cross-references to related loop notes + ADRs Main README.md at the top covers: - Folder map with thread numbers - Cross-folder dependency graph - Headline findings table (8 entries) - Reading order for newcomers (4 scripts in suggested order) - Honest scope (synthetic-physics caveats) All git mv operations preserve file history. Total: 46 files moved, 10 new READMEs (main + 9 sub) totalling ~1300 lines of organising documentation. |
||
|
|
759b487a82 |
research(R20.1): working Bayesian fusion demo for ADR-114 — empirically validates R13 NEG + doc 16 cube-law (#743)
Runnable numpy demo of ADR-114's three-input Bayesian fusion architecture. ~140 LOC pure NumPy. Validates the architecture before Rust implementation. Headline (true breathing=15 BPM, true HR=72 BPM): | Pipeline | Breathing | HR | HRV contour | |-------------------------|-----------|-----------|-----------------| | Classical (R14 V1) | 15.00 BPM | 105 BPM | not available | | | conf 69% | conf 38% | (R13 confirms) | | NV @ 1 m (6.25 pT) | n/a | 72.00 BPM | SDNN 119 ms | | NV @ 2 m (0.78 pT) | n/a | 96 marginal | degrading | | NV @ 3 m (0.23 pT) | n/a | 166 lost | NO | | FUSED (ADR-114) | 15.00 BPM | 84 BPM | SDNN 119 ms | Five confirmations: 1. Classical breathing rate is reliable (R14 V1 holds) 2. Classical HR is unreliable (R13 NEGATIVE EMPIRICALLY CONFIRMED: 38% confidence, 105 BPM estimate when truth was 72) 3. NV cardiac at 1 m works (R13 recovery validated) 4. CUBE-OF-DISTANCE FALLOFF IS REAL (doc 16 validated: 27x signal drop from 1 m to 3 m, matches 1/r^3 prediction) 5. Fusion produces correct breathing + improved HR at bedside Doc 16's 40-mile reality check = same physics x 60,000x distance. Press-release physics confirmed unphysical via working code. Caveat documented: demo's naive precision-weighted Bayesian gave 84 BPM (between classical 105 wrong and NV 72 right). Production fix catalogued — threshold-based hand-off when NV conf > 60% AND B-field > 3 pT, trust NV entirely. Engineering risk for ADR-114 Rust port (200 LOC, 3 weeks) lowered substantially: this 140 LOC numpy demo runs in <100 ms. Four-tick arc: - 11:15 UTC: R20 vision - 11:25 UTC: Doc 17 bridge - 11:35 UTC: ADR-114 spec - 11:40 UTC: R20.1 WORKING CODE Vision -> integration -> spec -> working code in 25 minutes. Honest scope: - Synthetic signals throughout - Cube-of-distance assumes clean dipole field - 5 deg phase noise assumes phase_align.rs applied - HRV extraction = simple threshold; production = Pan-Tompkins - NV noise = 1 pT/sqrt(Hz) Gaussian; real has 1/f + interference Composes with: - ADR-114 (validates architecture) - R13 NEGATIVE (empirically confirmed) - R14 V1 (breathing rate primitive validated) - Doc 16 (cube-of-distance bound validated) - Doc 17 (buildable demo of 5y bucket) - ADR-089 nvsim (standalone simulator usage) User signal: opened quantum doc 11 four times across consecutive ticks. Continuing the quantum-fusion direction with concrete code. Coordination: ticks/tick-40.md, no PROGRESS.md edit. Full quantum-classical fusion arc is now SHIPPABLE: - Vision (R20) - Integration (doc 17) - Spec (ADR-114) - Working demo (R20.1) |
||
|
|
f21d833c23 |
adr-114: cog-quantum-vitals — first quantum-augmented cog spec, recovers R13 NEGATIVE (#742)
Drafted in response to user's escalating signal (opened quantum-sensing doc 11 three times across consecutive ticks). Beyond R20 vision (tick 37) and doc 17 bridge (tick 38), this tick delivers a BUILDABLE ARTIFACT. First quantum-augmented cog spec. Bedside-only (1-2 m, inherits doc 16 sober posture). Composes nvsim (ADR-089) + R14 V1 + R12.1 pose-PABS + R3 AETHER + Bayesian fusion. Architecture: - ESP32 CSI -> R14 V1 breathing rate (classical primary) - nvsim NV -> R6.1 multi-source forward (cardiac magnetic, NV primary) - R12.1 pose-PABS hook for residual check - R3 + AETHER per-patient identity - Bayesian fusion: classical drives when confidence high; NV drives HRV contour (which R13 NEGATIVE ruled out classically) Outputs (with confidence scores per output): - Breathing rate +-0.1 BPM - Heart rate +-0.5 BPM - HRV CONTOUR (NV only - this is what R13 ruled out classically) - Per-patient identity (R3+AETHER, per-installation only) Cost analysis (bedside): - 4x ESP32-S3: 0 - 1x NV-diamond: 00-2000 today / ~00 by 2028 - Mount + cal: 0 - TOTAL: 10-2110 vs clinical monitor: 000-10000 Implementation: ~200 LOC, ~3 weeks - Crate scaffold: 30 - nvsim adapter: 40 - Bayesian fusion: 80 - R12.1 hook: 30 - Manifest schema: 20 Privacy chain unchanged: ADR-106 Layer 1 adds NV B(t) + HRV contour to on-device-only primitive list. ADR-100/109 dual signing for manifest. R14 V3 (attention-respecting) becomes shippable — was bound by R13's contour requirement; ADR-114 provides the contour. ADR chain after this tick (10 ADRs in loop's accumulated chain): - Existing: ADR-100, 103, 104 - Loop: ADR-105, 106, 107, 108, 109, 113, 114 - Critical dependency: ADR-089 (nvsim) Future ADRs catalogued: - ADR-115: cog-rydberg-anchor (7-10y) - ADR-116: real NV hardware bring-up - ADR-117: cog-quantum-vitals FDA/CE pathway - ADR-118: cog-mm-position (atomic-clock multistatic) The three-tick arc (R20 -> doc 17 -> ADR-114): - R20: vision (quantum recovers classical limits) - Doc 17: integration (bridges series 11-16 with loop) - ADR-114: shippable (concrete cog spec, 10-2110/bedside) Vision -> integration -> buildable in 35 minutes. Honest scope: - nvsim is deterministic SIMULATOR; cog ships with synthetic benefit until 2028-2030 real hardware - Cube-of-distance bounds <=2 m bedside (doc 16 posture) - Patient-side variability requires per-patient calibration - No bench validation on hybrid pipeline yet Composes with every loop thread (R3, R6.1, R12, R12.1, R13 NEG recovered, R14 V1/V2/V3, R15, R16-R20) + all ADRs (089, 100, 103-109, 113). Coordination: ticks/tick-39.md, no PROGRESS.md edit. |