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