use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use tokio::sync::mpsc;
use crate::types::{
BatteryReading, CalibrationReading, DeviceInfo, DiagnosticsReading, FrameReading, MendiEvent,
MENDI_FCC_ID,
};
#[derive(Debug, Clone)]
pub struct SimConfig {
pub frame_rate_hz: f64,
pub battery_voltage_mv: u32,
pub usb_connected: bool,
pub charging: bool,
pub battery_every_n_frames: u64,
pub calibration_every_n_frames: u64,
pub device_name: String,
pub firmware_version: String,
pub disconnect_after_frames: Option<u64>,
pub base_ir: i32,
pub base_red: i32,
pub base_ambient: i32,
pub temperature: f32,
}
impl Default for SimConfig {
fn default() -> Self {
Self {
frame_rate_hz: 25.0,
battery_voltage_mv: 3900,
usb_connected: false,
charging: false,
battery_every_n_frames: 100,
calibration_every_n_frames: 200,
device_name: "Mendi-SIM".into(),
firmware_version: "SIM-1.0.0".into(),
disconnect_after_frames: None,
base_ir: 50_000,
base_red: 40_000,
base_ambient: 1_000,
temperature: 36.5,
}
}
}
pub struct SimHandle {
running: Arc<AtomicBool>,
task: tokio::task::JoinHandle<()>,
}
impl SimHandle {
pub fn disconnect(&self) {
self.running.store(false, Ordering::SeqCst);
}
pub fn running_flag(&self) -> Arc<AtomicBool> {
Arc::clone(&self.running)
}
pub fn is_running(&self) -> bool {
!self.task.is_finished()
}
pub async fn join(self) {
let _ = self.task.await;
}
}
pub struct SimulatedDevice;
impl SimulatedDevice {
pub fn start(config: SimConfig) -> (mpsc::Receiver<MendiEvent>, SimHandle) {
let (tx, rx) = mpsc::channel::<MendiEvent>(256);
let running = Arc::new(AtomicBool::new(true));
let running_clone = Arc::clone(&running);
let task = tokio::spawn(async move {
Self::run(config, tx, running_clone).await;
});
let handle = SimHandle { running, task };
(rx, handle)
}
async fn run(config: SimConfig, tx: mpsc::Sender<MendiEvent>, running: Arc<AtomicBool>) {
let device_info = DeviceInfo {
name: config.device_name.clone(),
id: "SIM-00:00:00:00:00:00".into(),
firmware_version: Some(config.firmware_version.clone()),
hardware_version: Some("SIM-HW-1.0".into()),
fcc_id: MENDI_FCC_ID.into(),
};
if tx.send(MendiEvent::Connected(device_info)).await.is_err() {
return;
}
let diag = DiagnosticsReading {
timestamp: now_ms(),
adc: Some(BatteryReading {
timestamp: now_ms(),
voltage_mv: config.battery_voltage_mv,
charging: config.charging,
usb_connected: config.usb_connected,
}),
imu_ok: true,
sensor_ok: true,
};
if tx.send(MendiEvent::Diagnostics(diag)).await.is_err() {
return;
}
let interval = Duration::from_secs_f64(1.0 / config.frame_rate_hz);
let mut frame_count: u64 = 0;
let mut rng = StdRng::from_os_rng();
while running.load(Ordering::SeqCst) {
frame_count += 1;
if let Some(limit) = config.disconnect_after_frames {
if frame_count > limit {
break;
}
}
let ts = now_ms();
let t = frame_count as f64 / config.frame_rate_hz;
let sin_l = (t * 0.5 * std::f64::consts::TAU).sin(); let sin_r = (t * 0.5 * std::f64::consts::TAU + 0.3).sin();
let sin_p = (t * 1.2 * std::f64::consts::TAU).sin();
let n = |r: &mut StdRng| -> i32 { r.random_range(-200..=200) };
let frame = FrameReading {
timestamp: ts,
acc_x: rng.random_range(-50..=50),
acc_y: rng.random_range(-50..=50),
acc_z: 16384 + rng.random_range(-100..=100), ang_x: rng.random_range(-10..=10),
ang_y: rng.random_range(-10..=10),
ang_z: rng.random_range(-10..=10),
temperature: config.temperature,
ir_left: config.base_ir + (sin_l * 2000.0) as i32 + n(&mut rng),
red_left: config.base_red + (sin_l * 1500.0) as i32 + n(&mut rng),
amb_left: config.base_ambient + n(&mut rng) / 4,
ir_right: config.base_ir + (sin_r * 2000.0) as i32 + n(&mut rng),
red_right: config.base_red + (sin_r * 1500.0) as i32 + n(&mut rng),
amb_right: config.base_ambient + n(&mut rng) / 4,
ir_pulse: config.base_ir + (sin_p * 3000.0) as i32 + n(&mut rng),
red_pulse: config.base_red + (sin_p * 2000.0) as i32 + n(&mut rng),
amb_pulse: config.base_ambient + n(&mut rng) / 4,
};
if tx.try_send(MendiEvent::Frame(frame)).is_err() {
if tx.is_closed() {
return;
}
}
if frame_count.is_multiple_of(config.battery_every_n_frames) {
let battery = BatteryReading {
timestamp: now_ms(),
voltage_mv: config.battery_voltage_mv,
charging: config.charging,
usb_connected: config.usb_connected,
};
let _ = tx.send(MendiEvent::Battery(battery)).await;
}
if frame_count.is_multiple_of(config.calibration_every_n_frames) {
let cal = CalibrationReading {
timestamp: now_ms(),
offset_left: 0.0,
offset_right: 0.0,
offset_pulse: 0.0,
auto_calibration: true,
low_power_mode: false,
};
let _ = tx.send(MendiEvent::Calibration(cal)).await;
}
tokio::time::sleep(interval).await;
}
let _ = tx.send(MendiEvent::Disconnected).await;
}
}
pub struct MockDevice {
tx: mpsc::Sender<MendiEvent>,
}
impl MockDevice {
pub fn new(buffer: usize) -> (mpsc::Receiver<MendiEvent>, Self) {
let (tx, rx) = mpsc::channel(buffer);
(rx, Self { tx })
}
pub fn send(&self, event: MendiEvent) {
self.tx.try_send(event).expect("MockDevice channel full or closed");
}
pub fn try_send(&self, event: MendiEvent) -> bool {
self.tx.try_send(event).is_ok()
}
pub async fn send_async(&self, event: MendiEvent) {
let _ = self.tx.send(event).await;
}
pub fn connected(info: DeviceInfo, buffer: usize) -> (mpsc::Receiver<MendiEvent>, Self) {
let (rx, mock) = Self::new(buffer);
mock.send(MendiEvent::Connected(info));
mock.send(MendiEvent::Diagnostics(DiagnosticsReading {
timestamp: 0.0,
adc: Some(BatteryReading {
timestamp: 0.0,
voltage_mv: 3900,
charging: false,
usb_connected: false,
}),
imu_ok: true,
sensor_ok: true,
}));
(rx, mock)
}
pub fn with_frames(n: usize, buffer: usize) -> (mpsc::Receiver<MendiEvent>, Self) {
let (rx, mock) = Self::new(buffer.max(n + 3));
mock.send(MendiEvent::Connected(DeviceInfo {
name: "Mock".into(),
id: "MOCK-00".into(),
fcc_id: MENDI_FCC_ID.into(),
..Default::default()
}));
for i in 0..n {
mock.send(MendiEvent::Frame(FrameReading {
timestamp: i as f64 * 40.0, acc_x: 0,
acc_y: 0,
acc_z: 16384,
ang_x: 0,
ang_y: 0,
ang_z: 0,
temperature: 36.5,
ir_left: 50_000 + (i as i32 * 10),
red_left: 40_000 + (i as i32 * 8),
amb_left: 1_000,
ir_right: 50_000 + (i as i32 * 10),
red_right: 40_000 + (i as i32 * 8),
amb_right: 1_000,
ir_pulse: 50_000 + (i as i32 * 5),
red_pulse: 40_000 + (i as i32 * 4),
amb_pulse: 1_000,
}));
}
mock.send(MendiEvent::Disconnected);
(rx, mock)
}
}
fn now_ms() -> f64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before epoch")
.as_secs_f64()
* 1000.0
}