diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b9f884..f4a2903 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,10 @@ All notable changes to the `Serial Monitor` crate will be documented in this fil * allow to plot only every n-th point (max points is 5000, if dataset is larger, it will reduce it by showing only every 2nd point. if it is larger than 10000 only every 3rd point etc...) * Fixed the theme preference setting not being properly restored after restarting the application ( - thanks [@Rahinx](https://github.com/Rahix)) -* Fixed bug where you couldn't change the color and label of columns (thanks [@Rahinx](https://github.com/Rahix)) -* Fixed bug where for a single column of data the graph would never show (thanks [@Rahinx](https://github.com/Rahix)) + thanks [@Rahix](https://github.com/Rahix)) +* Fixed bug where you couldn't change the color and label of columns (thanks [@Rahix](https://github.com/Rahix)) +* Fixed bug where for a single column of data the graph would never show (thanks [@Rahix](https://github.com/Rahix)) +* Added a commandline interface for selecting the serial port and its settings. ## 0.3.4 diff --git a/Cargo.lock b/Cargo.lock index e59c1c3..15192b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2062,6 +2062,26 @@ dependencies = [ "gl_generator", ] +[[package]] +name = "gumdrop" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bc700f989d2f6f0248546222d9b4258f5b02a171a431f8285a81c08142629e3" +dependencies = [ + "gumdrop_derive", +] + +[[package]] +name = "gumdrop_derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "729f9bd3449d77e7831a18abfb7ba2f99ee813dfd15b8c2167c9a54ba20aa99d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "h2" version = "0.4.11" @@ -4338,6 +4358,7 @@ dependencies = [ "egui_extras", "egui_logger", "egui_plot", + "gumdrop", "image", "keepawake", "log", diff --git a/Cargo.toml b/Cargo.toml index d2e83fb..1c34066 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ tempfile = { version = "3.15", optional = true } reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls", "http2"], optional = true } semver = { version = "1.0.24", optional = true } crossbeam-channel = "0.5.14" +gumdrop = "0.8.1" [target.'cfg(not(target_os = "ios"))'.dependencies] eframe = { version = "0.32", features = ["persistence", "wayland", "x11"] } diff --git a/README.md b/README.md index 16797b8..76642fe 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev lib ### Compile from source -The source code can be run using ```cargo run``` or bundled to a platform-executable using cargo bundle. +The source code can be run using `cargo run` or bundled to a platform-executable using cargo bundle. Currently [cargo bundle](https://github.com/burtonageo/cargo-bundle) only supports linux and macOS bundles [see github issue](https://github.com/burtonageo/cargo-bundle/issues/77). As a work-around we can use [cargo wix](https://github.com/volks73/cargo-wix) to create a windows installer. @@ -70,6 +70,39 @@ cargo install cargo-wix cargo wix ``` +## Commandline Arguments +You can start the app with commandline arguments to automatically select a serial port and its settings: + +```text +Usage: serial-monitor-rust [OPTIONS] + +Positional arguments: + device Serial port device to open on startup + +Optional arguments: + -b, --baudrate BAUDRATE Baudrate (default=9600) + -d, --databits DATABITS Data bits (5, 6, 7, default=8) + -f, --flow FLOW Flow conrol (hard, soft, default=none) + -s, --stopbits STOPBITS Stop bits (default=1, 2) + -p, --parity PARITY Parity (odd, even, default=none) + -F, --file FILE Load data from a file instead of a serial port + --column COLUMN-LABELS Column labels, can be specified multiple times for more columns + --color COLUMN-COLORS Column colors (hex color without #), can be specified multiple times for more columns + -h, --help +``` + +Example usage: + +```sh +serial-monitor-rust /dev/ttyACM0 --baudrate 115200 +``` + +You can also preconfigure the column settings. The following example configures the name and color for two columns in the incoming data: + +```sh +serial-monitor-rust --column Raw --color '808080' --column Temperature --color 'ff8000' /dev/ttyACM0 +``` + ## Features: - [X] Plotting and printing of data simultaneously @@ -110,4 +143,4 @@ Tested on: - Windows 10 x86 - ... -One might have to delete the ```Cargo.lock``` file before compiling. +One might have to delete the `Cargo.lock` file before compiling. diff --git a/src/gui.rs b/src/gui.rs index f813529..6d986ae 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -157,6 +157,7 @@ pub struct MyApp { show_warning_window: WindowFeedback, do_not_show_clear_warning: bool, init: bool, + cli_column_colors: Vec, #[cfg(feature = "self_update")] new_release: Option, } @@ -176,6 +177,7 @@ impl MyApp { load_names_rx: Receiver>, send_tx: Sender, gui_cmd_tx: Sender, + cli_column_colors: Vec, ) -> Self { let mut file_dialog = FileDialog::default() //.initial_directory(PathBuf::from("/path/to/app")) @@ -256,6 +258,7 @@ impl MyApp { init: false, show_color_window: ColorWindow::NoShow, file_opened: false, + cli_column_colors, #[cfg(feature = "self_update")] new_release: None, settings_window_open: false, @@ -325,9 +328,15 @@ impl MyApp { } if self.colors.len() != self.labels.len() { self.colors = (0..max(self.labels.len(), 1)) - .map(|i| COLORS[i % COLORS.len()]) + .map(|i| { + self.cli_column_colors + .get(i) + .copied() + .unwrap_or(COLORS[i % COLORS.len()]) + }) .collect(); - self.color_vals = (0..max(self.data.plots.len(), 1)).map(|_| 0.0).collect(); + self.color_vals = + (0..max(self.data.plots.len(), 1)).map(|_| 0.0).collect(); } } diff --git a/src/main.rs b/src/main.rs index e7ca6fe..c0d2dc6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,12 +13,13 @@ use crossbeam_channel::{select, Receiver, Sender}; use eframe::egui::{vec2, ViewportBuilder}; use eframe::{egui, icon_data}; use egui_plot::PlotPoint; +pub use gumdrop::Options; use preferences::AppInfo; use std::cmp::max; use std::path::PathBuf; use std::sync::{Arc, RwLock}; +use std::thread; use std::time::Duration; -use std::{env, thread}; mod color_picker; mod custom_highlighter; @@ -76,6 +77,7 @@ fn main_thread( load_rx: Receiver, load_names_tx: Sender>, gui_cmd_rx: Receiver, + cli_column_labels: Vec, ) { // reads data from mutex, samples and saves if needed let mut data = DataContainer::default(); @@ -110,7 +112,7 @@ fn main_thread( data.dataset = vec![vec![]; max(split_data.len(), 1)]; if let Ok(mut gui_data) = data_lock.write() { gui_data.plots = (0..max(split_data.len(), 1)) - .map(|i| (format!("Column {i}"), vec![])) + .map(|i| (cli_column_labels.get(i).cloned().unwrap_or_else(|| format!("Column {i}")), vec![])) .collect(); } failed_format_counter = 0; @@ -262,13 +264,114 @@ fn main_thread( } } +fn parse_databits(s: &str) -> Result { + let d: u8 = s + .parse() + .map_err(|_e| format!("databits not a number: {s}"))?; + Ok(serialport::DataBits::try_from(d).map_err(|_e| format!("invalid databits: {s}"))?) +} + +fn parse_flow(s: &str) -> Result { + match s { + "none" => Ok(serialport::FlowControl::None), + "soft" => Ok(serialport::FlowControl::Software), + "hard" => Ok(serialport::FlowControl::Hardware), + _ => Err(format!("invalid flow-control: {s}")), + } +} + +fn parse_stopbits(s: &str) -> Result { + let d: u8 = s + .parse() + .map_err(|_e| format!("stopbits not a number: {s}"))?; + Ok(serialport::StopBits::try_from(d).map_err(|_e| format!("invalid stopbits: {s}"))?) +} + +fn parse_parity(s: &str) -> Result { + match s { + "none" => Ok(serialport::Parity::None), + "odd" => Ok(serialport::Parity::Odd), + "even" => Ok(serialport::Parity::Even), + _ => Err(format!("invalid parity setting: {s}")), + } +} + +fn parse_color(s: &str) -> Result { + Ok(egui::ecolor::HexColor::from_str_without_hash(s) + .map_err(|e| format!("invalid color {s:?}: {e:?}"))? + .color()) +} + +#[derive(Debug, Options)] +struct CliOptions { + /// Serial port device to open on startup + #[options(free)] + device: Option, + + /// Baudrate (default=9600) + #[options(short = "b")] + baudrate: Option, + + /// Data bits (5, 6, 7, default=8) + #[options(short = "d", parse(try_from_str = "parse_databits"))] + databits: Option, + + /// Flow conrol (hard, soft, default=none) + #[options(short = "f", parse(try_from_str = "parse_flow"))] + flow: Option, + + /// Stop bits (default=1, 2) + #[options(short = "s", parse(try_from_str = "parse_stopbits"))] + stopbits: Option, + + /// Parity (odd, even, default=none) + #[options(short = "p", parse(try_from_str = "parse_parity"))] + parity: Option, + + /// Load data from a file instead of a serial port + #[options(short = "F")] + file: Option, + + /// Column labels, can be specified multiple times for more columns + #[options(no_short, long = "column")] + column_labels: Vec, + + /// Column colors (hex color without #), can be specified multiple times for more columns + #[options(no_short, long = "color", parse(try_from_str = "parse_color"))] + column_colors: Vec, + + help: bool, +} + fn main() { egui_logger::builder().init().unwrap(); + let args = CliOptions::parse_args_default_or_exit(); + let gui_settings = load_gui_settings(); let saved_serial_device_configs = load_serial_settings(); - let device_lock = Arc::new(RwLock::new(Device::default())); + let mut device = Device::default(); + if let Some(name) = args.device { + device.name = name; + } + if let Some(baudrate) = args.baudrate { + device.baud_rate = baudrate; + } + if let Some(databits) = args.databits { + device.data_bits = databits; + } + if let Some(flow) = args.flow { + device.flow_control = flow; + } + if let Some(stopbits) = args.stopbits { + device.stop_bits = stopbits; + } + if let Some(parity) = args.parity { + device.parity = parity; + } + + let device_lock = Arc::new(RwLock::new(device)); let devices_lock = Arc::new(RwLock::new(vec![gui_settings.device.clone()])); let data_lock = Arc::new(RwLock::new(GuiOutputDataContainer::default())); let connected_lock = Arc::new(RwLock::new(false)); @@ -316,14 +419,12 @@ fn main() { load_rx, loaded_names_tx, gui_cmd_rx, + args.column_labels, ); }); - let args: Vec = env::args().collect(); - if args.len() > 1 { - load_tx - .send(PathBuf::from(&args[1])) - .expect("failed to send file"); + if let Some(file) = args.file { + load_tx.send(file).expect("failed to send file"); } let options = eframe::NativeOptions { @@ -372,6 +473,7 @@ fn main() { loaded_names_rx, send_tx, gui_cmd_tx, + args.column_colors, ))) }), ) {