mirror of
https://github.com/ruvnet/RuView.git
synced 2026-10-22 06:32:35 -05:00
feat(calibration): select explicit baseline source
This commit is contained in:
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.
|
||||
|
||||
Reference in new issue
Block a user