diff --git a/justfile b/justfile index ea6682b72a8b80b385a49c56e482096d98c68fb6..f537b2ebde3647d48e89f8c9074c4958475ef967 100644 --- a/justfile +++ b/justfile @@ -9,7 +9,7 @@ test *ARGS: cargo nextest run {{ARGS}} run LEVEL="debug": - RUST_LOG=segs={{LEVEL}} cargo r + RUST_BACKTRACE=full RUST_LOG=segs={{LEVEL}} cargo r doc: cargo doc --no-deps --open diff --git a/src/mavlink/reflection.rs b/src/mavlink/reflection.rs index e3ed70e723913efad04c48e64e1bf43777ec1ad0..4162f24df0f391d151a7c8664e6a747be13bb9cc 100644 --- a/src/mavlink/reflection.rs +++ b/src/mavlink/reflection.rs @@ -98,6 +98,21 @@ impl ReflectionContext { .map(|f| f.to_mav_field(msg.id, self).ok()) .collect() } + + pub fn get_all_state_fields( + &'static self, + message_id: impl MessageLike, + ) -> Option<Vec<IndexedField>> { + let msg = message_id.to_mav_message(self).ok()?; + msg.fields + .iter() + .filter(|f| { + f.name.to_lowercase().ends_with("state") + || f.name.to_lowercase().ends_with("status") + }) + .map(|f| f.to_mav_field(msg.id, self).ok()) + .collect() + } } #[derive(Debug, Clone)] diff --git a/src/ui/cache.rs b/src/ui/cache.rs index 031a361506c55edec402f8fc099aae7810d1e494..db6e95f540f6370bf103b2b0dc87eb485e4def49 100644 --- a/src/ui/cache.rs +++ b/src/ui/cache.rs @@ -158,7 +158,7 @@ impl ChangeTracker { /// ``` /// let initial_tracker = ChangeTracker::record_initial_state(&state); /// ``` - pub fn record_initial_state<T: Hash>(state: &T) -> Self { + pub fn record_initial_state<T: Hash>(state: T) -> Self { let mut hasher = DefaultHasher::new(); state.hash(&mut hasher); let integrity_digest = hasher.finish(); @@ -187,7 +187,7 @@ impl ChangeTracker { /// println!("The state has changed."); /// } /// ``` - pub fn has_changed<T: Hash>(&self, state: &T) -> bool { + 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/pid_drawing_tool.rs b/src/ui/panes/pid_drawing_tool.rs index e8fcfa868cfa1e494609566013112e70b0174e7a..7ee953105d5160d89fc9a2265ba00d911c5b7f5f 100644 --- a/src/ui/panes/pid_drawing_tool.rs +++ b/src/ui/panes/pid_drawing_tool.rs @@ -15,9 +15,10 @@ use strum::IntoEnumIterator; use symbols::{Symbol, icons::Icon}; use crate::{ + MAVLINK_PROFILE, error::ErrInstrument, - mavlink::{GSE_TM_DATA, MessageData, TimedMessage}, - ui::{app::PaneResponse, utils::egui_to_glam}, + mavlink::{GSE_TM_DATA, MessageData, TimedMessage, reflection::MessageLike}, + ui::{app::PaneResponse, cache::ChangeTracker, utils::egui_to_glam}, }; use super::PaneBehavior; @@ -32,20 +33,39 @@ enum Action { } /// Piping and instrumentation diagram -#[derive(Clone, Serialize, Deserialize, Default, Debug)] +#[derive(Clone, Serialize, Deserialize, Debug)] pub struct PidPane { + // Persistent internal state elements: Vec<Element>, connections: Vec<Connection>, - grid: GridInfo, + message_subscription_id: u32, + + // UI settings + center_content: bool, + // Temporary internal state #[serde(skip)] action: Option<Action>, - #[serde(skip)] editable: bool, + #[serde(skip)] + is_subs_window_visible: bool, +} - center_content: bool, +impl Default for PidPane { + fn default() -> Self { + Self { + elements: Vec::new(), + connections: Vec::new(), + grid: GridInfo::default(), + message_subscription_id: GSE_TM_DATA::ID, + center_content: false, + action: None, + editable: false, + is_subs_window_visible: false, + } + } } impl PartialEq for PidPane { @@ -69,7 +89,7 @@ impl PaneBehavior for PidPane { self.draw_grid(ui, theme); } self.draw_connections(ui, theme); - self.draw_elements(ui, theme); + self.elements_ui(ui, theme); // Handle things that require knowing the position of the pointer let (_, response) = ui.allocate_at_least(ui.max_rect().size(), Sense::click_and_drag()); @@ -96,6 +116,20 @@ impl PaneBehavior for PidPane { response.context_menu(|ui| self.draw_context_menu(ui, pointer_pos)); } + let change_tracker = ChangeTracker::record_initial_state(self.message_subscription_id); + egui::Window::new("Subscription") + .id(ui.auto_id_with("sub_settings")) + .auto_sized() + .collapsible(true) + .movable(true) + .open(&mut self.is_subs_window_visible) + .show(ui.ctx(), |ui| { + subscription_window(ui, &mut self.message_subscription_id) + }); + if change_tracker.has_changed(self.message_subscription_id) { + self.reset_subscriptions(); + } + PaneResponse::default() } @@ -106,13 +140,13 @@ impl PaneBehavior for PidPane { fn update(&mut self, messages: &[TimedMessage]) { if let Some(msg) = messages.last() { for element in &mut self.elements { - element.update(&msg.message); + element.update(&msg.message, self.message_subscription_id); } } } fn get_message_subscription(&self) -> Option<u32> { - Some(GSE_TM_DATA::ID) + Some(self.message_subscription_id) } } @@ -203,16 +237,16 @@ impl PidPane { } } - fn draw_elements(&mut self, ui: &mut Ui, theme: Theme) { + fn elements_ui(&mut self, ui: &mut Ui, theme: Theme) { for element in &mut self.elements { ui.scope(|ui| { - element.draw(&self.grid, ui, theme); + element.ui(ui, &self.grid, theme, self.message_subscription_id); }); } } fn draw_context_menu(&mut self, ui: &mut Ui, pointer_pos: Vec2) { - ui.set_max_width(120.0); // To make sure we wrap long text + ui.set_max_width(170.0); // To make sure we wrap long text if !self.editable { if ui.button("Enable editing").clicked() { @@ -229,12 +263,12 @@ impl PidPane { self.action = Some(Action::Connect(elem_idx)); ui.close_menu(); } - self.elements[elem_idx].context_menu(ui); if ui.button("Delete").clicked() { self.delete_element(elem_idx); self.action.take(); ui.close_menu(); } + self.elements[elem_idx].context_menu(ui); } else if let Some((conn_idx, segm_idx)) = self.hovers_connection(pointer_pos) { if ui.button("Split").clicked() { self.connections[conn_idx].split(segm_idx, self.grid.screen_to_grid(pointer_pos)); @@ -283,6 +317,11 @@ impl PidPane { }); } + if ui.button("Pane subscription settings…").clicked() { + self.is_subs_window_visible = true; + ui.close_menu(); + } + if ui.button("Disable editing").clicked() { self.editable = false; ui.close_menu(); @@ -400,4 +439,21 @@ impl PidPane { None => {} } } + + fn reset_subscriptions(&mut self) { + for element in &mut self.elements { + element.reset_subscriptions(); + } + } +} + +fn subscription_window(ui: &mut Ui, msg_id: &mut u32) { + let current_msg = msg_id.to_mav_message(&MAVLINK_PROFILE).log_unwrap(); + egui::ComboBox::from_label("Message subscription") + .selected_text(current_msg.name.as_str()) + .show_ui(ui, |ui| { + for msg in MAVLINK_PROFILE.get_sorted_msgs() { + ui.selectable_value(msg_id, msg.id, &msg.name); + } + }); } diff --git a/src/ui/panes/pid_drawing_tool/elements.rs b/src/ui/panes/pid_drawing_tool/elements.rs index 5cc6c6aed51245d54144fe57678edb3a5476398e..ddcf65e9a0463491e4618710a0deb6b3214d7851 100644 --- a/src/ui/panes/pid_drawing_tool/elements.rs +++ b/src/ui/panes/pid_drawing_tool/elements.rs @@ -35,8 +35,8 @@ impl Element { pub fn new(center: Vec2, symbol: Symbol) -> Self { Self { position: center - symbol.size() / 2.0, - rotation: 0.0, symbol, + rotation: 0.0, } } @@ -66,21 +66,17 @@ impl Element { } pub fn context_menu(&mut self, ui: &mut Ui) { - match &mut self.symbol { - Symbol::Icon(_) => { - if ui.button("Rotate 90° ⟲").clicked() { - self.rotate(-FRAC_PI_2); - ui.close_menu(); - } - if ui.button("Rotate 90° ⟳").clicked() { - self.rotate(FRAC_PI_2); - ui.close_menu(); - } + if let Symbol::Icon(_) = &mut self.symbol { + if ui.button("Rotate 90° ⟲").clicked() { + self.rotate(-FRAC_PI_2); + ui.close_menu(); } - Symbol::Label(label) => { - label.context_menu(ui); + if ui.button("Rotate 90° ⟳").clicked() { + self.rotate(FRAC_PI_2); + ui.close_menu(); } } + self.symbol.context_menu(ui); } /// Rotate the element by its center @@ -122,13 +118,18 @@ impl Element { self.position + Mat2::from_angle(self.rotation) * self.size() * 0.5 } - pub fn draw(&mut self, grid: &GridInfo, ui: &Ui, theme: Theme) { + pub fn ui(&mut self, ui: &mut Ui, grid: &GridInfo, theme: Theme, msg: u32) { let pos = grid.grid_to_screen(self.position); let size = grid.size(); self.symbol.paint(ui, theme, pos, size, self.rotation); + self.symbol.subscriptions_ui(ui, msg); + } + + pub fn update(&mut self, message: &MavMessage, subscribed_msg_id: u32) { + self.symbol.update(message, subscribed_msg_id); } - pub fn update(&mut self, message: &MavMessage) { - self.symbol.update(message); + pub fn reset_subscriptions(&mut self) { + self.symbol.reset_subscriptions(); } } diff --git a/src/ui/panes/pid_drawing_tool/symbols.rs b/src/ui/panes/pid_drawing_tool/symbols.rs index 00d8883c3a21907a3d965748504e563918c39089..6bbbe4c4557b5afeab304c095ef99615ec251021 100644 --- a/src/ui/panes/pid_drawing_tool/symbols.rs +++ b/src/ui/panes/pid_drawing_tool/symbols.rs @@ -26,7 +26,18 @@ impl Default for Symbol { #[enum_dispatch(Symbol)] pub trait SymbolBehavior { - fn paint(&mut self, ui: &Ui, theme: Theme, pos: Vec2, size: f32, rotation: f32); + /// Resets the subscriptions settings. + /// IMPORTANT: This method should be called every time the msg_id changes. + fn reset_subscriptions(&mut self); + + /// Updates the symbol based on the received message. + fn update(&mut self, message: &MavMessage, subscribed_msg_id: u32); + + /// Renders the symbol on the UI. + fn paint(&mut self, ui: &mut Ui, theme: Theme, pos: Vec2, size: f32, rotation: f32); + + /// Renders further elements related to the subscriptions settings + fn subscriptions_ui(&mut self, ui: &mut Ui, mavlink_id: u32); /// Anchor point in grid coordinates relative to the element's center /// @@ -37,8 +48,6 @@ pub trait SymbolBehavior { /// Symbol size in grid coordinates fn size(&self) -> Vec2; - fn update(&mut self, message: &MavMessage); - #[allow(unused_variables)] fn context_menu(&mut self, ui: &mut Ui) {} } diff --git a/src/ui/panes/pid_drawing_tool/symbols/icons.rs b/src/ui/panes/pid_drawing_tool/symbols/icons.rs index fcadab83a20f758aaa917cb29072f88396857d46..fd3378b72d9db4c98733ace0f8935dfcdeadeeac 100644 --- a/src/ui/panes/pid_drawing_tool/symbols/icons.rs +++ b/src/ui/panes/pid_drawing_tool/symbols/icons.rs @@ -1,6 +1,6 @@ mod motor_valve; -use egui::{ImageSource, Theme}; +use egui::{ImageSource, Theme, Ui}; use glam::Vec2; use motor_valve::MotorValve; use serde::{Deserialize, Serialize}; @@ -148,14 +148,19 @@ impl Icon { } impl SymbolBehavior for Icon { - fn paint( - &mut self, - ui: &egui::Ui, - theme: egui::Theme, - pos: glam::Vec2, - size: f32, - rotation: f32, - ) { + fn update(&mut self, message: &MavMessage, subscribed_msg_id: u32) { + if let Icon::MotorValve(state) = self { + state.update(message, subscribed_msg_id) + } + } + + fn reset_subscriptions(&mut self) { + if let Icon::MotorValve(state) = self { + state.reset_subscriptions() + } + } + + fn paint(&mut self, ui: &mut Ui, theme: Theme, pos: glam::Vec2, size: f32, rotation: f32) { let center = glam_to_egui(pos).to_pos2(); let image_rect = egui::Rect::from_min_size(center, glam_to_egui(self.size() * size)); egui::Image::new(self.get_image(theme)) @@ -163,9 +168,18 @@ impl SymbolBehavior for Icon { .paint_at(ui, image_rect); } - fn update(&mut self, message: &MavMessage) { + fn subscriptions_ui(&mut self, ui: &mut Ui, mavlink_id: u32) { + if let Icon::MotorValve(state) = self { + state.subscriptions_ui(ui, mavlink_id) + } + } + + fn context_menu(&mut self, ui: &mut Ui) { if let Icon::MotorValve(state) = self { - state.update(message) + if ui.button("Icon subscription settings…").clicked() { + state.is_subs_window_visible = true; + ui.close_menu(); + } } } diff --git a/src/ui/panes/pid_drawing_tool/symbols/icons/motor_valve.rs b/src/ui/panes/pid_drawing_tool/symbols/icons/motor_valve.rs index b16d910233c1e776df6aa49b12c26ec16650d421..1dd94786496619faae503531191fe0eb9637b770 100644 --- a/src/ui/panes/pid_drawing_tool/symbols/icons/motor_valve.rs +++ b/src/ui/panes/pid_drawing_tool/symbols/icons/motor_valve.rs @@ -1,39 +1,85 @@ +use egui::{RichText, Ui, Window}; use serde::{Deserialize, Serialize}; use crate::{ MAVLINK_PROFILE, error::ErrInstrument, - mavlink::{ - GSE_TM_DATA, MavMessage, Message, MessageData, - reflection::{FieldLike, IndexedField}, - }, + mavlink::{MavMessage, Message, reflection::IndexedField}, + ui::cache::ChangeTracker, }; -#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)] +#[derive(Clone, Serialize, Deserialize, PartialEq, Default, Debug)] pub struct MotorValve { - mavlink_field: IndexedField, + subscribed_field: Option<IndexedField>, /// false = closed, true = open #[serde(skip)] pub last_value: Option<bool>, + #[serde(skip)] + pub is_subs_window_visible: bool, } impl MotorValve { - pub(super) fn update(&mut self, msg: &MavMessage) { - if msg.message_id() == GSE_TM_DATA::ID { - let value = self.mavlink_field.extract_as_f64(msg).log_unwrap(); - self.last_value = Some(value != 0.0); + pub fn update(&mut self, msg: &MavMessage, subscribed_msg_id: u32) { + // Reset field if msg_id has changed + if let Some(inner_field) = &self.subscribed_field { + if inner_field.msg_id() != subscribed_msg_id { + self.subscribed_field = None; + } + } + + if let Some(field) = &self.subscribed_field { + if msg.message_id() == subscribed_msg_id { + let value = field.extract_as_f64(msg).log_unwrap(); + self.last_value = Some(value != 0.0); + } } } -} -impl Default for MotorValve { - fn default() -> Self { - Self { - mavlink_field: 19 - .to_mav_field(GSE_TM_DATA::ID, &MAVLINK_PROFILE) - .log_unwrap(), // n2_filling_valve_state for GSE_TM_DATA - last_value: None, + pub fn reset_subscriptions(&mut self) { + self.subscribed_field = None; + self.last_value = None; + } + + pub fn subscriptions_ui(&mut self, ui: &mut Ui, mavlink_id: u32) { + let change_tracker = ChangeTracker::record_initial_state(&self.subscribed_field); + Window::new("Subscriptions") + .id(ui.auto_id_with("subs_settings")) + .auto_sized() + .collapsible(true) + .movable(true) + .open(&mut self.is_subs_window_visible) + .show(ui.ctx(), |ui| { + subscription_window(ui, mavlink_id, &mut self.subscribed_field) + }); + // reset last_value if the subscribed field has changed + if change_tracker.has_changed(&self.subscribed_field) { + self.last_value = None; } } } + +fn subscription_window(ui: &mut Ui, msg_id: u32, field: &mut Option<IndexedField>) { + // Get all fields available for subscription + let fields = MAVLINK_PROFILE.get_all_state_fields(msg_id).log_unwrap(); + + // If no fields available for subscription + if fields.is_empty() { + ui.label( + RichText::new("No fields available for subscription") + .underline() + .strong(), + ); + return; + }; + + // Otherwise, select the first field available + let field = field.get_or_insert(fields[0].to_owned()); + egui::ComboBox::from_label("field") + .selected_text(&field.field().name) + .show_ui(ui, |ui| { + for msg in fields.iter() { + ui.selectable_value(field, msg.to_owned(), &msg.field().name); + } + }); +} diff --git a/src/ui/panes/pid_drawing_tool/symbols/labels.rs b/src/ui/panes/pid_drawing_tool/symbols/labels.rs index 10d2a9ede74451b98871f822bdee8902b64598d1..e20c55140a091fc3146f4d60a542fa33ae01c1d1 100644 --- a/src/ui/panes/pid_drawing_tool/symbols/labels.rs +++ b/src/ui/panes/pid_drawing_tool/symbols/labels.rs @@ -1,16 +1,18 @@ use serde::{Deserialize, Serialize}; -use egui::{Align2, Color32, CornerRadius, FontId, Stroke, StrokeKind, Theme, Ui}; +use egui::{ + Align2, Color32, CornerRadius, FontId, RichText, Stroke, StrokeKind, Theme, Ui, Window, +}; use glam::Vec2; use crate::{ MAVLINK_PROFILE, error::ErrInstrument, - mavlink::{ - GSE_TM_DATA, MavMessage, Message, MessageData, - reflection::{FieldLike, IndexedField}, + mavlink::{MavMessage, Message, reflection::IndexedField}, + ui::{ + cache::ChangeTracker, + utils::{egui_to_glam, glam_to_egui}, }, - ui::utils::{egui_to_glam, glam_to_egui}, }; use super::SymbolBehavior; @@ -19,34 +21,53 @@ const FONT_SIZE: f32 = 2.0; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct Label { - mavlink_field: IndexedField, + subscribed_field: Option<IndexedField>, size: Vec2, #[serde(skip)] last_value: Option<f32>, + #[serde(skip)] + is_subs_window_visible: bool, } impl Default for Label { fn default() -> Self { Self { - mavlink_field: 6 - .to_mav_field(GSE_TM_DATA::ID, &MAVLINK_PROFILE) - .log_unwrap(), // n2_vessel_1_pressure for GSE_TM_DATA + subscribed_field: None, last_value: Some(0.0), size: Vec2::new(FONT_SIZE * 0.6 * 4.0, FONT_SIZE), + is_subs_window_visible: false, } } } impl SymbolBehavior for Label { - fn paint(&mut self, ui: &Ui, theme: Theme, pos: Vec2, size: f32, _: f32) { + fn update(&mut self, message: &MavMessage, subscribed_msg_id: u32) { + if let Some(subscribed_field) = &self.subscribed_field { + if message.message_id() == subscribed_msg_id { + let value = subscribed_field.extract_as_f64(message).log_unwrap(); + self.last_value = Some(value as f32); + } + } + } + + fn reset_subscriptions(&mut self) { + self.subscribed_field = None; + self.last_value = None; + } + + fn paint(&mut self, ui: &mut Ui, theme: Theme, pos: Vec2, size: f32, _: f32) { let painter = ui.painter(); let color = match theme { Theme::Light => Color32::BLACK, Theme::Dark => Color32::WHITE, }; - let unit = self.mavlink_field.field().unit.as_deref().unwrap_or(""); + let unit = self + .subscribed_field + .as_ref() + .and_then(|f| f.field().unit.as_deref()) + .unwrap_or(""); let text = match self.last_value { Some(value) => format!("{:.2} {}", value, unit), None => "N/A".to_string(), @@ -71,10 +92,27 @@ impl SymbolBehavior for Label { ); } - fn update(&mut self, message: &MavMessage) { - if message.message_id() == GSE_TM_DATA::ID { - let value = self.mavlink_field.extract_as_f64(message).log_unwrap(); - self.last_value = Some(value as f32); + fn subscriptions_ui(&mut self, ui: &mut Ui, mavlink_id: u32) { + let change_tracker = ChangeTracker::record_initial_state(&self.subscribed_field); + Window::new("Subscriptions") + .id(ui.auto_id_with("subs_settings")) + .auto_sized() + .collapsible(true) + .movable(true) + .open(&mut self.is_subs_window_visible) + .show(ui.ctx(), |ui| { + subscription_window(ui, mavlink_id, &mut self.subscribed_field) + }); + // reset last_value if the subscribed field has changed + if change_tracker.has_changed(&self.subscribed_field) { + self.last_value = None; + } + } + + fn context_menu(&mut self, ui: &mut Ui) { + if ui.button("Label subscription settings…").clicked() { + self.is_subs_window_visible = true; + ui.close_menu(); } } @@ -86,3 +124,30 @@ impl SymbolBehavior for Label { self.size } } + +fn subscription_window(ui: &mut Ui, msg_id: u32, field: &mut Option<IndexedField>) { + // Get all fields available for subscription + let fields = MAVLINK_PROFILE + .get_plottable_fields(msg_id) + .log_expect("Invalid message id"); + + // If no fields available for subscription + if fields.is_empty() { + ui.label( + RichText::new("No fields available for subscription") + .underline() + .strong(), + ); + return; + } + + // Otherwise, select the first field available + let field = field.get_or_insert(fields[0].to_owned()); + egui::ComboBox::from_label("field") + .selected_text(&field.field().name) + .show_ui(ui, |ui| { + for msg in fields.iter() { + ui.selectable_value(field, msg.to_owned(), &msg.field().name); + } + }); +}