diff --git a/src/app.rs b/src/app.rs index 9b2fa03..d5ba042 100644 --- a/src/app.rs +++ b/src/app.rs @@ -12,7 +12,7 @@ use chrono::Local; use atomic_float::AtomicF32; use tokio::{sync::mpsc::{Sender}}; -use eframe::egui::{self, Button, CollapsingHeader, Color32, ComboBox, DragValue, Id, Key, Label, Layout, Modifiers, RichText, TextEdit, Widget}; +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}; @@ -70,6 +70,8 @@ pub struct App { pub periods_per_dft_sweep: Arc, Option>)>>, pub gui_logging_state: Arc>, log_filename: String, + log_marker_modal: bool, + log_marker: String, } struct TabViewer { @@ -404,6 +406,7 @@ impl TabViewer { ui.label("R: Reset view/plots"); ui.label("S: Toggle settings"); ui.label("L: Toggle logging"); + ui.label("A: Add marker (when logging)"); ui.label("CMD-W: Close window"); } } @@ -542,7 +545,9 @@ impl App { periods_per_dft, periods_per_dft_sweep, gui_logging_state: Arc::new(Mutex::new(LoggingStates::Idle)), - log_filename: format!("log_{}.csv", Local::now().format("%Y%m%d")) + log_filename: format!("log_{}_single.csv", Local::now().format("%Y%m%d")), + log_marker_modal: false, + log_marker: String::new(), }; // For testing purposes, populate the Bode plot with a sample low-pass filter response @@ -608,10 +613,27 @@ impl eframe::App for App { ui.separator(); - if ui.add_enabled(connected, toggle_start_stop(&mut *self.on.lock().unwrap())).changed() { - self.update_start_stop(); + let on = *self.on.lock().unwrap(); + + let (color, text) = match on { + true => (Color32::DARK_RED, "Stop"), + false => (Color32::DARK_GREEN, "Start"), }; + let start_stop_button = Button::new( + RichText::new(text) + .strong() + .color(Color32::WHITE) + ).fill(color) + .corner_radius(5.0) + .min_size(egui::vec2(45.0, 0.0)) + .frame(true); + + if ui.add_enabled(connected, start_stop_button).clicked() { + *self.on.lock().unwrap() = !on; + self.update_start_stop(); + } + ui.separator(); if ui.add_enabled(connected, Button::new("Reset view")).clicked() { @@ -620,33 +642,83 @@ impl eframe::App for App { ui.separator(); + ui.add_enabled(connected, Label::new("Logging:")); + let gui_logging_state = *self.gui_logging_state.lock().unwrap(); - let (color, signal) = match gui_logging_state { - LoggingStates::Idle => (Color32::DARK_RED, LoggingSignal::StartFileLogging(self.log_filename.clone())), - LoggingStates::Starting => (Color32::from_rgb(204, 153, 0), LoggingSignal::StopFileLogging), - LoggingStates::Logging => (Color32::DARK_GREEN, LoggingSignal::StopFileLogging), - }; + let mut logging_enabled = !matches!(gui_logging_state, LoggingStates::Idle); - let button = Button::new( - RichText::new("Logging") - .strong() - .color(Color32::WHITE) - ).fill(color) - .corner_radius(5.0) - .frame(true); + if ui.add_enabled(connected, toggle_start_stop(&mut logging_enabled)).changed() { + let signal = match gui_logging_state { + LoggingStates::Idle => LoggingSignal::StartFileLogging(self.log_filename.clone()), + LoggingStates::Starting | LoggingStates::Logging => LoggingSignal::StopFileLogging, + }; - if ui.add(button).clicked() { self.log_tx.try_send(signal).unwrap_or_else(|e| { error!("Failed to send logging toggle signal: {:?}", e); }); + }; + + ui.separator(); + + ui.add_enabled_ui(on && logging_enabled, |ui| { + if Button::new("Add marker").corner_radius(5.0).min_size(egui::vec2(20.0, 0.0)).ui(ui).on_hover_text("Add a marker to the current time series plots").clicked() { + self.log_marker_modal = true; + } + }); + + if self.log_marker_modal { + if Modal::new(Id::new("modal_marker")) + .show(ctx, |ui| { + ui.vertical_centered(|ui| { + ui.heading("Add marker"); + + ui.add_space(16.0); + + // Input field for marker + let text_edit_response = TextEdit::singleline(&mut self.log_marker) + .desired_width(100.0) + .id(Id::new("marker_field")) + .hint_text("Marker name") + .ui(ui); + + ui.add_space(16.0); + + // Centered Add button + let add_clicked = Button::new("Add") + .corner_radius(5.0) + .min_size(egui::vec2(80.0, 30.0)) + .ui(ui) + .clicked(); + + // Check for Enter key in the TextEdit + 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| { + error!("Failed to send logging marker signal: {:?}", e); + }); + self.log_marker = String::new(); + ui.close(); + } + + // Request focus on the text edit when the modal opens + text_edit_response.request_focus(); + }); + }) + .should_close() + { + self.log_marker_modal = false; + self.log_marker = String::new(); + } } ui.separator(); ui.add_enabled_ui(gui_logging_state == LoggingStates::Idle, |ui| { ui.label("Log filename:"); - TextEdit::singleline(&mut self.log_filename).desired_width(125.0).id(Id::new("file_name_field")).ui(ui); + TextEdit::singleline(&mut self.log_filename).desired_width(150.0).id(Id::new("file_name_field")).ui(ui); }); // Spacer to push the LED to the right @@ -691,6 +763,12 @@ impl eframe::App for App { self.tab_active = TabActive::Single; *self.on.lock().unwrap() = false; self.update_start_stop(); + if *self.gui_logging_state.lock().unwrap() == LoggingStates::Logging { + self.log_tx.try_send(LoggingSignal::StopFileLogging).unwrap_or_else(|e| { + error!("Failed to send logging logging signal: {:?}", e); + }); + } + self.log_filename = format!("log_{}_single.csv", Local::now().format("%Y%m%d")); info!("Switched to Single tab"); } } @@ -699,6 +777,12 @@ impl eframe::App for App { self.tab_active = TabActive::Sweep; *self.on.lock().unwrap() = false; self.update_start_stop(); + if *self.gui_logging_state.lock().unwrap() == LoggingStates::Logging { + self.log_tx.try_send(LoggingSignal::StopFileLogging).unwrap_or_else(|e| { + error!("Failed to send logging logging signal: {:?}", e); + }); + } + self.log_filename = format!("log_{}_sweep.csv", Local::now().format("%Y%m%d")); info!("Switched to Sweep tab"); } } @@ -725,7 +809,8 @@ impl eframe::App for App { // Check if the file name field is focused // If it is, we don't want to trigger shortcuts let file_name_field_is_focused = ctx.memory(|memory| memory.has_focus(Id::new("file_name_field"))); - if !file_name_field_is_focused { + let marker_field_is_focused = ctx.memory(|memory| memory.has_focus(Id::new("marker_field"))); + if !file_name_field_is_focused && !marker_field_is_focused { // Space to start/stop measurement if ctx.input(|i| i.key_pressed(Key::Space)) @@ -746,6 +831,11 @@ impl eframe::App for App { self.reset_view(); } + // Toggle marker modal + if ctx.input(|i| i.key_pressed(egui::Key::A)) && *self.gui_logging_state.lock().unwrap() == LoggingStates::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(); diff --git a/src/bin/main_gui.rs b/src/bin/main_gui.rs index ed6a352..87eace2 100644 --- a/src/bin/main_gui.rs +++ b/src/bin/main_gui.rs @@ -1,3 +1,6 @@ +use eframe::NativeOptions; +use eframe::egui::Vec2; + use simple_logger::SimpleLogger; use log::info; use tokio::runtime::Runtime; @@ -69,9 +72,11 @@ async fn main() { }); // 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)); let _ = eframe::run_native( "Impedance Visualizer [egui + tokio] - Hubald Verzijl - 2025", - eframe::NativeOptions::default(), + native_options, Box::new(|_cc| Ok(Box::new(app))), ); } \ No newline at end of file diff --git a/src/logging.rs b/src/logging.rs index a2662b8..979d706 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -23,6 +23,7 @@ pub async fn log_data( gui_logging_state: Arc>, ) { let mut file: Option = None; + let mut logging_marker = String::new(); loop { match data.recv().await { @@ -30,25 +31,40 @@ pub async fn log_data( match signal { LoggingSignal::SingleImpedance(timestamp, frequency, magnitude, phase) => { if let Some(f) = file.as_mut() { - let _ = f - .write_all(format!("{},{},{:.3},{:.3}\n", timestamp.duration_since(UNIX_EPOCH).unwrap().as_millis(), frequency, magnitude, phase).as_bytes()) - .await; + let _ = f.write_all(format!("{},{},{},{},{}\n", + timestamp.duration_since(UNIX_EPOCH).unwrap().as_millis(), + frequency, + magnitude, + phase, + logging_marker) + .as_bytes()) + .await; + } + if logging_marker.len() > 0 { + logging_marker.clear(); } } LoggingSignal::SweepImpedance(timestamp, frequencies, magnitudes, phases) => { if let Some(f) = file.as_mut() { for i in 0..frequencies.len() { let _ = f.write_all( - format!("{},{},{},{},{}\n", + format!("{},{},{},{},{},{}\n", timestamp.duration_since(UNIX_EPOCH).unwrap().as_millis(), frequencies[i], magnitudes[i], phases[i], - i // optional index + i, // optional index + logging_marker ).as_bytes() ).await; } } + if logging_marker.len() > 0 { + logging_marker.clear(); + } + } + LoggingSignal::AddMarker(marker) => { + logging_marker = marker; } LoggingSignal::StartFileLogging(filename) => { // Update global logging state diff --git a/src/signals.rs b/src/signals.rs index b6238cf..28d5e94 100644 --- a/src/signals.rs +++ b/src/signals.rs @@ -14,4 +14,5 @@ pub enum LoggingSignal { SweepImpedance(SystemTime, Vec, Vec, Vec), // frequency, magnitude, phase StartFileLogging(String), // e.g. filename StopFileLogging, + AddMarker(String), } \ No newline at end of file