diff --git a/README.md b/README.md index d3633eec..f6985200 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ node scripts/mincut-person-counter.js --port 5006 # Correct person counting > |--------|----------|------|----------|-------------| > | **ESP32 + Cognitum Seed** (recommended) | ESP32-S3 + [Cognitum Seed](https://cognitum.one) | ~$140 | Yes | Presence, motion, breathing, heart rate, fall detection, multi-person counting, 17-keypoint pose (signed Cog binary), 105-cog catalog, persistent vector store, kNN search, witness chain, MCP proxy | > | **ESP32 Mesh** | 3-6× ESP32-S3 + WiFi router | ~$54 | Yes | Same capabilities as above without the persistent-memory features | -> | **ESP32-C6 research node** ([ADR-110](docs/adr/ADR-110-esp32-c6-firmware-extension.md), [witness](docs/WITNESS-LOG-110.md), [reviewer guide](docs/ADR-110-REVIEW-GUIDE.md), [firmware v0.6.7](https://github.com/ruvnet/RuView/releases/tag/v0.6.7-esp32)) | ESP32-C6-DevKit ($6–10) | ~$10 | Yes (Wi-Fi 6 capable) | Same CSI pipeline as S3 with the dual-target firmware. **Wire format ready** for HE-LTF PPDU tagging in ADR-018 bytes 18-19 (firmware encoder + Rust + Python decoders verified end-to-end in 17 unit tests), ESP-NOW cross-node sync (4102 tx 0 fail cumulative across 120 s + 300 s soaks), and TWT graceful-NACK fallback (live exercised). **v0.6.7 adds** a real LP-core motion-gate RISC-V program (B4 code path) and a Wi-Fi 6 soft-AP with TWT Responder for two-board iTWT benches (B1/B2 unblock, no 11ax router required). **Hardware-gated for measurement**: HE-LTF live subcarrier capture needs the soft-AP bench or an 11ax AP; ~5 µA LP-core hibernation needs an INA meter to capture; 802.15.4 raw RX is broken in IDF v5.4 (workaround: ESP-NOW transport, shipped). See witness log for the empirical / claimed split. | +> | **ESP32-C6 research node** ([ADR-110](docs/adr/ADR-110-esp32-c6-firmware-extension.md), [witness](docs/WITNESS-LOG-110.md), [reviewer guide](docs/ADR-110-REVIEW-GUIDE.md), [firmware v0.7.0](https://github.com/ruvnet/RuView/releases/tag/v0.7.0-esp32)) | ESP32-C6-DevKit ($6–10) | ~$10 | Yes (Wi-Fi 6 capable) | Same CSI pipeline as S3 with the dual-target firmware. **Firmware-side ADR-110 substrate now closed** (v0.7.0): ESP-NOW cross-board mesh quantified at **99.56 % match / 104 µs smoothed offset stdev / 3.95× EMA suppression** over a 5-min two-board soak (witness §A0.10), 32-byte UDP sync packet with operator-tunable cadence (§A0.12), ADR-018 byte 19 bit 4 wire-fix sourced from the working ESP-NOW path (§A0.13). Wire format ready for HE-LTF PPDU tagging in ADR-018 bytes 18-19 (firmware encoder + Rust + Python decoders verified end-to-end across 23 unit tests). LP-core motion-gate RISC-V program and Wi-Fi 6 soft-AP with TWT Responder both ship as opt-in code paths (default off). **Hardware-gated for measurement**: HE-LTF live subcarrier capture needs an 11ax AP (IDF v5.4 doesn't expose AP-side HE config — §A0.6); ~5 µA LP-core hibernation needs an INA meter to capture; 802.15.4 raw RX is broken in IDF v5.4 (workaround: ESP-NOW transport, shipped + measured). See witness log for the empirical / claimed split. | > | **Research NIC** | Intel 5300 / Atheros AR9580 | ~$50-100 | Yes | Full CSI with 3x3 MIMO | > | **Any WiFi** | Windows, macOS, or Linux laptop | $0 | No | RSSI-only: coarse presence and motion (see [tutorial #36](https://github.com/ruvnet/RuView/issues/36)) | > diff --git a/archive/v1/tests/unit/test_esp32_binary_parser.py b/archive/v1/tests/unit/test_esp32_binary_parser.py index 64681a3a..f5a34d63 100644 --- a/archive/v1/tests/unit/test_esp32_binary_parser.py +++ b/archive/v1/tests/unit/test_esp32_binary_parser.py @@ -19,6 +19,8 @@ from hardware.csi_extractor import ( CSIExtractor, CSIParseError, CSIExtractionError, + SyncPacket, + SyncPacketParser, ) # ADR-018 constants @@ -257,3 +259,108 @@ class TestESP32BinaryParser: await extractor.disconnect() asyncio.run(run_test()) + + +# ============================================================================ +# ADR-110 §A0.12 — SyncPacket / SyncPacketParser tests (firmware v0.6.9+) +# ============================================================================ + +SYNC_MAGIC = 0xC511A110 +SYNC_SIZE = 32 +SYNC_FMT = ' bytes: + flags = 0 + if is_leader: flags |= 0x01 + if is_valid: flags |= 0x02 + if smoothed_used: flags |= 0x04 + return struct.pack( + SYNC_FMT, + SYNC_MAGIC, + node_id, proto_ver, flags, 0, + local_us, epoch_us, sequence, + ) + + +class TestSyncPacketParser: + """ADR-110 §A0.12: 32-byte UDP sync packet (magic 0xC511A110).""" + + def test_follower_typical_packet_roundtrips(self): + """Match the COM9-witnessed sync-pkt #1 byte-for-byte.""" + raw = build_sync_packet( + node_id=9, is_leader=False, is_valid=True, smoothed_used=True, + local_us=28798450, epoch_us=27634885, sequence=20, + ) + assert len(raw) == SYNC_SIZE + pkt = SyncPacketParser.parse(raw) + assert isinstance(pkt, SyncPacket) + assert pkt.node_id == 9 + assert pkt.proto_ver == 1 + assert pkt.is_leader is False + assert pkt.is_valid is True + assert pkt.smoothed_used is True + assert pkt.local_us == 28798450 + assert pkt.epoch_us == 27634885 + assert pkt.sequence == 20 + # The 1.16-second boot delta from §A0.10 should be recoverable + assert pkt.local_us - pkt.epoch_us == 1163565 + + def test_leader_packet_has_local_close_to_epoch(self): + """COM12 (leader) had flags=0x03 and epoch ≈ local.""" + raw = build_sync_packet( + node_id=12, is_leader=True, is_valid=True, smoothed_used=False, + local_us=28864932, epoch_us=28864939, sequence=20, + ) + pkt = SyncPacketParser.parse(raw) + assert pkt.node_id == 12 + assert pkt.is_leader is True + assert pkt.is_valid is True + assert pkt.smoothed_used is False + assert pkt.flags_raw == 0x03 + assert pkt.local_us - pkt.epoch_us == -7 # leader has zero offset + + def test_magic_mismatch_raises(self): + """A non-sync datagram must not silently decode.""" + raw = bytearray(build_sync_packet()) + raw[0] = 0x01 # corrupt magic low byte + with pytest.raises(CSIParseError, match="magic mismatch"): + SyncPacketParser.parse(bytes(raw)) + + def test_short_packet_raises(self): + """Below 32 bytes must error early, not silently truncate.""" + raw = build_sync_packet()[:16] + with pytest.raises(CSIParseError, match="too short"): + SyncPacketParser.parse(raw) + + def test_all_flag_combinations(self): + """Each flag bit decodes independently.""" + for is_leader in (False, True): + for is_valid in (False, True): + for smoothed_used in (False, True): + raw = build_sync_packet( + is_leader=is_leader, + is_valid=is_valid, + smoothed_used=smoothed_used, + ) + pkt = SyncPacketParser.parse(raw) + assert pkt.is_leader == is_leader + assert pkt.is_valid == is_valid + assert pkt.smoothed_used == smoothed_used + + def test_dispatch_distinguishes_csi_from_sync(self): + """A host can pick CSI vs sync by leading magic.""" + csi_magic = struct.unpack_from('