diff --git a/src/app.rs b/src/app.rs index 884ff62..01908d1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,32 +1,56 @@ use std::sync::{Arc, Mutex}; +use std::sync::atomic::{AtomicBool, Ordering}; use std::ops::RangeInclusive; use tokio::{sync::mpsc::{Sender}}; -use eframe::egui::{self, Color32, DragValue, Key, Layout, Modifiers}; +use eframe::egui::{self, Color32, DragValue, Key, Layout, Modifiers, }; use egui_plot::{Corner, Legend, Line, Plot, PlotPoints, Points, PlotBounds}; use crate::plot::TimeSeriesPlot; +use crate::signals::FrequencySignal; + pub struct App { - frequency: u32, - run_impedancemeter_tx: Sender, + frequency: f32, + run_impedancemeter_tx: Sender, pub magnitude: Arc>, pub phase: Arc>, pub magnitude_series: Arc>, pub phase_series: Arc>, + pub connected: Arc, + pub on: bool, } impl App { - pub fn new(run_impedancemeter_tx: Sender) -> Self { - App { - frequency: 2, // Default frequency + pub fn new(run_impedancemeter_tx: Sender) -> Self { + let app = App { + frequency: 2.0, // Default frequency run_impedancemeter_tx, 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())), + connected: Arc::new(AtomicBool::new(false)), + on: true, + }; + app.update_start_stop(); + app + } + + pub fn update_start_stop(&self) { + match self.on { + true => { + if let Err(e) = self.run_impedancemeter_tx.try_send(FrequencySignal::Start(self.frequency)) { + eprintln!("Failed to send start command: {:?}", e); + } + }, + false => { + if let Err(e) = self.run_impedancemeter_tx.try_send(FrequencySignal::Stop) { + eprintln!("Failed to send stop command: {:?}", e); + } + }, } } } @@ -36,34 +60,45 @@ 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 connected = self.connected.load(Ordering::Relaxed); + egui::widgets::global_theme_preference_switch(ui); ui.separator(); - if ui.add(DragValue::new(&mut self.frequency).speed(0.1).range(RangeInclusive::new(0, 50)).update_while_editing(false)).changed() { - if let Err(e) = self.run_impedancemeter_tx.try_send(0) { - eprintln!("Failed to send stop command: {:?}", e); - } - // Delay - if let Err(e) = self.run_impedancemeter_tx.try_send(self.frequency) { - eprintln!("Failed to send frequency update: {:?}", e); - } + let response = ui.add_enabled(connected, DragValue::new(&mut self.frequency).speed(0.1).update_while_editing(false).range(RangeInclusive::new(0, 50))); + + if response.changed() && response.lost_focus() { + self.update_start_stop(); + } + + ui.separator(); + + if ui.add_enabled(connected, toggle_start_stop(&mut self.on)).changed() { + self.update_start_stop(); }; - ui.separator(); + // Spacer to push the LED to the right + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.scope(|ui| { + let is_connected = self.connected.load(Ordering::Relaxed); + let color = if is_connected { Color32::GREEN } else { Color32::RED }; + let tooltip = if is_connected { "Connected" } else { "Disconnected" }; - if ui.button("Start").clicked() { - if let Err(e) = self.run_impedancemeter_tx.try_send(self.frequency) { - eprintln!("Failed to send start command: {:?}", e); - } - } + // Allocate a fixed-size rectangle for the LED + let led_size = egui::Vec2::splat(12.0); + let (rect, response) = ui.allocate_exact_size(led_size, egui::Sense::hover()); - ui.separator(); - - if ui.button("Stop").clicked() { - if let Err(e) = self.run_impedancemeter_tx.try_send(0) { - eprintln!("Failed to send stop command: {:?}", e); + // Draw the circle + let center = rect.center(); + let radius = 5.0; + ui.painter().circle_filled(center, radius, color); + + // Tooltip + if response.hovered() { + response.on_hover_text(tooltip); } - } + }); + }); }); }); @@ -130,5 +165,49 @@ impl eframe::App for App { ctx.send_viewport_cmd(egui::ViewportCommand::Close); } + // CMD- or control-W to close window + if ctx.input(|i| i.key_pressed(Key::Space)) + { + self.on = !self.on; + self.update_start_stop(); + } + ctx.request_repaint(); }} + + +fn toggle_ui_start_stop(ui: &mut egui::Ui, on: &mut bool) -> egui::Response { + let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0); + let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click()); + if response.clicked() { + *on = !*on; + response.mark_changed(); + } + response.widget_info(|| { + egui::WidgetInfo::selected(egui::WidgetType::Checkbox, ui.is_enabled(), *on, "") + }); + + if ui.is_rect_visible(rect) { + let how_on = ui.ctx().animate_bool_responsive(response.id, *on); + let visuals = ui.style().interact_selectable(&response, *on); + let rect = rect.expand(visuals.expansion); + let radius = 0.5 * rect.height(); + ui.painter().rect( + rect, + radius, + visuals.bg_fill, + visuals.bg_stroke, + egui::StrokeKind::Inside, + ); + let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on); + let center = egui::pos2(circle_x, rect.center().y); + ui.painter() + .circle(center, 0.75 * radius, visuals.bg_fill, visuals.fg_stroke); + } + + response +} + +pub fn toggle_start_stop(on: &mut bool) -> impl egui::Widget + '_ { + move |ui: &mut egui::Ui| toggle_ui_start_stop(ui, on) +} \ No newline at end of file diff --git a/src/bin/main_gui.rs b/src/bin/main_gui.rs index 3de4dfd..24a691b 100644 --- a/src/bin/main_gui.rs +++ b/src/bin/main_gui.rs @@ -8,6 +8,8 @@ use bioz_host_rs::communication::communicate_with_hardware; use tokio::sync::mpsc::{self}; +use bioz_host_rs::signals::FrequencySignal; + fn main() { SimpleLogger::new().init().expect("Failed to initialize logger"); log::set_max_level(log::LevelFilter::Info); @@ -18,13 +20,14 @@ fn main() { // Enter the runtime so that `tokio::spawn` is available immediately. // let _enter = rt.enter(); - let (run_impedancemeter_tx, run_impedancemeter_rx) = mpsc::channel::(2); + let (run_impedancemeter_tx, run_impedancemeter_rx) = mpsc::channel::(2); let app = App::new(run_impedancemeter_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 connected_clone = app.connected.clone(); // Execute the runtime in its own thread. std::thread::spawn(move || { @@ -33,7 +36,8 @@ fn main() { magnitude_clone, phase_clone, magnitude_series_clone, - phase_series_clone + phase_series_clone, + connected_clone, )); }); diff --git a/src/communication.rs b/src/communication.rs index 9cf6ad2..d1864c4 100644 --- a/src/communication.rs +++ b/src/communication.rs @@ -3,6 +3,7 @@ use log::{error, info}; use tokio::select; use tokio::sync::mpsc::Receiver; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use crate::icd; @@ -10,17 +11,21 @@ use crate::client::WorkbookClient; use crate::plot::TimeSeriesPlot; +use crate::signals::FrequencySignal; + pub async fn communicate_with_hardware( - mut run_impedancemeter_rx: Receiver, + mut run_impedancemeter_rx: Receiver, magnitude: Arc>, phase: Arc>, magnitude_series: Arc>, phase_series: Arc>, + connected: Arc, ) { loop { let workbook_client = match WorkbookClient::new() { Ok(client) => { info!("Connected to hardware successfully."); + connected.store(true, Ordering::Relaxed); client }, Err(e) => { @@ -57,27 +62,27 @@ pub async fn communicate_with_hardware( loop { select! { - Some(run) = run_impedancemeter_rx.recv() => { - if run > 0 { - // Start the impedancemeter - if let Err(e) = workbook_client.start_impedancemeter(run as f32).await { - error!("Failed to start impedancemeter: {:?}", e); - } else { - info!("Impedancemeter started."); - } - } else { - // Stop the impedancemeter - if let Err(e) = workbook_client.stop_impedancemeter().await { - error!("Failed to stop impedancemeter: {:?}", e); - } else { - info!("Impedancemeter stopped."); - } + Some(frequency) = run_impedancemeter_rx.recv() => { + match frequency { + FrequencySignal::Start(freq) => { + if let Err(e) = workbook_client.start_impedancemeter(freq).await { + error!("Failed to start impedancemeter: {:?}", e); + } else { + info!("Impedancemeter started at frequency: {}", freq); + } + }, + FrequencySignal::Stop => { + if let Err(e) = workbook_client.stop_impedancemeter().await { + error!("Failed to stop impedancemeter: {:?}", e); + } else { + info!("Impedancemeter stopped."); + } + }, } } _ = workbook_client.wait_closed() => { // Handle client closure info!("Client connection closed."); - tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; break; } @@ -89,6 +94,7 @@ pub async fn communicate_with_hardware( } } info!("Communication with hardware ended."); - // Wait for a short period before trying to reconnect + connected.store(false, Ordering::Relaxed); + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; } } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 446e6ce..efd5ce1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ pub mod client; pub mod app; pub mod communication; pub mod plot; +pub mod signals; pub use bioz_icd_rs as icd; pub async fn read_line() -> String { diff --git a/src/signals.rs b/src/signals.rs new file mode 100644 index 0000000..1fd2532 --- /dev/null +++ b/src/signals.rs @@ -0,0 +1,4 @@ +pub enum FrequencySignal { + Start(f32), + Stop, +} \ No newline at end of file