Implemented custom marker support during logging. Updated buttons.

This commit is contained in:
2025-12-17 14:51:26 +01:00
parent 652e319e01
commit 609a9a252c
4 changed files with 137 additions and 25 deletions

View File

@@ -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<Mutex<(Vec<u32>, Option<Vec<f32>>)>>,
pub gui_logging_state: Arc<Mutex<LoggingStates>>,
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();

View File

@@ -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))),
);
}

View File

@@ -23,6 +23,7 @@ pub async fn log_data(
gui_logging_state: Arc<Mutex<LoggingStates>>,
) {
let mut file: Option<File> = 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

View File

@@ -14,4 +14,5 @@ pub enum LoggingSignal {
SweepImpedance(SystemTime, Vec<u32>, Vec<f32>, Vec<f32>), // frequency, magnitude, phase
StartFileLogging(String), // e.g. filename
StopFileLogging,
AddMarker(String),
}