diff --git a/Cargo.lock b/Cargo.lock
index 4051aa103d01bc76ab27f69f43b56d1d47132a3d..704911bcb39c166185cde98f2fd3e5c30b20cbb9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -192,6 +192,12 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "anyhow"
+version = "1.0.93"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
+
 [[package]]
 name = "arboard"
 version = "3.4.1"
@@ -761,6 +767,12 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "crc-any"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a62ec9ff5f7965e4d7280bd5482acd20aadb50d632cf6c1d74493856b011fa73"
+
 [[package]]
 name = "crc32fast"
 version = "1.4.2"
@@ -770,6 +782,15 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
+dependencies = [
+ "crossbeam-utils",
+]
+
 [[package]]
 name = "crossbeam-utils"
 version = "0.8.20"
@@ -1468,6 +1489,18 @@ 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"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
+
 [[package]]
 name = "hermit-abi"
 version = "0.4.0"
@@ -1665,6 +1698,15 @@ dependencies = [
  "hashbrown 0.15.1",
 ]
 
+[[package]]
+name = "ioctl-rs"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7970510895cee30b3e9128319f2cefd4bde883a39f38baa279567ba3a7eb97d"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "itertools"
 version = "0.13.0"
@@ -1822,6 +1864,31 @@ dependencies = [
  "regex-automata 0.1.10",
 ]
 
+[[package]]
+name = "mavlink-bindgen"
+version = "0.13.2"
+source = "git+https://github.com/federico123579/rust-mavlink.git?branch=strum-integration#02a3eebe16ac1aacb93c2c61696dcd731f6f2ef4"
+dependencies = [
+ "crc-any",
+ "lazy_static",
+ "proc-macro2",
+ "quick-xml 0.36.2",
+ "quote",
+ "thiserror",
+]
+
+[[package]]
+name = "mavlink-core"
+version = "0.13.2"
+source = "git+https://github.com/federico123579/rust-mavlink.git?branch=strum-integration#02a3eebe16ac1aacb93c2c61696dcd731f6f2ef4"
+dependencies = [
+ "byteorder",
+ "crc-any",
+ "serde",
+ "serde_arrays",
+ "serial",
+]
+
 [[package]]
 name = "memchr"
 version = "2.7.4"
@@ -1871,6 +1938,18 @@ dependencies = [
  "simd-adler32",
 ]
 
+[[package]]
+name = "mio"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
+dependencies = [
+ "hermit-abi 0.3.9",
+ "libc",
+ "wasi",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "naga"
 version = "22.1.0"
@@ -1960,6 +2039,17 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "num-derive"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.87",
+]
+
 [[package]]
 name = "num-traits"
 version = "0.2.19"
