From 0a0e9cf318eb6463d07bcecc3ed5949b6bcc6b55 Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Thu, 20 Mar 2025 18:28:46 +0100
Subject: [PATCH] Updated MotorValve and Label to configurable subscriptions

---
 justfile                                      |  2 +-
 src/mavlink/reflection.rs                     | 15 +++
 src/ui/cache.rs                               |  4 +-
 src/ui/panes/pid_drawing_tool.rs              | 82 +++++++++++++---
 src/ui/panes/pid_drawing_tool/elements.rs     | 33 +++----
 src/ui/panes/pid_drawing_tool/symbols.rs      | 15 ++-
 .../panes/pid_drawing_tool/symbols/icons.rs   | 36 ++++---
 .../symbols/icons/motor_valve.rs              | 82 ++++++++++++----
 .../panes/pid_drawing_tool/symbols/labels.rs  | 95 ++++++++++++++++---
 9 files changed, 285 insertions(+), 79 deletions(-)

diff --git a/justfile b/justfile
index ea6682b..f537b2e 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 e3ed70e..4162f24 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 031a361..db6e95f 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 e8fcfa8..7ee9531 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 5cc6c6a..ddcf65e 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 00d8883..6bbbe4c 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 fcadab8..fd3378b 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 b16d910..1dd9478 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 10d2a9e..e20c551 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);
+            }
+        });
+}
-- 
GitLab