use rand::Rng;
use crate::protocol::{SampleRate, EEG_EVENT_ID, NUM_EEG_CHANNELS, PACKET_SIZE, SYNC_BYTE};
pub fn build_eeg_packet(counter: u8) -> Vec<u8> {
let mut rng = rand::rng();
let mut packet = vec![0u8; PACKET_SIZE];
packet[0] = SYNC_BYTE;
packet[1] = EEG_EVENT_ID;
packet[2] = 0x3C; packet[3] = counter;
let ref_val: f32 = rng.random_range(-50.0..50.0);
let drl_val: f32 = rng.random_range(-50.0..50.0);
packet[4..8].copy_from_slice(&ref_val.to_le_bytes());
packet[8..12].copy_from_slice(&drl_val.to_le_bytes());
for ch in 0..NUM_EEG_CHANNELS {
let offset = 12 + ch * 4;
let raw_adc: f32 = rng.random_range(-8000.0..8000.0);
packet[offset..offset + 4].copy_from_slice(&raw_adc.to_le_bytes());
}
packet[60] = 0x00;
let checksum: u16 = packet[..61].iter().map(|&b| b as u16).sum::<u16>();
packet[61] = (checksum & 0xFF) as u8;
packet[62] = (checksum >> 8) as u8;
packet
}
pub fn build_sim_packet(counter: u8, t: f64) -> Vec<u8> {
use std::f64::consts::PI;
let mut packet = vec![0u8; PACKET_SIZE];
packet[0] = SYNC_BYTE;
packet[1] = EEG_EVENT_ID;
packet[2] = 0x3C;
packet[3] = counter;
let ref_val = (5.0 * (2.0 * PI * 0.1 * t).sin()) as f32;
let drl_val = (3.0 * (2.0 * PI * 0.15 * t).cos()) as f32;
packet[4..8].copy_from_slice(&ref_val.to_le_bytes());
packet[8..12].copy_from_slice(&drl_val.to_le_bytes());
for ch in 0..NUM_EEG_CHANNELS {
let phi = ch as f64 * PI / 4.0;
let alpha = 3000.0 * (2.0 * PI * 10.0 * t + phi).sin();
let beta = 1000.0 * (2.0 * PI * 22.0 * t + phi * 1.7).sin();
let theta = 1500.0 * (2.0 * PI * 6.0 * t + phi * 0.9).sin();
let nx = t * 1000.7 + ch as f64 * 137.508;
let noise = ((nx.sin() * 9973.1).fract() - 0.5) * 1000.0;
let raw_adc = (alpha + beta + theta + noise) as f32;
let offset = 12 + ch * 4;
packet[offset..offset + 4].copy_from_slice(&raw_adc.to_le_bytes());
}
packet[60] = 0x00;
let checksum: u16 = packet[..61].iter().map(|&b| b as u16).sum::<u16>();
packet[61] = (checksum & 0xFF) as u8;
packet[62] = (checksum >> 8) as u8;
packet
}
pub fn spawn_simulator(
tx: tokio::sync::mpsc::Sender<crate::types::Mw75Event>,
deterministic: bool,
) -> tokio::task::JoinHandle<()> {
spawn_simulator_with_rate(tx, deterministic, SampleRate::default())
}
pub fn spawn_simulator_with_rate(
tx: tokio::sync::mpsc::Sender<crate::types::Mw75Event>,
deterministic: bool,
sample_rate: SampleRate,
) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
use crate::parse::PacketProcessor;
let mut counter: u8 = 0;
let mut processor = PacketProcessor::new(false);
let interval = tokio::time::Duration::from_micros(sample_rate.interval_micros());
let mut ticker = tokio::time::interval(interval);
let start = tokio::time::Instant::now();
let _ = tx
.send(crate::types::Mw75Event::Connected("MW75-SIM".into()))
.await;
let _ = tx
.send(crate::types::Mw75Event::Activated(
crate::types::ActivationStatus {
eeg_enabled: true,
raw_mode_enabled: sample_rate.needs_raw_mode(),
},
))
.await;
let _ = tx
.send(crate::types::Mw75Event::Battery(
crate::types::BatteryInfo { level: 85 },
))
.await;
loop {
ticker.tick().await;
let t = start.elapsed().as_secs_f64();
let packet = if deterministic {
build_sim_packet(counter, t)
} else {
build_eeg_packet(counter)
};
let events = processor.process_data(&packet);
for event in events {
if tx.send(event).await.is_err() {
return; }
}
counter = counter.wrapping_add(1);
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse::{validate_checksum, PacketProcessor};
use crate::protocol::{EEG_EVENT_ID, PACKET_SIZE, SYNC_BYTE};
#[test]
fn test_build_eeg_packet_structure() {
let pkt = build_eeg_packet(0);
assert_eq!(pkt.len(), PACKET_SIZE);
assert_eq!(pkt[0], SYNC_BYTE);
assert_eq!(pkt[1], EEG_EVENT_ID);
assert_eq!(pkt[3], 0);
}
#[test]
fn test_build_eeg_packet_valid_checksum() {
for counter in 0..10 {
let pkt = build_eeg_packet(counter);
let (valid, _, _) = validate_checksum(&pkt);
assert!(valid, "Packet with counter={counter} has invalid checksum");
}
}
#[test]
fn test_build_eeg_packet_counter_wraps() {
let pkt = build_eeg_packet(255);
assert_eq!(pkt[3], 255);
}
#[test]
fn test_build_sim_packet_structure() {
let pkt = build_sim_packet(42, 1.0);
assert_eq!(pkt.len(), PACKET_SIZE);
assert_eq!(pkt[0], SYNC_BYTE);
assert_eq!(pkt[1], EEG_EVENT_ID);
assert_eq!(pkt[3], 42);
}
#[test]
fn test_build_sim_packet_valid_checksum() {
for i in 0..20 {
let t = i as f64 * 0.002;
let pkt = build_sim_packet(i as u8, t);
let (valid, _, _) = validate_checksum(&pkt);
assert!(valid, "Sim packet at t={t} has invalid checksum");
}
}
#[test]
fn test_build_sim_packet_deterministic() {
let a = build_sim_packet(5, 0.5);
let b = build_sim_packet(5, 0.5);
assert_eq!(a, b, "Same inputs should produce identical packets");
}
#[test]
fn test_build_sim_packet_varies_with_time() {
let a = build_sim_packet(0, 0.0);
let b = build_sim_packet(0, 0.1);
assert_ne!(&a[12..60], &b[12..60]);
}
#[test]
fn test_build_eeg_packet_parseable() {
let pkt = build_eeg_packet(7);
let mut proc = PacketProcessor::new(false);
let events = proc.process_data(&pkt);
assert_eq!(events.len(), 1);
match &events[0] {
crate::types::Mw75Event::Eeg(eeg) => {
assert_eq!(eeg.counter, 7);
assert_eq!(eeg.channels.len(), 12);
assert!(eeg.checksum_valid);
}
_ => panic!("Expected Eeg event"),
}
}
#[test]
fn test_build_sim_packet_parseable() {
let pkt = build_sim_packet(99, 2.5);
let mut proc = PacketProcessor::new(false);
let events = proc.process_data(&pkt);
assert_eq!(events.len(), 1);
match &events[0] {
crate::types::Mw75Event::Eeg(eeg) => {
assert_eq!(eeg.counter, 99);
assert_eq!(eeg.channels.len(), 12);
for &v in &eeg.channels {
assert!(
v.abs() < 250.0,
"Channel value {v} out of expected ±250 µV range"
);
}
}
_ => panic!("Expected Eeg event"),
}
}
#[tokio::test]
async fn test_spawn_simulator_produces_events() {
let (tx, mut rx) = tokio::sync::mpsc::channel(256);
let handle = spawn_simulator(tx, true);
let mut eeg_count = 0;
let timeout = tokio::time::Duration::from_millis(100);
let deadline = tokio::time::Instant::now() + timeout;
while tokio::time::Instant::now() < deadline {
match tokio::time::timeout(tokio::time::Duration::from_millis(10), rx.recv()).await {
Ok(Some(crate::types::Mw75Event::Eeg(_))) => eeg_count += 1,
Ok(Some(_)) => {} _ => break,
}
}
handle.abort();
assert!(eeg_count > 0, "Should have received at least one EEG event");
}
}