# 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 '