diff --git a/src/app.rs b/src/app.rs index edea303..d8f8274 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,7 @@ use std::fmt::Debug; use log::{info, error}; +use tokio::sync; use core::f32; @@ -11,69 +12,27 @@ use std::sync::atomic::Ordering; use chrono::Local; -use atomic_float::AtomicF32; -use tokio::{sync::mpsc::{Sender}}; +use tokio::sync::{mpsc, watch}; use eframe::egui::{self, Button, CollapsingHeader, Color32, ComboBox, DragValue, Id, Key, Label, Layout, Modal, Modifiers, RichText, TextEdit, Widget}; use egui_plot::{Corner, GridInput, GridMark, Legend, Line, Plot, PlotPoint, Points}; use egui_dock::{DockArea, DockState, Style}; use egui_extras::{TableBuilder, Column}; +use crate::state::{AppState, ControlCommand, HardwareConnected, HardwareState, MeasurementDataState}; + use crate::logging::LoggingStates; use crate::plot::{TimeSeriesPlot, BodePlot}; use crate::signals::{LoggingSignal, StartStopSignal}; -use crate::icd::{BioImpedanceLeadMode, IcdDftNum, MeasurementPointSet, ElectrodeConfiguration, ElectrodeOptionsWithMultiplexer}; +use crate::icd::{BioImpedanceLeadMode, IcdDftNum, SweepPoints, ElectrodeConfiguration, ElectrodeOptionsWithMultiplexer}; const LEAD_MODES: [BioImpedanceLeadMode; 2] = [ BioImpedanceLeadMode::TwoLead, BioImpedanceLeadMode::FourLead, ]; -struct ElectrodeSettings { - with_multiplexer_2_lead: [ElectrodeOptionsWithMultiplexer; 2], - with_multiplexer_4_lead: [ElectrodeOptionsWithMultiplexer; 4], -} - -impl ElectrodeSettings { - pub fn new() -> Self { - Self { - with_multiplexer_2_lead: [ - ElectrodeOptionsWithMultiplexer::E1, // Electrode+ default - ElectrodeOptionsWithMultiplexer::E23, // Electrode- default - ], - with_multiplexer_4_lead: [ - ElectrodeOptionsWithMultiplexer::E1, // I+ default - ElectrodeOptionsWithMultiplexer::E23, // I- default - ElectrodeOptionsWithMultiplexer::E10, // V+ default - ElectrodeOptionsWithMultiplexer::E12, // V- default - ], - } - } - - pub fn to_electrode_config( - &self, - hardware_connected: HardwareConnected, - lead_mode: BioImpedanceLeadMode, - ) -> Option { - if hardware_connected != HardwareConnected::WithMultiplexer { - return None; - } - - match lead_mode { - BioImpedanceLeadMode::TwoLead => { - let [a, b] = self.with_multiplexer_2_lead; - Some(ElectrodeConfiguration::WithMultiplexer2Lead(a, b)) - } - BioImpedanceLeadMode::FourLead => { - let [a, b, c, d] = self.with_multiplexer_4_lead; - Some(ElectrodeConfiguration::WithMultiplexer4Lead(a, b, c, d)) - } - } - } -} - const DFTNUM_VARIANTS: [IcdDftNum; 13] = [ IcdDftNum::Num4, IcdDftNum::Num8, IcdDftNum::Num16, IcdDftNum::Num32, IcdDftNum::Num64, IcdDftNum::Num128, IcdDftNum::Num256, IcdDftNum::Num512, @@ -81,9 +40,9 @@ const DFTNUM_VARIANTS: [IcdDftNum; 13] = [ IcdDftNum::Num8192, IcdDftNum::Num16384, ]; -const MEASUREMENT_POINTS_VARIANTS: [MeasurementPointSet; 2] = [ - MeasurementPointSet::Eight, - MeasurementPointSet::Eighteen, +const SWEEP_POINTS_VARIANTS: [SweepPoints; 2] = [ + SweepPoints::Eight, + SweepPoints::Eighteen, ]; #[derive(Clone, Copy,Debug, PartialEq, Eq)] @@ -93,57 +52,31 @@ enum TabActive { Shortcuts, } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum HardwareConnected { - None, - WithoutMultiplexer, - WithMultiplexer, -} - pub struct App { tree: DockState, tab_viewer: TabViewer, - run_impedancemeter_tx: Sender, - log_tx: Sender, - pub magnitude: Arc>, - pub phase: Arc>, - pub magnitude_series: Arc>, - pub phase_series: Arc>, - pub bode_plot: Arc>, - pub hardware_connected: Arc>, - pub on: Arc>, tab_active: TabActive, - pub data_frequency: Arc, - pub single_frequency: Arc>, - pub lead_mode: Arc>, - electrode_settings: Arc>, - pub dft_num: Arc>, - pub measurement_points: Arc>, - pub periods_per_dft: Arc>>, - pub periods_per_dft_sweep: Arc, Option>)>>, + run_impedancemeter_tx: mpsc::Sender, + log_tx: mpsc::Sender, + pub on: Arc>, pub gui_logging_state: Arc>, log_filename: String, log_marker_modal: bool, log_marker: String, + measurement_data: Arc, + control_tx: mpsc::Sender, + app_state_rx: sync::watch::Receiver, + hardware_state_rx: sync::watch::Receiver } struct TabViewer { - magnitude: Arc>, - phase: Arc>, - magnitude_series: Arc>, - phase_series: Arc>, - bode_plot: Arc>, on: Arc>, - single_frequency: Arc>, - hardware_connected: Arc>, - lead_mode: Arc>, - electrode_settings: Arc>, - dft_num: Arc>, - measurement_points: Arc>, - periods_per_dft: Arc>>, - periods_per_dft_sweep: Arc, Option>)>>, show_settings: bool, show_settings_toggle: Option, + measurement_data: Arc, + control_tx: mpsc::Sender, + app_state_rx: sync::watch::Receiver, + hardware_state_rx: sync::watch::Receiver, } trait ElectrodeOption: Copy + Debug + PartialEq + 'static { @@ -158,46 +91,55 @@ fn electrode_combo( ui: &mut egui::Ui, id: &str, value: &mut T, -) { +) -> bool { + let mut changed = false; egui::ComboBox::from_id_salt(id) .selected_text(format!("{:?}", value)) .width(60.0) .show_ui(ui, |ui| { for &option in T::ALL { - ui.selectable_value(value, option, format!("{:?}", option)); + let response = ui.selectable_value(value, option, format!("{:?}", option)); + if response.changed() { + changed = true; + } } }); + changed } fn render_two_lead( ui: &mut egui::Ui, values: &mut [T; 2], -) { +) -> bool { + let mut changed = false; ui.horizontal(|ui| { ui.label("Drive/sense: Electrode+ ("); - electrode_combo(ui, "e1_select", &mut values[0]); + changed |= electrode_combo(ui, "e1_select", &mut values[0]); ui.label("), Electrode- ("); - electrode_combo(ui, "e2_select", &mut values[1]); + changed |= electrode_combo(ui, "e2_select", &mut values[1]); ui.label(")"); }); + changed } fn render_four_lead( ui: &mut egui::Ui, values: &mut [T; 4], -) { +) -> bool { + let mut changed = false; ui.horizontal(|ui| { ui.label("Drive: I+ ("); - electrode_combo(ui, "i1_select", &mut values[0]); + changed |= electrode_combo(ui, "i1_select", &mut values[0]); ui.label("), I- ("); - electrode_combo(ui, "i2_select", &mut values[1]); + changed |= electrode_combo(ui, "i2_select", &mut values[1]); ui.label(") | Sense: V+ ("); - electrode_combo(ui, "v1_select", &mut values[2]); + changed |= electrode_combo(ui, "v1_select", &mut values[2]); ui.label("), V- ("); - electrode_combo(ui, "v2_select", &mut values[3]); + changed |= electrode_combo(ui, "v2_select", &mut values[3]); ui.label(")"); }); + changed } impl TabViewer { @@ -211,12 +153,12 @@ impl TabViewer { ui.horizontal(|ui| { ui.label("Lead Mode:"); - let mut lead_mode = self.lead_mode.lock().unwrap(); + let mut lead_mode = self.app_state_rx.borrow().lead_mode; // Map current lead mode to index let mut index = LEAD_MODES .iter() - .position(|&m| m == *lead_mode) + .position(|&m| m == lead_mode) .unwrap_or(0); ComboBox::from_id_salt("LeadMode") @@ -230,19 +172,20 @@ impl TabViewer { }); // Update lead mode if changed - if *lead_mode != LEAD_MODES[index] { - *lead_mode = LEAD_MODES[index]; - info!("Lead Mode setting changed!"); + if lead_mode != LEAD_MODES[index] { + lead_mode = LEAD_MODES[index]; + self.control_tx.try_send(ControlCommand::ChangeLeadMode(lead_mode)).unwrap(); } }); ui.horizontal(|ui| { // Show lead configuration ui.label("Lead Configuration:"); - let hardware_connected = self.hardware_connected.lock().unwrap(); - let lead_mode = self.lead_mode.lock().unwrap(); + let hardware_connected = self.hardware_state_rx.borrow().connected; + let lead_mode = self.app_state_rx.borrow().lead_mode; - let mut settings = self.electrode_settings.lock().unwrap(); - match (*hardware_connected, *lead_mode) { + let mut settings = self.app_state_rx.borrow().electrode_settings; + + match (hardware_connected, lead_mode) { (HardwareConnected::WithoutMultiplexer, BioImpedanceLeadMode::TwoLead) => { ui.label(format!("Drive/sense: Electrode+ (CE0), Electrode- (AIN1)")); } @@ -250,10 +193,14 @@ impl TabViewer { ui.label(format!("Drive: I+ (CE0), I- (AIN1) | Sense: V+ (AIN2), V- (AIN3)")); } (HardwareConnected::WithMultiplexer, BioImpedanceLeadMode::TwoLead) => { - render_two_lead(ui, &mut settings.with_multiplexer_2_lead); + if render_two_lead(ui, &mut settings.with_multiplexer_2_lead) { + self.control_tx.try_send(ControlCommand::ChangeElectrodeSettings(settings)).unwrap(); + } } (HardwareConnected::WithMultiplexer, BioImpedanceLeadMode::FourLead) => { - render_four_lead(ui, &mut settings.with_multiplexer_4_lead); + if render_four_lead(ui, &mut settings.with_multiplexer_4_lead) { + self.control_tx.try_send(ControlCommand::ChangeElectrodeSettings(settings)).unwrap(); + } } (HardwareConnected::None, _) => {} } @@ -262,31 +209,33 @@ impl TabViewer { ui.add_enabled_ui(!*on, |ui| { ui.horizontal(|ui| { ui.label("Single Frequency:"); - if let Ok(mut freq) = self.single_frequency.lock() { - ui.add(DragValue::new(&mut *freq).speed(0.1)); + let mut freq = self.app_state_rx.borrow().single_frequency; + let response = ui.add(DragValue::new(&mut freq).speed(0.1)); + if response.changed() { + self.control_tx.try_send(ControlCommand::SetFrequency(freq)).unwrap(); } ui.label("Hz"); }); ui.horizontal(|ui| { ui.label("ADC samples per DFT:"); - let mut dft_num = self.dft_num.lock().unwrap(); - let mut index = DFTNUM_VARIANTS.iter().position(|&x| x == *dft_num).unwrap_or(0); + let mut dft_num = self.app_state_rx.borrow().dft_num; + let mut index = DFTNUM_VARIANTS.iter().position(|&x| x == dft_num).unwrap_or(0); ComboBox::from_id_salt("Dftnum") .width(75.0) .show_index(ui, &mut index, DFTNUM_VARIANTS.len(), |i| { format!("{}", 1 << (2 + i)) // 2^2 = 4, 2^3 = 8, ..., 2^14 = 16384 }); let new_value = DFTNUM_VARIANTS[index]; - if *dft_num != new_value { - *dft_num = new_value; - info!("DFTNUM setting changed!"); + if dft_num != new_value { + dft_num = new_value; + self.control_tx.try_send(ControlCommand::ChangeDftNum(dft_num)).unwrap(); }; }); }); ui.add_enabled_ui(*on, |ui| { ui.horizontal(|ui| { ui.label("Periods per DFT:"); - match (*on, *self.periods_per_dft.lock().unwrap()) { + match (*on, self.hardware_state_rx.borrow().periods_per_dft) { (true, Some(periods)) => { ui.add(Label::new(format!("{:.2}", periods))); }, @@ -316,14 +265,14 @@ impl TabViewer { Layout::top_down(egui::Align::Min), |ui| { // Magnitude - let magnitude = self.magnitude_series.lock().unwrap(); + let magnitude = self.measurement_data.magnitude_series.lock().unwrap(); Plot::new("magnitude") .legend(Legend::default().position(Corner::LeftTop)) .y_axis_label("Magnitude [Ω]") .y_axis_min_width(80.0) .show(ui, |plot_ui| { plot_ui.line( - Line::new(format!("Magnitude at {} Hz", self.single_frequency.lock().unwrap()), magnitude.plot_values()) + Line::new(format!("Magnitude at {} Hz", self.app_state_rx.borrow().single_frequency), magnitude.plot_values()) .color(Color32::BLUE) ); }); @@ -335,14 +284,14 @@ impl TabViewer { Layout::top_down(egui::Align::Min), |ui| { // Phase - let phase = self.phase_series.lock().unwrap(); + let phase = self.measurement_data.phase_series.lock().unwrap(); Plot::new("phase") .legend(Legend::default().position(Corner::LeftTop)) .y_axis_label("Phase [rad]") .y_axis_min_width(80.0) .show(ui, |plot_ui| { plot_ui.line( - Line::new(format!("Phase at {} Hz", self.single_frequency.lock().unwrap()), phase.plot_values()) + Line::new(format!("Phase at {} Hz", self.app_state_rx.borrow().single_frequency), phase.plot_values()) .color(Color32::RED) ); }); @@ -362,12 +311,12 @@ impl TabViewer { ui.horizontal(|ui| { ui.label("Lead Mode:"); - let mut lead_mode = self.lead_mode.lock().unwrap(); + let mut lead_mode = self.app_state_rx.borrow().lead_mode; // Map current lead mode to index let mut index = LEAD_MODES .iter() - .position(|&m| m == *lead_mode) + .position(|&m| m == lead_mode) .unwrap_or(0); ComboBox::from_id_salt("LeadMode") @@ -381,19 +330,19 @@ impl TabViewer { }); // Update lead mode if changed - if *lead_mode != LEAD_MODES[index] { - *lead_mode = LEAD_MODES[index]; - info!("Lead Mode setting changed!"); + if lead_mode != LEAD_MODES[index] { + lead_mode = LEAD_MODES[index]; + self.control_tx.try_send(ControlCommand::ChangeLeadMode(lead_mode)).unwrap(); } }); ui.horizontal(|ui| { // Show lead configuration ui.label("Lead Configuration:"); - let hardware_connected = self.hardware_connected.lock().unwrap(); - let lead_mode = self.lead_mode.lock().unwrap(); + let hardware_connected = self.hardware_state_rx.borrow().connected; + let lead_mode = self.app_state_rx.borrow().lead_mode; - let mut settings = self.electrode_settings.lock().unwrap(); - match (*hardware_connected, *lead_mode) { + let mut settings = self.app_state_rx.borrow().electrode_settings; + match (hardware_connected, lead_mode) { (HardwareConnected::WithoutMultiplexer, BioImpedanceLeadMode::TwoLead) => { ui.label(format!("Drive/sense: Electrode+ (CE0), Electrode- (AIN1)")); } @@ -401,10 +350,14 @@ impl TabViewer { ui.label("Drive: I+ (CE0), I- (AIN1) | Sense: V+ (AIN2), V- (AIN3)"); } (HardwareConnected::WithMultiplexer, BioImpedanceLeadMode::TwoLead) => { - render_two_lead(ui, &mut settings.with_multiplexer_2_lead); + if render_two_lead(ui, &mut settings.with_multiplexer_2_lead) { + self.control_tx.try_send(ControlCommand::ChangeElectrodeSettings(settings)).unwrap(); + } } (HardwareConnected::WithMultiplexer, BioImpedanceLeadMode::FourLead) => { - render_four_lead(ui, &mut settings.with_multiplexer_4_lead); + if render_four_lead(ui, &mut settings.with_multiplexer_4_lead) { + self.control_tx.try_send(ControlCommand::ChangeElectrodeSettings(settings)).unwrap(); + } } (HardwareConnected::None, _) => {} } @@ -413,22 +366,23 @@ impl TabViewer { ui.add_enabled_ui(!*on, |ui| { ui.horizontal(|ui| { ui.label("Measurement Points:"); - let mut measurement_points = self.measurement_points.lock().unwrap(); - let mut index = MEASUREMENT_POINTS_VARIANTS.iter().position(|&x| x == *measurement_points).unwrap_or(0); + let mut measurement_points = self.app_state_rx.borrow().sweep_points; + let mut index = SWEEP_POINTS_VARIANTS.iter().position(|&x| x == measurement_points).unwrap_or(0); ComboBox::from_id_salt("MeasurementPoints") .width(75.0) - .show_index(ui, &mut index, MEASUREMENT_POINTS_VARIANTS.len(), |i| { - format!("{:?}", MEASUREMENT_POINTS_VARIANTS[i].len()) + .show_index(ui, &mut index, SWEEP_POINTS_VARIANTS.len(), |i| { + format!("{:?}", SWEEP_POINTS_VARIANTS[i].len()) }); - let new_value = MEASUREMENT_POINTS_VARIANTS[index]; - if *measurement_points != new_value { - *measurement_points = new_value; - info!("Measurement Points setting changed!"); + let new_value = SWEEP_POINTS_VARIANTS[index]; + if measurement_points != new_value { + measurement_points = new_value; + self.control_tx.try_send(ControlCommand::ChangeSweepPoints(measurement_points)).unwrap(); + info!("Sweep Points setting changed!"); } }); }); ui.add_enabled_ui(*on, |ui| { - let (freq, periods_per_dft_vec) = self.periods_per_dft_sweep.lock().unwrap().clone(); + let (freq, periods_per_dft_vec) = self.hardware_state_rx.borrow().periods_per_dft_sweep.clone(); fn format_frequency(freq: u32) -> String { if freq >= 1_000 { @@ -499,7 +453,7 @@ impl TabViewer { Layout::top_down(egui::Align::Min), |ui| { // Magnitude - let bode_plot = self.bode_plot.lock().unwrap(); + let bode_plot = self.measurement_data.bode_plot.lock().unwrap(); Plot::new("bode_mag") .legend(Legend::default().position(Corner::LeftTop)) .y_axis_label("Magnitude [Ω]") @@ -529,7 +483,7 @@ impl TabViewer { Layout::top_down(egui::Align::Min), |ui| { // Phase - let bode_plot = self.bode_plot.lock().unwrap(); + let bode_plot = self.measurement_data.bode_plot.lock().unwrap(); Plot::new("bode_phase") .legend(Legend::default().position(Corner::LeftTop)) .y_axis_label("Phase [rad]") @@ -644,70 +598,45 @@ impl egui_dock::TabViewer for TabViewer { } impl App { - pub fn new(run_impedancemeter_tx: Sender, log_tx: Sender) -> Self { - // Step 1: Initialize shared fields first - let magnitude = Arc::new(Mutex::new(0.0)); - let phase = Arc::new(Mutex::new(0.0)); - let magnitude_series = Arc::new(Mutex::new(TimeSeriesPlot::new())); - let phase_series = Arc::new(Mutex::new(TimeSeriesPlot::new())); - let bode_plot = Arc::new(Mutex::new(BodePlot::new())); - let single_frequency = Arc::new(Mutex::new(50000)); - let hardware_connected = Arc::new(Mutex::new(HardwareConnected::None)); - let lead_mode = Arc::new(Mutex::new(BioImpedanceLeadMode::FourLead)); - let electrode_settings = Arc::new(Mutex::new(ElectrodeSettings::new())); - let dft_num = Arc::new(Mutex::new(IcdDftNum::Num2048)); - let measurement_points = Arc::new(Mutex::new(MeasurementPointSet::Eighteen)); - let periods_per_dft = Arc::new(Mutex::new(None)); - let periods_per_dft_sweep = Arc::new(Mutex::new((MeasurementPointSet::Eighteen.values().to_vec(), None))); + pub fn new(run_impedancemeter_tx: mpsc::Sender, + log_tx: mpsc::Sender, + measurement_data: Arc, + control_tx: mpsc::Sender, + app_state_rx: watch::Receiver, + hardware_state_rx: watch::Receiver) -> Self { + // Step 1: Initialize shared fields first let on = Arc::new(Mutex::new(true)); let tab_active = TabActive::Single; // Step 2: Now we can initialize tab_viewer - let tab_viewer = TabViewer { - magnitude: magnitude.clone(), - phase: phase.clone(), - magnitude_series: magnitude_series.clone(), - phase_series: phase_series.clone(), - bode_plot: bode_plot.clone(), - single_frequency: single_frequency.clone(), - hardware_connected: hardware_connected.clone(), - lead_mode: lead_mode.clone(), - electrode_settings: electrode_settings.clone(), - dft_num: dft_num.clone(), - measurement_points: measurement_points.clone(), - periods_per_dft: periods_per_dft.clone(), - periods_per_dft_sweep: periods_per_dft_sweep.clone(), + let control_tx_clone = control_tx.clone(); + let app_state_clone = app_state_rx.clone(); + let hardware_state_clone = hardware_state_rx.clone(); + let tab_viewer = TabViewer { measurement_data: measurement_data.clone(), on: on.clone(), show_settings: false, show_settings_toggle: None, + control_tx: control_tx_clone, + app_state_rx: app_state_clone, + hardware_state_rx: hardware_state_clone }; // Step 3: Construct App let app = App { tree: DockState::new(vec!["Single".to_string(), "Sweep".to_string(), "Shortcuts".to_string()]), tab_viewer, + tab_active, run_impedancemeter_tx, log_tx, - magnitude, - phase, - magnitude_series, - phase_series, - bode_plot, - hardware_connected, on, - tab_active, - data_frequency: Arc::new(AtomicF32::new(0.0)), - single_frequency, - lead_mode, - electrode_settings, - dft_num, - measurement_points, - periods_per_dft, - periods_per_dft_sweep, gui_logging_state: Arc::new(Mutex::new(LoggingStates::Idle)), log_filename: format!("log_{}_single.csv", Local::now().format("%Y%m%d")), log_marker_modal: false, log_marker: String::new(), + measurement_data, + control_tx, + app_state_rx, + hardware_state_rx, }; // For testing purposes, populate the Bode plot with a sample low-pass filter response @@ -735,17 +664,17 @@ impl App { pub fn update_start_stop(&self) { match (self.tab_active, *self.on.lock().unwrap()) { (TabActive::Single, true) => { - let lead_mode = *self.lead_mode.lock().unwrap(); - let electrode_config = self.electrode_settings.lock().unwrap().to_electrode_config(*self.hardware_connected.lock().unwrap(), lead_mode); + let lead_mode = self.app_state_rx.borrow().lead_mode; + let electrode_config = self.app_state_rx.borrow().electrode_settings.to_electrode_config(self.hardware_state_rx.borrow().connected, lead_mode); if let Err(e) = self.run_impedancemeter_tx.try_send( - StartStopSignal::StartSingle(*self.single_frequency.lock().unwrap(), lead_mode, electrode_config, *self.dft_num.lock().unwrap())) { + StartStopSignal::StartSingle(self.app_state_rx.borrow().single_frequency, lead_mode, electrode_config, self.app_state_rx.borrow().dft_num)) { error!("Failed to send start command: {:?}", e); } }, (TabActive::Sweep, true) => { - let lead_mode = *self.lead_mode.lock().unwrap(); - let electrode_config = self.electrode_settings.lock().unwrap().to_electrode_config(*self.hardware_connected.lock().unwrap(), lead_mode); - if let Err(e) = self.run_impedancemeter_tx.try_send(StartStopSignal::StartSweep(lead_mode, electrode_config, *self.measurement_points.lock().unwrap())) { + let lead_mode = self.app_state_rx.borrow().lead_mode; + let electrode_config = self.app_state_rx.borrow().electrode_settings.to_electrode_config(self.hardware_state_rx.borrow().connected, lead_mode); + if let Err(e) = self.run_impedancemeter_tx.try_send(StartStopSignal::StartSweep(lead_mode, electrode_config, self.app_state_rx.borrow().sweep_points)) { error!("Failed to send start command: {:?}", e); } }, @@ -759,8 +688,8 @@ impl App { } pub fn reset_view(&self) { - self.magnitude_series.lock().unwrap().clear(); - self.phase_series.lock().unwrap().clear(); + self.measurement_data.magnitude_series.lock().unwrap().clear(); + self.measurement_data.phase_series.lock().unwrap().clear(); } } @@ -769,12 +698,12 @@ impl eframe::App for App { // Egui add a top bar egui::TopBottomPanel::top("top_bar").show(ctx, |ui| { egui::MenuBar::new().ui(ui, |ui| { - let is_connected = *self.hardware_connected.lock().unwrap() != HardwareConnected::None; + let is_connected = self.hardware_state_rx.borrow().connected != HardwareConnected::None; egui::widgets::global_theme_preference_switch(ui); ui.separator(); - ui.label(format!("Data rate: {} Hz", self.data_frequency.load(Ordering::Relaxed))); + ui.label(format!("Data rate: {} Hz", self.measurement_data.sampling_rate.load(Ordering::Relaxed))); ui.separator(); @@ -889,7 +818,7 @@ impl eframe::App for App { // Spacer to push the LED to the right ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.scope(|ui| { - let (color, tooltip) = match *self.hardware_connected.lock().unwrap() { + let (color, tooltip) = match self.hardware_state_rx.borrow().connected { HardwareConnected::None => { (Color32::DARK_RED, "Disconnected") }, diff --git a/src/bin/main_gui.rs b/src/bin/main_gui.rs index b1db917..8b9f2fc 100644 --- a/src/bin/main_gui.rs +++ b/src/bin/main_gui.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use eframe::NativeOptions; use eframe::egui::Vec2; @@ -9,11 +11,14 @@ use bioz_host_rs::{app::App, signals::LoggingSignal}; use bioz_host_rs::communication::communicate_with_hardware; -use tokio::sync::mpsc::{self}; +use tokio::sync::{watch, mpsc}; use bioz_host_rs::signals::StartStopSignal; use bioz_host_rs::logging::log_data; +use bioz_host_rs::state::{AppState, ControlCommand, HardwareState, MeasurementDataState}; +use bioz_host_rs::control::control_loop; + #[tokio::main] async fn main() { SimpleLogger::new().init().expect("Failed to initialize logger"); @@ -23,26 +28,33 @@ async fn main() { // Enter the runtime so that `tokio::spawn` is available immediately. // let _enter = rt.enter(); + // Init watch for hardware state + let (hardware_state_tx, hardware_state_rx) = watch::channel(HardwareState::default()); + // Channel to communicate with the communication task. let (run_impedancemeter_tx, run_impedancemeter_rx) = mpsc::channel::(2); let run_impedancemeter_tx_clone = run_impedancemeter_tx.clone(); + // Control layer + let (control_tx, control_rx) = mpsc::channel::(32); + let (app_state_tx, app_state_rx) = watch::channel(AppState::default()); + + let control_tx_clone = control_tx.clone(); + + tokio::spawn(async move { + control_loop(control_rx, app_state_tx).await; + }); + + // 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 app = App::new(run_impedancemeter_tx, log_tx); - let magnitude_clone = app.magnitude.clone(); - let phase_clone = app.phase.clone(); - let magnitude_series_clone = app.magnitude_series.clone(); - let phase_series_clone = app.phase_series.clone(); - let bode_clone = app.bode_plot.clone(); - let hardware_connected_clone = app.hardware_connected.clone(); + let measurement_data_clone = measurement_data.clone(); - let data_frequency_clone = app.data_frequency.clone(); - - let periods_per_dft = app.periods_per_dft.clone(); - let periods_per_dft_sweep = app.periods_per_dft_sweep.clone(); + let app = App::new(run_impedancemeter_tx, log_tx, measurement_data_clone, control_tx_clone, app_state_rx, hardware_state_rx); let gui_logging_state_1 = app.gui_logging_state.clone(); let gui_logging_state_2 = app.gui_logging_state.clone(); @@ -63,15 +75,8 @@ async fn main() { rt.block_on(communicate_with_hardware( run_impedancemeter_rx, run_impedancemeter_tx_clone, - magnitude_clone, - phase_clone, - magnitude_series_clone, - phase_series_clone, - bode_clone, - hardware_connected_clone, - data_frequency_clone, - periods_per_dft, - periods_per_dft_sweep, + measurement_data, + hardware_state_tx, gui_logging_state_2, log_tx_clone, )); diff --git a/src/client.rs b/src/client.rs index 274715e..3d08c4b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -8,7 +8,7 @@ use bioz_icd_rs::{ BioImpedanceLeadMode, ElectrodeConfiguration, GetMultiplexerCapabilityEndpoint, GetUniqueIdEndpoint, ImpedanceInitResult, PingEndpoint, SetGreenLedEndpoint, SingleImpedanceStartRequest, StartSingleImpedanceEndpoint, StartSweepImpedanceEndpoint, StopImpedanceEndpoint, SweepImpedanceInitResult, SweepImpedanceStartRequest }; -use crate::icd::{IcdDftNum, MeasurementPointSet, MultiplexerCapability}; +use crate::icd::{IcdDftNum, SweepPoints, MultiplexerCapability}; #[derive(Debug)] pub struct WorkbookClient { @@ -84,7 +84,7 @@ impl WorkbookClient { &self, lead_mode: BioImpedanceLeadMode, electrode_config: Option, - points: MeasurementPointSet, + points: SweepPoints, ) -> Result> { let response = self.client .send_resp::(&SweepImpedanceStartRequest { lead_mode, electrode_config, points }) diff --git a/src/communication.rs b/src/communication.rs index fa62fbc..a965182 100644 --- a/src/communication.rs +++ b/src/communication.rs @@ -3,14 +3,16 @@ use std::time::SystemTime; use log::{error, info}; use tokio::select; -use tokio::sync::mpsc::{Receiver, Sender}; +use tokio::sync::{watch, mpsc::{Receiver, Sender}}; use std::sync::atomic::{AtomicU32, Ordering}; use atomic_float::AtomicF32; use std::sync::{Arc, Mutex}; -use bioz_icd_rs::{MeasurementPointSet, MultiplexerCapability}; +use bioz_icd_rs::{SweepPoints, MultiplexerCapability}; + +use crate::state::{HardwareState, MeasurementDataState}; use crate::icd; use crate::client::WorkbookClient; @@ -20,29 +22,24 @@ use crate::plot::{TimeSeriesPlot, BodePlot}; use crate::signals::{LoggingSignal, StartStopSignal}; -use crate::app::HardwareConnected; +use crate::state::HardwareConnected; pub async fn communicate_with_hardware( mut run_impedancemeter_rx: Receiver, run_impedancemeter_tx: Sender, - magnitude: Arc>, - phase: Arc>, - magnitude_series: Arc>, - phase_series: Arc>, - bode_series: Arc>, - connected: Arc>, - data_frequency: Arc, - periods_per_dft: Arc>>, - periods_per_dft_sweep: Arc, Option>)>>, + measurement_data: Arc, + hardware_state_tx: watch::Sender, gui_logging_state: Arc>, log_tx: Sender, ) { let data_counter = Arc::new(AtomicU32::new(0)); let data_counter_clone = data_counter.clone(); + let sampling_rate_clone = measurement_data.sampling_rate.clone(); + tokio::spawn(async move { loop { tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; - data_frequency.store(data_counter.load(Ordering::Relaxed) as f32, Ordering::Relaxed); + sampling_rate_clone.store(data_counter.load(Ordering::Relaxed) as f32, Ordering::Relaxed); data_counter.store(0, Ordering::Relaxed); } }); @@ -54,6 +51,8 @@ pub async fn communicate_with_hardware( let settings = Arc::new(Mutex::new(Settings::default())); loop { + let mut hardware_state = HardwareState::default(); + let workbook_client = match WorkbookClient::new() { Ok(client) => { info!("Connected to hardware successfully."); @@ -62,11 +61,13 @@ pub async fn communicate_with_hardware( } match client.get_device_info().await.unwrap() { MultiplexerCapability::Absent => { - *connected.lock().unwrap() = HardwareConnected::WithoutMultiplexer; + hardware_state.connected = HardwareConnected::WithoutMultiplexer; + hardware_state_tx.send(hardware_state.clone()).unwrap(); info!("Connected device: Without Multiplexer"); }, MultiplexerCapability::Present => { - *connected.lock().unwrap() = HardwareConnected::WithMultiplexer; + hardware_state.connected = HardwareConnected::WithMultiplexer; + hardware_state_tx.send(hardware_state.clone()).unwrap(); info!("Connected device: With Multiplexer"); }, } @@ -86,7 +87,7 @@ pub async fn communicate_with_hardware( .await .unwrap(); - let data = (magnitude_series.clone(), phase_series.clone(), magnitude.clone(), phase.clone()); + let data = (measurement_data.magnitude_series.clone(), measurement_data.phase_series.clone(), measurement_data.magnitude.clone(), measurement_data.phase.clone()); let data_counter_clone_single = data_counter_clone.clone(); // Clone log_tx for the task @@ -139,7 +140,7 @@ pub async fn communicate_with_hardware( .await .unwrap(); - let data = bode_series.clone(); + let data = measurement_data.bode_plot.clone(); let data_counter_clone_sweep = data_counter_clone.clone(); // Clone log_tx for the task @@ -151,30 +152,30 @@ pub async fn communicate_with_hardware( match val.points { - MeasurementPointSet::Eight => { + SweepPoints::Eight => { let magnitudes: Vec = val.magnitudes_8.into_iter().collect(); let phases: Vec = val.phases_8.into_iter().collect(); { let mut bode_plot = data.lock().unwrap(); - bode_plot.update_magnitudes(MeasurementPointSet::Eight, magnitudes.clone()); - bode_plot.update_phases(MeasurementPointSet::Eight, phases.clone()); + 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(), MeasurementPointSet::Eight.values().to_vec(), magnitudes.clone(), phases.clone())).await { + if let Err(e) = log_tx_clone.send(LoggingSignal::SweepImpedance(SystemTime::now(), SweepPoints::Eight.values().to_vec(), magnitudes.clone(), phases.clone())).await { error!("Failed to send logging signal: {:?}", e); } } }, - MeasurementPointSet::Eighteen => { + SweepPoints::Eighteen => { let magnitudes: Vec = val.magnitudes_18.into_iter().collect(); let phases: Vec = val.phases_18.into_iter().collect(); { let mut bode_plot = data.lock().unwrap(); - bode_plot.update_magnitudes(MeasurementPointSet::Eighteen, magnitudes.clone()); - bode_plot.update_phases(MeasurementPointSet::Eighteen, phases.clone()); + 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(), MeasurementPointSet::Eighteen.values().to_vec(), magnitudes.clone(), phases.clone())).await { + if let Err(e) = log_tx_clone.send(LoggingSignal::SweepImpedance(SystemTime::now(), SweepPoints::Eighteen.values().to_vec(), magnitudes.clone(), phases.clone())).await { error!("Failed to send logging signal: {:?}", e); } } @@ -195,7 +196,8 @@ pub async fn communicate_with_hardware( Ok(Ok(periods)) => { info!("Impedance meter started at frequency: {} with periods per DFT: {}", freq, periods); settings.lock().unwrap().mode = Some(StartStopSignal::StartSingle(freq, lead_mode, electrode_config, dft_num)); - *periods_per_dft.lock().unwrap() = Some(periods); + hardware_state.periods_per_dft = Some(periods); + hardware_state_tx.send(hardware_state.clone()).unwrap(); // When logging add electrode configuration to logging file if *gui_logging_state.lock().unwrap() == LoggingStates::Logging { @@ -206,11 +208,13 @@ pub async fn communicate_with_hardware( }, Ok(Err(e)) => { error!("Failed to init on hardware: {:?}", e); - *periods_per_dft.lock().unwrap() = None; + hardware_state.periods_per_dft = None; + hardware_state_tx.send(hardware_state.clone()).unwrap(); }, Err(e) => { error!("Communication error when starting impedancemeter: {:?}", e); - *periods_per_dft.lock().unwrap() = None; + hardware_state.periods_per_dft = None; + hardware_state_tx.send(hardware_state.clone()).unwrap(); } } }, @@ -220,11 +224,14 @@ pub async fn communicate_with_hardware( settings.lock().unwrap().mode = Some(StartStopSignal::StartSweep(lead_mode, electrode_config, num_points)); info!("Sweep Impedancemeter started."); match num_points { - MeasurementPointSet::Eight => { - *periods_per_dft_sweep.lock().unwrap() = (num_points.values().iter().copied().collect(), Some(periods.periods_per_dft_8.into_iter().collect())); + SweepPoints::Eight => { + hardware_state.periods_per_dft_sweep = (num_points.values().iter().copied().collect(), Some(periods.periods_per_dft_8.into_iter().collect())); + hardware_state_tx.send(hardware_state.clone()).unwrap(); + }, - MeasurementPointSet::Eighteen => { - *periods_per_dft_sweep.lock().unwrap() = (num_points.values().iter().copied().collect(), Some(periods.periods_per_dft_18.into_iter().collect())); + SweepPoints::Eighteen => { + hardware_state.periods_per_dft_sweep = (num_points.values().iter().copied().collect(), Some(periods.periods_per_dft_18.into_iter().collect())); + hardware_state_tx.send(hardware_state.clone()).unwrap(); }, } @@ -237,11 +244,13 @@ pub async fn communicate_with_hardware( }, Ok(Err(e)) => { error!("Failed to sweep-init on hardware: {:?}", e); - *periods_per_dft_sweep.lock().unwrap() = (num_points.values().iter().copied().collect(), None); + hardware_state.periods_per_dft_sweep = (num_points.values().iter().copied().collect(), None); + hardware_state_tx.send(hardware_state.clone()).unwrap(); }, Err(e) => { error!("Communication error when starting impedancemeter: {:?}", e); - *periods_per_dft_sweep.lock().unwrap() = (num_points.values().iter().copied().collect(), None); + hardware_state.periods_per_dft_sweep = (num_points.values().iter().copied().collect(), None); + hardware_state_tx.send(hardware_state.clone()).unwrap(); } } }, @@ -250,9 +259,10 @@ pub async fn communicate_with_hardware( error!("Failed to stop impedancemeter: {:?}", e); } else { settings.lock().unwrap().mode = Some(StartStopSignal::Stop); - *periods_per_dft.lock().unwrap() = None; - let (freq, _) = periods_per_dft_sweep.lock().unwrap().clone(); - *periods_per_dft_sweep.lock().unwrap() = (freq, None); + hardware_state.periods_per_dft = None; + let (freq, _) = hardware_state.periods_per_dft_sweep.clone(); + hardware_state.periods_per_dft_sweep = (freq, None); + hardware_state_tx.send(hardware_state.clone()).unwrap(); info!("Impedancemeter stopped."); } }, @@ -272,7 +282,8 @@ pub async fn communicate_with_hardware( } } info!("Communication with hardware ended."); - *connected.lock().unwrap() = HardwareConnected::None; + hardware_state.connected = HardwareConnected::None; + hardware_state_tx.send(hardware_state).unwrap(); tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; } } \ No newline at end of file diff --git a/src/control.rs b/src/control.rs new file mode 100644 index 0000000..ae022d9 --- /dev/null +++ b/src/control.rs @@ -0,0 +1,40 @@ +use log::info; +use tokio::sync::{mpsc::Receiver, watch::Sender}; +use crate::{state::{AppState, ControlCommand}}; + +pub async fn control_loop(mut rx: Receiver, tx: Sender) { + let mut state = AppState::default(); + + while let Some(cmd) = rx.recv().await { + match cmd { + ControlCommand::SetFrequency(freq) => { + state.single_frequency = freq; + info!("Frequency setting changed to {}!", freq); + } + ControlCommand::ChangeLeadMode(lead_mode) => { + state.lead_mode = lead_mode; + info!("Lead mode changed to {:?}!", lead_mode); + } + ControlCommand::ChangeDftNum(dft_num) => { + state.dft_num = dft_num; + info!("DFT number changed to {:?}!", dft_num); + } + ControlCommand::ChangeElectrodeSettings(electrode_settings) => { + state.electrode_settings = electrode_settings; + info!("Electrode settings changed to {:?}!", electrode_settings); + } + ControlCommand::ChangeSweepPoints(sweep_points) => { + state.sweep_points = sweep_points; + info!("Sweep points changed to {:?}!", sweep_points); + } + ControlCommand::Start => { + info!("Starting impedance hardware..."); + } + ControlCommand::Stop => { + info!("Stopping impedance hardware..."); + } + } + + tx.send(state.clone()).unwrap(); + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index c4aba97..3d14379 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,9 @@ pub mod app; pub mod communication; pub mod tcp; pub mod plot; +pub mod control; pub mod signals; +pub mod state; pub mod logging; pub use bioz_icd_rs as icd; diff --git a/src/plot.rs b/src/plot.rs index 9828fca..4dec283 100644 --- a/src/plot.rs +++ b/src/plot.rs @@ -3,7 +3,7 @@ use std::collections::VecDeque; use egui_plot::{PlotPoint, PlotPoints}; -use bioz_icd_rs::MeasurementPointSet; +use bioz_icd_rs::SweepPoints; pub struct TimeSeriesPlot { pub values: VecDeque, @@ -63,13 +63,13 @@ impl BodePlot { } } - pub fn update_magnitudes(&mut self, points: MeasurementPointSet, magnitudes: Vec) { + pub fn update_magnitudes(&mut self, points: SweepPoints, magnitudes: Vec) { let freqs = points.values().to_vec(); // self.magnitudes = freqs.into_iter().zip(magnitudes.into_iter()).map(|(f, m)| PlotPoint::new(f.log10(), 20.0 * m.log10() as f32)).collect(); self.magnitudes = freqs.into_iter().zip(magnitudes.into_iter()).map(|(f, m)| PlotPoint::new((f as f32).log10(), m)).collect(); // Convert to f32 first due to rouding errors } - pub fn update_phases(&mut self, points: MeasurementPointSet, phases: Vec) { + pub fn update_phases(&mut self, points: SweepPoints, phases: Vec) { let freqs = points.values().to_vec(); self.phases = freqs.into_iter().zip(phases.into_iter()).map(|(f, p)| PlotPoint::new((f as f32).log10(), p)).collect(); // Convert to f32 first due to rouding errors } diff --git a/src/signals.rs b/src/signals.rs index 57617cf..fcf73b0 100644 --- a/src/signals.rs +++ b/src/signals.rs @@ -1,11 +1,11 @@ use std::time::SystemTime; -use crate::icd::{BioImpedanceLeadMode, IcdDftNum, ElectrodeConfiguration, MeasurementPointSet}; +use crate::icd::{BioImpedanceLeadMode, IcdDftNum, ElectrodeConfiguration, SweepPoints}; #[derive(Copy, Clone, Debug)] pub enum StartStopSignal { StartSingle(u32, BioImpedanceLeadMode, Option, IcdDftNum), // frequency in Hz, lead mode, electrode configuration, DFT number - StartSweep(BioImpedanceLeadMode, Option, MeasurementPointSet), // lead mode, electrode configuration, number of points per measurement + StartSweep(BioImpedanceLeadMode, Option, SweepPoints), // lead mode, electrode configuration, number of points per measurement Stop, } diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..d9ca3d6 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,135 @@ +use std::default; +use std::sync::{Arc, Mutex}; + +use crate::icd::{BioImpedanceLeadMode, IcdDftNum, SweepPoints, ElectrodeConfiguration, ElectrodeOptionsWithMultiplexer}; + +use crate::plot::{TimeSeriesPlot, BodePlot}; + +use atomic_float::AtomicF32; + +// Measurement data state shared across the app +pub struct MeasurementDataState { + pub magnitude: Arc>, + pub phase: Arc>, + pub magnitude_series: Arc>, + pub phase_series: Arc>, + pub bode_plot: Arc>, + pub sampling_rate: Arc, +} + +impl Default for MeasurementDataState { + fn default() -> Self { + Self { + magnitude: Arc::new(Mutex::new(0.0)), + phase: Arc::new(Mutex::new(0.0)), + magnitude_series: Arc::new(Mutex::new(TimeSeriesPlot::new())), + phase_series: Arc::new(Mutex::new(TimeSeriesPlot::new())), + bode_plot: Arc::new(Mutex::new(BodePlot::new())), + sampling_rate: Arc::new(AtomicF32::new(0.0)), + } + } +} + + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum HardwareConnected { + None, + WithoutMultiplexer, + WithMultiplexer, +} + +pub enum ControlCommand { + SetFrequency(u32), + ChangeLeadMode(BioImpedanceLeadMode), + ChangeDftNum(IcdDftNum), + ChangeElectrodeSettings(ElectrodeSettings), + ChangeSweepPoints(SweepPoints), + Start, + Stop, +} + +#[derive(Clone, Copy, Debug)] +pub struct ElectrodeSettings { + pub with_multiplexer_2_lead: [ElectrodeOptionsWithMultiplexer; 2], + pub with_multiplexer_4_lead: [ElectrodeOptionsWithMultiplexer; 4], +} + +impl ElectrodeSettings { + pub fn new() -> Self { + Self { + with_multiplexer_2_lead: [ + ElectrodeOptionsWithMultiplexer::E1, // Electrode+ default + ElectrodeOptionsWithMultiplexer::E23, // Electrode- default + ], + with_multiplexer_4_lead: [ + ElectrodeOptionsWithMultiplexer::E1, // I+ default + ElectrodeOptionsWithMultiplexer::E23, // I- default + ElectrodeOptionsWithMultiplexer::E10, // V+ default + ElectrodeOptionsWithMultiplexer::E12, // V- default + ], + } + } + + pub fn to_electrode_config( + &self, + hardware_connected: HardwareConnected, + lead_mode: BioImpedanceLeadMode, + ) -> Option { + if hardware_connected != HardwareConnected::WithMultiplexer { + return None; + } + + match lead_mode { + BioImpedanceLeadMode::TwoLead => { + let [a, b] = self.with_multiplexer_2_lead; + Some(ElectrodeConfiguration::WithMultiplexer2Lead(a, b)) + } + BioImpedanceLeadMode::FourLead => { + let [a, b, c, d] = self.with_multiplexer_4_lead; + Some(ElectrodeConfiguration::WithMultiplexer4Lead(a, b, c, d)) + } + } + } +} + +// Application state that can be shared across the app and updated by the control loop +#[derive(Clone, Copy, Debug)] +pub struct AppState { + pub single_frequency: u32, + pub lead_mode: BioImpedanceLeadMode, + pub dft_num: IcdDftNum, + pub electrode_settings: ElectrodeSettings, + pub sweep_points: SweepPoints, +} + +impl Default for AppState { + fn default() -> Self { + Self { + single_frequency: 50000, + lead_mode: BioImpedanceLeadMode::TwoLead, + dft_num: IcdDftNum::Num2048, + electrode_settings: ElectrodeSettings::new(), + sweep_points: SweepPoints::Eighteen, + } + } +} + +// Hardware state that can be shared across the app and updated by the hardware loop +#[derive(Clone, Debug)] +pub struct HardwareState { + pub connected: HardwareConnected, + // pub running: bool, + pub periods_per_dft: Option, + pub periods_per_dft_sweep: (Vec, Option>), +} + +impl Default for HardwareState { + fn default() -> Self { + Self { + connected: HardwareConnected::None, + // running: false, + periods_per_dft: None, + periods_per_dft_sweep: (SweepPoints::Eighteen.values().to_vec(), None), + } + } +} \ No newline at end of file