# ADR-107 — REST Baseline Calibration + Auto-Recalibrate **Status**: Accepted **Date**: 2026-05-17 **Scope**: `v2/crates/wifi-densepose-sensing-server/src/main.rs` (`baseline_get`, `baseline_calibrate`, `auto_recalibrate_task`, `capture_baseline_to_disk`, `BASELINE_BUS`), `static/raw.html` (`calibrate empty` button), CLI flags `--auto-recalibrate-quiet-sec` / `--auto-recalibrate-min-age-sec`. ## Context ADR-103 introduced a persistent empty-room baseline at `data/baseline.json` so the classifier no longer needed a 60 s warm-up after every server restart. To refresh it the operator had to: 1. Step out of the room. 2. SSH / open a terminal, run `python scripts/record-baseline.py --duration 90`. 3. Wait for the "saved" message. 4. Restart the sensing-server (so it reloads the file). 5. Walk back in. Steps 2, 4 are friction. The operator asked to remove them so a fresh device that just wants to monitor a room doesn't need a CLI or a restart. Two changes: * **`POST /api/v1/baseline/calibrate`** — fires the same record-and- trim pipeline from inside the server, hot-reloads the override map on success. UI button in `raw.html` triggers it. * **Auto-recalibrate background task** — silently refreshes the baseline when the classifier reports `absent` and CV stays low for a long-enough window, without any operator action. ## Decisions ### D1 — `capture_baseline_to_disk` in-process Pure-Rust port of `scripts/record-baseline.py`: 1. Subscribe to `BASELINE_BUS` (a `tokio::sync::broadcast::Sender` that mirrors every WS JSON message published by the broadcaster). 2. Collect `duration_sec` of per-node `(t, amplitudes, rssi)`. 3. Trim `trim_sec` from head and tail. 4. Slide `clean_window_sec` window across, pick lowest-CV chunk per node. 5. Compute FULL-broadband mean/p50/p95/std/CV% (same schema as ADR-103 v2; reload uses the same `load_baseline_file`). 6. Write `data/baseline.json` (configurable via JSON body `out`). 7. Call `load_baseline_file(path)` to hot-reload `AMP_BASELINE_OVERRIDE` and `AMP_BASELINE_CV`. ### D2 — `BASELINE_BUS` broadcast forwarder Decouples baseline capture from individual WS clients. A small task spawned at startup subscribes to `AppState.tx` and re-publishes every message into `BASELINE_BUS`. Capture subscribers don't need a WS connection or any external network path. ### D3 — `POST /api/v1/baseline/calibrate` Optional JSON body: `{ duration_sec, trim_sec, clean_window_sec, out }`. Defaults: 90 / 15 / 30 s and `data/baseline.json`. Returns immediately with `{ "started": true, "hint": "..." }`. Subsequent calls while a job is running return `{ "started": false, "reason": "calibration already running" }`. ### D4 — `GET /api/v1/baseline` ```json { "nodes": { "1": {"full_broadband_p95": …, "full_broadband_cv_pct": …}, … }, "last_written_sec_ago": , "calibration_status": "idle" | "running" | "running (auto)" | "complete" | "complete (auto)" | "error: …" } ``` UI polls this every 2 s while a calibration is running to drive the button state machine. ### D5 — Auto-recalibrate background task Wakes every 5 s. State machine: * Read latest `classification.motion_level` and `confidence` (=CV). * `quiet = (motion_level == "absent") && (cv < 0.08)`. * If `quiet` is true continuously for `--auto-recalibrate-quiet-sec` (default 1800 = 30 min) **AND** the last baseline write is older than `--auto-recalibrate-min-age-sec` (default 3600 = 1 h), kick off `capture_baseline_to_disk(90, 5, 45, "data/baseline.json")` in the background. * On error, log + set `calibration_status` so the UI surfaces it. The 30-minute / 1-hour defaults are conservative: a person briefly walking through doesn't reset the baseline; long-term drift from WiFi reconfiguration or furniture rearrangement does. `--auto- recalibrate-quiet-sec 0` disables entirely. ### D6 — `raw.html` button `calibrate empty` next to the existing `reset` button. Click → `confirm()` reminds operator to step out → POSTs the endpoint → polls status every 2 s, updating the inline pill `recording… 12/90 s` → `baseline updated ✓` on success. Disables itself while running. ## Files Touched ``` v2/crates/wifi-densepose-sensing-server/src/main.rs - statics: BASELINE_LAST_WRITTEN, BASELINE_CALIBRATION_STATUS, BASELINE_BUS - fn capture_baseline_to_disk (D1) - fn auto_recalibrate_task (D5) - fn baseline_get (D4) - fn baseline_calibrate (D3) - routes /api/v1/baseline + /api/v1/baseline/calibrate - Args { auto_recalibrate_quiet_sec, auto_recalibrate_min_age_sec } - main(): bus init + auto-recalibrate spawn v2/crates/wifi-densepose-sensing-server/static/raw.html -