use std::collections::VecDeque;
use wifi_densepose_signal::ruvsense::field_model::{
CalibrationStatus, FieldModel, FieldModelConfig,
};
use super::score_to_person_count;
const OCCUPANCY_WINDOW: usize = 50;
const ENERGY_THRESH_2: f64 = 12.0;
const ENERGY_THRESH_3: f64 = 25.0;
const MAX_SINGLE_LINK_OCCUPANCY: usize = 3;
pub fn single_link_config() -> FieldModelConfig {
FieldModelConfig {
n_links: 1,
..FieldModelConfig::default()
}
}
pub fn occupancy_or_fallback(
field: &FieldModel,
frame_history: &VecDeque<Vec<f64>>,
smoothed_score: f64,
prev_count: usize,
) -> usize {
match field.status() {
CalibrationStatus::Fresh | CalibrationStatus::Stale => {
let frames: Vec<Vec<f64>> = frame_history
.iter()
.rev()
.take(OCCUPANCY_WINDOW)
.cloned()
.collect();
if frames.is_empty() {
return score_to_person_count(smoothed_score, prev_count);
}
if let Ok(count) = field.estimate_occupancy(&frames) {
return count.min(MAX_SINGLE_LINK_OCCUPANCY);
}
let observation = vec![frames[0].clone()];
match field.extract_perturbation(&observation) {
Ok(perturbation) => {
if perturbation.total_energy > ENERGY_THRESH_3 {
3
} else if perturbation.total_energy > ENERGY_THRESH_2 {
2
} else if perturbation.total_energy > 1.0 {
1
} else {
0
}
}
Err(_) => score_to_person_count(smoothed_score, prev_count),
}
}
_ => score_to_person_count(smoothed_score, prev_count),
}
}
pub fn maybe_feed_calibration(field: &mut FieldModel, frame_history: &VecDeque<Vec<f64>>) {
if field.status() != CalibrationStatus::Collecting {
return;
}
if let Some(latest) = frame_history.back() {
let observations = vec![latest.clone()];
if let Err(e) = field.feed_calibration(&observations) {
tracing::debug!("FieldModel calibration feed: {e}");
}
}
}
pub fn parse_node_positions(input: &str) -> Vec<[f32; 3]> {
if input.is_empty() {
return Vec::new();
}
input
.split(';')
.enumerate()
.filter_map(|(idx, triplet)| {
let parts: Vec<&str> = triplet.split(',').collect();
if parts.len() != 3 {
tracing::warn!(
"Skipping malformed node position entry {idx}: '{triplet}' (expected x,y,z)"
);
return None;
}
match (
parts[0].parse::<f32>(),
parts[1].parse::<f32>(),
parts[2].parse::<f32>(),
) {
(Ok(x), Ok(y), Ok(z)) => Some([x, y, z]),
_ => {
tracing::warn!("Skipping unparseable node position entry {idx}: '{triplet}'");
None
}
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_node_positions() {
let positions = parse_node_positions("0,0,1.5;3,0,1.5;1.5,3,1.5");
assert_eq!(positions.len(), 3);
assert_eq!(positions[0], [0.0, 0.0, 1.5]);
assert_eq!(positions[1], [3.0, 0.0, 1.5]);
assert_eq!(positions[2], [1.5, 3.0, 1.5]);
}
#[test]
fn test_parse_node_positions_empty() {
let positions = parse_node_positions("");
assert!(positions.is_empty());
}
#[test]
fn test_parse_node_positions_invalid() {
let positions = parse_node_positions("abc;1,2,3");
assert_eq!(positions.len(), 1);
assert_eq!(positions[0], [1.0, 2.0, 3.0]);
}
#[test]
fn test_parse_node_positions_partial_triplet() {
let positions = parse_node_positions("1,2;3,4,5");
assert_eq!(positions.len(), 1);
assert_eq!(positions[0], [3.0, 4.0, 5.0]);
}
}