From 28219dc9ee17c2d359a6a8dad3da2d3d51c4f797 Mon Sep 17 00:00:00 2001 From: hacknus Date: Wed, 19 Mar 2025 09:31:40 +0100 Subject: [PATCH 1/7] smarter parser --- src/main.rs | 100 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 70 insertions(+), 30 deletions(-) diff --git a/src/main.rs b/src/main.rs index 6c4fac8..ef25ee1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,12 +5,6 @@ extern crate csv; extern crate preferences; extern crate serde; -use std::cmp::max; -use std::path::PathBuf; -use std::sync::mpsc::{Receiver, Sender}; -use std::sync::{mpsc, Arc, RwLock}; -use std::{env, thread}; - use crate::data::{DataContainer, Packet}; use crate::gui::{load_gui_settings, MyApp, RIGHT_PANEL_WIDTH}; use crate::io::{open_from_csv, save_to_csv, FileOptions}; @@ -18,6 +12,11 @@ use crate::serial::{load_serial_settings, serial_thread, Device}; use eframe::egui::{vec2, ViewportBuilder, Visuals}; use eframe::{egui, icon_data}; use preferences::AppInfo; +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::mpsc::{Receiver, Sender}; +use std::sync::{mpsc, Arc, RwLock}; +use std::{env, thread}; mod color_picker; mod custom_highlighter; @@ -36,16 +35,33 @@ const APP_INFO: AppInfo = AppInfo { const PREFERENCES_KEY: &str = "config/gui"; const PREFERENCES_KEY_SERIAL: &str = "config/serial_devices"; -fn split(payload: &str) -> Vec { +fn split(payload: &str) -> (Option, Vec) { let mut split_data: Vec<&str> = vec![]; for s in payload.split(':') { split_data.extend(s.split(',')); } - split_data - .iter() - .map(|x| x.trim()) - .flat_map(|x| x.parse::()) - .collect() + if split_data.is_empty() { + return (None, vec![]); + } + // Try to parse the first entry as a number + let first_entry = split_data[0]; + if first_entry.parse::().is_ok() { + // First entry is a number → No identifier, process normally + let values: Vec = split_data + .iter() + .map(|x| x.trim()) + .flat_map(|x| x.parse::()) + .collect(); + (None, values) + } else { + // First entry is a string identifier → Process with identifier + let identifier = first_entry.to_string(); + let values: Vec = split_data[1..] + .iter() + .flat_map(|x| x.parse::()) + .collect(); + (Some(identifier), values) + } } fn main_thread( @@ -59,6 +75,7 @@ fn main_thread( ) { // reads data from mutex, samples and saves if needed let mut data = DataContainer::default(); + let mut identifier_map: HashMap = HashMap::new(); let mut failed_format_counter = 0; let mut file_opened = false; @@ -67,6 +84,7 @@ fn main_thread( if let Ok(cl) = clear_rx.try_recv() { if cl { data = DataContainer::default(); + identifier_map = HashMap::new(); failed_format_counter = 0; } } @@ -76,29 +94,51 @@ fn main_thread( if !packet.payload.is_empty() { sync_tx.send(true).expect("unable to send sync tx"); data.raw_traffic.push(packet.clone()); - let split_data = split(&packet.payload); + data.absolute_time.push(packet.absolute_time); + + let (identifier_opt, values) = split(&packet.payload); + if data.dataset.is_empty() || failed_format_counter > 10 { - // resetting dataset - data.dataset = vec![vec![]; max(split_data.len(), 1)]; + // Reset dataset + data.dataset = vec![vec![]; values.len()]; failed_format_counter = 0; - // log::error!("resetting dataset. split length = {}, length data.dataset = {}", split_data.len(), data.dataset.len()); - } else if split_data.len() == data.dataset.len() { - // appending data - for (i, set) in data.dataset.iter_mut().enumerate() { - set.push(split_data[i]); - failed_format_counter = 0; + } + + if let Some(identifier) = identifier_opt { + // Get or create the correct index for this identifier + let index = + *identifier_map.entry(identifier.clone()).or_insert_with(|| { + let new_index = data.dataset.len(); + data.dataset.push(vec![]); // Ensure space for new identifier + new_index + }); + + // Ensure dataset has enough columns + while data.dataset.len() <= index { + data.dataset.push(vec![]); } - data.time.push(packet.relative_time); - data.absolute_time.push(packet.absolute_time); - if data.time.len() != data.dataset[0].len() { - // resetting dataset - data.time = vec![]; - data.dataset = vec![vec![]; max(split_data.len(), 1)]; + + if values.len() == data.dataset.len() { + // Insert values at the correct dataset index + for &value in values.iter() { + data.dataset[index].push(value); + } + data.time.push(packet.relative_time); + data.absolute_time.push(packet.absolute_time); + } else { + failed_format_counter += 1; } } else { - // not same length - failed_format_counter += 1; - // log::error!("not same length in main! length split_data = {}, length data.dataset = {}", split_data.len(), data.dataset.len()) + // Handle unnamed datasets (pure numerical data, behaves as before) + if values.len() == data.dataset.len() { + for (i, &value) in values.iter().enumerate() { + data.dataset[i].push(value); + } + data.time.push(packet.relative_time); + data.absolute_time.push(packet.absolute_time); + } else { + failed_format_counter += 1; + } } } } From eaab01b199951da3b1deec6c918b6576a785be70 Mon Sep 17 00:00:00 2001 From: labdeglace_lini Date: Wed, 19 Mar 2025 09:59:44 +0100 Subject: [PATCH 2/7] not working yet --- src/main.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index ef25ee1..590794a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ extern crate csv; extern crate preferences; extern crate serde; -use crate::data::{DataContainer, Packet}; +use crate::data::{DataContainer, Packet, SerialDirection}; use crate::gui::{load_gui_settings, MyApp, RIGHT_PANEL_WIDTH}; use crate::io::{open_from_csv, save_to_csv, FileOptions}; use crate::serial::{load_serial_settings, serial_thread, Device}; @@ -96,6 +96,10 @@ fn main_thread( data.raw_traffic.push(packet.clone()); data.absolute_time.push(packet.absolute_time); + if packet.direction == SerialDirection::Send { + continue; + } + let (identifier_opt, values) = split(&packet.payload); if data.dataset.is_empty() || failed_format_counter > 10 { From 22652f9d17e703a3abc458b458328328bf5c9fe6 Mon Sep 17 00:00:00 2001 From: hacknus Date: Wed, 19 Mar 2025 10:16:30 +0100 Subject: [PATCH 3/7] adapt parser for time value --- src/data.rs | 4 ++-- src/gui.rs | 21 +++++++++++++-------- src/io.rs | 18 ++++++++++++++---- src/main.rs | 25 ++++++++++++------------- 4 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/data.rs b/src/data.rs index 20cffe4..f27d109 100644 --- a/src/data.rs +++ b/src/data.rs @@ -44,7 +44,7 @@ impl Default for Packet { #[derive(Clone, Debug)] pub struct DataContainer { - pub time: Vec, + pub time: Vec>, pub absolute_time: Vec, pub dataset: Vec>, pub raw_traffic: Vec, @@ -54,7 +54,7 @@ pub struct DataContainer { impl Default for DataContainer { fn default() -> DataContainer { DataContainer { - time: vec![], + time: vec![vec![]], absolute_time: vec![], dataset: vec![vec![]], raw_traffic: vec![], diff --git a/src/gui.rs b/src/gui.rs index 651b7ff..8e81ffc 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -357,14 +357,19 @@ impl MyApp { .len() .saturating_sub(self.plotting_range); - for (i, time) in self.data.time[window..].iter().enumerate() { - let x = *time / 1000.0; - for (graph, data) in graphs.iter_mut().zip(&self.data.dataset) { - if self.data.time.len() == data.len() { - if let Some(y) = data.get(i + window) { - graph.push(PlotPoint { x, y: *y as f64 }); - } - } + for (graph, (timepoints, datapoints)) in graphs + .iter_mut() + .zip(self.data.time.iter().zip(self.data.dataset.iter())) + { + for (time, data) in timepoints + .iter() + .skip(window) + .zip(datapoints.iter().skip(window)) + { + graph.push(PlotPoint { + x: *time / 1000.0, + y: *data as f64, + }); } } diff --git a/src/io.rs b/src/io.rs index 46e25a9..864850b 100644 --- a/src/io.rs +++ b/src/io.rs @@ -48,12 +48,17 @@ pub fn open_from_csv( let time_value = record.get(0).unwrap(); if csv_options.save_absolute_time { data.absolute_time.push(time_value.parse()?); - } else { - data.time.push(time_value.parse()?); } // Parse the remaining columns and populate the dataset for (i, value) in record.iter().skip(1).enumerate() { + if !csv_options.save_absolute_time { + if let Some(time_column) = data.time.get_mut(i) { + time_column.push(time_value.parse()?); + } else { + return Err("Unexpected number of time data columns in the CSV".into()); + } + } if let Some(dataset_column) = data.dataset.get_mut(i) { dataset_column.push(value.parse()?); } else { @@ -79,7 +84,8 @@ pub fn save_to_csv(data: &DataContainer, csv_options: &FileOptions) -> Result<() let time = if csv_options.save_absolute_time { data.absolute_time[j].to_string() } else { - data.time[j].to_string() + // TODO: this currently just takes the time value of the first entry + data.time[0][j].to_string() }; let mut data_to_write = vec![time]; for value in data.dataset.iter() { @@ -114,7 +120,11 @@ pub fn save_raw(data: &DataContainer, path: &PathBuf) -> Result<(), Box Date: Wed, 19 Mar 2025 10:33:14 +0100 Subject: [PATCH 4/7] fix proper alignment --- src/main.rs | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/main.rs b/src/main.rs index 9d31ecb..ec88847 100644 --- a/src/main.rs +++ b/src/main.rs @@ -112,25 +112,24 @@ fn main_thread( let index = *identifier_map.entry(identifier.clone()).or_insert_with(|| { let new_index = data.dataset.len(); - data.dataset.push(vec![]); // Ensure space for new identifier - data.time.push(vec![]); // Ensure time tracking for this identifier + for _ in 0..values.len() { + data.dataset.push(vec![]); // Ensure space for new identifier + data.time.push(vec![]); // Ensure time tracking for this identifier + } new_index }); - // Ensure dataset and time vectors have enough columns - while data.dataset.len() <= index { - data.dataset.push(vec![]); - data.time.push(vec![]); - } + // // Ensure dataset and time vectors have enough columns + // while data.dataset.len() <= index { + // data.dataset.push(vec![]); + // data.time.push(vec![]); + // } // Append values to corresponding dataset entries - for &value in values.iter() { - data.dataset[index].push(value); + for (i, &value) in values.iter().enumerate() { + data.dataset[index + i].push(value); + data.time[index + i].push(packet.relative_time); } - - // Store time in corresponding location - data.time[index].push(packet.relative_time); - data.absolute_time.push(packet.absolute_time); } else { // Handle unnamed datasets (pure numerical data) if values.len() == data.dataset.len() { @@ -138,7 +137,6 @@ fn main_thread( data.dataset[i].push(value); data.time[i].push(packet.relative_time); } - data.absolute_time.push(packet.absolute_time); } else { failed_format_counter += 1; } From 15fa6d62814ab1c6c9ab582e9db3dac83af3a51b Mon Sep 17 00:00:00 2001 From: labdeglace_lini Date: Wed, 19 Mar 2025 10:46:38 +0100 Subject: [PATCH 5/7] adjust readme and changelog --- CHANGELOG.md | 1 + README.md | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e4ff5d..e9f28e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to the `Serial Monitor` crate will be documented in this fil # Unreleased 0.3.x * Fixed sample rate issue +* Implement smarter data parser that also can read and assign multi-line data-packets * ... # 0.3.4 diff --git a/README.md b/README.md index 3a001ab..32cd180 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,14 @@ It can be compiled and run on all platforms. - [X] Automatic reconnect after device has been unplugged - [X] Color-picker for curves - [X] Open a CSV file and display data in plot +- [X] Smart data parser, multiline packets will be correctly assigned: + + ```DATA1: 0, 1, 2, 3``` + ```DATA2: 1, 2, 4, 9``` + + However, when one saves the data as a CSV, only the timestamp of the fist data-packet will be saved. +- [ ] Allow timestamp selection for CSV saving (in case of multiline data-packets) - [ ] Allow to select (and copy) more than just the displayed raw traffic (also implement ctrl + A) -- [ ] Smarter data parser - [ ] make serial print selectable and show corresponding datapoint in plot - [ ] COM-Port names on Windows (display manufacturer, name, pid or vid of device?) - [ ] current command entered is lost when navigating through the history From 7c0da452f72d2e52dca38052b0d063062d4ab8a9 Mon Sep 17 00:00:00 2001 From: hacknus Date: Tue, 25 Mar 2025 20:11:38 +0100 Subject: [PATCH 6/7] adapt for latest updates from main --- src/gui.rs | 14 ++-- src/main.rs | 202 ++++++++++++++++++++++++++++++++-------------------- 2 files changed, 131 insertions(+), 85 deletions(-) diff --git a/src/gui.rs b/src/gui.rs index 7d67bf1..8b51e77 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -454,15 +454,13 @@ impl MyApp { .min_scrolled_height(serial_height - top_spacing) .max_width(width) .show_rows(ui, row_height, num_rows, |ui, row_range| { - let content: String = row_range + let content: String = self + .data + .prints + .clone() .into_iter() - .flat_map(|i| { - if self.data.prints.is_empty() { - None - } else { - Some(self.data.prints[i].clone()) - } - }) + .skip(row_range.start) + .take(row_range.count()) .collect(); let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| { diff --git a/src/main.rs b/src/main.rs index f39469e..8e080e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,6 @@ use eframe::{egui, icon_data}; use egui_plot::PlotPoint; use preferences::AppInfo; use std::collections::HashMap; -use std::cmp::max; use std::path::PathBuf; use std::sync::{Arc, RwLock}; use std::time::Duration; @@ -61,7 +60,10 @@ fn split(payload: &str) -> (Option, Vec) { let identifier = first_entry.to_string(); let values: Vec = split_data[1..] .iter() - .flat_map(|x| x.parse::()) + .filter_map(|x| match x.trim().parse::() { + Ok(val) => Some(val), + Err(_) => None, + }) .collect(); (Some(identifier), values) } @@ -99,6 +101,7 @@ fn main_thread( let mut data = DataContainer::default(); let mut identifier_map: HashMap = HashMap::new(); let mut failed_format_counter = 0; + let mut failed_key_counter = 0; let mut show_timestamps = true; let mut show_sent_cmds = true; @@ -123,97 +126,141 @@ fn main_thread( } } - if packet.direction == SerialDirection::Send { - continue; - } + if packet.direction == SerialDirection::Receive { - let (identifier_opt, values) = split(&packet.payload); - if data.dataset.is_empty() || failed_format_counter > 10 { - // resetting dataset - data.time = vec![]; - data.dataset = vec![vec![]; values.len()]; - if let Ok(mut gui_data) = data_lock.write() { - gui_data.plots = (0..values.len()) - .map(|i| (format!("Column {i}"), vec![])) - .collect(); - } - failed_format_counter = 0; - // log::error!("resetting dataset. split length = {}, length data.dataset = {}", split_data.len(), data.dataset.len()); - } else if split_data.len() == data.dataset.len() { - // appending data - for (i, set) in data.dataset.iter_mut().enumerate() { - set.push(split_data[i]); - failed_format_counter = 0; - identifier_map = HashMap::new(); - } + let (identifier_opt, values) = split(&packet.payload); - data.time.push(packet.relative_time); - data.absolute_time.push(packet.absolute_time); - - // appending data for GUI thread - if let Ok(mut gui_data) = data_lock.write() { - // append plot-points - for ((_label, graph), data_i) in - gui_data.plots.iter_mut().zip(&data.dataset) - { - if data.time.len() == data_i.len() { - if let Some(y) = data_i.last() { - graph.push(PlotPoint { - x: packet.relative_time / 1000.0, - y: *y as f64, - }); - } - } - } - } - if data.time.len() != data.dataset[0].len() { + if data.dataset.is_empty() || failed_format_counter > 10 { // resetting dataset - data.time = vec![]; - data.dataset = vec![vec![]; max(split_data.len(), 1)]; + data.time = vec![vec![]; values.len()]; + data.dataset = vec![vec![]; values.len()]; if let Ok(mut gui_data) = data_lock.write() { - gui_data.prints = vec!["".to_string(); max(split_data.len(), 1)]; - gui_data.plots = (0..max(split_data.len(), 1)) + gui_data.plots = (0..values.len()) .map(|i| (format!("Column {i}"), vec![])) .collect(); } + failed_format_counter = 0; + // log::error!("resetting dataset. split length = {}, length data.dataset = {}", split_data.len(), data.dataset.len()); } - } else { - // not same length - failed_format_counter += 1; - // log::error!("not same length in main! length split_data = {}, length data.dataset = {}", split_data.len(), data.dataset.len()) - } + // else if split_data.len() == data.dataset.len() { + // // appending data + // for (i, set) in data.dataset.iter_mut().enumerate() { + // set.push(split_data[i]); + // failed_format_counter = 0; + // identifier_map = HashMap::new(); + // } + // + // data.time.push(packet.relative_time); + // data.absolute_time.push(packet.absolute_time); + // + // // appending data for GUI thread + // if let Ok(mut gui_data) = data_lock.write() { + // // append plot-points + // for ((_label, graph), data_i) in + // gui_data.plots.iter_mut().zip(&data.dataset) + // { + // if data.time.len() == data_i.len() { + // if let Some(y) = data_i.last() { + // graph.push(PlotPoint { + // x: packet.relative_time / 1000.0, + // y: *y as f64, + // }); + // } + // } + // } + // } + // if data.time.len() != data.dataset[0].len() { + // // resetting dataset + // data.time = vec![]; + // data.dataset = vec![vec![]; max(split_data.len(), 1)]; + // if let Ok(mut gui_data) = data_lock.write() { + // gui_data.prints = vec!["".to_string(); max(split_data.len(), 1)]; + // gui_data.plots = (0..max(split_data.len(), 1)) + // .map(|i| (format!("Column {i}"), vec![])) + // .collect(); + // } + // } + // } else { + // // not same length + // failed_format_counter += 1; + // // log::error!("not same length in main! length split_data = {}, length data.dataset = {}", split_data.len(), data.dataset.len()) + // } + + if let Some(identifier) = identifier_opt { + if !identifier_map.contains_key(&identifier) { + failed_key_counter += 1; + if failed_key_counter < 10 && !identifier_map.is_empty() { + continue; // skip outer loop iteration + } - if let Some(identifier) = identifier_opt { - let index = - *identifier_map.entry(identifier.clone()).or_insert_with(|| { let new_index = data.dataset.len(); for _ in 0..values.len() { - data.dataset.push(vec![]); // Ensure space for new identifier - data.time.push(vec![]); // Ensure time tracking for this identifier + data.dataset.push(vec![]); + data.time.push(vec![]); } - new_index - }); - // // Ensure dataset and time vectors have enough columns - // while data.dataset.len() <= index { - // data.dataset.push(vec![]); - // data.time.push(vec![]); - // } + if let Ok(mut gui_data) = data_lock.write() { + gui_data.plots = (0..data.dataset.len()) + .map(|i| (format!("Column {i}"), vec![])) + .collect(); + } - // Append values to corresponding dataset entries - for (i, &value) in values.iter().enumerate() { - data.dataset[index + i].push(value); - data.time[index + i].push(packet.relative_time); - } - } else { - // Handle unnamed datasets (pure numerical data) - if values.len() == data.dataset.len() { + identifier_map.insert(identifier.clone(), new_index); + } else { + failed_key_counter = 0; + } + + let index = identifier_map[&identifier]; + + // // Ensure dataset and time vectors have enough columns + // while data.dataset.len() <= index { + // data.dataset.push(vec![]); + // data.time.push(vec![]); + // } + + // Append values to corresponding dataset entries for (i, &value) in values.iter().enumerate() { - data.dataset[i].push(value); - data.time[i].push(packet.relative_time); + data.dataset[index + i].push(value); + data.time[index + i].push(packet.relative_time); + } + + if let Ok(mut gui_data) = data_lock.write() { + for( ((_label, graph), data_i), time_i) in + gui_data.plots.iter_mut().zip(&data.dataset).zip(&data.time) + { + if let (Some(y), Some(t)) = (data_i.last(), time_i.last() ){ + graph.push(PlotPoint { + x: *t / 1000.0, + y: *y as f64, + }); + } + + } } } else { - failed_format_counter += 1; + // Handle unnamed datasets (pure numerical data) + if values.len() == data.dataset.len() { + for (i, &value) in values.iter().enumerate() { + data.dataset[i].push(value); + data.time[i].push(packet.relative_time); + } + if let Ok(mut gui_data) = data_lock.write() { + for ((_label, graph), data_i) in + gui_data.plots.iter_mut().zip(&data.dataset) + { + if data.time.len() == data_i.len() { + if let Some(y) = data_i.last() { + graph.push(PlotPoint { + x: packet.relative_time / 1000.0, + y: *y as f64, + }); + } + } + } + } + } else { + failed_format_counter += 1; + } } } } @@ -271,7 +318,8 @@ fn main_thread( { for (y,t) in data_i.iter().zip(data.time.iter()) { graph.push(PlotPoint { - x: *t / 1000.0, + // TODO: this always takes the first time value + x: t[0] / 1000.0, y: *y as f64 , }); } From 15a4edc6800e356cde0eae2365c8f3a33e9c3e01 Mon Sep 17 00:00:00 2001 From: hacknus Date: Wed, 26 Mar 2025 08:05:37 +0100 Subject: [PATCH 7/7] reduce CPU load (remove clone from gui thread) --- src/data.rs | 14 +- src/gui.rs | 384 ++++++++++++++++++++++++++-------------------------- src/main.rs | 23 +++- 3 files changed, 213 insertions(+), 208 deletions(-) diff --git a/src/data.rs b/src/data.rs index e5d569e..282ff0a 100644 --- a/src/data.rs +++ b/src/data.rs @@ -43,7 +43,7 @@ impl Default for Packet { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct DataContainer { pub time: Vec>, pub absolute_time: Vec, @@ -52,18 +52,6 @@ pub struct DataContainer { pub loaded_from_file: bool, } -impl Default for DataContainer { - fn default() -> DataContainer { - DataContainer { - time: vec![vec![]], - absolute_time: vec![], - dataset: vec![vec![]], - raw_traffic: vec![], - loaded_from_file: false, - } - } -} - #[derive(Clone, Debug, Default)] pub struct GuiOutputDataContainer { pub prints: Vec, diff --git a/src/gui.rs b/src/gui.rs index 8b51e77..dec06fb 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -4,7 +4,7 @@ use std::cmp::max; use std::ops::RangeInclusive; use std::path::PathBuf; use std::sync::{Arc, RwLock}; -use std::time::Duration; +use std::time::{Duration, Instant}; use crate::color_picker::{color_picker_widget, color_picker_window, COLORS}; use crate::custom_highlighter::highlight_impl; @@ -125,7 +125,6 @@ pub struct MyApp { plot_serial_display_ratio: f32, picked_path: PathBuf, plot_location: Option, - data: GuiOutputDataContainer, file_dialog_state: FileDialogState, file_dialog: FileDialog, information_panel: InformationPanel, @@ -210,7 +209,6 @@ impl MyApp { picked_path: PathBuf::new(), device: "".to_string(), old_device: "".to_string(), - data: GuiOutputDataContainer::default(), file_dialog_state: FileDialogState::None, file_dialog, information_panel: InformationPanel::default().add_file_preview("csv", |ui, item| { @@ -316,206 +314,210 @@ impl MyApp { ui.add_space(left_border); ui.vertical(|ui| { if let Ok(gui_data) = self.data_lock.read() { - self.data = gui_data.clone(); self.labels = gui_data.plots.iter().map(|d| d.0.clone()).collect(); self.colors = (0..max(self.labels.len(), 1)) .map(|i| COLORS[i % COLORS.len()]) .collect(); - self.color_vals = (0..max(self.data.plots.len(), 1)).map(|_| 0.0).collect(); - } - - // TODO what about self.data.loaded_from_file - if self.file_opened { - if let Ok(labels) = self.load_names_rx.try_recv() { - self.labels = labels; - self.colors = (0..max(self.labels.len(), 1)) - .map(|i| COLORS[i % COLORS.len()]) - .collect(); - self.color_vals = (0..max(self.labels.len(), 1)).map(|_| 0.0).collect(); - } - } - if self.serial_devices.number_of_plots[self.device_idx] > 0 { - if self.data.plots.len() != self.labels.len() && !self.file_opened { - // self.labels = (0..max(self.data.dataset.len(), 1)) - // .map(|i| format!("Column {i}")) - // .collect(); - self.colors = (0..max(self.data.plots.len(), 1)) - .map(|i| COLORS[i % COLORS.len()]) - .collect(); - self.color_vals = - (0..max(self.data.plots.len(), 1)).map(|_| 0.0).collect(); + self.color_vals = (0..max(gui_data.plots.len(), 1)).map(|_| 0.0).collect(); + + // TODO what about self.data.loaded_from_file + if self.file_opened { + if let Ok(labels) = self.load_names_rx.try_recv() { + self.labels = labels; + self.colors = (0..max(self.labels.len(), 1)) + .map(|i| COLORS[i % COLORS.len()]) + .collect(); + self.color_vals = + (0..max(self.labels.len(), 1)).map(|_| 0.0).collect(); + } } + if self.serial_devices.number_of_plots[self.device_idx] > 0 { + if gui_data.plots.len() != self.labels.len() && !self.file_opened { + // self.labels = (0..max(self.data.dataset.len(), 1)) + // .map(|i| format!("Column {i}")) + // .collect(); + self.colors = (0..max(gui_data.plots.len(), 1)) + .map(|i| COLORS[i % COLORS.len()]) + .collect(); + self.color_vals = + (0..max(gui_data.plots.len(), 1)).map(|_| 0.0).collect(); + } - // offloaded to main thread - - // for (graph, (timepoints, datapoints)) in graphs - // .iter_mut() - // .zip(self.data.time.iter().zip(self.data.dataset.iter())) - // { - // for (time, data) in timepoints - // .iter() - // .skip(window) - // .zip(datapoints.iter().skip(window)) - // { - // graph.push(PlotPoint { - // x: *time / 1000.0, - // y: *data as f64, - // }); - // } - // } - - let window = if let Some(first_entry) = self.data.plots.first() { - first_entry.1.len().saturating_sub(self.plotting_range) - } else { - 0 - }; - - let t_fmt = |x: GridMark, _range: &RangeInclusive| { - format!("{:4.2} s", x.value) - }; + // offloaded to main thread + + // for (graph, (timepoints, datapoints)) in graphs + // .iter_mut() + // .zip(self.data.time.iter().zip(self.data.dataset.iter())) + // { + // for (time, data) in timepoints + // .iter() + // .skip(window) + // .zip(datapoints.iter().skip(window)) + // { + // graph.push(PlotPoint { + // x: *time / 1000.0, + // y: *data as f64, + // }); + // } + // } + + let window = if let Some(first_entry) = gui_data.plots.first() { + first_entry.1.len().saturating_sub(self.plotting_range) + } else { + 0 + }; - let plots_ui = ui.vertical(|ui| { - for graph_idx in 0..self.serial_devices.number_of_plots[self.device_idx] - { - if graph_idx != 0 { - ui.separator(); - } + let t_fmt = |x: GridMark, _range: &RangeInclusive| { + format!("{:4.2} s", x.value) + }; - let signal_plot = Plot::new(format!("data-{graph_idx}")) - .height(plot_height) - .width(width) - .legend(Legend::default()) - .x_grid_spacer(log_grid_spacer(10)) - .y_grid_spacer(log_grid_spacer(10)) - .x_axis_formatter(t_fmt); - - let plot_inner = signal_plot.show(ui, |signal_plot_ui| { - for (i, (_label, graph)) in self.data.plots.iter().enumerate() { - // this check needs to be here for when we change devices (not very elegant) - if i < self.labels.len() { - signal_plot_ui.line( - Line::new(PlotPoints::Owned( - graph[window..].to_vec(), - )) - .name(&self.labels[i]) - .color(self.colors[i]), - ); - } + let plots_ui = ui.vertical(|ui| { + for graph_idx in + 0..self.serial_devices.number_of_plots[self.device_idx] + { + if graph_idx != 0 { + ui.separator(); } - }); - self.plot_location = Some(plot_inner.response.rect); - } - let separator_response = ui.separator(); - let separator = ui - .interact( - separator_response.rect, - separator_response.id, - Sense::click_and_drag(), - ) - .on_hover_cursor(egui::CursorIcon::ResizeVertical); - - let resize_y = separator.drag_delta().y; - - if separator.double_clicked() { - self.plot_serial_display_ratio = 0.45; - } - self.plot_serial_display_ratio = (self.plot_serial_display_ratio - + resize_y / panel_height) - .clamp(0.1, 0.9); + let signal_plot = Plot::new(format!("data-{graph_idx}")) + .height(plot_height) + .width(width) + .legend(Legend::default()) + .x_grid_spacer(log_grid_spacer(10)) + .y_grid_spacer(log_grid_spacer(10)) + .x_axis_formatter(t_fmt); + + let plot_inner = signal_plot.show(ui, |signal_plot_ui| { + for (i, (_label, graph)) in + gui_data.plots.iter().enumerate() + { + // this check needs to be here for when we change devices (not very elegant) + if i < self.labels.len() { + signal_plot_ui.line( + Line::new(PlotPoints::Owned( + graph[window..].to_vec(), + )) + .name(&self.labels[i]) + .color(self.colors[i]), + ); + } + } + }); - ui.add_space(top_spacing); - }); - plot_ui_heigh = plots_ui.response.rect.height(); - } else { - plot_ui_heigh = 0.0; - } + self.plot_location = Some(plot_inner.response.rect); + } + let separator_response = ui.separator(); + let separator = ui + .interact( + separator_response.rect, + separator_response.id, + Sense::click_and_drag(), + ) + .on_hover_cursor(egui::CursorIcon::ResizeVertical); + + let resize_y = separator.drag_delta().y; + + if separator.double_clicked() { + self.plot_serial_display_ratio = 0.45; + } + self.plot_serial_display_ratio = (self.plot_serial_display_ratio + + resize_y / panel_height) + .clamp(0.1, 0.9); - let serial_height = - panel_height - plot_ui_heigh - left_border * 2.0 - top_spacing; + ui.add_space(top_spacing); + }); + plot_ui_heigh = plots_ui.response.rect.height(); + } else { + plot_ui_heigh = 0.0; + } - let num_rows = self.data.prints.len(); - let row_height = ui.text_style_height(&egui::TextStyle::Body); + let serial_height = + panel_height - plot_ui_heigh - left_border * 2.0 - top_spacing; - let color = if self.gui_conf.dark_mode { - Color32::WHITE - } else { - Color32::BLACK - }; + let num_rows = gui_data.prints.len(); + let row_height = ui.text_style_height(&egui::TextStyle::Body); - let mut text_edit_size = ui.available_size(); - text_edit_size.x = width; - egui::ScrollArea::vertical() - .id_salt("serial_output") - .auto_shrink([false; 2]) - .stick_to_bottom(true) - .enable_scrolling(true) - .max_height(serial_height - top_spacing) - .min_scrolled_height(serial_height - top_spacing) - .max_width(width) - .show_rows(ui, row_height, num_rows, |ui, row_range| { - let content: String = self - .data - .prints - .clone() - .into_iter() - .skip(row_range.start) - .take(row_range.count()) - .collect(); - - let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| { - let mut layout_job = highlight_impl( - ui.ctx(), - string, - self.serial_devices.highlight_labels[self.device_idx].clone(), - Color32::from_rgb(155, 164, 167), - ) - .unwrap(); - layout_job.wrap.max_width = wrap_width; - ui.fonts(|f| f.layout_job(layout_job)) - }; + let color = if self.gui_conf.dark_mode { + Color32::WHITE + } else { + Color32::BLACK + }; - ui.add( - egui::TextEdit::multiline(&mut content.as_str()) - .font(DEFAULT_FONT_ID) // for cursor height + let mut text_edit_size = ui.available_size(); + text_edit_size.x = width; + egui::ScrollArea::vertical() + .id_salt("serial_output") + .auto_shrink([false; 2]) + .stick_to_bottom(true) + .enable_scrolling(true) + .max_height(serial_height - top_spacing) + .min_scrolled_height(serial_height - top_spacing) + .max_width(width) + .show_rows(ui, row_height, num_rows, |ui, row_range| { + let content: String = gui_data + .prints + .clone() + .into_iter() + .skip(row_range.start) + .take(row_range.count()) + .collect(); + + let mut layouter = + |ui: &egui::Ui, string: &str, wrap_width: f32| { + let mut layout_job = highlight_impl( + ui.ctx(), + string, + self.serial_devices.highlight_labels[self.device_idx] + .clone(), + Color32::from_rgb(155, 164, 167), + ) + .unwrap(); + layout_job.wrap.max_width = wrap_width; + ui.fonts(|f| f.layout_job(layout_job)) + }; + + ui.add( + egui::TextEdit::multiline(&mut content.as_str()) + .font(DEFAULT_FONT_ID) // for cursor height + .lock_focus(true) + .text_color(color) + .desired_width(width) + .layouter(&mut layouter), + ); + }); + ui.horizontal(|ui| { + let cmd_line = ui.add( + egui::TextEdit::singleline(&mut self.command) + .desired_width(width - 50.0) .lock_focus(true) - .text_color(color) - .desired_width(width) - .layouter(&mut layouter), + .code_editor(), ); - }); - ui.horizontal(|ui| { - let cmd_line = ui.add( - egui::TextEdit::singleline(&mut self.command) - .desired_width(width - 50.0) - .lock_focus(true) - .code_editor(), - ); - let cmd_has_lost_focus = cmd_line.lost_focus(); - let key_pressed = ui.input(|i| i.key_pressed(egui::Key::Enter)); - if (key_pressed && cmd_has_lost_focus) || ui.button("Send").clicked() { - // send command - self.history.push(self.command.clone()); - self.index = self.history.len() - 1; - let eol = self.eol.replace("\\r", "\r").replace("\\n", "\n"); - if let Err(err) = self.send_tx.send(self.command.clone() + &eol) { - log::error!("send_tx thread send failed: {:?}", err); + let cmd_has_lost_focus = cmd_line.lost_focus(); + let key_pressed = ui.input(|i| i.key_pressed(egui::Key::Enter)); + if (key_pressed && cmd_has_lost_focus) || ui.button("Send").clicked() { + // send command + self.history.push(self.command.clone()); + self.index = self.history.len() - 1; + let eol = self.eol.replace("\\r", "\r").replace("\\n", "\n"); + if let Err(err) = self.send_tx.send(self.command.clone() + &eol) { + log::error!("send_tx thread send failed: {:?}", err); + } + // stay in focus! + cmd_line.request_focus(); } - // stay in focus! - cmd_line.request_focus(); - } - }); + }); - if ui.input(|i| i.key_pressed(egui::Key::ArrowUp)) { - self.index = self.index.saturating_sub(1); - if !self.history.is_empty() { - self.command = self.history[self.index].clone(); + if ui.input(|i| i.key_pressed(egui::Key::ArrowUp)) { + self.index = self.index.saturating_sub(1); + if !self.history.is_empty() { + self.command = self.history[self.index].clone(); + } } - } - if ui.input(|i| i.key_pressed(egui::Key::ArrowDown)) { - self.index = std::cmp::min(self.index + 1, self.history.len() - 1); - if !self.history.is_empty() { - self.command = self.history[self.index].clone(); + if ui.input(|i| i.key_pressed(egui::Key::ArrowDown)) { + self.index = std::cmp::min(self.index + 1, self.history.len() - 1); + if !self.history.is_empty() { + self.command = self.history[self.index].clone(); + } } } }); @@ -580,12 +582,16 @@ impl MyApp { // let selected_new_device = response.changed(); //somehow this does not work // if selected_new_device { if old_name != self.device { - if !self.data.prints.is_empty() { - self.show_warning_window = WindowFeedback::Waiting; - self.old_device = old_name; - } else { - self.show_warning_window = WindowFeedback::Clear; - } + // TODO: check this here + + // if !gui_data.prints.is_empty() { + // self.show_warning_window = WindowFeedback::Waiting; + // self.old_device = old_name; + // } else { + // self.show_warning_window = WindowFeedback::Clear; + // } + + self.show_warning_window = WindowFeedback::Clear; } }); match self.show_warning_window { @@ -625,7 +631,6 @@ impl MyApp { .send(GuiCommand::Clear) .expect("failed to send clear after choosing new device"); // need to clear the data here such that we don't get errors in the gui (plot) - self.data = GuiOutputDataContainer::default(); self.show_warning_window = WindowFeedback::None; } WindowFeedback::Cancel => { @@ -922,7 +927,6 @@ impl MyApp { log::error!("clear_tx thread send failed: {:?}", err); } // need to clear the data here in order to prevent errors in the gui (plot) - self.data = GuiOutputDataContainer::default(); // self.names_tx.send(self.serial_devices.labels[self.device_idx].clone()).expect("Failed to send names"); } ui.add_space(5.0); diff --git a/src/main.rs b/src/main.rs index 8e080e6..b134615 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,7 @@ use preferences::AppInfo; use std::collections::HashMap; use std::path::PathBuf; use std::sync::{Arc, RwLock}; -use std::time::Duration; +use std::time::{Duration, Instant}; use std::{env, thread}; mod color_picker; @@ -108,6 +108,10 @@ fn main_thread( let mut file_opened = false; + const MAX_FPS: u32 = 24; + let frame_duration = Duration::from_secs_f64(1.0 / MAX_FPS as f64); + let mut last_sent = Instant::now(); + loop { select! { recv(raw_data_rx) -> packet => { @@ -115,7 +119,10 @@ fn main_thread( if !file_opened { data.loaded_from_file = false; if !packet.payload.is_empty() { - sync_tx.send(true).expect("unable to send sync tx"); + if last_sent.elapsed() >= frame_duration { + sync_tx.send(true).expect("unable to send sync tx"); + last_sent = Instant::now(); + } data.raw_traffic.push(packet.clone()); data.absolute_time.push(packet.absolute_time); @@ -132,6 +139,7 @@ fn main_thread( if data.dataset.is_empty() || failed_format_counter > 10 { // resetting dataset + println!("resetting dataset with values: {}", values.len()); data.time = vec![vec![]; values.len()]; data.dataset = vec![vec![]; values.len()]; if let Ok(mut gui_data) = data_lock.write() { @@ -139,6 +147,11 @@ fn main_thread( .map(|i| (format!("Column {i}"), vec![])) .collect(); } + if let Some(ref identifier) = identifier_opt { + identifier_map.insert(identifier.clone(), 0); + failed_key_counter = 0; + } + failed_format_counter = 0; // log::error!("resetting dataset. split length = {}, length data.dataset = {}", split_data.len(), data.dataset.len()); } @@ -200,11 +213,11 @@ fn main_thread( } if let Ok(mut gui_data) = data_lock.write() { - gui_data.plots = (0..data.dataset.len()) - .map(|i| (format!("Column {i}"), vec![])) - .collect(); + gui_data.plots.push((format!("Column {new_index}"), vec![])); } + println!("pushing new index: {}", new_index); + identifier_map.insert(identifier.clone(), new_index); } else { failed_key_counter = 0;