use serde::Deserialize;
fn f64_to_f32(value: f64) -> Option<f32> {
if !value.is_finite() {
return None;
}
value.to_string().parse::<f32>().ok()
}
fn seconds_to_micros_i64(timestamp_secs: f64) -> Option<i64> {
if !timestamp_secs.is_finite() {
return None;
}
let micros = timestamp_secs * 1_000_000.0;
if !micros.is_finite() {
return None;
}
format!("{micros:.0}").parse::<i64>().ok()
}
#[derive(Debug, Deserialize)]
pub struct EegEvent {
pub sid: String,
pub time: f64,
pub eeg: Vec<serde_json::Value>,
}
#[derive(Debug, Clone)]
pub struct EegData {
pub timestamp: i64,
pub counter: u32,
pub interpolated: bool,
pub channels: Vec<f32>,
pub raw_cq: f32,
}
impl EegData {
#[must_use]
pub fn from_eeg_array(
eeg: &[serde_json::Value],
num_channels: usize,
timestamp: f64,
) -> Option<Self> {
if eeg.len() < 2 + num_channels + 3 {
return None;
}
let counter = u32::try_from(eeg[0].as_u64()?).ok()?;
let interpolated = eeg[1].as_u64()? != 0;
let channels: Vec<f32> = eeg[2..2 + num_channels]
.iter()
.map(|v| v.as_f64().and_then(f64_to_f32))
.collect::<Option<Vec<f32>>>()?;
let raw_cq = f64_to_f32(eeg[2 + num_channels].as_f64()?)?;
Some(Self {
timestamp: seconds_to_micros_i64(timestamp)?,
counter,
interpolated,
channels,
raw_cq,
})
}
}
#[derive(Debug, Deserialize)]
pub struct DevEvent {
pub sid: String,
pub time: f64,
pub dev: Vec<serde_json::Value>,
}
#[derive(Debug, Clone)]
pub struct DeviceQuality {
pub battery_level: u8,
pub signal_strength: f32,
pub channel_quality: Vec<f32>,
pub overall_quality: f32,
pub battery_percent: u8,
}
impl DeviceQuality {
#[must_use]
pub fn from_dev_array(dev: &[serde_json::Value], num_channels: usize) -> Option<Self> {
if dev.len() < 4 {
return None;
}
let battery_level = u8::try_from(dev[0].as_u64()?).ok()?;
let signal_strength = f64_to_f32(dev[1].as_f64()?)?;
let cq_array = dev[2].as_array()?;
if cq_array.len() < num_channels + 1 {
return None;
}
let channel_quality: Vec<f32> = cq_array[..num_channels]
.iter()
.map(|v| v.as_f64().and_then(|cq| f64_to_f32(cq / 4.0)))
.collect::<Option<Vec<f32>>>()?;
let overall_quality = f64_to_f32(cq_array.get(num_channels)?.as_f64()? / 100.0)?;
let battery_percent = u8::try_from(dev.get(3)?.as_u64()?).ok()?;
Some(Self {
battery_level,
signal_strength,
channel_quality,
overall_quality,
battery_percent,
})
}
}
#[derive(Debug, Deserialize)]
pub struct MotEvent {
pub sid: String,
pub time: f64,
pub mot: Vec<f64>,
}
#[derive(Debug, Clone)]
pub struct MotionData {
pub timestamp: i64,
pub quaternion: Option<[f32; 4]>,
pub accelerometer: [f32; 3],
pub magnetometer: [f32; 3],
}
impl MotionData {
#[must_use]
pub fn from_mot_array(mot: &[f64], timestamp: f64) -> Option<Self> {
if mot.len() < 12 {
return None;
}
Some(Self {
timestamp: seconds_to_micros_i64(timestamp)?,
quaternion: Some([
f64_to_f32(mot[2])?,
f64_to_f32(mot[3])?,
f64_to_f32(mot[4])?,
f64_to_f32(mot[5])?,
]),
accelerometer: [
f64_to_f32(mot[6])?,
f64_to_f32(mot[7])?,
f64_to_f32(mot[8])?,
],
magnetometer: [
f64_to_f32(mot[9])?,
f64_to_f32(mot[10])?,
f64_to_f32(mot[11])?,
],
})
}
}
#[derive(Debug, Deserialize)]
pub struct EqEvent {
pub sid: String,
pub time: f64,
pub eq: Vec<serde_json::Value>,
}
#[derive(Debug, Clone)]
pub struct EegQuality {
pub battery_percent: u8,
pub overall: f32,
pub sample_rate_quality: f32,
pub sensor_quality: Vec<f32>,
}
impl EegQuality {
#[must_use]
pub fn from_eq_array(eq: &[serde_json::Value], num_channels: usize) -> Option<Self> {
if eq.len() < 3 + num_channels {
return None;
}
let battery_percent = u8::try_from(eq[0].as_u64()?).ok()?;
let overall = f64_to_f32(eq[1].as_f64()? / 100.0)?;
let sample_rate_quality = f64_to_f32(eq[2].as_f64()?)?;
let sensor_quality: Vec<f32> = eq[3..3 + num_channels]
.iter()
.filter_map(serde_json::Value::as_f64)
.map(|q| f64_to_f32(q / 4.0)) .collect::<Option<Vec<f32>>>()?;
if sensor_quality.len() != num_channels {
return None;
}
Some(Self {
battery_percent,
overall,
sample_rate_quality,
sensor_quality,
})
}
}
#[derive(Debug, Deserialize)]
pub struct PowEvent {
pub sid: String,
pub time: f64,
pub pow: Vec<f64>,
}
#[derive(Debug, Clone)]
pub struct BandPowerData {
pub timestamp: i64,
pub channel_powers: Vec<[f32; 5]>,
}
impl BandPowerData {
#[must_use]
pub fn from_pow_array(pow: &[f64], num_channels: usize, timestamp: f64) -> Option<Self> {
if pow.len() < num_channels * 5 {
return None;
}
let channel_powers: Vec<[f32; 5]> = pow
.chunks_exact(5)
.take(num_channels)
.map(|chunk| {
Some([
f64_to_f32(chunk[0])?,
f64_to_f32(chunk[1])?,
f64_to_f32(chunk[2])?,
f64_to_f32(chunk[3])?,
f64_to_f32(chunk[4])?,
])
})
.collect::<Option<Vec<[f32; 5]>>>()?;
Some(Self {
timestamp: seconds_to_micros_i64(timestamp)?,
channel_powers,
})
}
}
#[derive(Debug, Deserialize)]
pub struct MetEvent {
pub sid: String,
pub time: f64,
pub met: Vec<serde_json::Value>,
}
#[derive(Debug, Clone)]
pub struct PerformanceMetrics {
pub timestamp: i64,
pub engagement: Option<f32>,
pub excitement: Option<f32>,
pub long_excitement: Option<f32>,
pub stress: Option<f32>,
pub relaxation: Option<f32>,
pub interest: Option<f32>,
pub attention: Option<f32>,
pub focus: Option<f32>,
}
#[derive(Debug, Deserialize)]
pub struct ComEvent {
pub sid: String,
pub time: f64,
pub com: Vec<serde_json::Value>,
}
#[derive(Debug, Clone)]
pub struct MentalCommand {
pub action: String,
pub power: f32,
}
#[derive(Debug, Deserialize)]
pub struct FacEvent {
pub sid: String,
pub time: f64,
pub fac: Vec<serde_json::Value>,
}
#[derive(Debug, Clone)]
pub struct FacialExpression {
pub eye_action: String,
pub upper_face_action: String,
pub upper_face_power: f32,
pub lower_face_action: String,
pub lower_face_power: f32,
}
#[derive(Debug, Deserialize)]
pub struct SysEvent {
pub sid: String,
pub time: f64,
pub sys: Vec<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
pub struct StreamEvent {
pub sid: Option<String>,
pub time: Option<f64>,
pub eeg: Option<Vec<serde_json::Value>>,
pub dev: Option<Vec<serde_json::Value>>,
pub mot: Option<Vec<f64>>,
pub eq: Option<Vec<serde_json::Value>>,
pub pow: Option<Vec<f64>>,
pub met: Option<Vec<serde_json::Value>>,
pub com: Option<Vec<serde_json::Value>>,
pub fac: Option<Vec<serde_json::Value>>,
pub sys: Option<Vec<serde_json::Value>>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deserialize_eeg_event() {
let json = r#"{
"sid": "session-uuid-123",
"time": 1609459200.123456,
"eeg": [29, 0, 4262.564, 4264.615, 4265.128, 4267.179, 4263.59, 0.0, 0, []]
}"#;
let event: EegEvent = serde_json::from_str(json).unwrap();
assert_eq!(event.sid, "session-uuid-123");
assert_eq!(event.eeg.len(), 10);
assert_eq!(event.eeg[0].as_u64(), Some(29)); assert!(event.eeg[9].is_array()); }
#[test]
fn test_parse_eeg_data_insight() {
let eeg: Vec<serde_json::Value> = serde_json::from_str(
r"[29, 0, 4262.564, 4264.615, 4265.128, 4267.179, 4263.59, 0.0, 0, []]",
)
.unwrap();
let data = EegData::from_eeg_array(&eeg, 5, 1609459200.0).unwrap();
assert_eq!(data.counter, 29);
assert!(!data.interpolated);
assert_eq!(data.channels.len(), 5);
assert!((data.channels[0] - 4262.564).abs() < 0.01);
assert!((data.channels[4] - 4263.59).abs() < 0.01);
assert!((data.raw_cq - 0.0).abs() < f32::EPSILON);
}
#[test]
fn test_parse_eeg_data_too_short() {
let eeg: Vec<serde_json::Value> = serde_json::from_str(r"[29, 0, 4262.564]").unwrap();
assert!(EegData::from_eeg_array(&eeg, 5, 1.0).is_none());
}
#[test]
fn test_parse_eeg_data_with_markers() {
let eeg: Vec<serde_json::Value> = serde_json::from_str(
r#"[30, 0, 4100.0, 4200.0, 4300.0, 4400.0, 4500.0, 1.0, 0, ["marker1"]]"#,
)
.unwrap();
let data = EegData::from_eeg_array(&eeg, 5, 2.0).unwrap();
assert_eq!(data.counter, 30);
assert_eq!(data.channels.len(), 5);
assert!((data.raw_cq - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_parse_device_quality_insight() {
let dev: Vec<serde_json::Value> =
serde_json::from_str(r"[4, 1.0, [4, 3, 2, 4, 1, 75], 88]").unwrap();
let quality = DeviceQuality::from_dev_array(&dev, 5).unwrap();
assert_eq!(quality.battery_level, 4);
assert_eq!(quality.signal_strength, 1.0);
assert_eq!(quality.channel_quality.len(), 5);
assert!((quality.channel_quality[0] - 1.0).abs() < f32::EPSILON); assert!((quality.channel_quality[1] - 0.75).abs() < f32::EPSILON); assert!((quality.channel_quality[2] - 0.5).abs() < f32::EPSILON); assert!((quality.overall_quality - 0.75).abs() < f32::EPSILON); assert_eq!(quality.battery_percent, 88);
}
#[test]
fn test_parse_device_quality_too_short() {
let dev: Vec<serde_json::Value> = serde_json::from_str(r"[4, 1]").unwrap();
assert!(DeviceQuality::from_dev_array(&dev, 5).is_none());
}
#[test]
fn test_parse_device_quality_nested_cq_too_short() {
let dev: Vec<serde_json::Value> = serde_json::from_str(r"[4, 1.0, [4, 3], 88]").unwrap();
assert!(DeviceQuality::from_dev_array(&dev, 5).is_none());
}
#[test]
fn test_parse_eq_quality_insight() {
let eq: Vec<serde_json::Value> =
serde_json::from_str(r"[88, 75, 0.9, 4, 3, 2, 1, 4]").unwrap();
let parsed = EegQuality::from_eq_array(&eq, 5).unwrap();
assert_eq!(parsed.battery_percent, 88);
assert!((parsed.overall - 0.75).abs() < f32::EPSILON);
assert!((parsed.sample_rate_quality - 0.9).abs() < f32::EPSILON);
assert_eq!(parsed.sensor_quality.len(), 5);
assert!((parsed.sensor_quality[0] - 1.0).abs() < f32::EPSILON);
assert!((parsed.sensor_quality[1] - 0.75).abs() < f32::EPSILON);
}
#[test]
fn test_parse_eq_quality_too_short() {
let eq: Vec<serde_json::Value> = serde_json::from_str(r"[88, 75, 1.0, 4]").unwrap();
assert!(EegQuality::from_eq_array(&eq, 5).is_none());
}
#[test]
fn test_parse_motion_data() {
let mot = vec![
123.0, 0.0, 0.707, 0.0, 0.707, 0.0, 0.01, -9.81, 0.02, 30.0, -15.0, 45.0,
];
let motion = MotionData::from_mot_array(&mot, 1609459200.0).unwrap();
let q = motion.quaternion.unwrap();
assert!((q[0] - 0.707).abs() < 0.001);
assert!((motion.accelerometer[1] - -9.81).abs() < 0.01);
assert!((motion.magnetometer[2] - 45.0).abs() < 0.01);
}
#[test]
fn test_parse_band_power() {
let mut pow = vec![0.0; 25];
pow[0] = 1.5; pow[1] = 2.3; pow[5] = 0.8;
let bp = BandPowerData::from_pow_array(&pow, 5, 1609459200.0).unwrap();
assert_eq!(bp.channel_powers.len(), 5);
assert!((bp.channel_powers[0][0] - 1.5).abs() < f32::EPSILON); assert!((bp.channel_powers[0][1] - 2.3).abs() < f32::EPSILON); assert!((bp.channel_powers[1][0] - 0.8).abs() < f32::EPSILON); }
#[test]
fn test_deserialize_dev_event() {
let json = r#"{
"sid": "session-uuid-123",
"time": 1609459200.0,
"dev": [4, 1.0, [4, 3, 2, 4, 1, 75], 88]
}"#;
let event: DevEvent = serde_json::from_str(json).unwrap();
assert_eq!(event.sid, "session-uuid-123");
assert_eq!(event.dev.len(), 4);
}
#[test]
fn test_deserialize_mot_event() {
let json = r#"{
"sid": "session-uuid-123",
"time": 1609459200.0,
"mot": [0.0, 0.0, 0.707, 0.0, 0.707, 0.0, 0.01, -9.81, 0.02, 30.0, -15.0, 45.0]
}"#;
let event: MotEvent = serde_json::from_str(json).unwrap();
assert_eq!(event.mot.len(), 12);
}
#[test]
fn test_deserialize_pow_event() {
let json = r#"{
"sid": "session-uuid-123",
"time": 1609459200.0,
"pow": [1.5, 2.3, 0.8, 1.1, 0.5]
}"#;
let event: PowEvent = serde_json::from_str(json).unwrap();
assert_eq!(event.pow.len(), 5);
}
#[test]
fn test_deserialize_met_event() {
let json = r#"{
"sid": "session-uuid-123",
"time": 1609459200.0,
"met": [0.2, 0.3, 0.4, 0.1]
}"#;
let event: MetEvent = serde_json::from_str(json).unwrap();
assert_eq!(event.sid, "session-uuid-123");
assert_eq!(event.met.len(), 4);
}
#[test]
fn test_deserialize_com_event() {
let json = r#"{
"sid": "session-uuid-123",
"time": 1609459200.0,
"com": ["push", 0.82]
}"#;
let event: ComEvent = serde_json::from_str(json).unwrap();
assert_eq!(event.com.len(), 2);
assert_eq!(event.com[0].as_str(), Some("push"));
}
#[test]
fn test_deserialize_fac_event() {
let json = r#"{
"sid": "session-uuid-123",
"time": 1609459200.0,
"fac": ["blink", "surprise", 0.9, "smile", 0.7]
}"#;
let event: FacEvent = serde_json::from_str(json).unwrap();
assert_eq!(event.fac.len(), 5);
assert_eq!(event.fac[0].as_str(), Some("blink"));
}
#[test]
fn test_deserialize_sys_event() {
let json = r#"{
"sid": "session-uuid-123",
"time": 1609459200.0,
"sys": ["mc_action", "start"]
}"#;
let event: SysEvent = serde_json::from_str(json).unwrap();
assert_eq!(event.sys.len(), 2);
assert_eq!(event.sys[0].as_str(), Some("mc_action"));
}
#[test]
fn test_deserialize_stream_event_eeg() {
let json = r#"{
"sid": "s1",
"time": 1.0,
"eeg": [29, 0, 4262.564, 4264.615, 4265.128, 4267.179, 4263.59, 0.0, 0, []]
}"#;
let event: StreamEvent = serde_json::from_str(json).unwrap();
assert!(event.eeg.is_some());
assert_eq!(event.eeg.as_ref().unwrap().len(), 10);
assert!(event.dev.is_none());
assert!(event.mot.is_none());
}
#[test]
fn test_deserialize_stream_event_dev() {
let json = r#"{
"sid": "s1",
"time": 1.0,
"dev": [4, 1.0, [3, 2, 1, 0, 4, 50], 75]
}"#;
let event: StreamEvent = serde_json::from_str(json).unwrap();
assert!(event.eeg.is_none());
assert!(event.dev.is_some());
}
}