diff --git a/README.md b/README.md index 3566ca2d..c805f32e 100644 --- a/README.md +++ b/README.md @@ -632,6 +632,8 @@ Verify the plugin structure: `bash plugins/ruview/scripts/smoke.sh`. Full detail |----------|-------------| | [User Guide](docs/user-guide.md) | Step-by-step guide: installation, first run, API usage, hardware setup, training | | [Build Guide](docs/build-guide.md) | Building from source (Rust and Python) | +| [Calibration & Room Training Guide](docs/calibration-guide.md) | What `calibrate`/`enroll`/`train-room` actually enforce: minimum frame counts, per-anchor quality gates, the pet/small-motion presence-detection caveat, and empty-room baseline conditions — grounded in the real code, not just ADR-135/151 | +| [Trust State & Engine Errors](docs/trust-and-engine-errors.md) | What `engine_error_count` and `demoted` mean on `/api/v1/status`, exact trigger conditions, the current diagnostic gap (no per-cause breakdown), the `WDP_GUARD_INTERVAL_US` recovery path, and why a converted Hugging Face model isn't shown to be the cause in code | | [**Home Assistant + Matter Integration**](docs/integrations/home-assistant.md) | **Works with Home Assistant** via MQTT auto-discovery + **Works with Matter** (Apple Home / Google Home / Alexa / SmartThings) — full entity catalog, 3 starter blueprints, Lovelace dashboards, privacy mode, threshold tuning ([ADR-115](docs/adr/ADR-115-home-assistant-integration.md)). | | [**BFLD — Beamforming Feedback Layer for Detection**](v2/crates/wifi-densepose-bfld/README.md) | New privacy-gated WiFi sensing layer that measures + structurally prevents identity leakage from 802.11ac/ax Beamforming Feedback Information. Three type-enforced invariants (raw BFI never exits node, identity embedding is in-RAM-only, cross-site correlation cryptographically impossible via per-site BLAKE3 keyed hash + daily rotation). Ships full operator surface (`BfldPipeline`, `BfldPipelineHandle`, the Soul Signature §3.6 per-channel matcher `EnrolledMatcher`/`SoulMatchOracle` — experimental; named identity is data-gated, **measured** as not-separable on WiFi-only channels alone), MQTT topic router + HA-DISCO + availability + LWT, 3 operator HA blueprints, two runnable examples, eclipse-mosquitto:2 CI service container. 327+ tests. [ADR-118](docs/adr/ADR-118-bfld-beamforming-feedback-layer-for-detection.md) umbrella + sub-ADRs [119](docs/adr/ADR-119-bfld-frame-format-and-wire-protocol.md)/[120](docs/adr/ADR-120-bfld-privacy-class-and-hash-rotation.md)/[121](docs/adr/ADR-121-bfld-identity-risk-scoring.md)/[122](docs/adr/ADR-122-bfld-ruview-ha-matter-exposure.md)/[123](docs/adr/ADR-123-bfld-capture-path-nexmon-and-esp32.md). Research dossier: [`docs/research/BFLD/`](docs/research/BFLD/) (11 files, 13,544 words). | | [**SENSE-BRIDGE — rvagent MCP server**](tools/ruview-mcp/README.md) | Dual-transport MCP server (`@ruvnet/rvagent`) bridging the RuView sensing stack to AI agents (Claude Code, Cursor, ruflo swarms). 6 tools wired: `ruview.presence.now`, `ruview.vitals.get_{breathing,heart_rate,all}`, `ruview.bfld.last_scan`, `ruview.bfld.subscribe`. stdio + Streamable HTTP (`POST /mcp`, Origin-validated, bearer-token auth, `127.0.0.1` bind). Full 20-tool Zod schema barrel + 5 RUVIEW-POLICY governance tools. 93 tests. [ADR-124](docs/adr/ADR-124-rvagent-mcp-ruvector-npm-integration.md). Try: `npx @ruvnet/rvagent stdio`. | diff --git a/docs/calibration-guide.md b/docs/calibration-guide.md new file mode 100644 index 00000000..dd174cc8 --- /dev/null +++ b/docs/calibration-guide.md @@ -0,0 +1,300 @@ +# Calibration & Room Training Guide + +This guide explains what actually happens — and what is actually *enforced* — +when you run `wifi-densepose calibrate`, `enroll`, and `train-room`. It is +written for the person setting up a room, not for developers. + +Everything below was checked against the real Rust implementation in +`v2/crates/wifi-densepose-calibration/`, `v2/crates/wifi-densepose-signal/src/ruvsense/calibration.rs`, +and `v2/crates/wifi-densepose-cli/`, not just the design ADRs. Where the design +documents (ADR-135, ADR-151) describe something that isn't actually built yet, +this guide says so explicitly. + +## The three-step pipeline + +``` +wifi-densepose calibrate --port # Stage 1: empty-room baseline (no people) +wifi-densepose enroll --room # Stage 2+3: 8 guided anchors (~4 minutes) +wifi-densepose train-room --room # Stage 4: fit the specialist bank +wifi-densepose room-status --room # check what trained / what's stale +wifi-densepose room-watch --room # live inference +``` + +`calibrate` must run first — `enroll` refuses to start without a baseline file +(`--baseline ./baseline.bin` by default), and `train-room` refuses to start +without an enrollment file. Each step writes a file the next step reads; there +is no way to skip a step. + +--- + +## 1. Is there a minimum amount of data required? + +**Yes, and for the empty-room baseline it is a hard, enforced minimum — not a +recommendation.** + +`wifi-densepose calibrate` will not produce a baseline file with fewer than +**600 recorded frames** (the default for every PHY tier: HT20, HT40, HE20, +HE40). This is `DEFAULT_MIN_FRAMES = 600` in +`v2/crates/wifi-densepose-signal/src/ruvsense/calibration.rs:48`, and it is +checked in `CalibrationRecorder::finalize()` +(`ruvsense/calibration.rs:532-538`): if fewer than `config.min_frames` frames +were recorded, `finalize()` returns +`CalibrationError::InsufficientFrames { got, need }` and calibration fails +outright — there is no partial/degraded baseline. This is pinned by a unit +test (`finalize_requires_min_frames`, same file) so it isn't accidental +behavior. + +**Important subtlety:** the CLI's `--duration-s` flag (default 30 seconds) +and the 600-frame minimum are checked *independently*. The capture loop in +`v2/crates/wifi-densepose-cli/src/calibrate.rs:135-183` stops as soon as +**either** the duration timer expires **or** 600 frames have been recorded, +whichever comes first. If your node streams CSI slower than the assumed 20 Hz +(e.g. congested WiFi, a busier ESP32), 30 seconds may not be enough to reach +600 frames, and `calibrate` will fail with an explicit +`"insufficient frames: have X, need 600"` error rather than silently +producing a short baseline. If you hit this, raise `--duration-s` rather than +overriding `--min-frames`. + +You *can* override the 600-frame floor with `--min-frames ` (0 = use the +tier default). The code prints an explicit warning when you do: + +> `[calibrate] WARN: --min-frames=N overrides ADR-135 tier default (600 for +> ht20). This relaxes the phase-concentration guarantee; do not use in +> production.` + +(`v2/crates/wifi-densepose-cli/src/calibrate.rs:112-119`). Treat this as a +debugging escape hatch, not a supported way to shorten setup. + +The CLI also independently rejects `--duration-s` below 10 seconds +(`"Fewer frames produce unreliable phase-concentration estimates"`, +`calibrate.rs:341-348`) and prints (but does not block on) a warning above 300 +seconds. + +**Guided enrollment (`enroll`) has a much lower, per-anchor floor.** Each of +the 8 guided anchors (`empty`, `stand_still`, `sit`, `lie_down`, +`breathe_slow`, `breathe_normal`, `small_move`, `sleep_posture`) is captured +for a fixed duration baked into the code — 20 seconds for the static/motion +anchors, 30 seconds for the two breathing anchors and `sleep_posture` +(`AnchorLabel::duration_s()`, `v2/crates/wifi-densepose-calibration/src/anchor.rs:98-104`). +This is **not** a CLI flag — you cannot currently shorten or lengthen an +individual anchor capture from the command line. + +Underneath that fixed duration, the anchor is only *accepted* if it clears a +quality gate (`AnchorQualityGate`, `v2/crates/wifi-densepose-calibration/src/enrollment.rs:43-53`): + +| Threshold | Default | What it checks | +|---|---|---| +| `min_frames` | **60 frames** | Anchor is rejected if fewer than 60 frames were captured — mainly catches "the ESP32 stopped streaming" mid-capture, not a real duration requirement (60 frames is a fraction of a second of streaming at typical rates) | +| `min_presence_z` | 1.5 | For anchors that expect a person, the mean amplitude z-score must exceed this or the anchor is rejected as "no person detected" | +| `empty_max_z` | 1.0 | For the `empty` anchor, the z-score must stay under this or it's rejected as "room not empty" | +| `max_still_motion` | 0.6 (60%) | For still anchors, motion-flagged frame fraction above this is rejected as "too much motion" | +| `min_move_motion` | 0.3 (30%) | For `small_move`, motion-flagged fraction below this is rejected as "not enough motion" | + +A rejected anchor is re-prompted, up to `--attempts` times (default **2**). +If an anchor is still rejected after all attempts, `enroll` moves on without +it and logs `"moving on without '