Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **`scripts/fake_esp32_csi_sender.py` — host-side CSI UDP test sender.** Emits ADR-018 CSI frames (magic `0xC511_0001`, 20-byte header + I/Q pairs) to the sensing-server's UDP port, so the `--source esp32` ingestion path (node registration, `/api/v1/nodes`, the Sensing-tab `LIVE — ESP32 HARDWARE` state) can be exercised without physical hardware. Configurable node count, subcarriers, frame rate, and synthesized breathing/motion.
- **ADR-261: RuVector graph-ANN index — a real HNSW baseline + a SymphonyQG-style quantized variant, MEASURED (honest negative).** Closes the [ADR-156 §5 #1](docs/adr/ADR-156-ruvector-fusion-beyond-sota.md) gap: the SymphonyQG (SIGMOD 2025) **3.5–17× QPS-over-HNSW** claim was CLAIMED-only because **no HNSW baseline existed to compare against**. This adds one. New pure-Rust, `--no-default-features`-buildable modules in `wifi-densepose-ruvector`: `hnsw.rs` (a correct float HNSW — Malkov & Yashunin: multi-layer NSW graph, `ef_construction`/`ef_search`, Algorithm-4 neighbour selection, **seeded-deterministic** level assignment via SplitMix64, L2 + cosine, full degenerate-case guards), `hnsw_quantized.rs` (the SymphonyQG-style variant — the **same** graph traversed by a cheap **1-bit Hamming** score over the RaBitQ Pass-2 rotated sign code, then **exact-float rerank**), `ann_measure.rs` + `benches/ann_bench.rs` (one shared deterministic planted-cluster fixture; the `ann_bench_report` test is the source of truth). **MEASURED (dim=128, N=10k, K=10, `--release`):** float HNSW = **~25× QPS over linear scan at recall ≥0.99** (the baseline this gap needed; recall@10 correctness gate ≥0.95 holds, L2 + cosine). **Honest negative:** the 1-bit quantized traversal is **too coarse to beat float HNSW at equal recall at this scale** — its best recall is **0.738**, never reaching the ≥0.90 equal-recall point, so there is **no QPS win** over float HNSW; the 3.5–17× is **not reproduced** by our 1-bit construction here. The recall gate also **caught a real index-out-of-bounds bug** in the insert path (disclosed in ADR-261 §4). Caveat: this is **our** HNSW + **our** 1-bit quant, not SymphonyQG's exact system — it tests the *direction* of the claim, with the expected crossover at large N + a multi-bit traversal code. **We did not tune to manufacture a speedup.** +20 tests (ruvector lib 131→151, 0 failed). ADR-156 §5 #1 / §8 backlog: CLAIMED → **MEASURED-direction-tested**. Python deterministic proof unchanged (off the signal proof path).
- **ADR-260: RuField MFS — the open specification for camera-free multimodal field sensing.** A common event / tensor / calibration / privacy / provenance model that sits *above* WiFi CSI/CIR/BFLD, UWB, BLE Channel Sounding, mmWave radar, ultrasound, subsonic, infrared, and future quantum sensors (each modality emits a normalized `FieldEvent` → `FieldTensor` → `FusionGraph` → `PrivacyClass` → `ProvenanceReceipt`). Published as a **standalone repo** [`ruvnet/rufield`](https://github.com/ruvnet/rufield) and vendored here as the `vendor/rufield` submodule (the `vendor/rvcsi` pattern — not a `v2/` workspace member). The v0.1 reference stack is a self-contained 6-crate Rust workspace (`rufield-core`, `-provenance` [sha256 + ed25519], `-privacy` [P0–P5 guard], `-adapters` [deterministic `SyntheticSim` across wifi_csi/mmwave_radar/infrared_thermal], `-fusion` [graph + TOML weighted-Bayes rules → 7 room-state inferences], `-bench` [deterministic runner + the §31 acceptance test]). **60 tests / 0 failed, clippy-clean.** §27 acceptance criteria 1–8 and 10 PASS; the live dashboard (9) is deferred. **All benchmark metrics are SYNTHETIC** (scored against the simulator's own ground truth — presence/breathing/bed_exit/room_transition F1 = 1.000, nocturnal_scratch 0.923 reported honestly, p95 latency ~0.01 ms, provenance coverage 100%, 0 privacy violations) — they prove the pipeline recovers known truth, **not** field accuracy; real hardware adapters (ESP32 CSI, mmWave, thermal IR) are a documented roadmap item, none validated in v0.1. The Python deterministic proof is unchanged (rufield is off the signal-processing proof path).

Expand Down
94 changes: 94 additions & 0 deletions scripts/fake_esp32_csi_sender.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Fake ESP32 CSI UDP sender — emits ADR-018 binary frames to the sensing-server.

Wire format (matches parse_esp32_frame in wifi-densepose-sensing-server/src/main.rs
and firmware csi_collector.c):

[0..3] magic = 0xC511_0001 (u32 LE)
[4] node_id (u8)
[5] n_antennas (u8)
[6..7] n_subcarriers (u16 LE)
[8..11] freq_mhz (u32 LE)
[12..15] sequence (u32 LE)
[16] rssi (i8)
[17] noise_floor (i8)
[18] ppdu_type (u8, 0 = HT/legacy)
[19] reserved
[20..] I/Q data: n_antennas * n_subcarriers pairs of (i8 I, i8 Q)

Use this to exercise the --source esp32 ingestion path without real hardware.
"""
import argparse
import math
import socket
import struct
import time

MAGIC = 0xC511_0001


def build_frame(node_id, n_antennas, n_subcarriers, freq_mhz, sequence,
rssi, noise_floor, t, breath_hz, motion):
hdr = struct.pack(
"<IBBHIIbbBB",
MAGIC, node_id, n_antennas, n_subcarriers, freq_mhz, sequence,
rssi, noise_floor, 0, 0,
)
# Synthesize I/Q: a steady carrier + a slow breathing oscillation + motion noise.
body = bytearray()
breath = math.sin(2 * math.pi * breath_hz * t)
for a in range(n_antennas):
for k in range(n_subcarriers):
base = 18 + (k % 30)
amp = base + 8 * breath + motion * math.sin(k * 0.4 + t * 6.0)
phase = 0.3 * k + t * 1.5
i = int(max(-127, min(127, amp * math.cos(phase))))
q = int(max(-127, min(127, amp * math.sin(phase))))
body += struct.pack("bb", i, q)
return hdr + bytes(body)


def main():
p = argparse.ArgumentParser()
p.add_argument("--host", default="127.0.0.1")
p.add_argument("--port", type=int, default=5005)
p.add_argument("--nodes", type=int, default=2, help="number of nodes (node_id 1..N)")
p.add_argument("--antennas", type=int, default=1)
p.add_argument("--subcarriers", type=int, default=64)
p.add_argument("--freq-mhz", type=int, default=2462)
p.add_argument("--rate", type=float, default=20.0, help="frames/sec per node")
p.add_argument("--rssi", type=int, default=-47)
p.add_argument("--noise", type=int, default=-90)
p.add_argument("--breath-hz", type=float, default=0.25, help="~15 BPM")
p.add_argument("--motion", type=float, default=4.0, help="0 = still, higher = more motion")
p.add_argument("--duration", type=float, default=0.0, help="seconds; 0 = forever")
args = p.parse_args()

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
period = 1.0 / args.rate
seq = 0
start = time.time()
print(f"Sending CSI frames -> {args.host}:{args.port} | "
f"{args.nodes} node(s), {args.subcarriers} subcarriers, {args.rate} Hz")
try:
while True:
t = time.time() - start
for node_id in range(1, args.nodes + 1):
frame = build_frame(
node_id, args.antennas, args.subcarriers, args.freq_mhz,
seq, args.rssi, args.noise, t, args.breath_hz, args.motion,
)
sock.sendto(frame, (args.host, args.port))
seq += 1
if seq % int(args.rate) == 0:
print(f" t={t:5.1f}s sent seq={seq} ({len(frame)} B/frame)")
if args.duration and t >= args.duration:
break
time.sleep(period)
except KeyboardInterrupt:
pass
print(f"Done. Sent {seq} sequences to {args.nodes} node(s).")


if __name__ == "__main__":
main()