feat(calibration): select explicit baseline source

This commit is contained in:
ruv committed 2026-09-06 11:38:30 -04:00
1 parent c4021f9884
commit 30404dd9cf
2 files changed
+47 -3

No files matched your search

@@ -25,8 +25,10 @@ prior only after two server controlled stages:
1. Normal calibration reaches both 1,000 accepted frames and 600 seconds.
The frame target represents twenty complete fifty frame runtime windows.
Collection binds to the first accepted ESP32 source and rejects frames from
other radios for that single link model.
A caller may select one currently live ESP32 source. The server checks its
freshness before replacing an active bootstrap prior, binds collection to
that source, and rejects frames from other radios. When no source is
selected, the first accepted ESP32 source preserves legacy behavior.
2. Promotion observes 12 new samples spaced by at least one second. Each
sample has a four second bounded acquisition timeout. At least 10 must
resolve empty, all ticks must be fresh, and none may publish numeric vital
@@ -89,6 +91,9 @@ occupied sequences. Empty data is never relabeled as human training.
gate as `sensing_update`. When the bootstrap background matches, it emits
absent state, zero people, null numeric rates, and an explicit abstention
reason instead of forwarding raw edge candidates.
7. `POST /api/v1/calibration/start?source_node_id=N` selects a live source for
deterministic single link capture. A missing or stale selection fails
without disabling the active bootstrap prior.
## Measured installation evidence
@@ -111,3 +116,5 @@ and data directory. Status must report `binding_mode=bootstrap_only`, zero
collection frames, and false authority for calibrated evidence and numeric
vitals. Twelve fresh empty samples must contain no numeric vital signs.
Starting a new room calibration must replace the prior and begin at frame zero.
When several radios are live, selecting Node 5 must leave Nodes 3 and 7 out of
the source set for the entire capture.
@@ -6159,6 +6159,14 @@ mod bootstrap_vital_publication_tests {
assert!(!calibration_source_accepts(&sources, 3));
}
#[test]
fn explicit_single_link_source_rejects_other_live_nodes() {
let sources = std::collections::BTreeSet::from([5]);
assert!(calibration_source_accepts(&sources, 5));
assert!(!calibration_source_accepts(&sources, 3));
assert!(!calibration_source_accepts(&sources, 7));
}
fn strong_candidates() -> VitalSigns {
VitalSigns {
breathing_rate_bpm: Some(15.0),
@@ -6249,8 +6257,33 @@ mod bootstrap_vital_publication_tests {
// ── Field model calibration endpoints (eigenvalue person counting) ──────────
async fn calibration_start(State(state): State<SharedState>) -> Json<serde_json::Value> {
#[derive(Debug, Default, Deserialize)]
struct CalibrationStartQuery {
/// Optional explicit single-link source. When omitted, the first accepted
/// ESP32 packet preserves the legacy binding behavior.
source_node_id: Option<u8>,
}
async fn calibration_start(
State(state): State<SharedState>,
Query(query): Query<CalibrationStartQuery>,
) -> Json<serde_json::Value> {
let mut s = state.write().await;
if let Some(source_node_id) = query.source_node_id {
let now = std::time::Instant::now();
let source_is_live = s.node_states.get(&source_node_id).is_some_and(|node| {
node.last_frame_time
.is_some_and(|seen| now.saturating_duration_since(seen) < ESP32_OFFLINE_TIMEOUT)
});
if !source_is_live {
return Json(serde_json::json!({
"success": false,
"error_code": "calibration_source_unavailable",
"error": "The requested calibration source is missing or stale.",
"source_node_id": source_node_id,
}));
}
}
if s.bootstrap_baseline_active {
s.field_model = None;
s.bootstrap_baseline_active = false;
@@ -6279,10 +6312,14 @@ async fn calibration_start(State(state): State<SharedState>) -> Json<serde_json:
s.field_model = Some(fm);
s.calibration_model_id = Some(opaque_calibration_model_id());
s.calibration_source_node_ids.clear();
if let Some(source_node_id) = query.source_node_id {
s.calibration_source_node_ids.insert(source_node_id);
}
Json(serde_json::json!({
"success": true,
"message": "Calibration started — keep room empty while frames accumulate.",
"model_id": s.calibration_model_id,
"source_node_ids": s.calibration_source_node_ids,
}))
}
// ADR-080 #2: FieldModel init error chain stays server-side only.