wifi-densepose/docs/adr/ADR-113-baseline-profiles.md

157 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:0020: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.