From 701bf63c05718d59d8841e13f9786ca7f92ccdac Mon Sep 17 00:00:00 2001 From: Mattis Kieffer Date: Fri, 6 Dec 2024 01:15:15 +0100 Subject: [PATCH 1/3] add abstraction over Bluetooth components --- src/controller/bluetooth.rs | 167 +++++++++++++++++++++++++++++------- src/main.rs | 3 +- 2 files changed, 140 insertions(+), 30 deletions(-) diff --git a/src/controller/bluetooth.rs b/src/controller/bluetooth.rs index 3bdea8e..97bd4b4 100644 --- a/src/controller/bluetooth.rs +++ b/src/controller/bluetooth.rs @@ -9,16 +9,19 @@ use crate::model::bluetooth::{BluetoothModelApi, DeviceDescriptor, HeartrateMess use crate::model::storage::ModelHandle; use crate::{core::events::AppEvent, model::bluetooth::AdapterDescriptor}; use async_trait::async_trait; -use btleplug::api::{Peripheral, ScanFilter}; +use btleplug::api::{Peripheral as _, ScanFilter}; use btleplug::{ api::{BDAddr, Central, Manager as _}, - platform::{Adapter, Manager}, + platform::{Adapter, Manager, Peripheral}, }; use anyhow::{anyhow, Result}; -use egui::ahash::HashMap; +use btleplug::api::{Characteristic, PeripheralProperties, ValueNotification}; +use futures::stream::Stream; use futures::StreamExt; use log::{trace, warn}; +use std::collections::HashMap; +use std::pin::Pin; use std::sync::Arc; use tokio::sync::broadcast::Sender; use tokio::sync::RwLock; @@ -53,9 +56,124 @@ pub trait BluetoothApi: Send + Sync { async fn stop_listening(&mut self) -> Result<()>; } +// Define a trait for the Bluetooth Adapter API +#[async_trait] +pub trait BluetoothAdapterApi: Send + Sync { + async fn start_scan(&self, filter: ScanFilter) -> Result<()>; + async fn stop_scan(&self) -> Result<()>; + async fn peripherals(&self) -> Result>>; + async fn get_name(&self) -> Result; + // Add other necessary methods +} + +#[async_trait] +pub trait AdapterDiscovery { + async fn discover_adapters() -> Result>; +} + +// Define a trait for the Bluetooth Peripheral API +#[async_trait] +pub trait BluetoothPeripheralApi: Send + Sync { + fn address(&self) -> BDAddr; + async fn connect(&self) -> Result<()>; + async fn disconnect(&self) -> Result<()>; + async fn discover_services(&self) -> Result<()>; + fn characteristics(&self) -> Result>; + async fn notifications(&self) -> Result + Send>>>; + async fn subscribe(&self, characteristic: &Characteristic) -> Result<()>; + async fn get_name(&self) -> Result; +} + +#[async_trait::async_trait] +impl BluetoothAdapterApi for Adapter { + async fn start_scan(&self, filter: ScanFilter) -> Result<()> { + Central::start_scan(self, filter) + .await + .map_err(|e| anyhow!(e)) + } + + async fn stop_scan(&self) -> Result<()> { + Central::stop_scan(self).await.map_err(|e| anyhow!(e)) + } + + async fn peripherals(&self) -> Result>> { + let peripherals = Central::peripherals(self).await?; + let wrapped: Vec> = peripherals + .into_iter() + .map(|p| Arc::new(p) as Arc) + .collect(); + Ok(wrapped) + } + + async fn get_name(&self) -> Result { + Ok(Central::adapter_info(self) + .await + .unwrap_or("unknown".to_string())) + } + // Implement other methods as needed +} + +#[async_trait] +impl AdapterDiscovery for Adapter { + async fn discover_adapters() -> Result> { + let manager = Manager::new().await?; + let adapters = manager.adapters().await?; + Ok(adapters) + } +} + +#[async_trait::async_trait] +impl BluetoothPeripheralApi for Peripheral { + fn address(&self) -> BDAddr { + btleplug::api::Peripheral::address(self) + } + + async fn connect(&self) -> Result<()> { + btleplug::api::Peripheral::connect(self) + .await + .map_err(|e| anyhow!(e)) + } + + async fn disconnect(&self) -> Result<()> { + btleplug::api::Peripheral::disconnect(self) + .await + .map_err(|e| anyhow!(e)) + } + + async fn discover_services(&self) -> Result<()> { + btleplug::api::Peripheral::discover_services(self) + .await + .map_err(|e| anyhow!(e)) + } + + fn characteristics(&self) -> Result> { + Ok(btleplug::api::Peripheral::characteristics(self) + .into_iter() + .collect()) + } + + async fn notifications(&self) -> Result + Send>>> { + btleplug::api::Peripheral::notifications(self) + .await + .map_err(|e| anyhow!(e)) + } + + async fn subscribe(&self, characteristic: &Characteristic) -> Result<()> { + btleplug::api::Peripheral::subscribe(self, characteristic) + .await + .map_err(|e| anyhow!(e)) + } + async fn get_name(&self) -> Result { + btleplug::api::Peripheral::properties(self) + .await + .map(|p| p.unwrap().local_name.unwrap_or("unknown".to_string())) + .map_err(|e| anyhow!(e)) + } +} + /// The Bluetooth Controller manages BLE interactions, including scanning for devices /// and starting listening sessions. -pub struct BluetoothController { +pub struct BluetoothController + 'static> { /// The Bluetooth model instance. model: Arc>, event_bus: Sender, @@ -64,10 +182,10 @@ pub struct BluetoothController { /// Handle for the listener task. listener_handle: Option>>, /// Event transmitter. - adapters: HashMap, + adapters: HashMap, // Use trait object for adapter } -impl BluetoothController { +impl> BluetoothController { /// Creates a new `BluetoothController` instance. /// /// # Arguments @@ -82,7 +200,7 @@ impl BluetoothController { event_bus, peri_updater_handle: None, listener_handle: None, - adapters: Default::default(), + adapters: HashMap::new(), } } @@ -90,7 +208,7 @@ impl BluetoothController { /// /// # Returns /// A reference to the selected adapter, or an error if none is selected. - async fn get_adapter(&self) -> Result<&Adapter> { + async fn get_adapter(&self) -> Result<&A> { let model = self.model.read().await; let desc = model .get_selected_adapter() @@ -111,7 +229,7 @@ impl BluetoothController { /// # Returns /// A future that resolves to a join handle for the listener task. async fn listen_to_peripheral( - adapter: Adapter, + adapter: A, peripheral_address: BDAddr, tx: Sender, ) -> Result>> { @@ -125,7 +243,7 @@ impl BluetoothController { cheststrap.discover_services().await?; let char = cheststrap - .characteristics() + .characteristics()? .iter() .find(|c| c.uuid == HEARTRATE_MEASUREMENT_UUID) .ok_or(anyhow!("Peripheral has no Heartrate attribute"))? @@ -162,7 +280,7 @@ impl BluetoothController { /// A future that resolves to a join handle for the updater task. async fn launch_periphal_updater( &self, - adapter: Adapter, + adapter: A, _channel: Sender, ) -> Result>> { let model = self.model.clone(); @@ -173,11 +291,8 @@ impl BluetoothController { let mut descriptors = Vec::new(); for peripheral in &peripherals { let address = peripheral.address(); - if let Some(props) = peripheral.properties().await? { - if let Some(name) = props.local_name { - descriptors.push(DeviceDescriptor { name, address }); - } - } + let name = peripheral.get_name().await?; + descriptors.push(DeviceDescriptor { name, address }); } // TODO: Send events when an error arises descriptors.sort(); @@ -189,17 +304,17 @@ impl BluetoothController { } #[async_trait] -impl BluetoothApi for BluetoothController { +impl + 'static> BluetoothApi + for BluetoothController +{ fn get_model(&self) -> Result> { Ok(self.model.clone().into()) } async fn discover_adapters(&mut self) -> Result<()> { - let manager = Manager::new().await?; - let adapters = manager.adapters().await?; let mut model = Vec::new(); - for adapter in adapters { - let name = adapter.adapter_info().await.unwrap_or("unknown".into()); + for adapter in A::discover_adapters().await? { + let name = adapter.get_name().await?; let desc = AdapterDescriptor::new(name); model.push(desc.clone()); self.adapters.insert(*desc.get_uuid(), adapter); @@ -228,10 +343,7 @@ impl BluetoothApi for BluetoothController { } let handle = self.get_adapter().await?; handle.start_scan(ScanFilter::default()).await?; - trace!( - "Scanning started on adapter {}.", - handle.adapter_info().await.unwrap_or("unknown".into()) - ); + trace!("Scanning started on adapter {}.", handle.get_name().await?); if self.peri_updater_handle.is_none() { self.peri_updater_handle = Some( self.launch_periphal_updater(handle.clone(), self.event_bus.clone()) @@ -247,10 +359,7 @@ impl BluetoothApi for BluetoothController { } let handle = self.get_adapter().await?; handle.stop_scan().await?; - trace!( - "Stopped scanning on adapter {}.", - handle.adapter_info().await? - ); + trace!("Stopped scanning on adapter {}.", handle.get_name().await?); if let Some(updater_handle) = &self.peri_updater_handle.take() { updater_handle.abort(); } diff --git a/src/main.rs b/src/main.rs index 318fb40..82f9329 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ //! data acquisition, BLE communication, and HRV computation. The tool is //! structured using a modular, event-driven MVC architecture. +use btleplug::platform::Adapter; use controller::{ acquisition::AcquisitionController, application::AppController, bluetooth::BluetoothController, }; @@ -96,7 +97,7 @@ fn main() { NativeOptions::default(), Box::new(|cc| { let app = AppController::new( - BluetoothController::new(bluetooth_model, event_bus.clone()), + BluetoothController::::new(bluetooth_model, event_bus.clone()), AcquisitionController::new(storage.clone(), event_bus.clone()), storage, event_bus, From 7c134e209bba3ff99a53fc03911e6f20fd3e6d09 Mon Sep 17 00:00:00 2001 From: Mattis Kieffer Date: Fri, 6 Dec 2024 01:52:26 +0100 Subject: [PATCH 2/3] add first mock test for BT controller --- src/controller/bluetooth.rs | 58 ++++++++++++++++++++++++++++++++++--- src/model/bluetooth.rs | 4 +++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/controller/bluetooth.rs b/src/controller/bluetooth.rs index 97bd4b4..6ec816f 100644 --- a/src/controller/bluetooth.rs +++ b/src/controller/bluetooth.rs @@ -9,14 +9,14 @@ use crate::model::bluetooth::{BluetoothModelApi, DeviceDescriptor, HeartrateMess use crate::model::storage::ModelHandle; use crate::{core::events::AppEvent, model::bluetooth::AdapterDescriptor}; use async_trait::async_trait; -use btleplug::api::{Peripheral as _, ScanFilter}; +use btleplug::api::{ ScanFilter}; use btleplug::{ api::{BDAddr, Central, Manager as _}, platform::{Adapter, Manager, Peripheral}, }; use anyhow::{anyhow, Result}; -use btleplug::api::{Characteristic, PeripheralProperties, ValueNotification}; +use btleplug::api::{Characteristic, ValueNotification}; use futures::stream::Stream; use futures::StreamExt; use log::{trace, warn}; @@ -28,6 +28,7 @@ use tokio::sync::RwLock; use tokio::task::JoinHandle; use uuid::Uuid; + /// API for Bluetooth operations. #[async_trait] pub trait BluetoothApi: Send + Sync { @@ -57,6 +58,7 @@ pub trait BluetoothApi: Send + Sync { } // Define a trait for the Bluetooth Adapter API +//#[cfg_attr(test, automock)] #[async_trait] pub trait BluetoothAdapterApi: Send + Sync { async fn start_scan(&self, filter: ScanFilter) -> Result<()>; @@ -72,6 +74,7 @@ pub trait AdapterDiscovery { } // Define a trait for the Bluetooth Peripheral API +//#[cfg_attr(test, automock)] #[async_trait] pub trait BluetoothPeripheralApi: Send + Sync { fn address(&self) -> BDAddr; @@ -84,7 +87,7 @@ pub trait BluetoothPeripheralApi: Send + Sync { async fn get_name(&self) -> Result; } -#[async_trait::async_trait] +#[async_trait] impl BluetoothAdapterApi for Adapter { async fn start_scan(&self, filter: ScanFilter) -> Result<()> { Central::start_scan(self, filter) @@ -122,7 +125,7 @@ impl AdapterDiscovery for Adapter { } } -#[async_trait::async_trait] +#[async_trait] impl BluetoothPeripheralApi for Peripheral { fn address(&self) -> BDAddr { btleplug::api::Peripheral::address(self) @@ -396,3 +399,50 @@ impl + 'static> BluetoothAp Ok(()) } } +#[cfg(test)] + mod tests { + use mockall::{mock, predicate::eq}; + use tokio::sync::broadcast; + use anyhow::Result; + use crate::model::bluetooth::MockBluetoothModelApi; + + use super::*; + mock!{ + Adapter{} + + #[async_trait] + impl BluetoothAdapterApi for Adapter{ + async fn start_scan(&self, filter: ScanFilter) -> Result<()>; + async fn stop_scan(&self) -> Result<()>; + async fn peripherals(&self) -> Result>>; + async fn get_name(&self) -> Result; + } + + #[async_trait] + impl AdapterDiscovery for Adapter{ + async fn discover_adapters() -> Result>; + } + impl Clone for Adapter{ + fn clone(&self) -> Self; + } + } + + #[tokio::test] + async fn test_discover_adapters() { + let ctx = MockAdapter::discover_adapters_context(); + ctx.expect().times(1..).returning(||{ + let mut adapter = MockAdapter::new(); + adapter.expect_get_name().times(1..).returning( ||Ok("Test Adapter".to_string())); + Ok(vec![adapter]) + } ); + let mut mock_model = MockBluetoothModelApi::new(); + mock_model.expect_set_adapters().return_const(()); + + let (tx, _rx) = broadcast::channel(16); + let model = Arc::new(RwLock::new(mock_model)); + let mut controller = BluetoothController::::new(model, tx); + controller.discover_adapters().await.unwrap(); + } + + + } \ No newline at end of file diff --git a/src/model/bluetooth.rs b/src/model/bluetooth.rs index 3afd136..197ab16 100644 --- a/src/model/bluetooth.rs +++ b/src/model/bluetooth.rs @@ -12,6 +12,9 @@ use serde::{Deserialize, Serialize}; use std::fmt; use std::fmt::Debug; use uuid::Uuid; +#[cfg(test)] +use mockall::automock; + /// Helper macro to check if a specific bit is set in a byte. macro_rules! is_bit_set { @@ -232,6 +235,7 @@ impl PartialOrd for AdapterDescriptor { /// - Managing Bluetooth adapters and their selection /// - Tracking discovered devices /// - Managing device scanning and connection states +#[cfg_attr(test, automock)] pub trait BluetoothModelApi: Debug + Send + Sync { /// Gets the list of Bluetooth adapters as a vector of `(Name, UUID)` tuples. /// From 27a3da3c2401c4ef41a20bfaad31d106d77a3f3a Mon Sep 17 00:00:00 2001 From: Mattis Kieffer Date: Fri, 6 Dec 2024 02:25:53 +0100 Subject: [PATCH 3/3] add mocked unit tests --- src/controller/bluetooth.rs | 203 ++++++++++++++++++++++++++++-------- src/model/bluetooth.rs | 9 +- 2 files changed, 165 insertions(+), 47 deletions(-) diff --git a/src/controller/bluetooth.rs b/src/controller/bluetooth.rs index 6ec816f..fb7d748 100644 --- a/src/controller/bluetooth.rs +++ b/src/controller/bluetooth.rs @@ -9,7 +9,7 @@ use crate::model::bluetooth::{BluetoothModelApi, DeviceDescriptor, HeartrateMess use crate::model::storage::ModelHandle; use crate::{core::events::AppEvent, model::bluetooth::AdapterDescriptor}; use async_trait::async_trait; -use btleplug::api::{ ScanFilter}; +use btleplug::api::ScanFilter; use btleplug::{ api::{BDAddr, Central, Manager as _}, platform::{Adapter, Manager, Peripheral}, @@ -28,7 +28,6 @@ use tokio::sync::RwLock; use tokio::task::JoinHandle; use uuid::Uuid; - /// API for Bluetooth operations. #[async_trait] pub trait BluetoothApi: Send + Sync { @@ -79,6 +78,7 @@ pub trait AdapterDiscovery { pub trait BluetoothPeripheralApi: Send + Sync { fn address(&self) -> BDAddr; async fn connect(&self) -> Result<()>; + #[allow(dead_code)] async fn disconnect(&self) -> Result<()>; async fn discover_services(&self) -> Result<()>; fn characteristics(&self) -> Result>; @@ -400,49 +400,164 @@ impl + 'static> BluetoothAp } } #[cfg(test)] - mod tests { - use mockall::{mock, predicate::eq}; - use tokio::sync::broadcast; - use anyhow::Result; - use crate::model::bluetooth::MockBluetoothModelApi; - - use super::*; - mock!{ - Adapter{} - - #[async_trait] - impl BluetoothAdapterApi for Adapter{ - async fn start_scan(&self, filter: ScanFilter) -> Result<()>; - async fn stop_scan(&self) -> Result<()>; - async fn peripherals(&self) -> Result>>; - async fn get_name(&self) -> Result; - } - - #[async_trait] - impl AdapterDiscovery for Adapter{ - async fn discover_adapters() -> Result>; - } - impl Clone for Adapter{ - fn clone(&self) -> Self; - } +mod tests { + use crate::model::bluetooth::MockBluetoothModelApi; + use anyhow::Result; + use mockall::mock; + use tokio::sync::broadcast; + + use super::*; + mock! { + Adapter{} + + #[async_trait] + impl BluetoothAdapterApi for Adapter { + async fn start_scan(&self, filter: ScanFilter) -> Result<()>; + async fn stop_scan(&self) -> Result<()>; + async fn peripherals(&self) -> Result>>; + async fn get_name(&self) -> Result; } - #[tokio::test] - async fn test_discover_adapters() { - let ctx = MockAdapter::discover_adapters_context(); - ctx.expect().times(1..).returning(||{ - let mut adapter = MockAdapter::new(); - adapter.expect_get_name().times(1..).returning( ||Ok("Test Adapter".to_string())); - Ok(vec![adapter]) - } ); - let mut mock_model = MockBluetoothModelApi::new(); - mock_model.expect_set_adapters().return_const(()); - - let (tx, _rx) = broadcast::channel(16); - let model = Arc::new(RwLock::new(mock_model)); - let mut controller = BluetoothController::::new(model, tx); - controller.discover_adapters().await.unwrap(); + #[async_trait] + impl AdapterDiscovery for Adapter { + async fn discover_adapters() -> Result>; } + impl Clone for Adapter { + fn clone(&self) -> Self; + } + } + + #[tokio::test] + async fn test_discover_adapters() { + let ctx = MockAdapter::discover_adapters_context(); + ctx.expect().times(1..).returning(|| { + let mut adapter = MockAdapter::new(); + adapter + .expect_get_name() + .times(1..) + .returning(|| Ok("Test Adapter".to_string())); + Ok(vec![adapter]) + }); + let mut mock_model = MockBluetoothModelApi::new(); + mock_model.expect_set_adapters().return_const(()); + + let (tx, _rx) = broadcast::channel(16); + let model = Arc::new(RwLock::new(mock_model)); + let mut controller = BluetoothController::::new(model, tx); + controller.discover_adapters().await.unwrap(); + } + + #[tokio::test] + async fn test_select_adapter() { + let mut mock_model = MockBluetoothModelApi::new(); + mock_model.expect_select_adapter().returning(|_x| Ok(())); + let (tx, _rx) = broadcast::channel(16); + let model = Arc::new(RwLock::new(mock_model)); + let controller = BluetoothController::::new(model, tx); + + let adapter_desc = AdapterDescriptor::new("Test Adapter".to_string()); + controller.select_adapter(&adapter_desc).await.unwrap(); + } + + #[tokio::test] + async fn test_start_scan() { + let mut mock_adapter = MockAdapter::new(); + mock_adapter.expect_start_scan().returning(|_| Ok(())); + mock_adapter + .expect_get_name() + .returning(|| Ok("Test Adapter".to_string())); + mock_adapter.expect_peripherals().returning(|| Ok(vec![])); + mock_adapter.expect_clone().returning(|| { + let mut adapter = MockAdapter::new(); + adapter + .expect_get_name() + .times(1..) + .returning(|| Ok("Test Adapter".to_string())); + adapter + }); + + let mut mock_model = MockBluetoothModelApi::new(); + mock_model.expect_is_scanning().return_const(false); + mock_model.expect_set_devices().return_const(()); + mock_model.expect_set_adapters().return_const(()); + + let (tx, _rx) = broadcast::channel(16); + let model = Arc::new(RwLock::new(mock_model)); + let mut controller = BluetoothController::::new(model.clone(), tx); + let desc = Uuid::new_v4(); + let ad = AdapterDescriptor::new_with_uuid("Test Adapter".to_string(), desc); + controller.adapters.insert(desc, mock_adapter); + model + .write() + .await + .expect_get_selected_adapter() + .return_const(Some(ad)); + + controller.start_scan().await.unwrap(); + } + + #[tokio::test] + async fn test_stop_scan() { + let mut mock_adapter = MockAdapter::new(); + mock_adapter.expect_stop_scan().returning(|| Ok(())); + mock_adapter + .expect_get_name() + .returning(|| Ok("Test Adapter".to_string())); + mock_adapter.expect_peripherals().returning(|| Ok(vec![])); + mock_adapter.expect_clone().returning(|| { + let mut adapter = MockAdapter::new(); + adapter + .expect_get_name() + .times(1..) + .returning(|| Ok("Test Adapter".to_string())); + adapter + }); + + let mut mock_model = MockBluetoothModelApi::new(); + mock_model.expect_is_scanning().return_const(true); + mock_model.expect_set_devices().return_const(()); + mock_model.expect_set_adapters().return_const(()); + + let (tx, _rx) = broadcast::channel(16); + let model = Arc::new(RwLock::new(mock_model)); + let mut controller = BluetoothController::::new(model.clone(), tx); + let desc = Uuid::new_v4(); + let ad = AdapterDescriptor::new_with_uuid("Test Adapter".to_string(), desc); + controller.adapters.insert(desc, mock_adapter); + model + .write() + .await + .expect_get_selected_adapter() + .return_const(Some(ad)); + + controller.stop_scan().await.unwrap(); + } + + #[tokio::test] + async fn test_select_peripheral() { + let mut mock_model = MockBluetoothModelApi::new(); + mock_model.expect_select_device().return_const(()); + + let (tx, _rx) = broadcast::channel(16); + let model = Arc::new(RwLock::new(mock_model)); + let controller = BluetoothController::::new(model, tx); + + let device_desc = DeviceDescriptor { + name: "Test Device".to_string(), + address: BDAddr::from_str_delim("00:11:22:33:44:55").unwrap(), + }; + controller.select_peripheral(&device_desc).await.unwrap(); + } - - } \ No newline at end of file + #[tokio::test] + async fn test_stop_listening() { + let mut mock_model = MockBluetoothModelApi::new(); + mock_model.expect_set_listening().return_const(()); + + let (tx, _rx) = broadcast::channel(16); + let model = Arc::new(RwLock::new(mock_model)); + let mut controller = BluetoothController::::new(model, tx); + + controller.stop_listening().await.unwrap(); + } +} diff --git a/src/model/bluetooth.rs b/src/model/bluetooth.rs index 197ab16..373f8e1 100644 --- a/src/model/bluetooth.rs +++ b/src/model/bluetooth.rs @@ -8,13 +8,12 @@ use anyhow::{anyhow, Result}; use btleplug::api::BDAddr; +#[cfg(test)] +use mockall::automock; use serde::{Deserialize, Serialize}; use std::fmt; use std::fmt::Debug; use uuid::Uuid; -#[cfg(test)] -use mockall::automock; - /// Helper macro to check if a specific bit is set in a byte. macro_rules! is_bit_set { @@ -211,6 +210,10 @@ impl AdapterDescriptor { uuid: Uuid::new_v4(), } } + #[cfg(test)] + pub fn new_with_uuid(name: String, uuid: Uuid) -> Self { + Self { name, uuid } + } pub fn get_name(&self) -> &str { &self.name }