@@ -2362,7 +2452,7 @@ checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
 dependencies = [
  "cfg-if",
  "concurrent-queue",
- "hermit-abi",
+ "hermit-abi 0.4.0",
  "pin-project-lite",
  "rustix",
  "tracing",
@@ -2565,6 +2655,12 @@ 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"
@@ -2609,13 +2705,18 @@ dependencies = [
 name = "segs"
 version = "0.1.0"
 dependencies = [
+ "anyhow",
+ "crossbeam-channel",
  "eframe",
  "egui",
  "egui_plot",
  "egui_tiles",
  "enum_dispatch",
+ "parking_lot",
  "serde",
  "serde_json",
+ "skyward_mavlink",
+ "strum",
  "tokio",
  "tracing",
  "tracing-subscriber",
@@ -2630,6 +2731,15 @@ dependencies = [
  "serde_derive",
 ]
 
+[[package]]
+name = "serde_arrays"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38636132857f68ec3d5f3eb121166d2af33cb55174c4d5ff645db6165cbef0fd"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "serde_derive"
 version = "1.0.215"
@@ -2664,6 +2774,48 @@ dependencies = [
  "syn 2.0.87",
 ]
 
+[[package]]
+name = "serial"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1237a96570fc377c13baa1b88c7589ab66edced652e43ffb17088f003db3e86"
+dependencies = [
+ "serial-core",
+ "serial-unix",
+ "serial-windows",
+]
+
+[[package]]
+name = "serial-core"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f46209b345401737ae2125fe5b19a77acce90cd53e1658cda928e4fe9a64581"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "serial-unix"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f03fbca4c9d866e24a459cbca71283f545a37f8e3e002ad8c70593871453cab7"
+dependencies = [
+ "ioctl-rs",
+ "libc",
+ "serial-core",
+ "termios",
+]
+
+[[package]]
+name = "serial-windows"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15c6d3b776267a75d31bbdfd5d36c0ca051251caafc285827052bc53bcdc8162"
+dependencies = [
+ "libc",
+ "serial-core",
+]
+
 [[package]]
 name = "sha1"
 version = "0.10.6"
@@ -2705,6 +2857,22 @@ version = "0.3.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 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"
+dependencies = [
+ "bitflags 2.6.0",
+ "mavlink-bindgen",
+ "mavlink-core",
+ "num-derive",
+ "num-traits",
+ "serde",
+ "serde_arrays",
+ "strum",
+ "strum_macros",
+]
+
 [[package]]
 name = "slab"
 version = "0.4.9"
@@ -2774,6 +2942,16 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "socket2"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "spirv"
 version = "0.3.0+sdk-1.3.268.0"
@@ -2801,6 +2979,25 @@ 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"
@@ -2856,6 +3053,15 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "termios"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "thiserror"
 version = "1.0.69"
@@ -2928,7 +3134,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33"
 dependencies = [
  "backtrace",
+ "libc",
+ "mio",
+ "parking_lot",
  "pin-project-lite",
+ "socket2",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 0461670afe27960259726f3c03af5ba5665bda90..116dfdfbe150e9d1ae4507ea64daa435b3ba59f1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,14 +13,30 @@ egui_tiles = "0.10"
 eframe = "0.29"
 egui = { version = "0.29" }
 egui_plot = "0.29"
-# =========== Asynchronous ===========
-tokio = { version = "1.41", features = ["rt-multi-thread"] }
 # ========= Persistency =========
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
 # =========== Logging ===========
 tracing = "0.1"
 tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+# =========== Performance ===========
+# for fast mutexes
+parking_lot = "0.12"
+# for fast channels
+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 70126df50bbf4f92eb8862ad6ea60d7d4d6ba87c..495e4e984723d1c4dc83e600f793baf195a5705c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,8 +1,15 @@
+mod mavlink;
+mod ui;
+
+use std::sync::OnceLock;
+
+use mavlink::MessageManager;
+use parking_lot::Mutex;
 use tokio::runtime::Runtime;
 use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer};
 use ui::ComposableView;
 
-mod ui;
+static MSG_MANAGER: OnceLock<Mutex<MessageManager>> = OnceLock::new();
 
 fn main() -> Result<(), eframe::Error> {
     // set up logging (USE RUST_LOG=debug to see logs)
@@ -30,6 +37,11 @@ fn main() -> Result<(), eframe::Error> {
     eframe::run_native(
         "segs", // This is the app id, used for example by Wayland
         native_options,
-        Box::new(|_| Ok(Box::<ComposableView>::default())),
+        Box::new(|cc| {
+            MSG_MANAGER
+                .set(Mutex::new(MessageManager::new(50, cc.egui_ctx.clone())))
+                .expect("Unable to set MessageManager");
+            Ok(Box::<ComposableView>::default())
+        }),
     )
 }
diff --git a/src/mavlink.rs b/src/mavlink.rs
new file mode 100644
index 0000000000000000000000000000000000000000..6f042703f7317cc35ff5ea6910eea803ab886938
--- /dev/null
+++ b/src/mavlink.rs
@@ -0,0 +1,128 @@
+use std::{
+    collections::HashMap,
+    sync::{
+        atomic::{AtomicBool, Ordering},
+        Arc,
+    },
+    time::Instant,
+};
+
+use anyhow::{Context, Result};
+use crossbeam_channel::{Receiver, Sender};
+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};
+
+pub const DEFAULT_ETHERNET_PORT: u16 = 42069;
+const UDP_BUFFER_SIZE: usize = 65527;
+
+#[derive(Debug)]
+pub struct MessageManager {
+    messages: HashMap<u32, Vec<TimedMessage>>,
+    tx: Sender<MavMessage>,
+    rx: Receiver<MavMessage>,
+    ctx: egui::Context,
+    running_flag: Arc<AtomicBool>,
+    task: Option<JoinHandle<Result<()>>>,
+}
+
+impl MessageManager {
+    pub fn new(channel_size: usize, ctx: egui::Context) -> Self {
+        let (tx, rx) = crossbeam_channel::bounded(channel_size);
+        Self {
+            messages: HashMap::new(),
+            tx,
+            rx,
+            ctx,
+            running_flag: Arc::new(AtomicBool::new(false)),
+            task: None,
+        }
+    }
+
+    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())
+    }
+
+    pub fn stop_listening(&mut self) {
+        self.running_flag.store(false, Ordering::Relaxed);
+        self.task.take().map(|t| t.abort());
+    }
+
+    pub fn listen_from_ethernet_port(&mut self, port: u16) {
+        // Stop the current listener if it exists
+        self.stop_listening();
+        self.running_flag.store(true, Ordering::Relaxed);
+
+        let tx = self.tx.clone();
+        let ctx = self.ctx.clone();
+
+        let bind_address = format!("0.0.0.0:{}", port);
+        let mut buf = Box::new([0; UDP_BUFFER_SIZE]);
+        let running_flag = self.running_flag.clone();
+
+        let handle = tokio::spawn(async move {
+            let socket = UdpSocket::bind(bind_address)
+                .await
+                .context("Failed to bind socket")?;
+            debug!("Listening on UDP");
+
+            while running_flag.load(Ordering::Relaxed) {
+                let (len, _) = socket
+                    .recv_from(buf.as_mut_slice())
+                    .await
+                    .context("Failed to receive message")?;
+                for (_, mav_message) in iter_messages(&buf[..len]) {
+                    tx.send(mav_message).context("Failed to send message")?;
+                    ctx.request_repaint();
+                }
+                // buf.iter_mut().for_each(|b| *b = 0);
+            }
+
+            Ok::<(), anyhow::Error>(())
+        });
+        self.task = Some(handle);
+    }
+
+    pub fn clear(&mut self) {
+        self.messages.clear();
+    }
+
+    fn add_message(&mut self, message: MavMessage) {
+        self.messages
+            .entry(message.message_id())
+            .or_default()
+            .push(TimedMessage::just_received(message));
+    }
+
+    // TODO: Implement a scheduler removal of old messages (configurable, must not hurt performance)
+    // TODO: Add a Dashmap if performance is a problem (Personally don't think it will be)
+}
+
+#[derive(Debug, Clone)]
+pub struct TimedMessage {
+    message: MavMessage,
+    time: Instant,
+}
+
+impl TimedMessage {
+    fn just_received(message: MavMessage) -> Self {
+        Self {
+            message,
+            time: Instant::now(),
+        }
+    }
+}
+
+/// Helper function to read a stream of bytes and return an iterator of MavLink messages
+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())
+}
diff --git a/src/ui/composable_view.rs b/src/ui/composable_view.rs
index db58813273f2e34aa1368fe12487da795d7a0a3c..5ba5b3e60f109eaa767fa4388ed4414365eaab78 100644
--- a/src/ui/composable_view.rs
+++ b/src/ui/composable_view.rs
@@ -1,3 +1,5 @@
+use crate::{mavlink, MSG_MANAGER};
+
 use super::{
     panes::{Pane, PaneBehavior},
     shortcuts,
@@ -9,6 +11,7 @@ use egui_tiles::{Behavior, Container, Linear, LinearDir, Tile, TileId, Tiles, Tr
 pub struct ComposableView {
     panes_tree: Tree<Pane>,
     behavior: ComposableBehavior,
+    sources_window: SourceWindow,
 }
 
 // Implementing the default trait allows us to define a default configuration for our app
@@ -21,6 +24,7 @@ impl Default for ComposableView {
         Self {
             panes_tree,
             behavior: Default::default(),
+            sources_window: Default::default(),
         }
     }
 }
@@ -97,7 +101,17 @@ impl eframe::App for ComposableView {
 
         // Show a panel at the bottom of the screen with few global controls
         egui::TopBottomPanel::bottom("bottom_control").show(ctx, |ui| {
-            egui::global_theme_preference_switch(ui);
+            // Window for the sources
+            self.sources_window.show_window(ui);
+
+            // Horizontal belt of controls
+            ui.horizontal(|ui| {
+                egui::global_theme_preference_switch(ui);
+
+                if ui.button("Sources").clicked() {
+                    self.sources_window.visible = !self.sources_window.visible;
+                }
+            })
         });
 
         // A central panel covers the remainder of the screen, i.e. whatever area is left after adding other panels.
@@ -107,6 +121,56 @@ impl eframe::App for ComposableView {
     }
 }
 
+struct SourceWindow {
+    port: u16,
+    visible: bool,
+}
+
+impl Default for SourceWindow {
+    fn default() -> Self {
+        Self {
+            port: mavlink::DEFAULT_ETHERNET_PORT,
+            visible: false,
+        }
+    }
+}
+
+impl SourceWindow {
+    fn show_window(&mut self, ui: &mut egui::Ui) {
+        let mut window_is_open = self.visible;
+        let mut can_be_closed = false;
+        egui::Window::new("Sources")
+            .id(ui.id())
+            .auto_sized()
+            .collapsible(true)
+            .movable(true)
+            .open(&mut window_is_open)
+            .show(ui.ctx(), |ui| {
+                self.ui(ui, &mut can_be_closed);
+            });
+        self.visible = window_is_open && !can_be_closed;
+    }
+
+    fn ui(&mut self, ui: &mut egui::Ui, can_be_closed: &mut bool) {
+        egui::Grid::new(ui.id())
+            .num_columns(2)
+            .spacing([10.0, 5.0])
+            .show(ui, |ui| {
+                ui.label("Ethernet Port:");
+                ui.add(egui::Slider::new(&mut self.port, 1024..=65535).text("Port"));
+                ui.end_row();
+            });
+        if ui.button("Connect").clicked() {
+            MSG_MANAGER
+                .get()
+                .unwrap()
+                .lock()
+                .listen_from_ethernet_port(self.port);
+            *can_be_closed = true;
+        }
+    }
+}
+
 /// Behavior for the tree of panes in the composable view
 #[derive(Default)]
 pub struct ComposableBehavior {