use chrono::Utc;
use crate::{
sensors::{accelerometer::AccelerometerSensor, bci::BciSensor},
types::{ActivityState, FusedReading},
};
pub struct SensorFusion {
bci: BciSensor,
accel: AccelerometerSensor,
}
impl SensorFusion {
#[must_use]
pub fn new() -> Self {
Self {
bci: BciSensor::new(),
accel: AccelerometerSensor::new(),
}
}
pub fn sample(&mut self, sequence_id: u64) -> FusedReading {
let bci = self.bci.sample();
let accel = self.accel.sample();
let total_power = bci.delta_hz + bci.theta_hz + bci.alpha_hz + bci.beta_hz + bci.gamma_hz;
let activity_boost = match accel.activity_state {
ActivityState::Stationary => 0.0,
ActivityState::Walking => 0.05,
ActivityState::Running => 0.12,
ActivityState::Gesture => 0.08,
};
let cognitive_load = ((bci.beta_hz / (bci.alpha_hz + bci.theta_hz + 1.0)) * 0.5
+ activity_boost)
.clamp(0.0, 1.0);
let emotional_valence =
((bci.alpha_hz - bci.beta_hz * 0.6) / (total_power + 1.0)).clamp(-1.0, 1.0);
let arousal_level = ((bci.beta_hz + bci.gamma_hz) / (total_power + 1.0)).clamp(0.0, 1.0);
FusedReading {
timestamp: Utc::now(),
sequence_id,
bci,
accelerometer: accel,
cognitive_load: round2(cognitive_load),
emotional_valence: round2(emotional_valence),
arousal_level: round2(arousal_level),
}
}
}
impl Default for SensorFusion {
fn default() -> Self {
Self::new()
}
}
fn round2(v: f64) -> f64 {
(v * 100.0).round() / 100.0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fused_features_within_expected_bounds() {
let mut fusion = SensorFusion::new();
for id in 1..=50_u64 {
let r = fusion.sample(id);
assert!(
(0.0..=1.0).contains(&r.cognitive_load),
"cognitive_load out of [0,1]: {}",
r.cognitive_load
);
assert!(
(-1.0..=1.0).contains(&r.emotional_valence),
"emotional_valence out of [-1,1]: {}",
r.emotional_valence
);
assert!(
(0.0..=1.0).contains(&r.arousal_level),
"arousal_level out of [0,1]: {}",
r.arousal_level
);
}
}
#[test]
fn sequence_id_preserved() {
let mut fusion = SensorFusion::new();
for id in [1_u64, 42, 1_000, u64::MAX / 2] {
let r = fusion.sample(id);
assert_eq!(r.sequence_id, id);
}
}
#[test]
fn default_constructs_without_panic() {
let mut fusion = SensorFusion::default();
let r = fusion.sample(1);
assert!(r.cognitive_load >= 0.0);
}
}