# ADR-113 — Multiple Baseline Profiles (Day/Night) **Status**: Accepted **Date**: 2026-05-17 **Scope**: `v2/crates/wifi-densepose-sensing-server/src/main.rs` (`resolve_baseline_profile`, `baseline_profile_watch`, `--baseline-profile` CLI flag). Closes the "Multiple baseline profiles" item in CHECKLIST. ## Context The empty-room baseline that ADR-103 / ADR-104 store in `data/baseline.json` is captured at one point in time. The channel state it reflects is sensitive to: * People walking through corridors / adjacent apartments at night vs. day (different building-wide ambient WiFi traffic). * AC / refrigerator compressor duty cycles (broadband noise at the ~Hz scale that changes per-time-of-day). * Sunlight on building walls (~mm-scale thermal expansion changes multipath). In the current deployment we observe the `absent` baseline mean shift by ~3-5 % between 14:00 and 04:00 — small but enough to push the CV of a stationary subcarrier across the ADR-103 threshold and trigger false `present_still` flags overnight. A single baseline can't model both regimes simultaneously. The lowest- complexity fix is to keep two: a day baseline and a night baseline, loaded at startup and hot-swapped at the day/night boundary. ## Decisions ### D1 — `--baseline-profile` selector with four modes ``` --baseline-profile {single,auto,day,night} (default: single) ``` | Mode | Behaviour | |----------|--------------------------------------------------------------------------------------------| | `single` | Legacy. Load `RUVIEW_BASELINE_FILE` or `data/baseline.json`. No watch task. **Default.** | | `auto` | Pick day/night by local hour. Hot-reload at 07:00 / 21:00 transitions. | | `day` | Force `data/baseline.day.json`. No auto switching. | | `night` | Force `data/baseline.night.json`. No auto switching. | Default is `single` so existing deployments don't have to migrate. Operators opt in by recording two profiles + flipping the flag. ### D2 — Day window: 07:00–20:59 local Hard-coded for now. The split matches the ambient-WiFi pattern in this deployment (residential building, no commercial traffic). Tunable in code (future ADR can parameterise if a second deployment needs different hours), but a flag is premature parameter sprawl. `chrono::Local::now().hour()` drives the choice — no UTC offset arithmetic; the OS provides the local hour directly. ### D3 — Filename convention ``` data/baseline.day.json data/baseline.night.json data/baseline.json (legacy / single-profile fallback) ``` Same JSON schema as ADR-103 v2 (`full_broadband_*`, `per_subcarrier_mean`, optionally `per_subcarrier_phase_mean` per ADR-104). The recording script and REST endpoint can write to any of the three paths via `--out` / `out` body field — no schema change. ### D4 — Missing-file fallback to `data/baseline.json` If a requested profile file doesn't exist (e.g., operator set `--baseline-profile auto` but only recorded `baseline.json`), the server logs a warning and loads the legacy single-baseline file instead. This makes the migration path "set the flag, then start recording per-profile baselines one at a time" — no big-bang switch. ### D5 — Hot-reload via `baseline_profile_watch` Background task fires every 5 min, re-resolves the profile, and if the profile tag changed (day → night or vice versa) calls `load_baseline_file` on the new path. `load_baseline_file` already hot-swaps in place — the per-node override maps and per-subcarrier baselines update without touching live frame ingest. 5 min cadence means transitions land within 5 min of the schedule — acceptable lag for a baseline whose channel-side variance is on the ~hour timescale. A `static` `CURRENT_BASELINE_PROFILE` mutex tracks the loaded tag so the watch avoids redundant disk reads when nothing changed. ### D6 — Watch is a no-op outside `auto` `single`, `day`, and `night` modes don't need switching — those are "set once at startup". The watch task logs a one-line "disabled" message and returns immediately. Saves a tokio task slot and suppresses log noise on the common single-profile deployment. ## Trade-offs * **Operator has to record two baselines.** Twice the operator time (~5 min × 2). Unavoidable for the use case. * **Hard-coded 07:00 / 21:00 split.** A different deployment (office, shift-work) would want different hours. Defer to a future ADR; for this deployment the residential cadence works. * **No smooth interpolation between profiles.** At 20:59 we use day, at 21:00 we use night — a step transition. For amplitude/baseline comparison the step is fine (the classifier already smooths over multiple frames). A weighted blend across the transition window would be feasible but adds complexity for limited gain. * **No more than two profiles.** Seasonal (summer/winter), weekday/ weekend etc. would need either more flags or a config-file driven approach. Out of scope. ## Files Touched ``` v2/crates/wifi-densepose-sensing-server/src/main.rs - --baseline-profile CLI flag (D1) - resolve_baseline_profile (D1, D2, D3, D4) - baseline_profile_file_or_fallback (D4) - baseline_profile_watch background task (D5, D6) - CURRENT_BASELINE_PROFILE static + init helper (D5) - startup uses resolve_baseline_profile (D1) - spawn baseline_profile_watch alongside other watches (D5) docs/adr/ADR-113-baseline-profiles.md (this) ``` ## Verified Acceptance * `cargo build --release -p wifi-densepose-sensing-server` clean. * `cargo test --release -p wifi-densepose-sensing-server --no-default-features` — 326 tests pass. * `sensing-server --help` shows the new `--baseline-profile` flag with the four-mode help text. * Running with `--baseline-profile single` (default) keeps the existing log line `baseline-profile: starting in 'single' mode → data/baseline.json` and disables the watch task with `Baseline profile watch disabled (--baseline-profile single)`. * Running with `--baseline-profile auto` while no `baseline.day.json` exists logs `baseline-profile day: file data/baseline.day.json not found, falling back to data/baseline.json` then proceeds. ## References * ADR-103 — persistent baseline storage + JSON schema this ADR reuses. * ADR-104 — per-subcarrier amplitude + phase drift; both consume whatever baseline the active profile loads. * ADR-107 — `POST /api/v1/baseline/calibrate` can write into any of the three paths via the `out` body field, so operators can record each profile via the same UI button.