From 8b001bb0445f3e89f5b0331dd9a30a73af75d0ff Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Sat, 8 Mar 2025 17:22:35 +0100 Subject: [PATCH 1/8] CHECKPOINT --- Cargo.lock | 58 ++++--- Cargo.toml | 12 +- src/communication/error.rs | 2 - src/communication/ethernet.rs | 2 +- src/main.rs | 2 +- src/mavlink.rs | 32 +++- src/mavlink/base.rs | 60 ------- src/mavlink/reflection.rs | 225 ++++++++++++++++++++------ src/ui/panes/plot.rs | 252 ++++++++++++++++++++++------- src/ui/panes/plot/source_window.rs | 221 +++++++++++++------------ 10 files changed, 569 insertions(+), 297 deletions(-) delete mode 100644 src/mavlink/base.rs diff --git a/Cargo.lock b/Cargo.lock index 9ba641e..a36da14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2005,14 +2005,26 @@ dependencies = [ [[package]] name = "mavlink-bindgen" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83b15a4ad504e29cabfb03fdc97250a22d2354a5404d80fc48dbf02e06acf5f" +version = "0.14.0" +dependencies = [ + "crc-any", + "lazy_static", + "proc-macro2", + "quick-xml 0.36.2", + "quote", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "mavlink-bindgen" +version = "0.14.0" +source = "git+https://git.skywarder.eu/avn/swd/mavlink/rust-mavlink.git?rev=b7446436b3c96ca4c40d28b54eeed346e7bf021e#b7446436b3c96ca4c40d28b54eeed346e7bf021e" dependencies = [ "crc-any", "lazy_static", "proc-macro2", - "quick-xml 0.26.0", + "quick-xml 0.36.2", "quote", "serde", "thiserror 1.0.69", @@ -2020,9 +2032,8 @@ dependencies = [ [[package]] name = "mavlink-core" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e64d975ca3cf0ad8a7c278553f91d77de15fcde9b79bf6bc542e209dd0c7dee" +version = "0.14.0" +source = "git+https://git.skywarder.eu/avn/swd/mavlink/rust-mavlink.git?rev=b7446436b3c96ca4c40d28b54eeed346e7bf021e#b7446436b3c96ca4c40d28b54eeed346e7bf021e" dependencies = [ "byteorder", "crc-any", @@ -2698,21 +2709,21 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.26.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" dependencies = [ "memchr", + "serde", ] [[package]] name = "quick-xml" -version = "0.30.0" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" dependencies = [ "memchr", - "serde", ] [[package]] @@ -2752,7 +2763,7 @@ checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", - "zerocopy 0.8.21", + "zerocopy 0.8.23", ] [[package]] @@ -2976,7 +2987,7 @@ dependencies = [ "egui_plot", "egui_tiles", "enum_dispatch", - "mavlink-bindgen", + "mavlink-bindgen 0.14.0", "profiling", "rand 0.9.0", "ring-channel", @@ -3025,9 +3036,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.139" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -3150,11 +3161,10 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "skyward_mavlink" -version = "0.1.0" -source = "git+https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git?branch=rust-strum#0a67d0afc508c38faecc611a58819479686bee27" +version = "0.1.1" dependencies = [ "bitflags 2.9.0", - "mavlink-bindgen", + "mavlink-bindgen 0.14.0 (git+https://git.skywarder.eu/avn/swd/mavlink/rust-mavlink.git?rev=b7446436b3c96ca4c40d28b54eeed346e7bf021e)", "mavlink-core", "num-derive", "num-traits", @@ -4696,11 +4706,11 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.21" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf01143b2dd5d134f11f545cf9f1431b13b749695cb33bcce051e7568f99478" +checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" dependencies = [ - "zerocopy-derive 0.8.21", + "zerocopy-derive 0.8.23", ] [[package]] @@ -4716,9 +4726,9 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.8.21" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712c8386f4f4299382c9abee219bee7084f78fb939d88b6840fcc1320d5f6da2" +checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 03ca628..f6be7f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,12 +18,20 @@ egui_file = "0.22" # =========== Asynchronous =========== tokio = { version = "1.41", features = ["rt-multi-thread", "net", "sync"] } # =========== Mavlink =========== -skyward_mavlink = { git = "https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git", branch = "rust-strum", features = [ +# skyward_mavlink = { git = "https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git", branch = "rust-strum", features = [ +# "reflection", +# "orion", +# "serde", +# ] } +skyward_mavlink = { path = "../mavlink-skyward-lib/mavlink_rust", features = [ "reflection", "orion", "serde", ] } -mavlink-bindgen = { version = "0.13.1", features = ["serde"] } +# mavlink-bindgen = { version = "0.14", features = ["serde"] } +mavlink-bindgen = { path = "../rust-mavlink/mavlink-bindgen", features = [ + "serde", +] } serialport = "4.7.0" # ========= Persistency ========= serde = { version = "1.0", features = ["derive"] } diff --git a/src/communication/error.rs b/src/communication/error.rs index 42b0f1c..d688e86 100644 --- a/src/communication/error.rs +++ b/src/communication/error.rs @@ -21,8 +21,6 @@ pub enum ConnectionError { WrongConfiguration(String), #[error("IO error: {0}")] Io(#[from] std::io::Error), - #[error("Unknown error")] - Unknown(String), } impl From<MessageWriteError> for CommunicationError { diff --git a/src/communication/ethernet.rs b/src/communication/ethernet.rs index 629887c..00331ed 100644 --- a/src/communication/ethernet.rs +++ b/src/communication/ethernet.rs @@ -29,7 +29,7 @@ impl Connectable for EthernetConfiguration { #[profiling::function] fn connect(&self) -> Result<Self::Connected, ConnectionError> { let incoming_addr = format!("udpin:0.0.0.0:{}", self.port); - let outgoing_addr = format!("udpbcast:255.255.255.255:{}", self.port); + let outgoing_addr = format!("udpcast:255.255.255.255:{}", self.port); let mut incoming_conn: BoxedConnection = mavlink::connect(&incoming_addr)?; let mut outgoing_conn: BoxedConnection = mavlink::connect(&outgoing_addr)?; incoming_conn.set_protocol_version(MavlinkVersion::V1); diff --git a/src/main.rs b/src/main.rs index 9093f87..87495b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,7 @@ use tokio::runtime::Runtime; use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::SubscriberInitExt}; use error::ErrInstrument; -use mavlink::ReflectionContext; +use mavlink::reflection::ReflectionContext; use ui::App; /// ReflectionContext singleton, used to get access to the Mavlink message definitions diff --git a/src/mavlink.rs b/src/mavlink.rs index 9119346..f1517cc 100644 --- a/src/mavlink.rs +++ b/src/mavlink.rs @@ -3,13 +3,35 @@ //! It serves also as an abstraction wrapper around the `skyward_mavlink` crate, facilitating //! rapid switching between different mavlink versions and profiles (_dialects_). -mod base; mod error; -mod reflection; +pub mod reflection; -// Export all the types from the base module as if they were defined in this module -pub use base::*; -pub use reflection::ReflectionContext; +use std::time::Instant; + +// Re-export from the mavlink crate +pub use skyward_mavlink::{ + mavlink::*, orion::*, + reflection::ORION_MAVLINK_PROFILE_SERIALIZED as MAVLINK_PROFILE_SERIALIZED, +}; /// Default port for the Ethernet connection pub const DEFAULT_ETHERNET_PORT: u16 = 42069; + +/// A wrapper around the `MavMessage` struct, adding a received time field. +#[derive(Debug, Clone)] +pub struct TimedMessage { + /// The underlying mavlink message + pub message: MavMessage, + /// The time instant at which the message was received + pub time: Instant, +} + +impl TimedMessage { + /// Create a new `TimedMessage` instance with the given message and the current time + pub fn just_received(message: MavMessage) -> Self { + Self { + message, + time: Instant::now(), + } + } +} diff --git a/src/mavlink/base.rs b/src/mavlink/base.rs deleted file mode 100644 index fb64c4c..0000000 --- a/src/mavlink/base.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! Wrapper around the `skyward_mavlink` crate -//! -//! This facilitates rapid switching between different mavlink versions and profiles. -//! -//! In addition, it provides few utility functions to work with mavlink messages. - -use std::time::Instant; - -// Re-export from the mavlink crate -pub use skyward_mavlink::{ - mavlink::*, orion::*, - reflection::ORION_MAVLINK_PROFILE_SERIALIZED as MAVLINK_PROFILE_SERIALIZED, -}; - -use crate::error::ErrInstrument; - -use super::error::{MavlinkError, Result}; - -/// A wrapper around the `MavMessage` struct, adding a received time field. -#[derive(Debug, Clone)] -pub struct TimedMessage { - /// The underlying mavlink message - pub message: MavMessage, - /// The time instant at which the message was received - pub time: Instant, -} - -impl TimedMessage { - /// Create a new `TimedMessage` instance with the given message and the current time - pub fn just_received(message: MavMessage) -> Self { - Self { - message, - time: Instant::now(), - } - } -} - -/// Extract fields from a MavLink message using string keys -#[profiling::function] -pub fn extract_from_message<K, T>( - message: &MavMessage, - fields: impl IntoIterator<Item = K>, -) -> Result<Vec<T>> -where - K: AsRef<str>, - T: serde::de::DeserializeOwned + Default, -{ - let value: serde_json::Value = - serde_json::to_value(message).log_expect("MavMessage should be serializable"); - Ok(fields - .into_iter() - .flat_map(|field| { - let field = field.as_ref(); - let value = value - .get(field) - .ok_or(MavlinkError::UnknownField(field.to_string()))?; - serde_json::from_value::<T>(value.clone()).map_err(MavlinkError::from) - }) - .collect()) -} diff --git a/src/mavlink/reflection.rs b/src/mavlink/reflection.rs index 528ffde..8f81521 100644 --- a/src/mavlink/reflection.rs +++ b/src/mavlink/reflection.rs @@ -6,19 +6,20 @@ use std::collections::HashMap; -use anyhow::anyhow; use mavlink_bindgen::parser::{MavProfile, MavType}; use crate::error::ErrInstrument; use super::MAVLINK_PROFILE_SERIALIZED; +pub use mavlink_bindgen::parser::{MavField, MavMessage}; + /// Reflection context for MAVLink messages. /// /// This struct provides methods to query information about MAVLink messages and their fields. pub struct ReflectionContext { mavlink_profile: MavProfile, - id_name_map: HashMap<u32, String>, + id_msg_map: HashMap<u32, MavMessage>, } impl ReflectionContext { @@ -26,57 +27,56 @@ impl ReflectionContext { pub fn new() -> Self { let profile: MavProfile = serde_json::from_str(MAVLINK_PROFILE_SERIALIZED) .log_expect("Failed to deserialize MavProfile"); - let id_name_map = profile + let id_msg_map = profile .messages - .iter() - .map(|(name, m)| (m.id, name.clone())) + .values() + .map(|m| (m.id, m.clone())) .collect(); Self { mavlink_profile: profile, - id_name_map, + id_msg_map, } } /// Get the name of a message by its ID. - pub fn get_name_from_id(&self, message_id: u32) -> Option<&str> { - self.id_name_map.get(&message_id).map(|s| s.as_str()) + pub fn get_msg(&self, msg: impl MessageLike) -> Option<&MavMessage> { + msg.to_mav_message(self).ok() } - /// Get all message names in a sorted vector. - pub fn sorted_messages(&self) -> Vec<&str> { - let mut msgs: Vec<&str> = self - .mavlink_profile - .messages - .keys() - .map(|s| s.as_str()) - .collect(); - msgs.sort(); - msgs + /// Get all field names for a message by its ID. + pub fn get_fields(&self, message_id: impl MessageLike) -> Option<Vec<IndexedField<'_>>> { + message_id.to_mav_message(self).ok().map(|msg| { + msg.fields + .iter() + .enumerate() + .map(|(i, f)| IndexedField { + id: i, + msg, + field: f, + }) + .collect() + }) } - /// Get all field names for a message by its ID. - pub fn get_fields_by_id(&self, message_id: u32) -> anyhow::Result<Vec<&str>> { - Ok(self + /// Get all message names in a sorted vector. + pub fn get_sorted_msgs(&self) -> Vec<&MavMessage> { + let mut msgs: Vec<(&str, &MavMessage)> = self .mavlink_profile .messages .iter() - .find(|(_, m)| m.id == message_id) - .map(|(_, m)| &m.fields) - .ok_or(anyhow!("Message ID {} not found in profile", message_id))? - .iter() - .map(|f| f.name.as_str()) - .collect()) + .map(|(k, m)| (k.as_str(), m)) + .collect(); + msgs.sort_by_cached_key(|(k, _)| *k); + msgs.into_iter().map(|(_, m)| m).collect() } /// Get all plottable field names for a message by its ID. - pub fn get_plottable_fields_by_id(&self, message_id: u32) -> anyhow::Result<Vec<&str>> { - Ok(self - .mavlink_profile - .messages - .iter() - .find(|(_, m)| m.id == message_id) - .map(|(_, m)| &m.fields) - .ok_or(anyhow!("Message ID {} not found in profile", message_id))? + pub fn get_plottable_fields( + &self, + message_id: impl MessageLike, + ) -> Option<Vec<IndexedField<'_>>> { + let msg = message_id.to_mav_message(self).ok()?; + msg.fields .iter() .filter(|f| { matches!( @@ -93,21 +93,152 @@ impl ReflectionContext { | MavType::Double ) }) - .map(|f| f.name.as_str()) - .collect()) + .map(|f| f.to_mav_field(msg.id, self).ok()) + .collect() } +} - /// Get all field names for a message by its name. - pub fn get_fields_by_name(&self, message_name: &str) -> anyhow::Result<Vec<&str>> { - Ok(self - .mavlink_profile +#[derive(Clone)] +pub struct IndexedField<'a> { + id: usize, + msg: &'a MavMessage, + field: &'a MavField, +} + +impl<'a> IndexedField<'a> { + pub fn msg(&self) -> &MavMessage { + self.msg + } + + pub fn msg_id(&self) -> u32 { + self.msg.id + } + + pub fn id(&self) -> usize { + self.id + } + + pub fn field(&self) -> &MavField { + self.field + } + + pub fn name(&self) -> &str { + &self.field.name + } +} + +pub trait MessageLike { + fn to_mav_message<'a, 'b>( + &'a self, + ctx: &'b ReflectionContext, + ) -> Result<&'b MavMessage, String>; +} + +pub trait FieldLike<'a, 'b> { + fn to_mav_field( + &'a self, + msg_id: u32, + ctx: &'b ReflectionContext, + ) -> Result<IndexedField<'b>, String>; +} + +impl MessageLike for u32 { + fn to_mav_message<'a, 'b>( + &'a self, + ctx: &'b ReflectionContext, + ) -> Result<&'b MavMessage, String> { + ctx.id_msg_map + .get(self) + .ok_or_else(|| format!("Message {} not found", self)) + } +} + +impl MessageLike for &str { + fn to_mav_message<'a, 'b>( + &'a self, + ctx: &'b ReflectionContext, + ) -> Result<&'b MavMessage, String> { + ctx.mavlink_profile .messages .iter() - .find(|(_, m)| m.name == message_name) - .map(|(_, m)| &m.fields) - .ok_or(anyhow!("Message {} not found in profile", message_name))? - .iter() - .map(|f| f.name.as_str()) - .collect()) + .find(|(_, m)| m.name == *self) + .map(|(_, m)| m) + .ok_or_else(|| format!("Message {} not found", self)) + } +} + +impl<'b> FieldLike<'_, 'b> for &MavField { + fn to_mav_field( + &self, + msg_id: u32, + ctx: &'b ReflectionContext, + ) -> Result<IndexedField<'b>, String> { + ctx.id_msg_map + .get(&msg_id) + .and_then(|msg| { + msg.fields + .iter() + .enumerate() + .find(|(_, f)| f == self) + .map(|(i, f)| IndexedField { + id: i, + msg, + field: f, + }) + }) + .ok_or_else(|| format!("Field {} not found in message {}", self.name, msg_id)) + } +} + +impl<'b> FieldLike<'b, 'b> for IndexedField<'b> { + fn to_mav_field( + &self, + _msg_id: u32, + _ctx: &ReflectionContext, + ) -> Result<IndexedField<'_>, String> { + Ok(IndexedField { + id: self.id, + msg: self.msg, + field: self.field, + }) + } +} +impl<'b> FieldLike<'_, 'b> for usize { + fn to_mav_field( + &self, + msg_id: u32, + ctx: &'b ReflectionContext, + ) -> Result<IndexedField<'b>, String> { + ctx.id_msg_map + .get(&msg_id) + .and_then(|msg| { + msg.fields.get(*self).map(|f| IndexedField { + id: *self, + msg, + field: f, + }) + }) + .ok_or_else(|| format!("Field {} not found in message {}", self, msg_id)) + } +} +impl<'b> FieldLike<'_, 'b> for &str { + fn to_mav_field( + &self, + msg_id: u32, + ctx: &'b ReflectionContext, + ) -> Result<IndexedField<'b>, String> { + ctx.id_msg_map + .get(&msg_id) + .and_then(|msg| { + msg.fields + .iter() + .find(|f| f.name == *self) + .map(|f| IndexedField { + id: msg.fields.iter().position(|f2| f2 == f).unwrap(), + msg, + field: f, + }) + }) + .ok_or_else(|| format!("Field {} not found in message {}", self, msg_id)) } } diff --git a/src/ui/panes/plot.rs b/src/ui/panes/plot.rs index e83bd2f..a2a48b2 100644 --- a/src/ui/panes/plot.rs +++ b/src/ui/panes/plot.rs @@ -2,38 +2,40 @@ mod source_window; use super::PaneBehavior; use crate::{ + MAVLINK_PROFILE, error::ErrInstrument, - mavlink::{MessageData, ROCKET_FLIGHT_TM_DATA, TimedMessage, extract_from_message}, + mavlink::{ + Message, MessageData, ROCKET_FLIGHT_TM_DATA, TimedMessage, + reflection::{self, FieldLike}, + }, ui::app::PaneResponse, }; use egui::{Color32, Vec2b}; -use egui_plot::{Legend, Line, PlotPoints}; +use egui_plot::{Legend, Line, PlotPoint, PlotPoints}; use egui_tiles::TileId; +use mavlink_bindgen::parser::MavType; use serde::{Deserialize, Serialize}; -use source_window::{SourceSettings, sources_window}; -use std::iter::zip; +use source_window::{ChangeTracker, sources_window}; +use std::{hash::Hash, iter::zip}; #[derive(Clone, Default, Debug, Serialize, Deserialize)] pub struct Plot2DPane { + settings: PlotSettings, // UI settings #[serde(skip)] - pub contains_pointer: bool, + line_data: Vec<Vec<PlotPoint>>, #[serde(skip)] - settings_visible: bool, - - line_settings: Vec<LineSettings>, + state_valid: bool, + // UI settings #[serde(skip)] - line_data: Vec<Vec<[f64; 2]>>, - - settings: MsgSources, - + settings_visible: bool, #[serde(skip)] - state_valid: bool, + pub contains_pointer: bool, } impl PartialEq for Plot2DPane { fn eq(&self, other: &Self) -> bool { - self.settings == other.settings && self.line_settings == other.line_settings + self.settings == other.settings } } @@ -44,6 +46,7 @@ impl PaneBehavior for Plot2DPane { let ctrl_pressed = ui.input(|i| i.modifiers.ctrl); + // plot last 100 messages egui_plot::Plot::new("plot") .auto_bounds(Vec2b::TRUE) .legend(Legend::default()) @@ -54,13 +57,15 @@ impl PaneBehavior for Plot2DPane { response.set_drag_started(); } - for (settings, points) in zip(&self.line_settings, &mut self.line_data) { + for ((field, settings), points) in zip(self.settings.plot_lines(), &self.line_data) + { plot_ui.line( - // TODO: remove clone when PlotPoints supports borrowing - Line::new(PlotPoints::from(points.clone())) - .color(settings.color) - .width(settings.width) - .name(&settings.field), + Line::new(PlotPoints::from( + &points[points.len().saturating_sub(100)..], + )) + .color(settings.color) + .width(settings.width) + .name(&field.field.name), ); } plot_ui @@ -68,16 +73,16 @@ impl PaneBehavior for Plot2DPane { .context_menu(|ui| show_menu(ui, &mut self.settings_visible)); }); - let mut settings = SourceSettings::new(&mut self.settings, &mut self.line_settings); + let settings_hash = ChangeTracker::record_initial_state(&self.settings); egui::Window::new("Plot Settings") .id(ui.auto_id_with("plot_settings")) // TODO: fix this issue with ids .auto_sized() .collapsible(true) .movable(true) .open(&mut self.settings_visible) - .show(ui.ctx(), |ui| sources_window(ui, &mut settings)); + .show(ui.ctx(), |ui| sources_window(ui, &mut self.settings)); - if settings.are_sources_changed() { + if settings_hash.has_changed(&self.settings) { self.state_valid = false; } @@ -94,23 +99,26 @@ impl PaneBehavior for Plot2DPane { self.line_data.clear(); } - let MsgSources { + let PlotSettings { x_field, y_fields, .. } = &self.settings; for msg in messages { - let x: f64 = extract_from_message(&msg.message, [x_field]).log_unwrap()[0]; - let ys: Vec<f64> = extract_from_message(&msg.message, y_fields).log_unwrap(); + let x: f64 = x_field.extract_from_message(&msg.message).log_unwrap(); + let ys: Vec<f64> = y_fields + .iter() + .map(|(field, _)| field.extract_from_message(&msg.message).log_unwrap()) + .collect(); if self.line_data.len() < ys.len() { self.line_data.resize(ys.len(), Vec::new()); } for (line, y) in zip(&mut self.line_data, ys) { - let point = if x_field == "timestamp" { - [x / 1e6, y] + let point = if x_field.field.name == "timestamp" { + PlotPoint::new(x / 1e6, y) } else { - [x, y] + PlotPoint::new(x, y) }; line.push(point); @@ -121,7 +129,7 @@ impl PaneBehavior for Plot2DPane { } fn get_message_subscription(&self) -> Option<u32> { - Some(self.settings.msg_id) + Some(self.settings.plot_message_id) } fn should_send_message_history(&self) -> bool { @@ -129,34 +137,111 @@ impl PaneBehavior for Plot2DPane { } } -#[derive(Clone, Debug, Serialize, Deserialize)] -struct MsgSources { - msg_id: u32, - x_field: String, - y_fields: Vec<String>, +fn show_menu(ui: &mut egui::Ui, settings_visible: &mut bool) { + ui.set_max_width(200.0); // To make sure we wrap long text + + if ui.button("Settings…").clicked() { + *settings_visible = true; + ui.close_menu(); + } } -impl Default for MsgSources { +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +struct PlotSettings { + plot_message_id: u32, + x_field: FieldWithID, + y_fields: Vec<(FieldWithID, LineSettings)>, +} + +impl PlotSettings { + fn plot_lines(&self) -> &[(FieldWithID, LineSettings)] { + &self.y_fields + } + + fn fields_empty(&self) -> bool { + self.y_fields.is_empty() + } + + fn get_msg_id(&self) -> u32 { + self.plot_message_id + } + + fn get_x_field(&self) -> &FieldWithID { + &self.x_field + } + + fn get_y_fields(&self) -> Vec<&FieldWithID> { + self.y_fields.iter().map(|(field, _)| field).collect() + } + + // fn get_mut_msg_id(&mut self) -> &mut u32 { + // &mut self.msg_sources.plot_message_id + // } + + fn get_mut_x_field(&mut self) -> &mut FieldWithID { + &mut self.x_field + } + + fn get_mut_y_fields(&mut self) -> &mut [(FieldWithID, LineSettings)] { + &mut self.y_fields[..] + } + + fn set_x_field(&mut self, field: FieldWithID) { + self.x_field = field; + } + + fn fields_len(&self) -> usize { + self.y_fields.len() + } + + // fn is_msg_id_changed(&self) -> bool { + // self.msg_sources.plot_message_id != self.old_msg_sources.plot_message_id + // } + + fn contains_field(&self, field: &FieldWithID) -> bool { + self.y_fields.iter().any(|(f, _)| f == field) + } + + fn add_field(&mut self, field: FieldWithID) { + let line_settings = LineSettings::default(); + self.y_fields.push((field, line_settings)); + } + + fn clear_fields(&mut self) { + self.x_field = 0 + .to_mav_field(self.plot_message_id, &MAVLINK_PROFILE) + .log_unwrap() + .into(); + self.y_fields.clear(); + } +} + +impl Default for PlotSettings { fn default() -> Self { + let msg_id = ROCKET_FLIGHT_TM_DATA::ID; + let x_field = FieldWithID::new(msg_id, 0).log_unwrap(); + let y_fields = vec![( + FieldWithID::new(msg_id, 1).log_unwrap(), + LineSettings::default(), + )]; Self { - msg_id: ROCKET_FLIGHT_TM_DATA::ID, - x_field: "timestamp".to_owned(), - y_fields: Vec::new(), + plot_message_id: msg_id, + x_field, + y_fields, } } } -impl PartialEq for MsgSources { - fn eq(&self, other: &Self) -> bool { - self.msg_id == other.msg_id - && self.x_field == other.x_field - && self.y_fields == other.y_fields +impl Hash for PlotSettings { + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + self.plot_message_id.hash(state); + self.x_field.hash(state); + self.y_fields.hash(state); } } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] struct LineSettings { - field: String, width: f32, color: Color32, } @@ -164,27 +249,84 @@ struct LineSettings { impl Default for LineSettings { fn default() -> Self { Self { - field: "".to_owned(), width: 1.0, color: Color32::BLUE, } } } +impl Hash for LineSettings { + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + self.width.to_bits().hash(state); + self.color.hash(state); + } +} + impl LineSettings { - fn new(field_y: String) -> Self { - Self { - field: field_y, - ..Default::default() + fn new(width: f32, color: Color32) -> Self { + Self { width, color } + } +} + +/// A struct to hold a field and its ID in a message +/// We use this and not `reflection::IndexedField` because we need to serialize it +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +struct FieldWithID { + id: usize, + field: reflection::MavField, +} + +impl FieldWithID { + fn new(msg_id: u32, field_id: usize) -> Option<Self> { + Some(Self { + id: field_id, + field: field_id + .to_mav_field(msg_id, &MAVLINK_PROFILE) + .ok()? + .field() + .clone(), + }) + } + + fn extract_from_message(&self, message: &impl Message) -> Result<f64, String> { + macro_rules! downcast { + ($value: expr, $type: ty) => { + Ok(*$value + .downcast::<$type>() + .map_err(|_| "Type mismatch".to_string())? as f64) + }; + } + + let value = message + .get_field(self.id) + .ok_or("Field not found".to_string())?; + match self.field.mavtype { + MavType::UInt8 => downcast!(value, u8), + MavType::UInt16 => downcast!(value, u16), + MavType::UInt32 => downcast!(value, u32), + MavType::UInt64 => downcast!(value, u64), + MavType::Int8 => downcast!(value, i8), + MavType::Int16 => downcast!(value, i16), + MavType::Int32 => downcast!(value, i32), + MavType::Int64 => downcast!(value, i64), + MavType::Float => downcast!(value, f32), + MavType::Double => downcast!(value, f64), + _ => Err("Field type not supported".to_string()), } } } -fn show_menu(ui: &mut egui::Ui, settings_visible: &mut bool) { - ui.set_max_width(200.0); // To make sure we wrap long text +impl Hash for FieldWithID { + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + self.id.hash(state); + } +} - if ui.button("Settings…").clicked() { - *settings_visible = true; - ui.close_menu(); +impl From<reflection::IndexedField<'_>> for FieldWithID { + fn from(indexed_field: reflection::IndexedField<'_>) -> Self { + Self { + id: indexed_field.id(), + field: indexed_field.field().clone(), + } } } diff --git a/src/ui/panes/plot/source_window.rs b/src/ui/panes/plot/source_window.rs index f6def54..b4dca59 100644 --- a/src/ui/panes/plot/source_window.rs +++ b/src/ui/panes/plot/source_window.rs @@ -1,47 +1,52 @@ -use crate::{ - MAVLINK_PROFILE, - mavlink::{MavMessage, Message}, +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, }; +use crate::{MAVLINK_PROFILE, ui::panes::plot::FieldWithID}; + use crate::error::ErrInstrument; -use super::{LineSettings, MsgSources}; +use super::{LineSettings, PlotSettings}; #[profiling::function] -pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut SourceSettings) { +pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut PlotSettings) { + let settings_hash = ChangeTracker::record_initial_state(&plot_settings); + // extract the msg name from the id to show it in the combo box let msg_name = MAVLINK_PROFILE - .get_name_from_id(*plot_settings.get_msg_id()) + .get_msg(plot_settings.plot_message_id) + .map(|m| m.name.clone()) .unwrap_or_default(); // show the first combo box with the message name selection egui::ComboBox::from_label("Message Kind") .selected_text(msg_name) .show_ui(ui, |ui| { - for msg in MAVLINK_PROFILE.sorted_messages() { - ui.selectable_value( - plot_settings.get_mut_msg_id(), - MavMessage::message_id_from_name(msg).log_expect("Invalid message name"), - msg, - ); + for msg in MAVLINK_PROFILE.get_sorted_msgs() { + ui.selectable_value(&mut plot_settings.plot_message_id, msg.id, &msg.name); } }); // reset fields if the message is changed - if plot_settings.is_msg_id_changed() { + if settings_hash.has_changed(plot_settings) { plot_settings.clear_fields(); } // check fields and assign a default field_x and field_y once the msg is changed - let fields = MAVLINK_PROFILE - .get_plottable_fields_by_id(*plot_settings.get_msg_id()) - .log_expect("Invalid message id"); + let fields: Vec<FieldWithID> = MAVLINK_PROFILE + .get_plottable_fields(plot_settings.get_msg_id()) + .log_expect("Invalid message id") + .into_iter() + .map(|f| f.into()) + .collect::<Vec<_>>(); // get the first field that is in the list of fields or the previous if valid let x_field = plot_settings.get_x_field(); let new_field_x = fields - .contains(&x_field) + .iter() + .any(|f| f == x_field) .then(|| x_field.to_owned()) - .or(fields.first().map(|s| s.to_string())); + .or(fields.first().map(|s| s.to_owned())); // if there are no fields, reset the field_x and plot_lines let Some(new_field_x) = new_field_x else { @@ -54,16 +59,16 @@ pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut SourceSettings) { // if fields are valid, show the combo boxes for the x_axis egui::ComboBox::from_label("X Axis") - .selected_text(x_field.as_str()) + .selected_text(&x_field.field.name) .show_ui(ui, |ui| { for msg in fields.iter() { - ui.selectable_value(x_field, (*msg).to_owned(), *msg); + ui.selectable_value(x_field, msg.to_owned(), &msg.field.name); } }); // populate the plot_lines with the first field if it is empty and there are more than 1 fields if plot_settings.fields_empty() && fields.len() > 1 { - plot_settings.add_field(fields[1].to_string()); + plot_settings.add_field(fields[1].to_owned()); } // check how many fields are left and how many are selected @@ -72,22 +77,20 @@ pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut SourceSettings) { .num_columns(3) .spacing([10.0, 2.5]) .show(ui, |ui| { - for (i, line_settings) in plot_settings.line_settings.iter_mut().enumerate() { - let LineSettings { - field, - width, - color, - } = line_settings; + for (i, (field, line_settings)) in + plot_settings.get_mut_y_fields().into_iter().enumerate() + { + let LineSettings { width, color } = line_settings; let widget_label = if plot_lines_len > 1 { format!("Y Axis {}", i + 1) } else { "Y Axis".to_owned() }; egui::ComboBox::from_label(widget_label) - .selected_text(field.as_str()) + .selected_text(&field.field.name) .show_ui(ui, |ui| { for msg in fields.iter() { - ui.selectable_value(field, (*msg).to_owned(), *msg); + ui.selectable_value(field, msg.to_owned(), &msg.field.name); } }); ui.color_edit_button_srgba(color); @@ -96,8 +99,6 @@ pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut SourceSettings) { ui.end_row(); } }); - // Sync changes applied to line_settings with msg_sources - plot_settings.sync_fields_with_lines(); // if we have fields left, show the add button if fields.len().saturating_sub(plot_lines_len + 1) > 0 @@ -110,81 +111,101 @@ pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut SourceSettings) { .iter() .find(|f| !plot_settings.contains_field(f)) .log_unwrap(); - plot_settings.add_field(next_field.to_string()); + plot_settings.add_field(next_field.to_owned()); } } -pub struct SourceSettings<'a> { - msg_sources: &'a mut MsgSources, - old_msg_sources: MsgSources, - line_settings: &'a mut Vec<LineSettings>, +pub struct ChangeTracker { + integrity_digest: u64, } -impl<'a> SourceSettings<'a> { - pub fn new(msg_sources: &'a mut MsgSources, line_settings: &'a mut Vec<LineSettings>) -> Self { - Self { - old_msg_sources: msg_sources.clone(), - msg_sources, - line_settings, - } - } - - pub fn are_sources_changed(&self) -> bool { - self.msg_sources != &self.old_msg_sources - } - - pub fn fields_empty(&self) -> bool { - self.msg_sources.y_fields.is_empty() - } - - fn get_msg_id(&self) -> &u32 { - &self.msg_sources.msg_id - } - - fn get_x_field(&self) -> &str { - &self.msg_sources.x_field - } - - fn get_mut_msg_id(&mut self) -> &mut u32 { - &mut self.msg_sources.msg_id - } - - fn get_mut_x_field(&mut self) -> &mut String { - &mut self.msg_sources.x_field +impl ChangeTracker { + pub fn record_initial_state<T: Hash>(state: &T) -> Self { + let mut hasher = DefaultHasher::new(); + state.hash(&mut hasher); + let integrity_digest = hasher.finish(); + Self { integrity_digest } } - fn set_x_field(&mut self, field: String) { - self.msg_sources.x_field = field; - } - - fn fields_len(&self) -> usize { - self.msg_sources.y_fields.len() - } - - fn is_msg_id_changed(&self) -> bool { - self.msg_sources.msg_id != self.old_msg_sources.msg_id - } - - fn contains_field(&self, field: &str) -> bool { - self.msg_sources.y_fields.contains(&field.to_owned()) - } - - fn sync_fields_with_lines(&mut self) { - self.msg_sources.y_fields = self - .line_settings - .iter() - .map(|ls| ls.field.clone()) - .collect(); - } - - fn add_field(&mut self, field: String) { - self.line_settings.push(LineSettings::new(field.clone())); - self.msg_sources.y_fields.push(field); - } - - fn clear_fields(&mut self) { - self.msg_sources.y_fields.clear(); - self.line_settings.clear(); - self.msg_sources.x_field = "".to_owned(); + pub fn has_changed<T: Hash>(&self, state: &T) -> bool { + let mut hasher = DefaultHasher::new(); + state.hash(&mut hasher); + self.integrity_digest != hasher.finish() } } + +// pub struct SourceSettings<'a> { +// msg_sources: &'a mut PlotSettings, +// } + +// impl<'a> SourceSettings<'a> { +// pub fn new( +// msg_sources: &'a mut PlotSettings, +// line_settings: &'a mut Vec<LineSettings>, +// ) -> Self { +// Self { +// old_msg_sources: msg_sources.clone(), +// msg_sources, +// line_settings, +// } +// } + +// pub fn are_sources_changed(&self) -> bool { +// self.msg_sources != &self.old_msg_sources +// } + +// pub fn fields_empty(&self) -> bool { +// self.msg_sources.y_field_ids.is_empty() +// } + +// fn get_msg_id(&self) -> u32 { +// self.msg_sources.plot_message_id +// } + +// fn get_x_field_id(&self) -> usize { +// self.msg_sources.x_field_id +// } + +// fn get_mut_msg_id(&mut self) -> &mut u32 { +// &mut self.msg_sources.plot_message_id +// } + +// fn get_mut_x_field_id(&mut self) -> &mut usize { +// &mut self.msg_sources.x_field_id +// } + +// fn set_x_field_id(&mut self, field_id: usize) { +// self.msg_sources.x_field_id = field_id; +// } + +// fn fields_len(&self) -> usize { +// self.msg_sources.y_field_ids.len() +// } + +// fn is_msg_id_changed(&self) -> bool { +// self.msg_sources.plot_message_id != self.old_msg_sources.plot_message_id +// } + +// fn contains_field(&self, field_id: usize) -> bool { +// self.msg_sources.y_field_ids.contains(&field_id) +// } + +// fn sync_fields_with_lines(&mut self) { +// self.msg_sources.y_field_ids = self +// .line_settings +// .iter() +// .map(|ls| ls.field_id.clone()) +// .collect(); +// } + +// fn add_field(&mut self, field_id: usize) { +// self.line_settings.push(LineSettings::new(field_id)); +// self.msg_sources.y_field_ids.push(field_id); +// } + +// fn clear_fields(&mut self) { +// self.msg_sources.y_field_ids.clear(); +// self.line_settings.clear(); +// self.msg_sources.x_field_id = 0; +// } +// } -- GitLab From ca3733d89a6aa07e29ebc9c0b08022aabb5ae9bf Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Sat, 8 Mar 2025 17:31:05 +0100 Subject: [PATCH 2/8] removed commented code, moved and documented ChangeTracker --- src/mavlink/error.rs | 4 -- src/mavlink/reflection.rs | 4 +- src/ui/cache.rs | 72 +++++++++++++++++++- src/ui/panes/plot.rs | 22 +----- src/ui/panes/plot/source_window.rs | 105 ++--------------------------- 5 files changed, 79 insertions(+), 128 deletions(-) diff --git a/src/mavlink/error.rs b/src/mavlink/error.rs index 23975eb..b0aca6e 100644 --- a/src/mavlink/error.rs +++ b/src/mavlink/error.rs @@ -1,11 +1,7 @@ use thiserror::Error; -pub type Result<T> = std::result::Result<T, MavlinkError>; - #[derive(Debug, Error)] pub enum MavlinkError { - #[error("Error parsing field: {0}")] - UnknownField(String), #[error("Error parsing message: {0}")] ParseError(#[from] serde_json::Error), } diff --git a/src/mavlink/reflection.rs b/src/mavlink/reflection.rs index 8f81521..68937c6 100644 --- a/src/mavlink/reflection.rs +++ b/src/mavlink/reflection.rs @@ -105,7 +105,7 @@ pub struct IndexedField<'a> { field: &'a MavField, } -impl<'a> IndexedField<'a> { +impl IndexedField<'_> { pub fn msg(&self) -> &MavMessage { self.msg } @@ -234,7 +234,7 @@ impl<'b> FieldLike<'_, 'b> for &str { .iter() .find(|f| f.name == *self) .map(|f| IndexedField { - id: msg.fields.iter().position(|f2| f2 == f).unwrap(), + id: msg.fields.iter().position(|f2| f2 == f).log_unwrap(), msg, field: f, }) diff --git a/src/ui/cache.rs b/src/ui/cache.rs index 46b29cc..23d894d 100644 --- a/src/ui/cache.rs +++ b/src/ui/cache.rs @@ -1,7 +1,11 @@ //! Module for caching expensive UI calls using egui's temporary memory storage. //! It provides utilities for caching the results of functions to avoid frequent recalculations. -use std::time::{Duration, Instant}; +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, + time::{Duration, Instant}, +}; use egui::Context; use serialport::SerialPortInfo; @@ -90,3 +94,69 @@ pub fn cached_first_stm32_port(ctx: &Context) -> Result<Option<SerialPortInfo>, SERIAL_PORT_REFRESH_INTERVAL, ) } + +/// ChangeTracker manages the tracking of state changes using an integrity digest. +/// +/// The `integrity_digest` field holds a 64-bit unsigned integer that represents +/// a summary (or hash) of the current state. This can be used to verify that the +/// cached UI state remains consistent, and to quickly detect any modifications. +pub struct ChangeTracker { + integrity_digest: u64, +} + +impl ChangeTracker { + /// Records the initial state of a hashable value by computing its hash digest. + /// + /// This method takes a reference to any value that implements the `Hash` trait, + /// computes its hash using the default hasher, and stores the resulting digest in a + /// newly created `ChangeTracker` instance. This digest serves as a reference point + /// for future state comparisons. + /// + /// # Parameters + /// + /// - `state`: A reference to the value whose state is to be recorded. + /// + /// # Returns + /// + /// A `ChangeTracker` initialized with the computed hash digest. + /// + /// # Examples + /// + /// ``` + /// let initial_tracker = ChangeTracker::record_initial_state(&state); + /// ``` + pub fn record_initial_state<T: Hash>(state: &T) -> Self { + let mut hasher = DefaultHasher::new(); + state.hash(&mut hasher); + let integrity_digest = hasher.finish(); + Self { integrity_digest } + } + + /// Checks whether the hash of the current state differs from the initially recorded state. + /// + /// This method computes the hash digest of the current state (which must implement the + /// `Hash` trait) and compares it with the digest stored in the `ChangeTracker`. If the digests + /// differ, it indicates that the state has changed since the initial recording. + /// + /// # Parameters + /// + /// - `state`: A reference to the current state to be checked for changes. + /// + /// # Returns + /// + /// `true` if the current state's hash digest does not match the initially recorded digest, + /// indicating a change; `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// if tracker.has_changed(&state) { + /// println!("The state has changed."); + /// } + /// ``` + pub fn has_changed<T: Hash>(&self, state: &T) -> bool { + let mut hasher = DefaultHasher::new(); + state.hash(&mut hasher); + self.integrity_digest != hasher.finish() + } +} diff --git a/src/ui/panes/plot.rs b/src/ui/panes/plot.rs index a2a48b2..7c2e27d 100644 --- a/src/ui/panes/plot.rs +++ b/src/ui/panes/plot.rs @@ -8,14 +8,14 @@ use crate::{ Message, MessageData, ROCKET_FLIGHT_TM_DATA, TimedMessage, reflection::{self, FieldLike}, }, - ui::app::PaneResponse, + ui::{app::PaneResponse, cache::ChangeTracker}, }; use egui::{Color32, Vec2b}; use egui_plot::{Legend, Line, PlotPoint, PlotPoints}; use egui_tiles::TileId; use mavlink_bindgen::parser::MavType; use serde::{Deserialize, Serialize}; -use source_window::{ChangeTracker, sources_window}; +use source_window::sources_window; use std::{hash::Hash, iter::zip}; #[derive(Clone, Default, Debug, Serialize, Deserialize)] @@ -170,14 +170,6 @@ impl PlotSettings { &self.x_field } - fn get_y_fields(&self) -> Vec<&FieldWithID> { - self.y_fields.iter().map(|(field, _)| field).collect() - } - - // fn get_mut_msg_id(&mut self) -> &mut u32 { - // &mut self.msg_sources.plot_message_id - // } - fn get_mut_x_field(&mut self) -> &mut FieldWithID { &mut self.x_field } @@ -194,10 +186,6 @@ impl PlotSettings { self.y_fields.len() } - // fn is_msg_id_changed(&self) -> bool { - // self.msg_sources.plot_message_id != self.old_msg_sources.plot_message_id - // } - fn contains_field(&self, field: &FieldWithID) -> bool { self.y_fields.iter().any(|(f, _)| f == field) } @@ -262,12 +250,6 @@ impl Hash for LineSettings { } } -impl LineSettings { - fn new(width: f32, color: Color32) -> Self { - Self { width, color } - } -} - /// A struct to hold a field and its ID in a message /// We use this and not `reflection::IndexedField` because we need to serialize it #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] diff --git a/src/ui/panes/plot/source_window.rs b/src/ui/panes/plot/source_window.rs index b4dca59..e49df73 100644 --- a/src/ui/panes/plot/source_window.rs +++ b/src/ui/panes/plot/source_window.rs @@ -1,10 +1,8 @@ -use std::{ - collections::hash_map::DefaultHasher, - hash::{Hash, Hasher}, +use crate::{ + MAVLINK_PROFILE, + ui::{cache::ChangeTracker, panes::plot::FieldWithID}, }; -use crate::{MAVLINK_PROFILE, ui::panes::plot::FieldWithID}; - use crate::error::ErrInstrument; use super::{LineSettings, PlotSettings}; @@ -78,7 +76,7 @@ pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut PlotSettings) { .spacing([10.0, 2.5]) .show(ui, |ui| { for (i, (field, line_settings)) in - plot_settings.get_mut_y_fields().into_iter().enumerate() + plot_settings.get_mut_y_fields().iter_mut().enumerate() { let LineSettings { width, color } = line_settings; let widget_label = if plot_lines_len > 1 { @@ -114,98 +112,3 @@ pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut PlotSettings) { plot_settings.add_field(next_field.to_owned()); } } - -pub struct ChangeTracker { - integrity_digest: u64, -} - -impl ChangeTracker { - pub fn record_initial_state<T: Hash>(state: &T) -> Self { - let mut hasher = DefaultHasher::new(); - state.hash(&mut hasher); - let integrity_digest = hasher.finish(); - Self { integrity_digest } - } - - pub fn has_changed<T: Hash>(&self, state: &T) -> bool { - let mut hasher = DefaultHasher::new(); - state.hash(&mut hasher); - self.integrity_digest != hasher.finish() - } -} - -// pub struct SourceSettings<'a> { -// msg_sources: &'a mut PlotSettings, -// } - -// impl<'a> SourceSettings<'a> { -// pub fn new( -// msg_sources: &'a mut PlotSettings, -// line_settings: &'a mut Vec<LineSettings>, -// ) -> Self { -// Self { -// old_msg_sources: msg_sources.clone(), -// msg_sources, -// line_settings, -// } -// } - -// pub fn are_sources_changed(&self) -> bool { -// self.msg_sources != &self.old_msg_sources -// } - -// pub fn fields_empty(&self) -> bool { -// self.msg_sources.y_field_ids.is_empty() -// } - -// fn get_msg_id(&self) -> u32 { -// self.msg_sources.plot_message_id -// } - -// fn get_x_field_id(&self) -> usize { -// self.msg_sources.x_field_id -// } - -// fn get_mut_msg_id(&mut self) -> &mut u32 { -// &mut self.msg_sources.plot_message_id -// } - -// fn get_mut_x_field_id(&mut self) -> &mut usize { -// &mut self.msg_sources.x_field_id -// } - -// fn set_x_field_id(&mut self, field_id: usize) { -// self.msg_sources.x_field_id = field_id; -// } - -// fn fields_len(&self) -> usize { -// self.msg_sources.y_field_ids.len() -// } - -// fn is_msg_id_changed(&self) -> bool { -// self.msg_sources.plot_message_id != self.old_msg_sources.plot_message_id -// } - -// fn contains_field(&self, field_id: usize) -> bool { -// self.msg_sources.y_field_ids.contains(&field_id) -// } - -// fn sync_fields_with_lines(&mut self) { -// self.msg_sources.y_field_ids = self -// .line_settings -// .iter() -// .map(|ls| ls.field_id.clone()) -// .collect(); -// } - -// fn add_field(&mut self, field_id: usize) { -// self.line_settings.push(LineSettings::new(field_id)); -// self.msg_sources.y_field_ids.push(field_id); -// } - -// fn clear_fields(&mut self) { -// self.msg_sources.y_field_ids.clear(); -// self.line_settings.clear(); -// self.msg_sources.x_field_id = 0; -// } -// } -- GitLab From 177eae3809b7c284b30734da6b9617b63ae0c569 Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Sun, 9 Mar 2025 13:55:01 +0100 Subject: [PATCH 3/8] CHECKPOINT --- Cargo.lock | 18 ++--------- Cargo.toml | 16 ++++------ src/communication/serial.rs | 34 +++++++++++++++++++++ src/mavlink/reflection.rs | 17 ++++------- src/ui.rs | 2 +- src/ui/cache.rs | 56 ++++++++++++++++------------------- src/ui/panes/plot.rs | 2 +- src/ui/windows/connections.rs | 7 +++-- 8 files changed, 80 insertions(+), 72 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a36da14..1dea7a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2003,19 +2003,6 @@ dependencies = [ "regex-automata 0.1.10", ] -[[package]] -name = "mavlink-bindgen" -version = "0.14.0" -dependencies = [ - "crc-any", - "lazy_static", - "proc-macro2", - "quick-xml 0.36.2", - "quote", - "serde", - "thiserror 1.0.69", -] - [[package]] name = "mavlink-bindgen" version = "0.14.0" @@ -2987,7 +2974,7 @@ dependencies = [ "egui_plot", "egui_tiles", "enum_dispatch", - "mavlink-bindgen 0.14.0", + "mavlink-bindgen", "profiling", "rand 0.9.0", "ring-channel", @@ -3162,9 +3149,10 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "skyward_mavlink" version = "0.1.1" +source = "git+https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git?branch=rust-strum#8f0700acc0def0cdb2d2ae490e0703fd6c820643" dependencies = [ "bitflags 2.9.0", - "mavlink-bindgen 0.14.0 (git+https://git.skywarder.eu/avn/swd/mavlink/rust-mavlink.git?rev=b7446436b3c96ca4c40d28b54eeed346e7bf021e)", + "mavlink-bindgen", "mavlink-core", "num-derive", "num-traits", diff --git a/Cargo.toml b/Cargo.toml index f6be7f4..d7507aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,20 +18,11 @@ egui_file = "0.22" # =========== Asynchronous =========== tokio = { version = "1.41", features = ["rt-multi-thread", "net", "sync"] } # =========== Mavlink =========== -# skyward_mavlink = { git = "https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git", branch = "rust-strum", features = [ -# "reflection", -# "orion", -# "serde", -# ] } -skyward_mavlink = { path = "../mavlink-skyward-lib/mavlink_rust", features = [ +skyward_mavlink = { git = "https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git", branch = "rust-strum", features = [ "reflection", "orion", "serde", ] } -# mavlink-bindgen = { version = "0.14", features = ["serde"] } -mavlink-bindgen = { path = "../rust-mavlink/mavlink-bindgen", features = [ - "serde", -] } serialport = "4.7.0" # ========= Persistency ========= serde = { version = "1.0", features = ["derive"] } @@ -51,5 +42,10 @@ anyhow = "1.0" ring-channel = "0.12.0" thiserror = "2.0.7" +[dependencies.mavlink-bindgen] +git = "https://git.skywarder.eu/avn/swd/mavlink/rust-mavlink.git" +rev = "b7446436b3c96ca4c40d28b54eeed346e7bf021e" +features = ["serde"] + [dev-dependencies] rand = "0.9.0" diff --git a/src/communication/serial.rs b/src/communication/serial.rs index 8f1c6fa..b4d3897 100644 --- a/src/communication/serial.rs +++ b/src/communication/serial.rs @@ -51,6 +51,40 @@ pub fn find_first_stm32_port() -> Result<Option<SerialPortInfo>, serialport::Err Ok(None) } +pub mod cached { + use egui::Context; + + use crate::ui::cache::CacheCall; + + use super::*; + + /// Returns a cached list of all available USB ports. + /// + /// # Arguments + /// * `ctx` - The egui context used for caching. + /// + /// # Returns + /// * A Result containing a vector of `SerialPortInfo` or a `serialport::Error`. + pub fn cached_list_all_usb_ports( + ctx: &Context, + ) -> Result<Vec<SerialPortInfo>, serialport::Error> { + ctx.call_cached_short(&"list_usb_ports", list_all_usb_ports) + } + + /// Returns the first cached STM32 port found, if any. + /// + /// # Arguments + /// * `ctx` - The egui context used for caching. + /// + /// # Returns + /// * A Result containing an Option of `SerialPortInfo` or a `serialport::Error`. + pub fn cached_first_stm32_port( + ctx: &Context, + ) -> Result<Option<SerialPortInfo>, serialport::Error> { + ctx.call_cached_short(&"find_first_stm32_port", find_first_stm32_port) + } +} + /// Configuration for a serial connection. #[derive(Debug, Clone)] pub struct SerialConfiguration { diff --git a/src/mavlink/reflection.rs b/src/mavlink/reflection.rs index 68937c6..c9f8a7f 100644 --- a/src/mavlink/reflection.rs +++ b/src/mavlink/reflection.rs @@ -128,10 +128,7 @@ impl IndexedField<'_> { } pub trait MessageLike { - fn to_mav_message<'a, 'b>( - &'a self, - ctx: &'b ReflectionContext, - ) -> Result<&'b MavMessage, String>; + fn to_mav_message<'b>(&self, ctx: &'b ReflectionContext) -> Result<&'b MavMessage, String>; } pub trait FieldLike<'a, 'b> { @@ -143,10 +140,7 @@ pub trait FieldLike<'a, 'b> { } impl MessageLike for u32 { - fn to_mav_message<'a, 'b>( - &'a self, - ctx: &'b ReflectionContext, - ) -> Result<&'b MavMessage, String> { + fn to_mav_message<'b>(&self, ctx: &'b ReflectionContext) -> Result<&'b MavMessage, String> { ctx.id_msg_map .get(self) .ok_or_else(|| format!("Message {} not found", self)) @@ -154,10 +148,7 @@ impl MessageLike for u32 { } impl MessageLike for &str { - fn to_mav_message<'a, 'b>( - &'a self, - ctx: &'b ReflectionContext, - ) -> Result<&'b MavMessage, String> { + fn to_mav_message<'b>(&self, ctx: &'b ReflectionContext) -> Result<&'b MavMessage, String> { ctx.mavlink_profile .messages .iter() @@ -203,6 +194,7 @@ impl<'b> FieldLike<'b, 'b> for IndexedField<'b> { }) } } + impl<'b> FieldLike<'_, 'b> for usize { fn to_mav_field( &self, @@ -221,6 +213,7 @@ impl<'b> FieldLike<'_, 'b> for usize { .ok_or_else(|| format!("Field {} not found in message {}", self, msg_id)) } } + impl<'b> FieldLike<'_, 'b> for &str { fn to_mav_field( &self, diff --git a/src/ui.rs b/src/ui.rs index 383fabd..077abdb 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,5 +1,5 @@ mod app; -mod cache; +pub mod cache; mod panes; mod persistency; mod shortcuts; diff --git a/src/ui/cache.rs b/src/ui/cache.rs index 23d894d..9835924 100644 --- a/src/ui/cache.rs +++ b/src/ui/cache.rs @@ -13,6 +13,8 @@ use serialport::SerialPortInfo; use crate::{communication, error::ErrInstrument}; const SERIAL_PORT_REFRESH_INTERVAL: Duration = Duration::from_millis(500); +const SHORT_REFRESH_INTERVAL: Duration = Duration::from_millis(500); +const INDEF_REFRESH_INTERVAL: Duration = Duration::MAX; /// Internal helper function that caches the result of a given function call for a specified duration. /// @@ -42,7 +44,9 @@ where /// A trait to extend egui's Context with a caching function. pub trait CacheCall { - /// Calls the provided function and caches its result. + /// Calls the provided function and caches its result. Every time this + /// function is called, it will return the cached value if it is still + /// valid. /// /// # Arguments /// * `id` - A unique identifier for the cached value. @@ -52,6 +56,26 @@ pub trait CacheCall { where F: Fn() -> T, T: Clone + Send + Sync + 'static; + + fn call_cached_short<F, T, H>(&self, hashable: &H, fun: F) -> T + where + F: Fn() -> T, + T: Clone + Send + Sync + 'static, + H: Hash, + { + let id = egui::Id::new(hashable); + self.call_cached(id, fun, SHORT_REFRESH_INTERVAL) + } + + fn call_cached_indef<F, T, H>(&self, hashable: &H, fun: F) -> T + where + F: Fn() -> T, + T: Clone + Send + Sync + 'static, + H: Hash, + { + let id = egui::Id::new(hashable); + self.call_cached(id, fun, INDEF_REFRESH_INTERVAL) + } } impl CacheCall for egui::Context { @@ -65,36 +89,6 @@ impl CacheCall for egui::Context { } } -/// Returns a cached list of all available USB ports. -/// -/// # Arguments -/// * `ctx` - The egui context used for caching. -/// -/// # Returns -/// * A Result containing a vector of `SerialPortInfo` or a `serialport::Error`. -pub fn cached_list_all_usb_ports(ctx: &Context) -> Result<Vec<SerialPortInfo>, serialport::Error> { - ctx.call_cached( - egui::Id::new("list_usb_ports"), - communication::serial::list_all_usb_ports, - SERIAL_PORT_REFRESH_INTERVAL, - ) -} - -/// Returns the first cached STM32 port found, if any. -/// -/// # Arguments -/// * `ctx` - The egui context used for caching. -/// -/// # Returns -/// * A Result containing an Option of `SerialPortInfo` or a `serialport::Error`. -pub fn cached_first_stm32_port(ctx: &Context) -> Result<Option<SerialPortInfo>, serialport::Error> { - ctx.call_cached( - egui::Id::new("list_usb_ports"), - communication::serial::find_first_stm32_port, - SERIAL_PORT_REFRESH_INTERVAL, - ) -} - /// ChangeTracker manages the tracking of state changes using an integrity digest. /// /// The `integrity_digest` field holds a 64-bit unsigned integer that represents diff --git a/src/ui/panes/plot.rs b/src/ui/panes/plot.rs index 7c2e27d..9d9ad09 100644 --- a/src/ui/panes/plot.rs +++ b/src/ui/panes/plot.rs @@ -61,7 +61,7 @@ impl PaneBehavior for Plot2DPane { { plot_ui.line( Line::new(PlotPoints::from( - &points[points.len().saturating_sub(100)..], + &points[points.len().saturating_sub(100)..], // FIXME: don't show just the last 100 points )) .color(settings.color) .width(settings.width) diff --git a/src/ui/windows/connections.rs b/src/ui/windows/connections.rs index ebc34c3..30ffe5e 100644 --- a/src/ui/windows/connections.rs +++ b/src/ui/windows/connections.rs @@ -4,12 +4,15 @@ use tracing::{error, warn}; use crate::{ communication::{ - ConnectionError, EthernetConfiguration, SerialConfiguration, serial::DEFAULT_BAUD_RATE, + ConnectionError, EthernetConfiguration, SerialConfiguration, + serial::{ + DEFAULT_BAUD_RATE, + cached::{cached_first_stm32_port, cached_list_all_usb_ports}, + }, }, error::ErrInstrument, mavlink::DEFAULT_ETHERNET_PORT, message_broker::MessageBroker, - ui::cache::{cached_first_stm32_port, cached_list_all_usb_ports}, }; #[derive(Default)] -- GitLab From 912530389576fc541e43d72592fecad366a67f64 Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Sun, 9 Mar 2025 18:56:08 +0100 Subject: [PATCH 4/8] CHECKPOINT --- src/mavlink/reflection.rs | 197 ++++++++++++++++++++++++----- src/ui/cache.rs | 5 +- src/ui/panes/plot.rs | 138 +++++++++----------- src/ui/panes/plot/source_window.rs | 20 +-- 4 files changed, 238 insertions(+), 122 deletions(-) diff --git a/src/mavlink/reflection.rs b/src/mavlink/reflection.rs index c9f8a7f..c766536 100644 --- a/src/mavlink/reflection.rs +++ b/src/mavlink/reflection.rs @@ -7,6 +7,7 @@ use std::collections::HashMap; use mavlink_bindgen::parser::{MavProfile, MavType}; +use skyward_mavlink::mavlink::Message; use crate::error::ErrInstrument; @@ -39,12 +40,12 @@ impl ReflectionContext { } /// Get the name of a message by its ID. - pub fn get_msg(&self, msg: impl MessageLike) -> Option<&MavMessage> { + pub fn get_msg(&'static self, msg: impl MessageLike) -> Option<&'static MavMessage> { msg.to_mav_message(self).ok() } /// Get all field names for a message by its ID. - pub fn get_fields(&self, message_id: impl MessageLike) -> Option<Vec<IndexedField<'_>>> { + pub fn get_fields(&'static self, message_id: impl MessageLike) -> Option<Vec<IndexedField>> { message_id.to_mav_message(self).ok().map(|msg| { msg.fields .iter() @@ -72,9 +73,9 @@ impl ReflectionContext { /// Get all plottable field names for a message by its ID. pub fn get_plottable_fields( - &self, + &'static self, message_id: impl MessageLike, - ) -> Option<Vec<IndexedField<'_>>> { + ) -> Option<Vec<IndexedField>> { let msg = message_id.to_mav_message(self).ok()?; msg.fields .iter() @@ -98,14 +99,36 @@ impl ReflectionContext { } } -#[derive(Clone)] -pub struct IndexedField<'a> { +#[derive(Debug, Clone)] +pub struct IndexedField { id: usize, - msg: &'a MavMessage, - field: &'a MavField, + msg: &'static MavMessage, + field: &'static MavField, } -impl IndexedField<'_> { +macro_rules! extract_as_type { + ($as_type: ty, $func: ident, $($mav_ty: ident, $rust_ty: ty),+) => { + pub fn $func(&self, message: &impl Message) -> Result<$as_type, String> { + macro_rules! downcast { + ($value: expr, $type: ty) => { + Ok(*$value + .downcast::<$type>() + .map_err(|_| "Type mismatch".to_string())? as $as_type) + }; + } + + let value = message + .get_field(self.id) + .ok_or("Field not found".to_string())?; + match self.field.mavtype { + $(MavType::$mav_ty => downcast!(value, $rust_ty),)+ + _ => Err("Field type not supported".to_string()), + } + } + }; +} + +impl IndexedField { pub fn msg(&self) -> &MavMessage { self.msg } @@ -127,16 +150,136 @@ impl IndexedField<'_> { } } +/// ### Extractors +/// These methods allow to extract the value of a field from a message, casting +/// it to the desired type. +impl IndexedField { + #[rustfmt::skip] + extract_as_type!(f32, extract_as_f32, + UInt8, u8, + UInt16, u16, + UInt32, u32, + UInt64, u64, + Int8, i8, + Int16, i16, + Int32, i32, + Int64, i64, + Float, f32, + Double, f64 + ); + + #[rustfmt::skip] + extract_as_type!(f64, extract_as_f64, + UInt8, u8, + UInt16, u16, + UInt32, u32, + UInt64, u64, + Int8, i8, + Int16, i16, + Int32, i32, + Int64, i64, + Float, f32, + Double, f64 + ); + + #[rustfmt::skip] + extract_as_type!(u8, extract_as_u8, + UInt8, u8, + Char, char + ); + + #[rustfmt::skip] + extract_as_type!(u16, extract_as_u16, + UInt8, u8, + Int8, i8, + UInt16, u16 + ); + + #[rustfmt::skip] + extract_as_type!(u32, extract_as_u32, + UInt8, u8, + Int8, i8, + UInt16, u16, + Int16, i16, + UInt32, u32 + ); + + #[rustfmt::skip] + extract_as_type!(u64, extract_as_u64, + UInt8, u8, + Int8, i8, + UInt16, u16, + Int16, i16, + UInt32, u32, + Int32, i32, + UInt64, u64 + ); + + #[rustfmt::skip] + extract_as_type!(i8, extract_as_i8, + Int8, i8 + ); + + #[rustfmt::skip] + extract_as_type!(i16, extract_as_i16, + UInt8, u8, + Int8, i8, + Int16, i16 + ); + + #[rustfmt::skip] + extract_as_type!(i32, extract_as_i32, + UInt8, u8, + Int8, i8, + UInt16, u16, + Int16, i16, + Int32, i32 + ); + + #[rustfmt::skip] + extract_as_type!(i64, extract_as_i64, + UInt8, u8, + Int8, i8, + UInt16, u16, + Int16, i16, + UInt32, u32, + Int32, i32, + Int64, i64 + ); + + #[rustfmt::skip] + extract_as_type!(char, extract_as_char, + UInt8, u8, + Char, char + ); +} + +impl std::hash::Hash for IndexedField { + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + self.id.hash(state); + self.msg.id.hash(state); + } +} + +impl PartialEq for IndexedField { + fn eq(&self, other: &Self) -> bool { + self.id == other.id && self.msg.id == other.msg.id + } +} + pub trait MessageLike { - fn to_mav_message<'b>(&self, ctx: &'b ReflectionContext) -> Result<&'b MavMessage, String>; + fn to_mav_message( + &self, + ctx: &'static ReflectionContext, + ) -> Result<&'static MavMessage, String>; } -pub trait FieldLike<'a, 'b> { +pub trait FieldLike { fn to_mav_field( - &'a self, + &self, msg_id: u32, - ctx: &'b ReflectionContext, - ) -> Result<IndexedField<'b>, String>; + ctx: &'static ReflectionContext, + ) -> Result<IndexedField, String>; } impl MessageLike for u32 { @@ -158,12 +301,12 @@ impl MessageLike for &str { } } -impl<'b> FieldLike<'_, 'b> for &MavField { +impl FieldLike for &MavField { fn to_mav_field( &self, msg_id: u32, - ctx: &'b ReflectionContext, - ) -> Result<IndexedField<'b>, String> { + ctx: &'static ReflectionContext, + ) -> Result<IndexedField, String> { ctx.id_msg_map .get(&msg_id) .and_then(|msg| { @@ -181,12 +324,8 @@ impl<'b> FieldLike<'_, 'b> for &MavField { } } -impl<'b> FieldLike<'b, 'b> for IndexedField<'b> { - fn to_mav_field( - &self, - _msg_id: u32, - _ctx: &ReflectionContext, - ) -> Result<IndexedField<'_>, String> { +impl FieldLike for IndexedField { + fn to_mav_field(&self, _msg_id: u32, _ctx: &ReflectionContext) -> Result<IndexedField, String> { Ok(IndexedField { id: self.id, msg: self.msg, @@ -195,12 +334,12 @@ impl<'b> FieldLike<'b, 'b> for IndexedField<'b> { } } -impl<'b> FieldLike<'_, 'b> for usize { +impl FieldLike for usize { fn to_mav_field( &self, msg_id: u32, - ctx: &'b ReflectionContext, - ) -> Result<IndexedField<'b>, String> { + ctx: &'static ReflectionContext, + ) -> Result<IndexedField, String> { ctx.id_msg_map .get(&msg_id) .and_then(|msg| { @@ -214,12 +353,12 @@ impl<'b> FieldLike<'_, 'b> for usize { } } -impl<'b> FieldLike<'_, 'b> for &str { +impl FieldLike for &str { fn to_mav_field( &self, msg_id: u32, - ctx: &'b ReflectionContext, - ) -> Result<IndexedField<'b>, String> { + ctx: &'static ReflectionContext, + ) -> Result<IndexedField, String> { ctx.id_msg_map .get(&msg_id) .and_then(|msg| { diff --git a/src/ui/cache.rs b/src/ui/cache.rs index 9835924..ec089b2 100644 --- a/src/ui/cache.rs +++ b/src/ui/cache.rs @@ -7,10 +7,7 @@ use std::{ time::{Duration, Instant}, }; -use egui::Context; -use serialport::SerialPortInfo; - -use crate::{communication, error::ErrInstrument}; +use crate::error::ErrInstrument; const SERIAL_PORT_REFRESH_INTERVAL: Duration = Duration::from_millis(500); const SHORT_REFRESH_INTERVAL: Duration = Duration::from_millis(500); diff --git a/src/ui/panes/plot.rs b/src/ui/panes/plot.rs index 9d9ad09..01b5849 100644 --- a/src/ui/panes/plot.rs +++ b/src/ui/panes/plot.rs @@ -5,16 +5,15 @@ use crate::{ MAVLINK_PROFILE, error::ErrInstrument, mavlink::{ - Message, MessageData, ROCKET_FLIGHT_TM_DATA, TimedMessage, - reflection::{self, FieldLike}, + MessageData, ROCKET_FLIGHT_TM_DATA, TimedMessage, + reflection::{FieldLike, IndexedField}, }, ui::{app::PaneResponse, cache::ChangeTracker}, }; use egui::{Color32, Vec2b}; use egui_plot::{Legend, Line, PlotPoint, PlotPoints}; use egui_tiles::TileId; -use mavlink_bindgen::parser::MavType; -use serde::{Deserialize, Serialize}; +use serde::{self, Deserialize, Serialize, ser::SerializeStruct}; use source_window::sources_window; use std::{hash::Hash, iter::zip}; @@ -65,7 +64,7 @@ impl PaneBehavior for Plot2DPane { )) .color(settings.color) .width(settings.width) - .name(&field.field.name), + .name(&field.field().name), ); } plot_ui @@ -104,10 +103,10 @@ impl PaneBehavior for Plot2DPane { } = &self.settings; for msg in messages { - let x: f64 = x_field.extract_from_message(&msg.message).log_unwrap(); + let x: f64 = x_field.extract_as_f64(&msg.message).log_unwrap(); let ys: Vec<f64> = y_fields .iter() - .map(|(field, _)| field.extract_from_message(&msg.message).log_unwrap()) + .map(|(field, _)| field.extract_as_f64(&msg.message).log_unwrap()) .collect(); if self.line_data.len() < ys.len() { @@ -115,7 +114,7 @@ impl PaneBehavior for Plot2DPane { } for (line, y) in zip(&mut self.line_data, ys) { - let point = if x_field.field.name == "timestamp" { + let point = if x_field.field().name == "timestamp" { PlotPoint::new(x / 1e6, y) } else { PlotPoint::new(x, y) @@ -146,15 +145,15 @@ fn show_menu(ui: &mut egui::Ui, settings_visible: &mut bool) { } } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, PartialEq)] struct PlotSettings { plot_message_id: u32, - x_field: FieldWithID, - y_fields: Vec<(FieldWithID, LineSettings)>, + x_field: IndexedField, + y_fields: Vec<(IndexedField, LineSettings)>, } impl PlotSettings { - fn plot_lines(&self) -> &[(FieldWithID, LineSettings)] { + fn plot_lines(&self) -> &[(IndexedField, LineSettings)] { &self.y_fields } @@ -166,19 +165,19 @@ impl PlotSettings { self.plot_message_id } - fn get_x_field(&self) -> &FieldWithID { + fn get_x_field(&self) -> &IndexedField { &self.x_field } - fn get_mut_x_field(&mut self) -> &mut FieldWithID { + fn get_mut_x_field(&mut self) -> &mut IndexedField { &mut self.x_field } - fn get_mut_y_fields(&mut self) -> &mut [(FieldWithID, LineSettings)] { + fn get_mut_y_fields(&mut self) -> &mut [(IndexedField, LineSettings)] { &mut self.y_fields[..] } - fn set_x_field(&mut self, field: FieldWithID) { + fn set_x_field(&mut self, field: IndexedField) { self.x_field = field; } @@ -186,11 +185,11 @@ impl PlotSettings { self.y_fields.len() } - fn contains_field(&self, field: &FieldWithID) -> bool { + fn contains_field(&self, field: &IndexedField) -> bool { self.y_fields.iter().any(|(f, _)| f == field) } - fn add_field(&mut self, field: FieldWithID) { + fn add_field(&mut self, field: IndexedField) { let line_settings = LineSettings::default(); self.y_fields.push((field, line_settings)); } @@ -198,8 +197,7 @@ impl PlotSettings { fn clear_fields(&mut self) { self.x_field = 0 .to_mav_field(self.plot_message_id, &MAVLINK_PROFILE) - .log_unwrap() - .into(); + .log_unwrap(); self.y_fields.clear(); } } @@ -207,9 +205,9 @@ impl PlotSettings { impl Default for PlotSettings { fn default() -> Self { let msg_id = ROCKET_FLIGHT_TM_DATA::ID; - let x_field = FieldWithID::new(msg_id, 0).log_unwrap(); + let x_field = 0.to_mav_field(msg_id, &MAVLINK_PROFILE).log_unwrap(); let y_fields = vec![( - FieldWithID::new(msg_id, 1).log_unwrap(), + 1.to_mav_field(msg_id, &MAVLINK_PROFILE).log_unwrap(), LineSettings::default(), )]; Self { @@ -250,65 +248,53 @@ impl Hash for LineSettings { } } -/// A struct to hold a field and its ID in a message -/// We use this and not `reflection::IndexedField` because we need to serialize it -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -struct FieldWithID { - id: usize, - field: reflection::MavField, -} - -impl FieldWithID { - fn new(msg_id: u32, field_id: usize) -> Option<Self> { - Some(Self { - id: field_id, - field: field_id - .to_mav_field(msg_id, &MAVLINK_PROFILE) - .ok()? - .field() - .clone(), - }) +impl Serialize for PlotSettings { + fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { + let mut state = serializer.serialize_struct("PlotSettings", 3)?; + let y_fields: Vec<_> = self.y_fields.iter().map(|(f, s)| (f.id(), s)).collect(); + state.serialize_field("msg_id", &self.plot_message_id)?; + state.serialize_field("x_field", &self.x_field.id())?; + state.serialize_field("y_fields", &y_fields)?; + state.end() } +} - fn extract_from_message(&self, message: &impl Message) -> Result<f64, String> { - macro_rules! downcast { - ($value: expr, $type: ty) => { - Ok(*$value - .downcast::<$type>() - .map_err(|_| "Type mismatch".to_string())? as f64) - }; +impl<'de> Deserialize<'de> for PlotSettings { + fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { + #[derive(Deserialize)] + struct FieldSettings { + field: usize, + settings: LineSettings, } - let value = message - .get_field(self.id) - .ok_or("Field not found".to_string())?; - match self.field.mavtype { - MavType::UInt8 => downcast!(value, u8), - MavType::UInt16 => downcast!(value, u16), - MavType::UInt32 => downcast!(value, u32), - MavType::UInt64 => downcast!(value, u64), - MavType::Int8 => downcast!(value, i8), - MavType::Int16 => downcast!(value, i16), - MavType::Int32 => downcast!(value, i32), - MavType::Int64 => downcast!(value, i64), - MavType::Float => downcast!(value, f32), - MavType::Double => downcast!(value, f64), - _ => Err("Field type not supported".to_string()), + #[derive(Deserialize)] + struct PlotSettingsData { + msg_id: u32, + x_field: usize, + y_fields: Vec<FieldSettings>, } - } -} -impl Hash for FieldWithID { - fn hash<H: std::hash::Hasher>(&self, state: &mut H) { - self.id.hash(state); - } -} - -impl From<reflection::IndexedField<'_>> for FieldWithID { - fn from(indexed_field: reflection::IndexedField<'_>) -> Self { - Self { - id: indexed_field.id(), - field: indexed_field.field().clone(), - } + let data = PlotSettingsData::deserialize(deserializer)?; + let x_field = data + .x_field + .to_mav_field(data.msg_id, &MAVLINK_PROFILE) + .log_unwrap(); + let y_fields = data + .y_fields + .into_iter() + .map(|FieldSettings { field, settings }| { + ( + field + .to_mav_field(data.msg_id, &MAVLINK_PROFILE) + .log_unwrap(), + settings, + ) + }) + .collect(); + Ok(Self { + plot_message_id: data.msg_id, + x_field, + y_fields, + }) } } diff --git a/src/ui/panes/plot/source_window.rs b/src/ui/panes/plot/source_window.rs index e49df73..bf636a7 100644 --- a/src/ui/panes/plot/source_window.rs +++ b/src/ui/panes/plot/source_window.rs @@ -1,7 +1,4 @@ -use crate::{ - MAVLINK_PROFILE, - ui::{cache::ChangeTracker, panes::plot::FieldWithID}, -}; +use crate::{MAVLINK_PROFILE, ui::cache::ChangeTracker}; use crate::error::ErrInstrument; @@ -32,12 +29,9 @@ pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut PlotSettings) { } // check fields and assign a default field_x and field_y once the msg is changed - let fields: Vec<FieldWithID> = MAVLINK_PROFILE + let fields = MAVLINK_PROFILE .get_plottable_fields(plot_settings.get_msg_id()) - .log_expect("Invalid message id") - .into_iter() - .map(|f| f.into()) - .collect::<Vec<_>>(); + .log_expect("Invalid message id"); // get the first field that is in the list of fields or the previous if valid let x_field = plot_settings.get_x_field(); let new_field_x = fields @@ -57,10 +51,10 @@ pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut PlotSettings) { // if fields are valid, show the combo boxes for the x_axis egui::ComboBox::from_label("X Axis") - .selected_text(&x_field.field.name) + .selected_text(&x_field.field().name) .show_ui(ui, |ui| { for msg in fields.iter() { - ui.selectable_value(x_field, msg.to_owned(), &msg.field.name); + ui.selectable_value(x_field, msg.to_owned(), &msg.field().name); } }); @@ -85,10 +79,10 @@ pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut PlotSettings) { "Y Axis".to_owned() }; egui::ComboBox::from_label(widget_label) - .selected_text(&field.field.name) + .selected_text(&field.field().name) .show_ui(ui, |ui| { for msg in fields.iter() { - ui.selectable_value(field, msg.to_owned(), &msg.field.name); + ui.selectable_value(field, msg.to_owned(), &msg.field().name); } }); ui.color_edit_button_srgba(color); -- GitLab From d1888318d90881578642a42bcfab7233c8242fcb Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Sun, 16 Mar 2025 20:05:13 +0100 Subject: [PATCH 5/8] CHECKPOINT --- Cargo.lock | 282 ++++++++++++++++------------- Cargo.toml | 12 +- src/communication/serial.rs | 2 +- src/ui/cache.rs | 60 +++++- src/ui/panes/plot.rs | 278 +++++++++++++++++++--------- src/ui/panes/plot/source_window.rs | 15 +- src/utils.rs | 2 +- src/utils/units.rs | 70 +++++++ 8 files changed, 486 insertions(+), 235 deletions(-) create mode 100644 src/utils/units.rs diff --git a/Cargo.lock b/Cargo.lock index 1dea7a6..14c6a43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -190,9 +190,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.96" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] name = "arboard" @@ -300,7 +300,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix", + "rustix 0.38.44", "slab", "tracing", "windows-sys 0.59.0", @@ -332,7 +332,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix", + "rustix 0.38.44", "tracing", ] @@ -344,7 +344,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -359,7 +359,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix", + "rustix 0.38.44", "signal-hook-registry", "slab", "windows-sys 0.59.0", @@ -373,13 +373,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.86" +version = "0.1.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" +checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -541,9 +541,9 @@ checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "bytemuck" -version = "1.21.0" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" +checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" dependencies = [ "bytemuck_derive", ] @@ -556,7 +556,7 @@ checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -573,9 +573,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "calloop" @@ -586,7 +586,7 @@ dependencies = [ "bitflags 2.9.0", "log", "polling", - "rustix", + "rustix 0.38.44", "slab", "thiserror 1.0.69", ] @@ -598,7 +598,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" dependencies = [ "calloop", - "rustix", + "rustix 0.38.44", "wayland-backend", "wayland-client", ] @@ -837,7 +837,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -872,15 +872,15 @@ checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" [[package]] name = "dyn-clone" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35" +checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" [[package]] name = "ecolor" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "878e9005799dd739e5d5d89ff7480491c12d0af571d44399bcaefa1ee172dd76" +checksum = "bc4feb366740ded31a004a0e4452fbf84e80ef432ecf8314c485210229672fd1" dependencies = [ "bytemuck", "emath", @@ -889,9 +889,9 @@ dependencies = [ [[package]] name = "eframe" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eba4c50d905804fe9ec4e159fde06b9d38f9440228617ab64a03d7a2091ece63" +checksum = "d0dfe0859f3fb1bc6424c57d41e10e9093fe938f426b691e42272c2f336d915c" dependencies = [ "ahash", "bytemuck", @@ -928,9 +928,9 @@ dependencies = [ [[package]] name = "egui" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d2768eaa6d5c80a6e2a008da1f0e062dff3c83eb2b28605ea2d0732d46e74d6" +checksum = "25dd34cec49ab55d85ebf70139cb1ccd29c977ef6b6ba4fe85489d6877ee9ef3" dependencies = [ "accesskit", "ahash", @@ -946,9 +946,9 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d8151704bcef6271bec1806c51544d70e79ef20e8616e5eac01facfd9c8c54a" +checksum = "d319dfef570f699b6e9114e235e862a2ddcf75f0d1a061de9e1328d92146d820" dependencies = [ "ahash", "bytemuck", @@ -966,9 +966,9 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace791b367c1f63e6044aef2f3834904509d1d1a6912fd23ebf3f6a9af92cd84" +checksum = "7d9dfbb78fe4eb9c3a39ad528b90ee5915c252e77bbab9d4ebc576541ab67e13" dependencies = [ "accesskit_winit", "ahash", @@ -987,9 +987,9 @@ dependencies = [ [[package]] name = "egui_extras" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b5cf69510eb3d19211fc0c062fb90524f43fe8e2c012967dcf0e2d81cb040f" +checksum = "624659a2e972a46f4d5f646557906c55f1cd5a0836eddbe610fdf1afba1b4226" dependencies = [ "ahash", "egui", @@ -1011,9 +1011,9 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a53e2374a964c3c793cb0b8ead81bca631f24974bc0b747d1a5622f4e39fdd0" +checksum = "910906e3f042ea6d2378ec12a6fd07698e14ddae68aed2d819ffe944a73aab9e" dependencies = [ "ahash", "bytemuck", @@ -1053,15 +1053,15 @@ dependencies = [ [[package]] name = "either" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "emath" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55b7b6be5ad1d247f11738b0e4699d9c20005ed366f2c29f5ec1f8e1de180bc2" +checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b" dependencies = [ "bytemuck", "serde", @@ -1091,7 +1091,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -1103,7 +1103,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -1124,7 +1124,7 @@ checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -1135,14 +1135,14 @@ checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] name = "epaint" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "275b665a7b9611d8317485187e5458750850f9e64604d3c58434bb3fc1d22915" +checksum = "41fcc0f5a7c613afd2dee5e4b30c3e6acafb8ad6f0edb06068811f708a67c562" dependencies = [ "ab_glyph", "ahash", @@ -1159,9 +1159,9 @@ dependencies = [ [[package]] name = "epaint_default_fonts" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9343d356d7cac894dacafc161b4654e0881301097bdf32a122ed503d97cb94b6" +checksum = "fc7e7a64c02cf7a5b51e745a9e45f60660a286f151c238b9d397b3e923f5082f" [[package]] name = "equivalent" @@ -1255,7 +1255,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -1342,7 +1342,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -1722,7 +1722,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -1808,9 +1808,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jni" @@ -1906,7 +1906,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.9.0", "libc", - "redox_syscall 0.5.9", + "redox_syscall 0.5.10", ] [[package]] @@ -1935,6 +1935,12 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" + [[package]] name = "litemap" version = "0.7.5" @@ -2006,7 +2012,7 @@ dependencies = [ [[package]] name = "mavlink-bindgen" version = "0.14.0" -source = "git+https://git.skywarder.eu/avn/swd/mavlink/rust-mavlink.git?rev=b7446436b3c96ca4c40d28b54eeed346e7bf021e#b7446436b3c96ca4c40d28b54eeed346e7bf021e" +source = "git+https://git.skywarder.eu/avn/swd/mavlink/rust-mavlink.git?rev=da4add3de8243d3b8194b9793677e4c950686ddc#da4add3de8243d3b8194b9793677e4c950686ddc" dependencies = [ "crc-any", "lazy_static", @@ -2123,7 +2129,7 @@ dependencies = [ "spirv", "strum", "termcolor", - "thiserror 2.0.11", + "thiserror 2.0.12", "unicode-xid", ] @@ -2220,7 +2226,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -2250,7 +2256,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -2547,7 +2553,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.9", + "redox_syscall 0.5.10", "smallvec", "windows-targets 0.52.6", ] @@ -2566,22 +2572,22 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -2609,9 +2615,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "png" @@ -2636,7 +2642,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix", + "rustix 0.38.44", "tracing", "windows-sys 0.59.0", ] @@ -2658,18 +2664,18 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] @@ -2681,7 +2687,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" dependencies = [ "profiling-procmacros", - "tracy-client", + "tracy-client 0.17.6", ] [[package]] @@ -2691,7 +2697,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" dependencies = [ "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -2724,9 +2730,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.38" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" dependencies = [ "proc-macro2", ] @@ -2808,9 +2814,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" dependencies = [ "bitflags 2.9.0", ] @@ -2912,21 +2918,34 @@ dependencies = [ "bitflags 2.9.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dade4812df5c384711475be5fcd8c162555352945401aed22a35bffeab61f657" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.9.2", "windows-sys 0.59.0", ] [[package]] name = "rustversion" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] name = "ryu" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -2984,7 +3003,7 @@ dependencies = [ "skyward_mavlink", "strum", "strum_macros", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tracing", "tracing-appender", @@ -3018,7 +3037,7 @@ checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -3035,13 +3054,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -3149,7 +3168,7 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "skyward_mavlink" version = "0.1.1" -source = "git+https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git?branch=rust-strum#8f0700acc0def0cdb2d2ae490e0703fd6c820643" +source = "git+https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git?rev=03d37888f7b5a84b5032ca1af392a16da7f39df2#03d37888f7b5a84b5032ca1af392a16da7f39df2" dependencies = [ "bitflags 2.9.0", "mavlink-bindgen", @@ -3199,7 +3218,7 @@ dependencies = [ "libc", "log", "memmap2", - "rustix", + "rustix 0.38.44", "thiserror 1.0.69", "wayland-backend", "wayland-client", @@ -3293,7 +3312,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -3309,9 +3328,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.98" +version = "2.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2" dependencies = [ "proc-macro2", "quote", @@ -3326,20 +3345,20 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] name = "tempfile" -version = "3.17.1" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" +checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567" dependencies = [ "cfg-if", "fastrand", "getrandom 0.3.1", "once_cell", - "rustix", + "rustix 1.0.1", "windows-sys 0.59.0", ] @@ -3372,11 +3391,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.12", ] [[package]] @@ -3387,18 +3406,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -3490,9 +3509,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.43.0" +version = "1.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a" dependencies = [ "backtrace", "libc", @@ -3550,7 +3569,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -3613,7 +3632,7 @@ checksum = "0eaa1852afa96e0fe9e44caa53dc0bd2d9d05e0f2611ce09f97f8677af56e4ba" dependencies = [ "tracing-core", "tracing-subscriber", - "tracy-client", + "tracy-client 0.18.0", ] [[package]] @@ -3627,6 +3646,17 @@ dependencies = [ "tracy-client-sys", ] +[[package]] +name = "tracy-client" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d90a2c01305b02b76fdd89ac8608bae27e173c829a35f7d76a345ab5d33836db" +dependencies = [ + "loom", + "once_cell", + "tracy-client-sys", +] + [[package]] name = "tracy-client-sys" version = "0.24.3" @@ -3686,9 +3716,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-segmentation" @@ -3790,7 +3820,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", "wasm-bindgen-shared", ] @@ -3825,7 +3855,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3847,7 +3877,7 @@ checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" dependencies = [ "cc", "downcast-rs", - "rustix", + "rustix 0.38.44", "scoped-tls", "smallvec", "wayland-sys", @@ -3860,7 +3890,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" dependencies = [ "bitflags 2.9.0", - "rustix", + "rustix 0.38.44", "wayland-backend", "wayland-scanner", ] @@ -3882,7 +3912,7 @@ version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a93029cbb6650748881a00e4922b076092a6a08c11e7fbdb923f064b23968c5d" dependencies = [ - "rustix", + "rustix 0.38.44", "wayland-client", "xcursor", ] @@ -4037,7 +4067,7 @@ dependencies = [ "raw-window-handle", "rustc-hash", "smallvec", - "thiserror 2.0.11", + "thiserror 2.0.12", "wgpu-hal", "wgpu-types", ] @@ -4076,7 +4106,7 @@ dependencies = [ "renderdoc-sys", "rustc-hash", "smallvec", - "thiserror 2.0.11", + "thiserror 2.0.12", "wasm-bindgen", "web-sys", "wgpu-types", @@ -4157,7 +4187,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -4168,7 +4198,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -4436,7 +4466,7 @@ dependencies = [ "pin-project", "raw-window-handle", "redox_syscall 0.4.1", - "rustix", + "rustix 0.38.44", "sctk-adwaita", "smithay-client-toolkit", "smol_str", @@ -4508,7 +4538,7 @@ dependencies = [ "libc", "libloading", "once_cell", - "rustix", + "rustix 0.38.44", "x11rb-protocol", ] @@ -4579,7 +4609,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", "synstructure", ] @@ -4639,7 +4669,7 @@ checksum = "709ab20fc57cb22af85be7b360239563209258430bccf38d8b979c5a2ae3ecce" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", "zbus-lockstep", "zbus_xml", "zvariant", @@ -4654,7 +4684,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", "zvariant_utils", ] @@ -4709,7 +4739,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -4720,7 +4750,7 @@ checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -4740,7 +4770,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", "synstructure", ] @@ -4763,7 +4793,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] [[package]] @@ -4788,7 +4818,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", "zvariant_utils", ] @@ -4800,5 +4830,5 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.99", ] diff --git a/Cargo.toml b/Cargo.toml index d7507aa..0d667b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,11 +18,6 @@ egui_file = "0.22" # =========== Asynchronous =========== tokio = { version = "1.41", features = ["rt-multi-thread", "net", "sync"] } # =========== Mavlink =========== -skyward_mavlink = { git = "https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git", branch = "rust-strum", features = [ - "reflection", - "orion", - "serde", -] } serialport = "4.7.0" # ========= Persistency ========= serde = { version = "1.0", features = ["derive"] } @@ -42,9 +37,14 @@ anyhow = "1.0" ring-channel = "0.12.0" thiserror = "2.0.7" +[dependencies.skyward_mavlink] +git = "https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git" +rev = "03d37888f7b5a84b5032ca1af392a16da7f39df2" +features = ["reflection", "orion", "serde"] + [dependencies.mavlink-bindgen] git = "https://git.skywarder.eu/avn/swd/mavlink/rust-mavlink.git" -rev = "b7446436b3c96ca4c40d28b54eeed346e7bf021e" +rev = "da4add3de8243d3b8194b9793677e4c950686ddc" features = ["serde"] [dev-dependencies] diff --git a/src/communication/serial.rs b/src/communication/serial.rs index b4d3897..a27efdb 100644 --- a/src/communication/serial.rs +++ b/src/communication/serial.rs @@ -54,7 +54,7 @@ pub fn find_first_stm32_port() -> Result<Option<SerialPortInfo>, serialport::Err pub mod cached { use egui::Context; - use crate::ui::cache::CacheCall; + use crate::ui::cache::RecentCallCache; use super::*; diff --git a/src/ui/cache.rs b/src/ui/cache.rs index ec089b2..031a361 100644 --- a/src/ui/cache.rs +++ b/src/ui/cache.rs @@ -7,6 +7,8 @@ use std::{ time::{Duration, Instant}, }; +use egui::{Context, Id, Ui}; + use crate::error::ErrInstrument; const SERIAL_PORT_REFRESH_INTERVAL: Duration = Duration::from_millis(500); @@ -20,7 +22,7 @@ const INDEF_REFRESH_INTERVAL: Duration = Duration::MAX; /// * `id` - The unique identifier for the cached item. /// * `fun` - The function whose return value is to be cached. /// * `expiration_duration` - The duration after which the cache should be refreshed. -fn call<T, F>(ctx: &egui::Context, id: egui::Id, fun: F, expiration_duration: Duration) -> T +fn call<T, F>(ctx: &Context, id: Id, fun: F, expiration_duration: Duration) -> T where F: Fn() -> T, T: Clone + Send + Sync + 'static, @@ -40,7 +42,7 @@ where } /// A trait to extend egui's Context with a caching function. -pub trait CacheCall { +pub trait RecentCallCache { /// Calls the provided function and caches its result. Every time this /// function is called, it will return the cached value if it is still /// valid. @@ -49,7 +51,7 @@ pub trait CacheCall { /// * `id` - A unique identifier for the cached value. /// * `fun` - The function to be cached. /// * `expiration_duration` - The cache expiration duration. - fn call_cached<F, T>(&self, id: egui::Id, fun: F, expiration_duration: Duration) -> T + fn cached_function_call_for<F, T>(&self, id: Id, fun: F, expiration_duration: Duration) -> T where F: Fn() -> T, T: Clone + Send + Sync + 'static; @@ -60,8 +62,8 @@ pub trait CacheCall { T: Clone + Send + Sync + 'static, H: Hash, { - let id = egui::Id::new(hashable); - self.call_cached(id, fun, SHORT_REFRESH_INTERVAL) + let id = Id::new(hashable); + self.cached_function_call_for(id, fun, SHORT_REFRESH_INTERVAL) } fn call_cached_indef<F, T, H>(&self, hashable: &H, fun: F) -> T @@ -70,14 +72,14 @@ pub trait CacheCall { T: Clone + Send + Sync + 'static, H: Hash, { - let id = egui::Id::new(hashable); - self.call_cached(id, fun, INDEF_REFRESH_INTERVAL) + let id = Id::new(hashable); + self.cached_function_call_for(id, fun, INDEF_REFRESH_INTERVAL) } } -impl CacheCall for egui::Context { +impl RecentCallCache for Context { /// Implements the caching call using the internal `call` function. - fn call_cached<F, T>(&self, id: egui::Id, fun: F, expiration_duration: Duration) -> T + fn cached_function_call_for<F, T>(&self, id: Id, fun: F, expiration_duration: Duration) -> T where F: Fn() -> T, T: Clone + Send + Sync + 'static, @@ -86,6 +88,46 @@ impl CacheCall for egui::Context { } } +impl RecentCallCache for &Ui { + /// Implements the caching call using the context of the UI. + fn cached_function_call_for<F, T>(&self, id: Id, fun: F, expiration_duration: Duration) -> T + where + F: Fn() -> T, + T: Clone + Send + Sync + 'static, + { + call(self.ctx(), id, fun, expiration_duration) + } +} + +pub trait CacheWithCondition { + fn cache_result_if<F, T, H>(&self, hashable: H, condition: bool, fun: F) -> T + where + F: Fn() -> T, + T: Clone + Send + Sync + 'static, + H: Hash; +} + +impl CacheWithCondition for Ui { + fn cache_result_if<F, T, H>(&self, hashable: H, condition: bool, fun: F) -> T + where + F: Fn() -> T, + T: Clone + Send + Sync + 'static, + H: Hash, + { + let id = self.next_auto_id().with(hashable); + self.memory_mut(|m| { + let value = m.data.get_temp::<T>(id); + if !condition || value.is_none() { + let value = fun(); + m.data.insert_temp(id, value.clone()); + value + } else { + value.log_unwrap() + } + }) + } +} + /// ChangeTracker manages the tracking of state changes using an integrity digest. /// /// The `integrity_digest` field holds a 64-bit unsigned integer that represents diff --git a/src/ui/panes/plot.rs b/src/ui/panes/plot.rs index 01b5849..7159a19 100644 --- a/src/ui/panes/plot.rs +++ b/src/ui/panes/plot.rs @@ -8,14 +8,18 @@ use crate::{ MessageData, ROCKET_FLIGHT_TM_DATA, TimedMessage, reflection::{FieldLike, IndexedField}, }, - ui::{app::PaneResponse, cache::ChangeTracker}, + ui::app::PaneResponse, + utils::units::UnitOfMeasure, }; use egui::{Color32, Vec2b}; -use egui_plot::{Legend, Line, PlotPoint, PlotPoints}; +use egui_plot::{AxisHints, HPlacement, Legend, Line, PlotPoint, log_grid_spacer}; use egui_tiles::TileId; -use serde::{self, Deserialize, Serialize, ser::SerializeStruct}; +use serde::{self, Deserialize, Serialize}; use source_window::sources_window; -use std::{hash::Hash, iter::zip}; +use std::{ + hash::{DefaultHasher, Hash, Hasher}, + iter::zip, +}; #[derive(Clone, Default, Debug, Serialize, Deserialize)] pub struct Plot2DPane { @@ -25,7 +29,6 @@ pub struct Plot2DPane { line_data: Vec<Vec<PlotPoint>>, #[serde(skip)] state_valid: bool, - // UI settings #[serde(skip)] settings_visible: bool, #[serde(skip)] @@ -42,46 +45,132 @@ impl PaneBehavior for Plot2DPane { #[profiling::function] fn ui(&mut self, ui: &mut egui::Ui, _: TileId) -> PaneResponse { let mut response = PaneResponse::default(); + let data_settings_digest = self.settings.data_digest(); let ctrl_pressed = ui.input(|i| i.modifiers.ctrl); - // plot last 100 messages - egui_plot::Plot::new("plot") + let x_unit = UnitOfMeasure::from( + &self + .settings + .x_field + .field() + .unit + .clone() + .unwrap_or_default(), + ); + let y_units = self + .settings + .y_fields + .iter() + .map(|(field, _)| field.field().unit.as_ref().map(UnitOfMeasure::from)) + .collect::<Vec<_>>(); + // define y_unit as the common unit of the y_fields if they are all the same + let y_unit = y_units + .iter() + .fold(y_units.first().log_unwrap(), |acc, unit| { + match (acc, unit) { + (Some(uom), Some(unit)) if uom == unit => acc, + _ => &None, + } + }); + let x_name = self.settings.x_field.field().name.clone(); + + let x_axis = match x_unit { + UnitOfMeasure::Time(ref time_unit) => { + AxisHints::new_x().label(&x_name).formatter(move |m, r| { + let scaling_factor_to_nanos = time_unit.scale() * 1e9; + let r_span_in_nanos = (r.end() - r.start()).abs() * scaling_factor_to_nanos; + let m_in_nanos = m.value * scaling_factor_to_nanos; + // all the following numbers are arbitrary + // they are chosen based on common sense + if r_span_in_nanos < 4e3 { + format!("{:.0}ns", m_in_nanos) + } else if r_span_in_nanos < 4e6 { + format!("{:.0}µs", m_in_nanos / 1e3) + } else if r_span_in_nanos < 4e9 { + format!("{:.0}ms", m_in_nanos / 1e6) + } else if r_span_in_nanos < 24e10 { + format!("{:.0}s", m_in_nanos / 1e9) + } else if r_span_in_nanos < 144e11 { + format!("{:.0}m{:.0}s", m_in_nanos / 60e9, (m_in_nanos % 60e9) / 1e9) + } else if r_span_in_nanos < 3456e11 { + format!( + "{:.0}h{:.0}m", + m_in_nanos / 3600e9, + (m_in_nanos % 3600e9) / 60e9 + ) + } else { + format!( + "{:.0}d{:.0}h", + m_in_nanos / 86400e9, + (m_in_nanos % 86400e9) / 3600e9 + ) + } + }) + } + _ => AxisHints::new_x().label(&x_name), + }; + let y_axis = AxisHints::new_y().placement(HPlacement::Right); + + let cursor_formatter = |name: &str, value: &PlotPoint| { + let x_unit = format!(" [{}]", x_unit); + let y_unit = y_unit + .as_ref() + .map(|unit| format!(" [{}]", unit)) + .unwrap_or_default(); + if name.is_empty() { + format!( + "{}: {:.2}{}\ny: {:.2}{}", + x_name, value.x, x_unit, value.y, y_unit + ) + } else { + format!( + "{}: {:.2}{}\n{}: {:.2}{}", + x_name, value.x, x_unit, name, value.y, y_unit + ) + } + }; + + let mut plot = egui_plot::Plot::new("plot") + .x_grid_spacer(log_grid_spacer(4)) // 4 was an arbitrary choice .auto_bounds(Vec2b::TRUE) .legend(Legend::default()) - .label_formatter(|name, value| format!("{} - x:{:.2} y:{:.2}", name, value.x, value.y)) - .show(ui, |plot_ui| { - self.contains_pointer = plot_ui.response().contains_pointer(); - if plot_ui.response().dragged() && ctrl_pressed { - response.set_drag_started(); - } + .label_formatter(cursor_formatter); + + if self.settings.axes_visible { + plot = plot.custom_x_axes(vec![x_axis]).custom_y_axes(vec![y_axis]); + } else { + plot = plot.show_axes(Vec2b::FALSE); + } + + plot.show(ui, |plot_ui| { + self.contains_pointer = plot_ui.response().contains_pointer(); + if plot_ui.response().dragged() && ctrl_pressed { + response.set_drag_started(); + } - for ((field, settings), points) in zip(self.settings.plot_lines(), &self.line_data) - { - plot_ui.line( - Line::new(PlotPoints::from( - &points[points.len().saturating_sub(100)..], // FIXME: don't show just the last 100 points - )) + for ((field, settings), points) in zip(self.settings.plot_lines(), &self.line_data) { + plot_ui.line( + Line::new(&points[..]) .color(settings.color) .width(settings.width) .name(&field.field().name), - ); - } - plot_ui - .response() - .context_menu(|ui| show_menu(ui, &mut self.settings_visible)); - }); + ); + } + plot_ui + .response() + .context_menu(|ui| show_menu(ui, &mut self.settings_visible, &mut self.settings)); + }); - let settings_hash = ChangeTracker::record_initial_state(&self.settings); egui::Window::new("Plot Settings") - .id(ui.auto_id_with("plot_settings")) // TODO: fix this issue with ids + .id(ui.auto_id_with("plot_settings")) .auto_sized() .collapsible(true) .movable(true) .open(&mut self.settings_visible) .show(ui.ctx(), |ui| sources_window(ui, &mut self.settings)); - if settings_hash.has_changed(&self.settings) { + if data_settings_digest != self.settings.data_digest() { self.state_valid = false; } @@ -114,13 +203,7 @@ impl PaneBehavior for Plot2DPane { } for (line, y) in zip(&mut self.line_data, ys) { - let point = if x_field.field().name == "timestamp" { - PlotPoint::new(x / 1e6, y) - } else { - PlotPoint::new(x, y) - }; - - line.push(point); + line.push(PlotPoint::new(x, y)); } } @@ -136,13 +219,15 @@ impl PaneBehavior for Plot2DPane { } } -fn show_menu(ui: &mut egui::Ui, settings_visible: &mut bool) { +fn show_menu(ui: &mut egui::Ui, settings_visible: &mut bool, settings: &mut PlotSettings) { ui.set_max_width(200.0); // To make sure we wrap long text - if ui.button("Settings…").clicked() { + if ui.button("Source Data Settings…").clicked() { *settings_visible = true; ui.close_menu(); } + + ui.checkbox(&mut settings.axes_visible, "Show Axes"); } #[derive(Clone, Debug, PartialEq)] @@ -150,6 +235,7 @@ struct PlotSettings { plot_message_id: u32, x_field: IndexedField, y_fields: Vec<(IndexedField, LineSettings)>, + axes_visible: bool, } impl PlotSettings { @@ -200,6 +286,15 @@ impl PlotSettings { .log_unwrap(); self.y_fields.clear(); } + + fn data_digest(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + self.x_field.hash(&mut hasher); + for (field, _) in &self.y_fields { + field.hash(&mut hasher); + } + hasher.finish() + } } impl Default for PlotSettings { @@ -214,18 +309,11 @@ impl Default for PlotSettings { plot_message_id: msg_id, x_field, y_fields, + axes_visible: true, } } } -impl Hash for PlotSettings { - fn hash<H: std::hash::Hasher>(&self, state: &mut H) { - self.plot_message_id.hash(state); - self.x_field.hash(state); - self.y_fields.hash(state); - } -} - #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] struct LineSettings { width: f32, @@ -248,53 +336,69 @@ impl Hash for LineSettings { } } -impl Serialize for PlotSettings { - fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { - let mut state = serializer.serialize_struct("PlotSettings", 3)?; - let y_fields: Vec<_> = self.y_fields.iter().map(|(f, s)| (f.id(), s)).collect(); - state.serialize_field("msg_id", &self.plot_message_id)?; - state.serialize_field("x_field", &self.x_field.id())?; - state.serialize_field("y_fields", &y_fields)?; - state.end() +mod plot_serde { + use serde::{Deserialize, Serialize}; + + use super::*; + + #[derive(Serialize, Deserialize)] + struct FieldSettings { + field: usize, + settings: LineSettings, } -} -impl<'de> Deserialize<'de> for PlotSettings { - fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { - #[derive(Deserialize)] - struct FieldSettings { - field: usize, - settings: LineSettings, - } + #[derive(Serialize, Deserialize)] + struct PlotSettingsData { + msg_id: u32, + x_field: usize, + y_fields: Vec<FieldSettings>, + axes_visible: bool, + } - #[derive(Deserialize)] - struct PlotSettingsData { - msg_id: u32, - x_field: usize, - y_fields: Vec<FieldSettings>, + impl Serialize for PlotSettings { + fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { + let data = PlotSettingsData { + msg_id: self.plot_message_id, + x_field: self.x_field.id(), + y_fields: self + .y_fields + .iter() + .map(|(field, settings)| FieldSettings { + field: field.id(), + settings: settings.clone(), + }) + .collect(), + axes_visible: self.axes_visible, + }; + data.serialize(serializer) } + } - let data = PlotSettingsData::deserialize(deserializer)?; - let x_field = data - .x_field - .to_mav_field(data.msg_id, &MAVLINK_PROFILE) - .log_unwrap(); - let y_fields = data - .y_fields - .into_iter() - .map(|FieldSettings { field, settings }| { - ( - field - .to_mav_field(data.msg_id, &MAVLINK_PROFILE) - .log_unwrap(), - settings, - ) + impl<'de> Deserialize<'de> for PlotSettings { + fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { + let data = PlotSettingsData::deserialize(deserializer)?; + let x_field = data + .x_field + .to_mav_field(data.msg_id, &MAVLINK_PROFILE) + .log_unwrap(); + let y_fields = data + .y_fields + .into_iter() + .map(|FieldSettings { field, settings }| { + ( + field + .to_mav_field(data.msg_id, &MAVLINK_PROFILE) + .log_unwrap(), + settings, + ) + }) + .collect(); + Ok(Self { + plot_message_id: data.msg_id, + x_field, + y_fields, + axes_visible: data.axes_visible, }) - .collect(); - Ok(Self { - plot_message_id: data.msg_id, - x_field, - y_fields, - }) + } } } diff --git a/src/ui/panes/plot/source_window.rs b/src/ui/panes/plot/source_window.rs index bf636a7..61ca3c7 100644 --- a/src/ui/panes/plot/source_window.rs +++ b/src/ui/panes/plot/source_window.rs @@ -1,4 +1,4 @@ -use crate::{MAVLINK_PROFILE, ui::cache::ChangeTracker}; +use crate::MAVLINK_PROFILE; use crate::error::ErrInstrument; @@ -6,7 +6,7 @@ use super::{LineSettings, PlotSettings}; #[profiling::function] pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut PlotSettings) { - let settings_hash = ChangeTracker::record_initial_state(&plot_settings); + let data_settings_digest = plot_settings.data_digest(); // extract the msg name from the id to show it in the combo box let msg_name = MAVLINK_PROFILE @@ -24,7 +24,7 @@ pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut PlotSettings) { }); // reset fields if the message is changed - if settings_hash.has_changed(plot_settings) { + if data_settings_digest != plot_settings.data_digest() { plot_settings.clear_fields(); } @@ -86,8 +86,13 @@ pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut PlotSettings) { } }); ui.color_edit_button_srgba(color); - ui.add(egui::DragValue::new(width).speed(0.1).suffix(" pt")) - .on_hover_text("Width of the line in points"); + ui.add( + egui::DragValue::new(width) + .range(0.0..=10.0) + .speed(0.02) + .suffix(" pt"), + ) + .on_hover_text("Width of the line in points"); ui.end_row(); } }); diff --git a/src/utils.rs b/src/utils.rs index 7e22ff5..503a473 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,2 +1,2 @@ mod ring_buffer; - +pub mod units; diff --git a/src/utils/units.rs b/src/utils/units.rs new file mode 100644 index 0000000..98267d9 --- /dev/null +++ b/src/utils/units.rs @@ -0,0 +1,70 @@ +use std::{fmt::Display, str::FromStr}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum UnitOfMeasure { + Time(TimeUnits), + Other(String), +} + +impl<T: AsRef<str>> From<T> for UnitOfMeasure { + fn from(s: T) -> Self { + if let Ok(unit) = TimeUnits::from_str(s.as_ref()) { + UnitOfMeasure::Time(unit) + } else { + UnitOfMeasure::Other(s.as_ref().to_string()) + } + } +} + +impl Display for UnitOfMeasure { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UnitOfMeasure::Time(unit) => write!(f, "{}", unit), + UnitOfMeasure::Other(unit) => write!(f, "{}", unit), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TimeUnits { + Second, // s + Millisecond, // ms + Microsecond, // us + Nanosecond, // ns +} + +impl TimeUnits { + pub fn scale(&self) -> f64 { + match self { + TimeUnits::Second => 1.0, + TimeUnits::Millisecond => 1e-3, + TimeUnits::Microsecond => 1e-6, + TimeUnits::Nanosecond => 1e-9, + } + } +} + +impl FromStr for TimeUnits { + type Err = (); + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "s" => Ok(TimeUnits::Second), + "ms" => Ok(TimeUnits::Millisecond), + "us" => Ok(TimeUnits::Microsecond), + "ns" => Ok(TimeUnits::Nanosecond), + _ => Err(()), + } + } +} + +impl Display for TimeUnits { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TimeUnits::Second => write!(f, "s"), + TimeUnits::Millisecond => write!(f, "ms"), + TimeUnits::Microsecond => write!(f, "µs"), + TimeUnits::Nanosecond => write!(f, "ns"), + } + } +} -- GitLab From d8da28f32d767b15e1c53af5c6f021e63acb27c3 Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Sun, 16 Mar 2025 21:11:42 +0100 Subject: [PATCH 6/8] CHECKPOINT --- src/mavlink/reflection.rs | 29 +++++- src/ui/panes/plot.rs | 156 ++++++++++------------------- src/ui/panes/plot/source_window.rs | 42 +++++--- 3 files changed, 108 insertions(+), 119 deletions(-) diff --git a/src/mavlink/reflection.rs b/src/mavlink/reflection.rs index c766536..e3ed70e 100644 --- a/src/mavlink/reflection.rs +++ b/src/mavlink/reflection.rs @@ -7,9 +7,10 @@ use std::collections::HashMap; use mavlink_bindgen::parser::{MavProfile, MavType}; +use serde::ser::SerializeStruct; use skyward_mavlink::mavlink::Message; -use crate::error::ErrInstrument; +use crate::{MAVLINK_PROFILE, error::ErrInstrument}; use super::MAVLINK_PROFILE_SERIALIZED; @@ -267,6 +268,32 @@ impl PartialEq for IndexedField { } } +impl serde::Serialize for IndexedField { + fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { + let mut state = serializer.serialize_struct("IndexedField", 3)?; + state.serialize_field("id", &self.id)?; + state.serialize_field("msg_id", &self.msg.id)?; + state.end() + } +} + +impl<'de> serde::Deserialize<'de> for IndexedField { + fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { + #[derive(serde::Deserialize)] + struct IndexedFieldDe { + id: usize, + msg_id: u32, + } + + let de = IndexedFieldDe::deserialize(deserializer)?; + let field = de + .id + .to_mav_field(de.msg_id, &MAVLINK_PROFILE) + .map_err(|u| serde::de::Error::custom(format!("Invalid field: {}", u)))?; + Ok(field) + } +} + pub trait MessageLike { fn to_mav_message( &self, diff --git a/src/ui/panes/plot.rs b/src/ui/panes/plot.rs index 7159a19..c778a69 100644 --- a/src/ui/panes/plot.rs +++ b/src/ui/panes/plot.rs @@ -19,6 +19,7 @@ use source_window::sources_window; use std::{ hash::{DefaultHasher, Hash, Hasher}, iter::zip, + time::{Duration, Instant}, }; #[derive(Clone, Default, Debug, Serialize, Deserialize)] @@ -26,7 +27,7 @@ pub struct Plot2DPane { settings: PlotSettings, // UI settings #[serde(skip)] - line_data: Vec<Vec<PlotPoint>>, + line_data: Vec<TimeAwarePlotPoints>, #[serde(skip)] state_valid: bool, #[serde(skip)] @@ -149,7 +150,9 @@ impl PaneBehavior for Plot2DPane { response.set_drag_started(); } - for ((field, settings), points) in zip(self.settings.plot_lines(), &self.line_data) { + for ((field, settings), TimeAwarePlotPoints { points, .. }) in + zip(&self.settings.y_fields, &self.line_data) + { plot_ui.line( Line::new(&points[..]) .color(settings.color) @@ -188,9 +191,17 @@ impl PaneBehavior for Plot2DPane { } let PlotSettings { - x_field, y_fields, .. + x_field, + y_fields, + points_lifespan, + .. } = &self.settings; + // purge old data + for line in &mut self.line_data { + line.purge_old(*points_lifespan); + } + for msg in messages { let x: f64 = x_field.extract_as_f64(&msg.message).log_unwrap(); let ys: Vec<f64> = y_fields @@ -199,11 +210,11 @@ impl PaneBehavior for Plot2DPane { .collect(); if self.line_data.len() < ys.len() { - self.line_data.resize(ys.len(), Vec::new()); + self.line_data.resize(ys.len(), TimeAwarePlotPoints::new()); } - for (line, y) in zip(&mut self.line_data, ys) { - line.push(PlotPoint::new(x, y)); + for (points, y) in zip(&mut self.line_data, ys) { + points.push(msg.time, PlotPoint::new(x, y)); } } @@ -230,51 +241,21 @@ fn show_menu(ui: &mut egui::Ui, settings_visible: &mut bool, settings: &mut Plot ui.checkbox(&mut settings.axes_visible, "Show Axes"); } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] struct PlotSettings { - plot_message_id: u32, - x_field: IndexedField, - y_fields: Vec<(IndexedField, LineSettings)>, - axes_visible: bool, + /// The message id to plot + pub(super) plot_message_id: u32, + /// The field to plot on the x-axis + pub(super) x_field: IndexedField, + /// The fields to plot, with their respective line settings + pub(super) y_fields: Vec<(IndexedField, LineSettings)>, + /// Whether to show the axes of the plot + pub(super) axes_visible: bool, + /// Points will be shown for this duration before being removed + pub(super) points_lifespan: Duration, } impl PlotSettings { - fn plot_lines(&self) -> &[(IndexedField, LineSettings)] { - &self.y_fields - } - - fn fields_empty(&self) -> bool { - self.y_fields.is_empty() - } - - fn get_msg_id(&self) -> u32 { - self.plot_message_id - } - - fn get_x_field(&self) -> &IndexedField { - &self.x_field - } - - fn get_mut_x_field(&mut self) -> &mut IndexedField { - &mut self.x_field - } - - fn get_mut_y_fields(&mut self) -> &mut [(IndexedField, LineSettings)] { - &mut self.y_fields[..] - } - - fn set_x_field(&mut self, field: IndexedField) { - self.x_field = field; - } - - fn fields_len(&self) -> usize { - self.y_fields.len() - } - - fn contains_field(&self, field: &IndexedField) -> bool { - self.y_fields.iter().any(|(f, _)| f == field) - } - fn add_field(&mut self, field: IndexedField) { let line_settings = LineSettings::default(); self.y_fields.push((field, line_settings)); @@ -310,6 +291,7 @@ impl Default for PlotSettings { x_field, y_fields, axes_visible: true, + points_lifespan: Duration::from_secs(600), } } } @@ -336,69 +318,33 @@ impl Hash for LineSettings { } } -mod plot_serde { - use serde::{Deserialize, Serialize}; - - use super::*; - - #[derive(Serialize, Deserialize)] - struct FieldSettings { - field: usize, - settings: LineSettings, - } +#[derive(Clone, Debug)] +struct TimeAwarePlotPoints { + times: Vec<Instant>, + points: Vec<PlotPoint>, +} - #[derive(Serialize, Deserialize)] - struct PlotSettingsData { - msg_id: u32, - x_field: usize, - y_fields: Vec<FieldSettings>, - axes_visible: bool, +impl TimeAwarePlotPoints { + fn new() -> Self { + Self { + times: Vec::new(), + points: Vec::new(), + } } - impl Serialize for PlotSettings { - fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { - let data = PlotSettingsData { - msg_id: self.plot_message_id, - x_field: self.x_field.id(), - y_fields: self - .y_fields - .iter() - .map(|(field, settings)| FieldSettings { - field: field.id(), - settings: settings.clone(), - }) - .collect(), - axes_visible: self.axes_visible, - }; - data.serialize(serializer) - } + fn push(&mut self, time: Instant, point: PlotPoint) { + self.times.push(time); + self.points.push(point); } - impl<'de> Deserialize<'de> for PlotSettings { - fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { - let data = PlotSettingsData::deserialize(deserializer)?; - let x_field = data - .x_field - .to_mav_field(data.msg_id, &MAVLINK_PROFILE) - .log_unwrap(); - let y_fields = data - .y_fields - .into_iter() - .map(|FieldSettings { field, settings }| { - ( - field - .to_mav_field(data.msg_id, &MAVLINK_PROFILE) - .log_unwrap(), - settings, - ) - }) - .collect(); - Ok(Self { - plot_message_id: data.msg_id, - x_field, - y_fields, - axes_visible: data.axes_visible, - }) + fn purge_old(&mut self, lifespan: Duration) { + while let Some(time) = self.times.first().copied() { + if time.elapsed() > lifespan { + self.times.remove(0); + self.points.remove(0); + } else { + break; + } } } } diff --git a/src/ui/panes/plot/source_window.rs b/src/ui/panes/plot/source_window.rs index 61ca3c7..a62d1f9 100644 --- a/src/ui/panes/plot/source_window.rs +++ b/src/ui/panes/plot/source_window.rs @@ -1,13 +1,30 @@ -use crate::MAVLINK_PROFILE; +use std::time::Duration; -use crate::error::ErrInstrument; +use crate::{MAVLINK_PROFILE, error::ErrInstrument}; use super::{LineSettings, PlotSettings}; #[profiling::function] pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut PlotSettings) { - let data_settings_digest = plot_settings.data_digest(); + // select how many points are shown on the plot + let mut points_lifespan_sec = plot_settings.points_lifespan.as_secs(); + ui.horizontal(|ui| { + let res1 = ui.add(egui::Label::new("Points Lifespan: ")); + let res2 = ui.add( + egui::DragValue::new(&mut points_lifespan_sec) + .range(5..=1800) + .speed(1) + .suffix(" seconds"), + ); + res1.union(res2) + }) + .inner + .on_hover_text("How long the data is shown on the plot"); + plot_settings.points_lifespan = Duration::from_secs(points_lifespan_sec); + + ui.add_sized([250., 10.], egui::Separator::default()); + let data_settings_digest = plot_settings.data_digest(); // extract the msg name from the id to show it in the combo box let msg_name = MAVLINK_PROFILE .get_msg(plot_settings.plot_message_id) @@ -30,10 +47,10 @@ pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut PlotSettings) { // check fields and assign a default field_x and field_y once the msg is changed let fields = MAVLINK_PROFILE - .get_plottable_fields(plot_settings.get_msg_id()) + .get_plottable_fields(plot_settings.plot_message_id) .log_expect("Invalid message id"); // get the first field that is in the list of fields or the previous if valid - let x_field = plot_settings.get_x_field(); + let x_field = &plot_settings.x_field; let new_field_x = fields .iter() .any(|f| f == x_field) @@ -46,10 +63,10 @@ pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut PlotSettings) { return; }; // update the field_x - plot_settings.set_x_field(new_field_x); - let x_field = plot_settings.get_mut_x_field(); + plot_settings.x_field = new_field_x; // if fields are valid, show the combo boxes for the x_axis + let x_field = &mut plot_settings.x_field; egui::ComboBox::from_label("X Axis") .selected_text(&x_field.field().name) .show_ui(ui, |ui| { @@ -59,19 +76,17 @@ pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut PlotSettings) { }); // populate the plot_lines with the first field if it is empty and there are more than 1 fields - if plot_settings.fields_empty() && fields.len() > 1 { + if plot_settings.y_fields.is_empty() && fields.len() > 1 { plot_settings.add_field(fields[1].to_owned()); } // check how many fields are left and how many are selected - let plot_lines_len = plot_settings.fields_len(); + let plot_lines_len = plot_settings.y_fields.len(); egui::Grid::new(ui.auto_id_with("y_axis")) .num_columns(3) .spacing([10.0, 2.5]) .show(ui, |ui| { - for (i, (field, line_settings)) in - plot_settings.get_mut_y_fields().iter_mut().enumerate() - { + for (i, (field, line_settings)) in plot_settings.y_fields[..].iter_mut().enumerate() { let LineSettings { width, color } = line_settings; let widget_label = if plot_lines_len > 1 { format!("Y Axis {}", i + 1) @@ -104,9 +119,10 @@ pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut PlotSettings) { .on_hover_text("Add another Y axis") .clicked() { + // get the first field that is not in the plot_lines let next_field = fields .iter() - .find(|f| !plot_settings.contains_field(f)) + .find(|field| !plot_settings.y_fields.iter().any(|(f, _)| f == *field)) .log_unwrap(); plot_settings.add_field(next_field.to_owned()); } -- GitLab From 4a925e383fe5c9ddb5363e75c1416feb163265e7 Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Sun, 16 Mar 2025 21:35:18 +0100 Subject: [PATCH 7/8] completed refactor of plot --- src/ui/panes/plot.rs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/ui/panes/plot.rs b/src/ui/panes/plot.rs index c778a69..4a98372 100644 --- a/src/ui/panes/plot.rs +++ b/src/ui/panes/plot.rs @@ -11,7 +11,7 @@ use crate::{ ui::app::PaneResponse, utils::units::UnitOfMeasure, }; -use egui::{Color32, Vec2b}; +use egui::{Color32, Vec2, Vec2b}; use egui_plot::{AxisHints, HPlacement, Legend, Line, PlotPoint, log_grid_spacer}; use egui_tiles::TileId; use serde::{self, Deserialize, Serialize}; @@ -135,6 +135,7 @@ impl PaneBehavior for Plot2DPane { let mut plot = egui_plot::Plot::new("plot") .x_grid_spacer(log_grid_spacer(4)) // 4 was an arbitrary choice .auto_bounds(Vec2b::TRUE) + .set_margin_fraction(Vec2::splat(0.)) .legend(Legend::default()) .label_formatter(cursor_formatter); @@ -197,12 +198,11 @@ impl PaneBehavior for Plot2DPane { .. } = &self.settings; - // purge old data - for line in &mut self.line_data { - line.purge_old(*points_lifespan); - } - - for msg in messages { + // iter on filtered messages based on lifespan set + for msg in messages + .iter() + .filter(|msg| points_lifespan > &msg.time.elapsed()) + { let x: f64 = x_field.extract_as_f64(&msg.message).log_unwrap(); let ys: Vec<f64> = y_fields .iter() @@ -218,6 +218,11 @@ impl PaneBehavior for Plot2DPane { } } + // clear points older than lifespan set + for line in &mut self.line_data { + line.clear_older_than(*points_lifespan); + } + self.state_valid = true; } @@ -268,12 +273,16 @@ impl PlotSettings { self.y_fields.clear(); } + /// Returns a digest of the data settings, used to check if the settings + /// have changed IMPORTANT: To trigger a redraw, hash the settings that need + /// to redraw the plot here fn data_digest(&self) -> u64 { let mut hasher = DefaultHasher::new(); self.x_field.hash(&mut hasher); for (field, _) in &self.y_fields { field.hash(&mut hasher); } + self.points_lifespan.as_secs().hash(&mut hasher); hasher.finish() } } @@ -337,7 +346,7 @@ impl TimeAwarePlotPoints { self.points.push(point); } - fn purge_old(&mut self, lifespan: Duration) { + fn clear_older_than(&mut self, lifespan: Duration) { while let Some(time) = self.times.first().copied() { if time.elapsed() > lifespan { self.times.remove(0); -- GitLab From 0f275618d384d457ab3c5d37e0b00bc4d3c6fe8c Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Sun, 16 Mar 2025 21:47:05 +0100 Subject: [PATCH 8/8] minor --- src/utils/ring_buffer.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utils/ring_buffer.rs b/src/utils/ring_buffer.rs index c25df11..5f6ee57 100644 --- a/src/utils/ring_buffer.rs +++ b/src/utils/ring_buffer.rs @@ -1,5 +1,3 @@ - - #[derive(Debug)] pub struct RingBuffer<const G: usize> { buffer: Box<[u8; G]>, -- GitLab