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] 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