mirror of
https://github.com/hubaldv/bioz-host-rs.git
synced 2026-03-09 23:40:31 +00:00
Implemented custom marker support during logging. Updated buttons.
This commit is contained in:
128
src/app.rs
128
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<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);
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
let button = Button::new(
|
||||
RichText::new("Logging")
|
||||
.strong()
|
||||
.color(Color32::WHITE)
|
||||
).fill(color)
|
||||
.corner_radius(5.0)
|
||||
.frame(true);
|
||||
|
||||
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();
|
||||
|
||||
@@ -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))),
|
||||
);
|
||||
}
|
||||
@@ -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())
|
||||
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
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
Reference in New Issue
Block a user