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;
+        }
+    }
 }