use std::pin::Pin; use std::sync::{Arc, Mutex}; use std::time::Duration; use async_trait::async_trait; use futures::stream::{self, Stream}; use futures::StreamExt; use crate::ble::source::SensorSource; use crate::ble::BleError; use crate::commands::Command; use crate::device::{get_device_types, Device, DeviceType}; use crate::telemetry::Telemetry; /// Виртуальный источник: scripted-поток, без реального BLE. pub(crate) struct MockSource { devices: Vec, commands: Arc>>, } impl MockSource { pub(crate) fn new() -> Self { let devices = vec![ Device { id: "mock-hr".into(), address: "00:00:00:00:00:01".into(), display_name: Some("Mock HR".into()), rssi: Some(-45), service_uuids: vec!["0000180d-0000-1000-8000-00805f9b34fb".into()], characteristics: vec!["180d::2a37".into()], manufacturer_data: None, metadata: None, }, Device { id: "mock-cad".into(), address: "00:00:00:00:00:02".into(), display_name: Some("Mock Cadence".into()), rssi: Some(-50), service_uuids: vec!["00001816-0000-1000-8000-00805f9b34fb".into()], characteristics: vec!["1816::2a5b".into()], manufacturer_data: None, metadata: None, }, Device { id: "mock-trainer".into(), address: "00:00:00:00:00:03".into(), display_name: Some("Mock Trainer".into()), rssi: Some(-55), service_uuids: vec!["00001826-0000-1000-8000-00805f9b34fb".into()], characteristics: vec!["1826::2ad2".into()], manufacturer_data: None, metadata: None, }, ]; Self { devices, commands: Arc::new(Mutex::new(Vec::new())), } } /// Записанные команды — читаются только в тестах движка воркаута. #[allow(dead_code)] pub(crate) fn commands(&self) -> Vec { self.commands.lock().unwrap().clone() } } #[async_trait] impl SensorSource for MockSource { async fn scan(&self, _seconds: u64) -> Result, BleError> { Ok(self.devices.clone()) } async fn stream(&self, device: &Device) -> Result + Send>>, BleError> { let types = get_device_types(device); let s: Pin + Send>> = if types.contains(&DeviceType::Trainer) { // мощность 150..300 Вт + каденс 85..95 rpm (как FTMS 0x2AD2 — оба поля) stream::unfold(0u16, |i| async move { tokio::time::sleep(Duration::from_millis(250)).await; let item = if i % 2 == 0 { let watts = 150 + ((i / 2 % 16) * 10) as i16; Telemetry::Power { watts } } else { let rpm = 85.0 + (i / 2 % 10) as f32; Telemetry::Cadence { rpm } }; Some((item, i.wrapping_add(1))) }) .boxed() } else if types.contains(&DeviceType::CadSensor) { // каденс 85..95 rpm stream::unfold(0u16, |i| async move { tokio::time::sleep(Duration::from_millis(500)).await; let rpm = 85.0 + (i % 10) as f32; Some((Telemetry::Cadence { rpm }, i.wrapping_add(1))) }) .boxed() } else { // пульс 120..169 stream::unfold(0u16, |i| async move { tokio::time::sleep(Duration::from_millis(1000)).await; let bpm = 120 + (i % 50); Some((Telemetry::HeartRate { bpm }, i.wrapping_add(1))) }) .boxed() }; Ok(s) } async fn command(&self, _device: &Device, cmd: Command) -> Result<(), BleError> { self.commands.lock().unwrap().push(cmd); Ok(()) } async fn request_control(&self, _device: &Device) -> Result<(), BleError> { Ok(()) } async fn battery(&self, device: &Device) -> Result { let percent = match device.id.as_str() { "mock-hr" => 15, "mock-cad" => 60, "mock-trainer" => 85, _ => 100, }; Ok(percent) } } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_mock_scan_returns_three_devices() { let src = MockSource::new(); let devices = src.scan(0).await.unwrap(); assert_eq!(devices.len(), 3); } #[tokio::test] async fn test_mock_stream_yields_telemetry() { let src = MockSource::new(); let devices = src.scan(0).await.unwrap(); let hr = devices.iter().find(|d| d.id == "mock-hr").unwrap(); let mut stream = src.stream(hr).await.unwrap(); let first = stream.next().await; assert!(matches!(first, Some(Telemetry::HeartRate { .. }))); } #[tokio::test] async fn test_mock_trainer_emits_power_and_cadence() { let src = MockSource::new(); let devices = src.scan(0).await.unwrap(); let trainer = devices.iter().find(|d| d.id == "mock-trainer").unwrap(); let mut stream = src.stream(trainer).await.unwrap(); let mut saw_power = false; let mut saw_cadence = false; for _ in 0..8 { match stream.next().await { Some(Telemetry::Power { .. }) => saw_power = true, Some(Telemetry::Cadence { .. }) => saw_cadence = true, _ => {} } } assert!(saw_power, "тренер должен эмитить мощность"); assert!(saw_cadence, "тренер должен эмитить каденс"); } }