Skip to content

Commit

Permalink
Merge pull request #2 from mat-kie/version-0.3.4
Browse files Browse the repository at this point in the history
Version 0.3.4
  • Loading branch information
mat-kie authored Dec 5, 2024
2 parents ba9413d + e165443 commit fc40ba2
Show file tree
Hide file tree
Showing 16 changed files with 737 additions and 289 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "hrv-rs"
version = "0.3.3"
version = "0.3.4"
edition = "2021"

[features]
Expand Down
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# Hrv-rs
[![Pipeline Status](https://github.com/mat-kie/hrv-rs/actions/workflows/rust.yml/badge.svg)](https://github.com/mat-kie/hrv-rs/actions/workflows/rust.yml)
[![Coverage](https://codecov.io/gh/mat-kie/hrv-rs/branch/main/graph/badge.svg?token=YOUR_CODECOV_TOKEN)](https://codecov.io/gh/mat-kie/hrv-rs)
Expand Down Expand Up @@ -54,22 +53,27 @@ The project uses a modular, event-driven MVC architecture.
### Modules

#### 1. **Core**
- **Constants**: Application-wide constants.
- **Events**: Defines application events, including user interactions and system updates.
- **Macros**: Provides utility macros to simplify error handling and data extraction.
- **View Trait**: Trait definitions for views.

#### 2. **Controller**
- **Acquisition**: Handles data acquisition from BLE devices.
- **Bluetooth**: Manages Bluetooth adapters, device discovery, and communication.
- **Application**: Orchestrates application logic, including transitions between views.
- **Bluetooth**: Manages Bluetooth adapters, device discovery, and communication.

#### 3. **Model**
- **Acquisition**: Handles runtime and stored data related to HRV measurement sessions.
- **Bluetooth**: Represents BLE devices, adapters, and connections.
- **HRV**: Structures and methods for storing, calculating, and retrieving HRV statistics.
- **Acquisition**: Handles runtime and stored data related to HRV measurement sessions.
- **Storage**: General data storage and processing.

#### 4. **Math**
- **HRV**: Functions and structures for HRV computation.

#### 4. **View**
- **Bluetooth**: UI for managing Bluetooth connections.
- **HRV Analysis**: Displays computed HRV statistics and visualizes Poincaré plots.
#### 5. **View**
- **Acquisition**: UI for managing Bluetooth connections and data acquisition.
- **Overview**: Displays computed HRV statistics and visualizes Poincaré plots.
- **Manager**: Coordinates transitions between views and manages their lifecycle.

## License
Expand Down
220 changes: 208 additions & 12 deletions src/controller/acquisition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ use crate::{
core::events::{AppEvent, BluetoothEvent},
model::{
acquisition::AcquisitionModelApi,
storage::{ModelHandle, StorageModel, StorageModelApi},
storage::{ModelHandle, StorageModelApi},
},
};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use log::error;
use serde::{de::DeserializeOwned, Serialize};
use time::Duration;
use tokio::{
sync::{
Expand All @@ -29,17 +28,22 @@ use tokio::{
/// It provides methods for starting, storing, and discarding acquisitions, as well as handling events.
#[async_trait]
pub trait DataAcquisitionApi {
/// Start recording an Acquisition, returns the model handle and the sender for the events
/// Start recording an Acquisition, returns the model handle and the sender for the events.
fn start_acquisition(&mut self) -> Result<ModelHandle<dyn AcquisitionModelApi>>;
/// stop the current acquisition
/// Stop the current acquisition.
fn stop_acquisition(&mut self) -> Result<()>;

/// Discard the current acquisition.
fn discard_acquisition(&mut self) -> Result<()>;
/// Store the current acquisition asynchronously.
async fn store_acquisition(&mut self) -> Result<()>;

/// Set the active acquisition by index asynchronously.
async fn set_active_acq(&mut self, idx: usize) -> Result<()>;

/// Set the statistics window for the active acquisition asynchronously.
async fn set_stats_window(&mut self, window: &Duration) -> Result<()>;
/// Set the outlier filter value for the active acquisition asynchronously.
async fn set_outlier_filter_value(&mut self, filter: f64) -> Result<()>;
}

Expand All @@ -48,23 +52,35 @@ pub trait DataAcquisitionApi {
///
/// # Type Parameters
/// * `AMT` - A type that implements the `AcquisitionModelApi` trait, representing the underlying data model.
pub struct AcquisitionController<AMT: AcquisitionModelApi + Default> {
pub struct AcquisitionController<AMT, SMT>
where
AMT: AcquisitionModelApi + Default + 'static,
SMT: StorageModelApi<AcqModelType = AMT> + Send + Sync,
{
/// A thread-safe, shared reference to the acquisition model.
model: Arc<RwLock<StorageModel<AMT>>>,
model: Arc<RwLock<SMT>>,
/// A sender for broadcasting application events.
event_bus: Sender<AppEvent>,
/// A handle for the listener task.
listener_handle: Option<JoinHandle<()>>,
/// The currently active acquisition, if any.
active_acquisition: Option<Arc<RwLock<AMT>>>,
}

impl<AMT: AcquisitionModelApi + Default> AcquisitionController<AMT> {
impl<AMT, SMT> AcquisitionController<AMT, SMT>
where
AMT: AcquisitionModelApi + Default + 'static,
SMT: StorageModelApi<AcqModelType = AMT> + Send + Sync,
{
/// Creates a new `AcquisitionController` instance.
///
/// # Arguments
/// * `model` - An `Arc<Mutex<AMT>>` representing the thread-safe shared model.
/// * `model` - An `Arc<RwLock<SMT>>` representing the thread-safe shared model.
/// * `event_bus` - A `Sender<AppEvent>` for broadcasting application events.
///
/// # Returns
/// A new instance of `AcquisitionController`.
pub fn new(model: Arc<RwLock<StorageModel<AMT>>>, event_bus: Sender<AppEvent>) -> Self {
pub fn new(model: Arc<RwLock<SMT>>, event_bus: Sender<AppEvent>) -> Self {
Self {
model,
event_bus,
Expand All @@ -73,6 +89,11 @@ impl<AMT: AcquisitionModelApi + Default> AcquisitionController<AMT> {
}
}

/// Listens for messages on the event bus and processes them.
///
/// # Arguments
/// * `acq` - A shared reference to the active acquisition model.
/// * `channel` - A receiver for application events.
async fn msg_listener(acq: Arc<RwLock<AMT>>, mut channel: Receiver<AppEvent>) {
loop {
match channel.recv().await {
Expand All @@ -90,6 +111,10 @@ impl<AMT: AcquisitionModelApi + Default> AcquisitionController<AMT> {
}
}

/// Retrieves the currently active acquisition.
///
/// # Returns
/// A reference to the active acquisition, or an error if none is active.
fn get_active_acq(&self) -> Result<&Arc<RwLock<AMT>>> {
self.active_acquisition
.as_ref()
Expand All @@ -98,8 +123,10 @@ impl<AMT: AcquisitionModelApi + Default> AcquisitionController<AMT> {
}

#[async_trait]
impl<AMT: AcquisitionModelApi + Default + Serialize + DeserializeOwned + 'static> DataAcquisitionApi
for AcquisitionController<AMT>
impl<AMT, SMT> DataAcquisitionApi for AcquisitionController<AMT, SMT>
where
AMT: AcquisitionModelApi + Default + 'static,
SMT: StorageModelApi<AcqModelType = AMT> + Send + Sync + 'static,
{
async fn set_active_acq(&mut self, idx: usize) -> Result<()> {
let model = self.model.read().await;
Expand All @@ -110,7 +137,7 @@ impl<AMT: AcquisitionModelApi + Default + Serialize + DeserializeOwned + 'static
Err(anyhow!(
"requested an out of bounds acquisition index: {}, #acquisitions: {}",
idx,
model.get_acquisitions().len()
model.get_mut_acquisitions().len()
))
}
}
Expand Down Expand Up @@ -151,6 +178,7 @@ impl<AMT: AcquisitionModelApi + Default + Serialize + DeserializeOwned + 'static
}
Ok(())
}

async fn store_acquisition(&mut self) -> Result<()> {
self.stop_acquisition()?;
if let Some(acq) = self.active_acquisition.take() {
Expand All @@ -163,3 +191,171 @@ impl<AMT: AcquisitionModelApi + Default + Serialize + DeserializeOwned + 'static
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{acquisition::MockAcquisitionModelApi, storage::MockStorageModelApi};

use tokio::sync::broadcast;
#[tokio::test]
async fn test_start_acquisition() {
let (tx, _rx) = broadcast::channel(16);
let model = Arc::new(RwLock::new(MockStorageModelApi::default()));
let mut controller = AcquisitionController::new(model, tx);

let result = controller.start_acquisition();
assert!(result.is_ok());
assert!(controller.active_acquisition.is_some());
}

#[tokio::test]
async fn test_stop_acquisition() {
let (tx, _rx) = broadcast::channel(16);
let model = Arc::new(RwLock::new(MockStorageModelApi::default()));
let mut controller = AcquisitionController::new(model, tx);

controller.start_acquisition().unwrap();
let result = controller.stop_acquisition();
assert!(result.is_ok());
assert!(controller.listener_handle.is_none());
}

#[tokio::test]
async fn test_discard_acquisition() {
let (tx, _rx) = broadcast::channel(16);
let model = Arc::new(RwLock::new(MockStorageModelApi::default()));
let mut controller = AcquisitionController::new(model, tx);

controller.start_acquisition().unwrap();
let result = controller.discard_acquisition();
assert!(result.is_ok());
assert!(controller.active_acquisition.is_none());
}

#[tokio::test]
async fn test_store_acquisition() {
let (tx, _rx) = broadcast::channel(16);
let model = Arc::new(RwLock::new(MockStorageModelApi::default()));
model
.write()
.await
.expect_store_acquisition()
.once()
.return_const(());
let mut controller = AcquisitionController::new(model, tx);

controller.start_acquisition().unwrap();
let result = controller.store_acquisition().await;
assert!(result.is_ok());
assert!(controller.active_acquisition.is_none());
let result = controller.store_acquisition().await;
assert!(result.is_err());
assert!(controller.active_acquisition.is_none());
}

#[tokio::test]
async fn test_set_active_acq() {
let (tx, _rx) = broadcast::channel(16);
let model = Arc::new(RwLock::new(MockStorageModelApi::default()));
model
.write()
.await
.expect_get_mut_acquisitions()
.times(1..)
.return_const(Vec::default());
let mut controller = AcquisitionController::new(model, tx);
controller.start_acquisition().unwrap();
let result = controller.set_active_acq(0).await;
assert!(controller.active_acquisition.is_some());
assert!(result.is_err());
}

#[tokio::test]
async fn test_set_active_acq_nonzero() {
let (tx, _rx) = broadcast::channel(16);
let vec = vec![Arc::new(RwLock::new(MockAcquisitionModelApi::default()))];
let model = Arc::new(RwLock::new(MockStorageModelApi::default()));
model
.write()
.await
.expect_get_mut_acquisitions()
.times(1..)
.return_const(vec);
let mut controller = AcquisitionController::new(model, tx);
let result = controller.set_active_acq(0).await;
assert!(result.is_ok());
assert!(controller.active_acquisition.is_some());
let result = controller.set_active_acq(1).await;
assert!(result.is_err());
}

#[tokio::test]
async fn test_set_stats_window() {
let (tx, _rx) = broadcast::channel(16);
let model = Arc::new(RwLock::new(MockStorageModelApi::default()));
let mut controller = AcquisitionController::new(model, tx);

controller.start_acquisition().unwrap();
let window = Duration::seconds(60);

controller
.get_active_acq()
.unwrap()
.write()
.await
.expect_set_stats_window()
.once()
.returning(|x| {
assert_eq!(x, &Duration::seconds(60));
Ok(())
});

let result = controller.set_stats_window(&window).await;
assert!(result.is_ok());
}

#[tokio::test]
async fn test_set_outlier_filter_value() {
let (tx, _rx) = broadcast::channel(16);
let model = Arc::new(RwLock::new(MockStorageModelApi::default()));
let mut controller = AcquisitionController::new(model, tx);

controller.start_acquisition().unwrap();

controller
.get_active_acq()
.unwrap()
.write()
.await
.expect_set_outlier_filter_value()
.once()
.returning(|x| {
assert_eq!(x, 1.5);
Ok(())
});

let result = controller.set_outlier_filter_value(1.5).await;
assert!(result.is_ok());
}

#[tokio::test]
async fn test_get_active_acq() {
let (tx, _rx) = broadcast::channel(16);
let model = Arc::new(RwLock::new(MockStorageModelApi::default()));
let mut controller = AcquisitionController::new(model, tx);

controller.start_acquisition().unwrap();
let result = controller.get_active_acq();
assert!(result.is_ok());
}

#[tokio::test]
async fn test_get_active_acq_none() {
let (tx, _rx) = broadcast::channel(16);
let model = Arc::new(RwLock::new(MockStorageModelApi::default()));
let controller = AcquisitionController::new(model, tx);

let result = controller.get_active_acq();
assert!(result.is_err());
}
}
Loading

0 comments on commit fc40ba2

Please sign in to comment.