diff --git a/src/app.rs b/src/app.rs index 2963ba0..faa9b7a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -20,7 +20,7 @@ use egui_dock::{DockArea, DockState, Style, TabViewer}; use egui_extras::{TableBuilder, Column}; use crate::state::{AppState, ControlCommand, HardwareConnected, HardwareState, MeasurementDataState, Mode}; -use crate::logging::LoggingStates; +use crate::logging::LoggingState; use crate::signals::LoggingSignal; use crate::icd::{BioImpedanceLeadMode, IcdDftNum, SweepPoints, ElectrodeOptionsWithMultiplexer}; @@ -52,15 +52,15 @@ pub struct App { tree: DockState, tab_viewer: CustomTabs, // tab_active: TabActive, - log_tx: mpsc::Sender, - pub gui_logging_state: Arc>, log_filename: Arc>, log_marker_modal: bool, log_marker: String, measurement_data: Arc, control_tx: mpsc::Sender, + logging_control_tx: mpsc::Sender, app_state_rx: sync::watch::Receiver, - hardware_state_rx: sync::watch::Receiver + hardware_state_rx: sync::watch::Receiver, + logging_state_rx: sync::watch::Receiver, } struct CustomTabs { @@ -616,11 +616,13 @@ impl TabViewer for CustomTabs { } impl App { - pub fn new(log_tx: mpsc::Sender, - measurement_data: Arc, + pub fn new(measurement_data: Arc, control_tx: mpsc::Sender, + logging_control_tx: mpsc::Sender, app_state_rx: watch::Receiver, - hardware_state_rx: watch::Receiver) -> Self { + hardware_state_rx: watch::Receiver, + logging_state_rx: watch::Receiver + ) -> Self { // Step 1: Initialize shared fields first // let tab_active = app_state_rx.borrow().tab_active; @@ -647,15 +649,15 @@ impl App { tree: DockState::new(vec!["Single".to_string(), "Sweep".to_string(), "Shortcuts".to_string()]), tab_viewer, // tab_active, - log_tx, - gui_logging_state: Arc::new(Mutex::new(LoggingStates::Idle)), log_filename, log_marker_modal: false, log_marker: String::new(), measurement_data, control_tx, + logging_control_tx, app_state_rx, hardware_state_rx, + logging_state_rx, }; // For testing purposes, populate the Bode plot with a sample low-pass filter response @@ -745,17 +747,17 @@ impl eframe::App for App { ui.add_enabled(is_connected, Label::new("Logging:")); - let gui_logging_state = *self.gui_logging_state.lock().unwrap(); + let gui_logging_state = *self.logging_state_rx.borrow(); - let mut logging_enabled = !matches!(gui_logging_state, LoggingStates::Idle); + let mut logging_enabled = !matches!(gui_logging_state, LoggingState::Idle); if ui.add_enabled(is_connected, custom_toggle_widget(&mut logging_enabled)).changed() { let signal = match gui_logging_state { - LoggingStates::Idle => LoggingSignal::StartFileLogging(self.log_filename.lock().unwrap().clone()), - LoggingStates::Starting | LoggingStates::Logging => LoggingSignal::StopFileLogging, + LoggingState::Idle => LoggingSignal::StartFileLogging(self.log_filename.lock().unwrap().clone()), + LoggingState::Starting | LoggingState::Logging => LoggingSignal::StopFileLogging, }; - self.log_tx.try_send(signal).unwrap_or_else(|e| { + self.logging_control_tx.try_send(signal).unwrap_or_else(|e| { error!("Failed to send logging toggle signal: {:?}", e); }); }; @@ -796,8 +798,7 @@ impl eframe::App for App { let enter_pressed = text_edit_response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)); if add_clicked || enter_pressed { - info!("Adding marker: {}", self.log_marker); - self.log_tx.try_send(LoggingSignal::AddMarker(self.log_marker.clone())).unwrap_or_else(|e| { + self.logging_control_tx.try_send(LoggingSignal::AddMarker(self.log_marker.clone())).unwrap_or_else(|e| { error!("Failed to send logging marker signal: {:?}", e); }); self.log_marker = String::new(); @@ -817,7 +818,7 @@ impl eframe::App for App { ui.separator(); - ui.add_enabled_ui(gui_logging_state == LoggingStates::Idle, |ui| { + ui.add_enabled_ui(gui_logging_state == LoggingState::Idle, |ui| { ui.label("Log filename:"); TextEdit::singleline(&mut *self.log_filename.lock().unwrap()).desired_width(150.0).id(Id::new("file_name_field")).ui(ui); }); @@ -894,25 +895,25 @@ impl eframe::App for App { } // Toggle marker modal - if ctx.input(|i| i.key_pressed(egui::Key::A)) && *self.gui_logging_state.lock().unwrap() == LoggingStates::Logging { + if ctx.input(|i| i.key_pressed(egui::Key::A)) && *self.logging_state_rx.borrow() == LoggingState::Logging { self.log_marker_modal = !self.log_marker_modal; } // Enable/disable GUI logging if ctx.input(|i| i.key_pressed(egui::Key::L)) { - let gui_logging_enabled = *self.gui_logging_state.lock().unwrap(); + let gui_logging_enabled = *self.logging_state_rx.borrow(); match gui_logging_enabled { - LoggingStates::Starting => { + LoggingState::Starting => { // If currently starting, do nothing return; }, - LoggingStates::Logging => { - if let Err(e) = self.log_tx.try_send(LoggingSignal::StopFileLogging) { + LoggingState::Logging => { + if let Err(e) = self.logging_control_tx.try_send(LoggingSignal::StopFileLogging) { error!("Failed to send logging signal: {:?}", e); } }, - LoggingStates::Idle => { - if let Err(e) = self.log_tx.try_send(LoggingSignal::StartFileLogging(self.log_filename.lock().unwrap().clone())) { + LoggingState::Idle => { + if let Err(e) = self.logging_control_tx.try_send(LoggingSignal::StartFileLogging(self.log_filename.lock().unwrap().clone())) { error!("Failed to send logging signal: {:?}", e); } }, diff --git a/src/bin/main_gui.rs b/src/bin/main_gui.rs index a1470bc..10b266d 100644 --- a/src/bin/main_gui.rs +++ b/src/bin/main_gui.rs @@ -14,7 +14,7 @@ use bioz_host_rs::communication::communicate_with_hardware; use tokio::sync::{watch, mpsc}; use bioz_host_rs::signals::StartStopSignal; -use bioz_host_rs::logging::log_data; +use bioz_host_rs::logging::{LoggingState, log_data}; use bioz_host_rs::state::{AppState, ControlCommand, HardwareState, MeasurementDataState}; use bioz_host_rs::control::app_control_loop; @@ -25,65 +25,62 @@ async fn main() { log::set_max_level(log::LevelFilter::Info); info!("Starting Bioz Impedance Visualizer..."); - // Enter the runtime so that `tokio::spawn` is available immediately. - // let _enter = rt.enter(); - - // Init watches for hardware state and app state - let (hardware_state_tx, hardware_state_rx) = watch::channel(HardwareState::default()); + // Init watches for hardware state, app state and logging state let (app_state_tx, app_state_rx) = watch::channel(AppState::default()); + let (hardware_state_tx, hardware_state_rx) = watch::channel(HardwareState::default()); + let (logging_state_tx, logging_state_rx) = watch::channel(LoggingState::default()); - // Init control channels to update hardware and app - let (hardware_control_tx, hardware_control_rx) = mpsc::channel::(2); + // Init control channels to update hardware, app and logging let (app_control_tx, app_control_rx) = mpsc::channel::(32); - - // let run_impedancemeter_tx_clone = run_impedancemeter_tx.clone(); - let hardware_state_rx_clone = hardware_state_rx.clone(); - let hardware_control_tx_clone = hardware_control_tx.clone(); - let app_control_tx_clone = app_control_tx.clone(); - - tokio::spawn(async move { - app_control_loop(app_control_rx, app_state_tx, hardware_state_rx, hardware_control_tx_clone).await; - }); + let (hardware_control_tx, hardware_control_rx) = mpsc::channel::(2); + let (logging_control_tx, logging_control_rx) = mpsc::channel::(10); // Init measurement data state let measurement_data = Arc::new(MeasurementDataState::default()); - - // Logging - let (log_tx, log_rx) = mpsc::channel::(10); - let log_tx_clone = log_tx.clone(); - let measurement_data_clone = measurement_data.clone(); - - let app = App::new(log_tx, measurement_data_clone, app_control_tx_clone, app_state_rx, hardware_state_rx_clone); - - let gui_logging_state_1 = app.gui_logging_state.clone(); - let gui_logging_state_2 = app.gui_logging_state.clone(); - - // Log thread - tokio::spawn(async move { log_data(log_rx, gui_logging_state_1).await }); - - // TCP command server in separate thread - let rt = Runtime::new().expect("Unable to create Runtime"); - - let app_control_tx_clone = app_control_tx.clone(); - std::thread::spawn(move || { - rt.block_on(bioz_host_rs::tcp::tcp_command_server(app_control_tx_clone)); + // Start control loop + let hardware_control_tx_clone = hardware_control_tx.clone(); + let hardware_state_rx_clone = hardware_state_rx.clone(); + tokio::spawn(async move { + app_control_loop(app_control_rx, app_state_tx, hardware_control_tx_clone, hardware_state_rx_clone).await; }); - // Execute the runtime in its own thread. + // Start hardware communication loop in separate thread let rt = Runtime::new().expect("Unable to create Runtime"); + let logging_control_tx_clone = logging_control_tx.clone(); + let logging_state_rx_clone = logging_state_rx.clone(); std::thread::spawn(move || { rt.block_on(communicate_with_hardware( hardware_control_rx, hardware_control_tx, measurement_data, hardware_state_tx, - gui_logging_state_2, - log_tx_clone, + logging_control_tx_clone, + logging_state_rx_clone, )); }); + // Start logging loop + tokio::spawn(async move { log_data(logging_control_rx, logging_state_tx).await }); + + // Start TCP command server in separate thread + let rt = Runtime::new().expect("Unable to create Runtime"); + + let app_control_tx_clone = app_control_tx.clone(); + std::thread::spawn(move || { + rt.block_on(bioz_host_rs::tcp::tcp_command_server(app_control_tx_clone)); + }); + + // Create the app and run it in the main thread + let app = App::new( + measurement_data_clone, + app_control_tx, + logging_control_tx, + app_state_rx, + hardware_state_rx, + logging_state_rx); + // Run the GUI in the main thread. let mut native_options = NativeOptions::default(); native_options.viewport.inner_size = Some(Vec2::new(850.0, 600.0)); diff --git a/src/communication.rs b/src/communication.rs index aa359bd..bf29daa 100644 --- a/src/communication.rs +++ b/src/communication.rs @@ -16,7 +16,7 @@ use crate::state::{HardwareState, MeasurementDataState}; use crate::icd; use crate::client::WorkbookClient; -use crate::logging::LoggingStates; +use crate::logging::LoggingState; use crate::signals::{LoggingSignal, StartStopSignal}; @@ -27,8 +27,8 @@ pub async fn communicate_with_hardware( hardware_control_tx: Sender, measurement_data: Arc, hardware_state_tx: watch::Sender, - gui_logging_state: Arc>, - log_tx: Sender, + logging_control_tx: Sender, + logging_state_rx: watch::Receiver, ) { let data_counter = Arc::new(AtomicU32::new(0)); let data_counter_clone = data_counter.clone(); @@ -89,8 +89,8 @@ pub async fn communicate_with_hardware( let data_counter_clone_single = data_counter_clone.clone(); // Clone log_tx for the task - let gui_logging_state_clone = gui_logging_state.clone(); - let log_tx_clone = log_tx.clone(); + let logging_control_tx_clone = logging_control_tx.clone(); + let logging_state_rx_clone = logging_state_rx.clone(); let settings_clone = settings.clone(); @@ -111,11 +111,11 @@ pub async fn communicate_with_hardware( data_counter_clone_single.fetch_add(1, Ordering::Relaxed); } // Send logging signal - if *gui_logging_state_clone.lock().unwrap() == LoggingStates::Logging { + if *logging_state_rx_clone.borrow() == LoggingState::Logging { let settings = *settings_clone.lock().unwrap(); match settings.mode { Some(StartStopSignal::StartSingle(freq, _, _, _)) => { - if let Err(e) = log_tx_clone.try_send(LoggingSignal::SingleImpedance(SystemTime::now(), freq, val.magnitude, val.phase)) { + if let Err(e) = logging_control_tx_clone.try_send(LoggingSignal::SingleImpedance(SystemTime::now(), freq, val.magnitude, val.phase)) { error!("Failed to send logging signal: {:?}", e); } }, @@ -142,8 +142,8 @@ pub async fn communicate_with_hardware( let data_counter_clone_sweep = data_counter_clone.clone(); // Clone log_tx for the task - let gui_logging_state_clone = gui_logging_state.clone(); - let log_tx_clone = log_tx.clone(); + let logging_control_tx_clone = logging_control_tx.clone(); + let logging_state_rx_clone = logging_state_rx.clone(); tokio::spawn(async move { while let Ok(val) = sweep_impedance_sub.recv().await { @@ -158,8 +158,8 @@ pub async fn communicate_with_hardware( bode_plot.update_magnitudes(SweepPoints::Eight, magnitudes.clone()); bode_plot.update_phases(SweepPoints::Eight, phases.clone()); } - if *gui_logging_state_clone.lock().unwrap() == LoggingStates::Logging { - if let Err(e) = log_tx_clone.send(LoggingSignal::SweepImpedance(SystemTime::now(), SweepPoints::Eight.values().to_vec(), magnitudes.clone(), phases.clone())).await { + if *logging_state_rx_clone.borrow() == LoggingState::Logging { + if let Err(e) = logging_control_tx_clone.try_send(LoggingSignal::SweepImpedance(SystemTime::now(), SweepPoints::Eight.values().to_vec(), magnitudes.clone(), phases.clone())) { error!("Failed to send logging signal: {:?}", e); } } @@ -172,8 +172,8 @@ pub async fn communicate_with_hardware( bode_plot.update_magnitudes(SweepPoints::Eighteen, magnitudes.clone()); bode_plot.update_phases(SweepPoints::Eighteen, phases.clone()); } - if *gui_logging_state_clone.lock().unwrap() == LoggingStates::Logging { - if let Err(e) = log_tx_clone.send(LoggingSignal::SweepImpedance(SystemTime::now(), SweepPoints::Eighteen.values().to_vec(), magnitudes.clone(), phases.clone())).await { + if *logging_state_rx_clone.borrow() == LoggingState::Logging { + if let Err(e) = logging_control_tx_clone.try_send(LoggingSignal::SweepImpedance(SystemTime::now(), SweepPoints::Eighteen.values().to_vec(), magnitudes.clone(), phases.clone())) { error!("Failed to send logging signal: {:?}", e); } } @@ -184,7 +184,7 @@ pub async fn communicate_with_hardware( } }); - let log_tx_clone = log_tx.clone(); + let logging_control_tx_clone = logging_control_tx.clone(); loop { select! { Some(frequency) = hardware_control_rx.recv() => { @@ -199,8 +199,8 @@ pub async fn communicate_with_hardware( hardware_state_tx.send(hardware_state.clone()).unwrap(); // When logging add electrode configuration to logging file - if *gui_logging_state.lock().unwrap() == LoggingStates::Logging { - if let Err(e) = log_tx_clone.send(LoggingSignal::ElectrodeCongiguration(electrode_config)).await { + if *logging_state_rx.borrow() == LoggingState::Logging { + if let Err(e) = logging_control_tx_clone.try_send(LoggingSignal::ElectrodeCongiguration(electrode_config)) { error!("Failed to send logging signal: {:?}", e); } } @@ -238,8 +238,8 @@ pub async fn communicate_with_hardware( } // When logging add electrode configuration to logging file - if *gui_logging_state.lock().unwrap() == LoggingStates::Logging { - if let Err(e) = log_tx_clone.send(LoggingSignal::ElectrodeCongiguration(electrode_config)).await { + if *logging_state_rx.borrow() == LoggingState::Logging { + if let Err(e) = logging_control_tx_clone.try_send(LoggingSignal::ElectrodeCongiguration(electrode_config)) { error!("Failed to send logging signal: {:?}", e); } } diff --git a/src/control.rs b/src/control.rs index dadfb0a..db03894 100644 --- a/src/control.rs +++ b/src/control.rs @@ -6,8 +6,8 @@ use crate::state::Mode; pub async fn app_control_loop(mut app_control_rx: mpsc::Receiver, app_state_tx: watch::Sender, - hardware_state_rx: watch::Receiver, - hardware_control_tx: mpsc::Sender) { + hardware_control_tx: mpsc::Sender, + hardware_state_rx: watch::Receiver) { let mut state = AppState::default(); while let Some(cmd) = app_control_rx.recv().await { diff --git a/src/logging.rs b/src/logging.rs index 9b440f2..b33025f 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -1,10 +1,9 @@ use bioz_icd_rs::ElectrodeConfiguration; use log::info; -use std::sync::{Arc, Mutex}; use std::time::UNIX_EPOCH; -use tokio::sync::mpsc::Receiver; +use tokio::sync::{mpsc::Receiver, watch::Sender}; use tokio::fs::File; use tokio::io::AsyncWriteExt; @@ -13,22 +12,28 @@ use rfd::AsyncFileDialog; use crate::signals::LoggingSignal; #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum LoggingStates { +pub enum LoggingState { Idle, Starting, Logging, } +impl Default for LoggingState { + fn default() -> Self { + LoggingState::Idle + } +} + pub async fn log_data( - mut data: Receiver, - gui_logging_state: Arc>, + mut logging_control_rx: Receiver, + logging_state_tx: Sender, ) { let mut file: Option = None; let mut logging_marker = String::new(); let mut electrode_configuration = String::new(); loop { - match data.recv().await { + match logging_control_rx.recv().await { Some(signal) => { match signal { LoggingSignal::SingleImpedance(timestamp, frequency, magnitude, phase) => { @@ -75,7 +80,7 @@ pub async fn log_data( } LoggingSignal::StartFileLogging(filename) => { // Update global logging state - *gui_logging_state.lock().unwrap() = LoggingStates::Starting; + logging_state_tx.send(LoggingState::Starting).unwrap(); // Open a folder picker for the user let folder = match AsyncFileDialog::new() @@ -85,7 +90,7 @@ pub async fn log_data( Some(f) => f, None => { info!("File logging cancelled by user"); - *gui_logging_state.lock().unwrap() = LoggingStates::Idle; + logging_state_tx.send(LoggingState::Idle).unwrap(); continue; } }; @@ -123,7 +128,7 @@ pub async fn log_data( } // Update global logging state - *gui_logging_state.lock().unwrap() = LoggingStates::Logging; + logging_state_tx.send(LoggingState::Logging).unwrap(); } LoggingSignal::StopFileLogging => { if file.is_some() { @@ -131,9 +136,10 @@ pub async fn log_data( file = None; } // Update global logging state - *gui_logging_state.lock().unwrap() = LoggingStates::Idle; + logging_state_tx.send(LoggingState::Idle).unwrap(); } LoggingSignal::AddMarker(marker) => { + info!("Adding marker: {}", marker); logging_marker = marker; } LoggingSignal::ElectrodeCongiguration(config) => { diff --git a/src/state.rs b/src/state.rs index 9703d4c..a2ff5ba 100644 --- a/src/state.rs +++ b/src/state.rs @@ -2,7 +2,6 @@ use std::sync::{Arc, Mutex}; use crate::app::TabActive; use crate::icd::{BioImpedanceLeadMode, IcdDftNum, SweepPoints, ElectrodeConfiguration, ElectrodeOptionsWithMultiplexer}; -use crate::logging::LoggingStates; use crate::plot::{TimeSeriesPlot, BodePlot}; use atomic_float::AtomicF32; @@ -108,7 +107,6 @@ pub struct AppState { pub electrode_settings: ElectrodeSettings, pub sweep_points: SweepPoints, pub tab_active: TabActive, - pub logging: LoggingStates, } impl Default for AppState { @@ -121,7 +119,6 @@ impl Default for AppState { electrode_settings: ElectrodeSettings::new(), sweep_points: SweepPoints::Eighteen, tab_active: TabActive::Single, - logging: LoggingStates::Idle, } } }