diff --git a/Cargo.lock b/Cargo.lock index 704911bcb39c166185cde98f2fd3e5c30b20cbb9..23a50414ff12d791c698a56f3596a0877280f8a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1489,12 +1489,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - [[package]] name = "hermit-abi" version = "0.3.9" @@ -1866,21 +1860,24 @@ dependencies = [ [[package]] name = "mavlink-bindgen" -version = "0.13.2" -source = "git+https://github.com/federico123579/rust-mavlink.git?branch=strum-integration#02a3eebe16ac1aacb93c2c61696dcd731f6f2ef4" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83b15a4ad504e29cabfb03fdc97250a22d2354a5404d80fc48dbf02e06acf5f" dependencies = [ "crc-any", "lazy_static", "proc-macro2", - "quick-xml 0.36.2", + "quick-xml 0.26.0", "quote", + "serde", "thiserror", ] [[package]] name = "mavlink-core" -version = "0.13.2" -source = "git+https://github.com/federico123579/rust-mavlink.git?branch=strum-integration#02a3eebe16ac1aacb93c2c61696dcd731f6f2ef4" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e64d975ca3cf0ad8a7c278553f91d77de15fcde9b79bf6bc542e209dd0c7dee" dependencies = [ "byteorder", "crc-any", @@ -2498,6 +2495,15 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +[[package]] +name = "quick-xml" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.30.0" @@ -2655,12 +2661,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rustversion" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" - [[package]] name = "ryu" version = "1.0.18" @@ -2712,11 +2712,11 @@ dependencies = [ "egui_plot", "egui_tiles", "enum_dispatch", + "mavlink-bindgen", "parking_lot", "serde", "serde_json", "skyward_mavlink", - "strum", "tokio", "tracing", "tracing-subscriber", @@ -2753,9 +2753,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", @@ -2860,17 +2860,17 @@ 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#5b515ef056e5f783dc1cbc1c73eadd2f08a5652d" +source = "git+https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git?branch=rust-strum#a766c38ea2dada7450cd2531b8610cbabcb6a449" dependencies = [ "bitflags 2.6.0", "mavlink-bindgen", "mavlink-core", "num-derive", "num-traits", + "paste", "serde", "serde_arrays", - "strum", - "strum_macros", + "serde_json", ] [[package]] @@ -2979,25 +2979,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.87", -] - [[package]] name = "syn" version = "1.0.109" diff --git a/Cargo.toml b/Cargo.toml index 116dfdfbe150e9d1ae4507ea64daa435b3ba59f1..86847c816c571072d4445219f8deb2d9ab1d6318 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,20 @@ egui_tiles = "0.10" eframe = "0.29" egui = { version = "0.29" } egui_plot = "0.29" +# =========== Asynchronous =========== +tokio = { version = "1.41", features = [ + "rt-multi-thread", + "net", + "parking_lot", + "sync", +] } +# =========== Mavlink =========== +skyward_mavlink = { git = "https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git", branch = "rust-strum", features = [ + "reflection", + "lyra", + "serde", +] } +mavlink-bindgen = { version = "0.13.1", features = ["serde"] } # ========= Persistency ========= serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -27,16 +41,4 @@ crossbeam-channel = "0.5" # =========== Utility =========== # for dynamic dispatch enum_dispatch = "0.3" -strum = "0.26" anyhow = "1.0" - -# =========== Asynchronous =========== -[dependencies.tokio] -version = "1.41" -features = ["rt-multi-thread", "net", "parking_lot", "sync"] - -# =========== Mavlink =========== -[dependencies.skyward_mavlink] -git = "https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git" -branch = "rust-strum" -features = ["lyra", "serde", "strum"] diff --git a/src/main.rs b/src/main.rs index 495e4e984723d1c4dc83e600f793baf195a5705c..fa14876c6ea98322c8b35b9f049ecf2db9768e50 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,16 @@ mod mavlink; mod ui; -use std::sync::OnceLock; +use std::sync::{LazyLock, OnceLock}; -use mavlink::MessageManager; +use mavlink::{MessageManager, ReflectionContext}; use parking_lot::Mutex; use tokio::runtime::Runtime; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer}; use ui::ComposableView; static MSG_MANAGER: OnceLock<Mutex<MessageManager>> = OnceLock::new(); +static MAVLINK_PROFILE: LazyLock<ReflectionContext> = LazyLock::new(|| ReflectionContext::new()); fn main() -> Result<(), eframe::Error> { // set up logging (USE RUST_LOG=debug to see logs) diff --git a/src/mavlink.rs b/src/mavlink.rs index 6f042703f7317cc35ff5ea6910eea803ab886938..5b2214f49eab10352ebbb16bba65a0a03c1c6560 100644 --- a/src/mavlink.rs +++ b/src/mavlink.rs @@ -9,13 +9,13 @@ use std::{ use anyhow::{Context, Result}; use crossbeam_channel::{Receiver, Sender}; +use mavlink_bindgen::parser::{MavProfile, MavType}; use skyward_mavlink::{ lyra::MavMessage, mavlink::{peek_reader::PeekReader, read_v1_msg, MavHeader, Message}, }; -use strum::VariantNames; use tokio::{net::UdpSocket, task::JoinHandle}; -use tracing::{debug, info}; +use tracing::debug; pub const DEFAULT_ETHERNET_PORT: u16 = 42069; const UDP_BUFFER_SIZE: usize = 65527; @@ -45,7 +45,6 @@ impl MessageManager { pub fn get_message(&mut self, message_id: u32) -> Option<&[TimedMessage]> { while let Ok(message) = self.rx.try_recv() { - info!("Received message: {:?}", message); self.add_message(message); } self.messages.get(&message_id).map(|v| v.as_slice()) @@ -108,8 +107,8 @@ impl MessageManager { #[derive(Debug, Clone)] pub struct TimedMessage { - message: MavMessage, - time: Instant, + pub message: MavMessage, + pub time: Instant, } impl TimedMessage { @@ -126,3 +125,92 @@ fn iter_messages(buf: &[u8]) -> impl Iterator<Item = (MavHeader, MavMessage)> + let mut reader = PeekReader::new(buf); std::iter::from_fn(move || read_v1_msg(&mut reader).ok()) } + +pub struct ReflectionContext { + mavlink_profile: MavProfile, + id_name_map: HashMap<u32, String>, +} + +impl ReflectionContext { + pub fn new() -> Self { + let profile: MavProfile = + serde_json::from_str(skyward_mavlink::reflection::LYRA_MAVLINK_PROFILE_SERIALIZED) + .expect("Failed to deserialize MavProfile"); + let id_name_map = profile + .messages + .iter() + .map(|(name, m)| (m.id, name.clone())) + .collect(); + Self { + mavlink_profile: profile, + id_name_map, + } + } + + 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 messages(&self) -> Vec<&str> { + self.mavlink_profile + .messages + .keys() + .map(|s| s.as_str()) + .collect() + } + + pub fn get_fields_by_id(&self, message_id: u32) -> Vec<&str> { + self.mavlink_profile + .messages + .iter() + .find(|(_, m)| m.id == message_id) + .map(|(_, m)| &m.fields) + .unwrap_or_else(|| { + panic!("Message ID {} not found in profile", message_id); + }) + .into_iter() + .map(|f| f.name.as_str()) + .collect() + } + + pub fn get_plottable_fields_by_id(&self, message_id: u32) -> Vec<&str> { + self.mavlink_profile + .messages + .iter() + .find(|(_, m)| m.id == message_id) + .map(|(_, m)| &m.fields) + .unwrap_or_else(|| { + panic!("Message ID {} not found in profile", message_id); + }) + .into_iter() + .filter(|f| match f.mavtype { + MavType::UInt8 + | MavType::UInt16 + | MavType::UInt32 + | MavType::UInt64 + | MavType::Int8 + | MavType::Int16 + | MavType::Int32 + | MavType::Int64 + | MavType::Float + | MavType::Double => true, + _ => false, + }) + .map(|f| f.name.as_str()) + .collect() + } + + pub fn get_fields_by_name(&self, message_name: &str) -> Vec<&str> { + self.mavlink_profile + .messages + .iter() + .find(|(_, m)| m.name == message_name) + .map(|(_, m)| &m.fields) + .unwrap_or_else(|| { + panic!("Message {} not found in profile", message_name); + }) + .into_iter() + .map(|f| f.name.as_str()) + .collect() + } +} diff --git a/src/ui/panes/plot_2d.rs b/src/ui/panes/plot_2d.rs index a3ad8079da0f8a0c36444ed70ade6d5b06e4e680..ecaec8c3083a15529e1e31abdb7c9164e15cd1cd 100644 --- a/src/ui/panes/plot_2d.rs +++ b/src/ui/panes/plot_2d.rs @@ -1,20 +1,30 @@ -use crate::ui::composable_view::PaneResponse; +use crate::{ui::composable_view::PaneResponse, MAVLINK_PROFILE, MSG_MANAGER}; use super::PaneBehavior; +use egui::Color32; use egui_plot::{Line, PlotPoints}; use serde::{Deserialize, Serialize}; +use skyward_mavlink::{ + lyra::{MavMessage, ROCKET_FLIGHT_TM_DATA}, + mavlink::{Message, MessageData}, +}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Plot2DPane { + // UI settings #[serde(skip)] pub contains_pointer: bool, settings_visible: bool, - n_points: u32, - frequency: f64, + sources_visible: bool, + // Mavlink settings + msg_id: u32, + field_x: String, + fields_y: Vec<String>, + plot_active: bool, + // Plot specific settings width: f32, - color: egui::Color32, - open: bool, + color: Color32, } impl Default for Plot2DPane { @@ -22,11 +32,13 @@ impl Default for Plot2DPane { Self { contains_pointer: false, settings_visible: false, - n_points: 2, - frequency: 1.0, + sources_visible: false, + msg_id: ROCKET_FLIGHT_TM_DATA::ID, + field_x: "timestamp".to_owned(), + fields_y: vec![], + plot_active: false, width: 1.0, - color: egui::Color32::from_rgb(0, 120, 240), - open: false, + color: Color32::from_rgb(0, 120, 240), } } } @@ -35,34 +47,80 @@ impl PaneBehavior for Plot2DPane { fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse { let mut response = PaneResponse::default(); - let mut window_visible = self.settings_visible; + // Spawn windows + let mut settings_window_visible = self.settings_visible; egui::Window::new("Plot Settings") - .id(ui.id()) + .id(ui.make_persistent_id("plot_settings")) .auto_sized() .collapsible(true) .movable(true) - .open(&mut window_visible) + .open(&mut settings_window_visible) .show(ui.ctx(), |ui| self.settings_window(ui)); - self.settings_visible = window_visible; + self.settings_visible = settings_window_visible; + + let mut sources_window_visible = self.sources_visible; + egui::Window::new("Plot Sources") + .id(ui.make_persistent_id("plot_sources")) + .auto_sized() + .collapsible(true) + .movable(true) + .open(&mut sources_window_visible) + .show(ui.ctx(), |ui| self.sources_window(ui)); + self.sources_visible = sources_window_visible; let ctrl_pressed = ui.input(|i| i.modifiers.ctrl); - let plot = egui_plot::Plot::new("plot"); + let mut plot_lines = Vec::new(); + if self.plot_active { + let acc_points = MSG_MANAGER + .get() + .unwrap() + .lock() + .get_message(self.msg_id) + .map(|msg| { + msg.into_iter() + .map(|msg| { + let value: serde_json::Value = + serde_json::to_value(msg.message.clone()).unwrap(); + + let x = value.get(&self.field_x).unwrap(); + let x = serde_json::from_value::<f64>(x.clone()).unwrap(); + let mut ys = Vec::new(); + for field in self.fields_y.iter() { + let y = value.get(field).unwrap(); + ys.push(serde_json::from_value::<f64>(y.clone()).unwrap()); + } + (x, ys) + }) + .collect::<Vec<(f64, Vec<f64>)>>() + }) + .unwrap_or_default(); + + if !acc_points.is_empty() { + for i in 0..self.fields_y.len() { + let plot_line: Vec<[f64; 2]> = acc_points + .iter() + .map(|(timestamp, acc)| [*timestamp as f64, acc[i] as f64]) + .collect(); + plot_lines.push(plot_line); + } + } + } + + let plot = egui_plot::Plot::new("plot").auto_bounds([true, true].into()); plot.show(ui, |plot_ui| { self.contains_pointer = plot_ui.response().contains_pointer(); if plot_ui.response().dragged() && ctrl_pressed { println!("ctrl + drag"); response.set_drag_started(); } - let points: Vec<[f64; 2]> = (0..self.n_points) - .map(|i| i as f64 * 100.0 / (self.n_points - 1) as f64) - .map(|i| [i, (i * std::f64::consts::PI * 2.0 * self.frequency).sin()]) - .collect(); - plot_ui.line( - Line::new(PlotPoints::from(points)) - .color(self.color) - .width(self.width), - ); + for plot_line in plot_lines { + plot_ui.line( + Line::new(PlotPoints::from(plot_line)) + .color(self.color) + .width(self.width), + ); + } plot_ui.response().context_menu(|ui| self.menu(ui)); }); @@ -82,6 +140,11 @@ impl Plot2DPane { self.settings_visible = true; ui.close_menu(); } + + if ui.button("Sources…").clicked() { + self.sources_visible = true; + ui.close_menu(); + } } fn settings_window(&mut self, ui: &mut egui::Ui) { @@ -89,14 +152,6 @@ impl Plot2DPane { .num_columns(2) .spacing([10.0, 5.0]) .show(ui, |ui| { - ui.label("Size:"); - ui.add(egui::Slider::new(&mut self.n_points, 2..=1000).text("Points")); - ui.end_row(); - - ui.label("Frequency:"); - ui.add(egui::Slider::new(&mut self.frequency, 0.1..=10.0).text("Hz")); - ui.end_row(); - ui.label("Color:"); ui.color_edit_button_srgba(&mut self.color); ui.end_row(); @@ -106,4 +161,110 @@ impl Plot2DPane { ui.end_row(); }); } + + fn sources_window(&mut self, ui: &mut egui::Ui) { + let old_msg_id = self.msg_id; + let msg_name = MAVLINK_PROFILE + .get_name_from_id(self.msg_id) + .unwrap_or_default(); + egui::ComboBox::from_label("Message Kind") + .selected_text(msg_name) + .show_ui(ui, |ui| { + for msg in MAVLINK_PROFILE.messages() { + ui.selectable_value( + &mut self.msg_id, + MavMessage::message_id_from_name(msg).unwrap(), + msg, + ); + } + }); + + // reset fields if the message is changed + if self.msg_id != old_msg_id { + self.fields_y.truncate(1); + } + + // check fields and assing a default field_x and field_y once the msg is changed + let fields = MAVLINK_PROFILE.get_plottable_fields_by_id(self.msg_id); + // get the first field that is in the list of fields or the previous if valid + let mut field_x = fields + .contains(&self.field_x.as_str()) + .then(|| self.field_x.clone()) + .or(fields.get(0).map(|s| s.to_string())); + // get the second field that is in the list of fields or the previous if valid + let mut field_y = self + .fields_y + .get(0) + .map(|s| fields.contains(&s.as_str()).then_some(s.to_owned())) + .flatten() + .or(fields.get(1).map(|s| s.to_string())); + + // if fields are valid, show the combo boxes for the x_axis + if field_x.is_some() { + let field_x = field_x.as_mut().unwrap(); + egui::ComboBox::from_label("X Axis") + .selected_text(field_x.as_str()) + .show_ui(ui, |ui| { + for msg in fields.iter() { + ui.selectable_value(field_x, (*msg).to_owned(), *msg); + } + }); + } + // if fields are more than 1, show the combo boxes for the y_axis + if field_y.is_some() { + let field_y = field_y.as_mut().unwrap(); + let widget_label = if self.fields_y.len() > 1 { + "Y Axis 1" + } else { + "Y Axis" + }; + egui::ComboBox::from_label(widget_label) + .selected_text(field_y.as_str()) + .show_ui(ui, |ui| { + for msg in fields.iter() { + ui.selectable_value(field_y, (*msg).to_owned(), *msg); + } + }); + } + // check how many fields are left and how many are selected + let fields_selected = self.fields_y.len() + 1; + let fields_left_to_draw = fields.len().saturating_sub(2); + for i in 0..fields_left_to_draw.min(fields_selected.saturating_sub(2)) { + let field = self.fields_y.get_mut(1 + i).unwrap(); + let widget_label = format!("Y Axis {}", i + 2); + egui::ComboBox::from_label(widget_label) + .selected_text(field.as_str()) + .show_ui(ui, |ui| { + for msg in fields.iter() { + ui.selectable_value(field, (*msg).to_owned(), *msg); + } + }); + self.fields_y[1 + i] = field.clone(); + } + + // if we have fields left, show the add button + let fields_left_to_draw = fields.len().saturating_sub(fields_selected); + if fields_left_to_draw > 0 { + if ui + .button("Add Y Axis") + .on_hover_text("Add another Y axis") + .clicked() + { + self.fields_y.push(fields[fields_selected].to_string()); + } + } + + // update fields and flag for active plot + self.field_x = field_x.unwrap_or_default(); + if field_y.is_some() { + if self.fields_y.get(0).is_none() { + self.fields_y.push(field_y.unwrap()); + } else { + self.fields_y[0] = field_y.unwrap(); + } + self.plot_active = true; + } else { + self.plot_active = false; + } + } }