From 0087e50ae48e47513719dae574dd46a5a789931c Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Mon, 3 Mar 2025 23:43:02 +0100
Subject: [PATCH 01/14] CHECKPOINT

---
 Cargo.lock                  |  65 ++++++++-
 Cargo.toml                  |   3 +
 justfile                    |   4 +
 src/communication.rs        |   3 +
 src/communication/serial.rs | 239 +++++++++++++++++++++++++++++++++
 src/main.rs                 |   2 +-
 src/serial.rs               |  28 ----
 src/ui/app.rs               | 156 +++++++++++++---------
 src/utils.rs                |  59 +--------
 src/utils/ring_buffer.rs    | 258 ++++++++++++++++++++++++++++++++++++
 10 files changed, 659 insertions(+), 158 deletions(-)
 create mode 100644 src/communication.rs
 create mode 100644 src/communication/serial.rs
 delete mode 100644 src/serial.rs
 create mode 100644 src/utils/ring_buffer.rs

diff --git a/Cargo.lock b/Cargo.lock
index 3e2ead8..8c97a46 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -140,7 +140,7 @@ dependencies = [
  "once_cell",
  "serde",
  "version_check",
- "zerocopy",
+ "zerocopy 0.7.35",
 ]
 
 [[package]]
@@ -2625,7 +2625,7 @@ version = "0.2.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
 dependencies = [
- "zerocopy",
+ "zerocopy 0.7.35",
 ]
 
 [[package]]
@@ -2710,8 +2710,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
 dependencies = [
  "libc",
- "rand_chacha",
- "rand_core",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.3",
+ "zerocopy 0.8.21",
 ]
 
 [[package]]
@@ -2721,7 +2732,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
 dependencies = [
  "ppv-lite86",
- "rand_core",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.3",
 ]
 
 [[package]]
@@ -2733,6 +2754,15 @@ dependencies = [
  "getrandom 0.2.15",
 ]
 
+[[package]]
+name = "rand_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+dependencies = [
+ "getrandom 0.3.1",
+]
+
 [[package]]
 name = "raw-window-handle"
 version = "0.6.2"
@@ -2918,6 +2948,7 @@ dependencies = [
  "enum_dispatch",
  "mavlink-bindgen",
  "profiling",
+ "rand 0.9.0",
  "ring-channel",
  "serde",
  "serde_json",
@@ -4502,7 +4533,7 @@ dependencies = [
  "hex",
  "nix 0.29.0",
  "ordered-stream",
- "rand",
+ "rand 0.8.5",
  "serde",
  "serde_repr",
  "sha1",
@@ -4584,7 +4615,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
 dependencies = [
  "byteorder",
- "zerocopy-derive",
+ "zerocopy-derive 0.7.35",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcf01143b2dd5d134f11f545cf9f1431b13b749695cb33bcce051e7568f99478"
+dependencies = [
+ "zerocopy-derive 0.8.21",
 ]
 
 [[package]]
@@ -4598,6 +4638,17 @@ dependencies = [
  "syn 2.0.98",
 ]
 
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712c8386f4f4299382c9abee219bee7084f78fb939d88b6840fcc1320d5f6da2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.98",
+]
+
 [[package]]
 name = "zerofrom"
 version = "0.1.6"
diff --git a/Cargo.toml b/Cargo.toml
index 60bd536..8c9d9fb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -42,3 +42,6 @@ thiserror = "2.0.7"
 uuid = { version = "1.12.1", features = ["serde", "v7"] }
 profiling = { version = "1.0", features = ["profile-with-tracy"] }
 tracing-tracy = "0.11.4"
+
+[dev-dependencies]
+rand = "0.9.0"
diff --git a/justfile b/justfile
index 613e908..ea6682b 100644
--- a/justfile
+++ b/justfile
@@ -1,9 +1,13 @@
 alias r := run
+alias t := test
 alias d := doc
 
 default:
     just run
 
+test *ARGS:
+    cargo nextest run {{ARGS}}
+
 run LEVEL="debug":
     RUST_LOG=segs={{LEVEL}} cargo r
 
diff --git a/src/communication.rs b/src/communication.rs
new file mode 100644
index 0000000..70fb136
--- /dev/null
+++ b/src/communication.rs
@@ -0,0 +1,3 @@
+mod serial;
+
+pub use serial::{SerialConnection, SerialPortCandidate};
diff --git a/src/communication/serial.rs b/src/communication/serial.rs
new file mode 100644
index 0000000..e71153e
--- /dev/null
+++ b/src/communication/serial.rs
@@ -0,0 +1,239 @@
+//! Serial port utilities
+//!
+//! This module provides utilities for working with serial ports, such as
+//! listing all available serial ports and finding the first serial port that
+//! contains "STM32" or "ST-LINK" in its product name.
+
+use std::{
+    collections::VecDeque,
+    sync::{
+        Arc, Mutex,
+        atomic::{AtomicBool, Ordering},
+    },
+    thread::JoinHandle,
+};
+
+use anyhow::Context;
+use serialport::{SerialPort, SerialPortInfo, SerialPortType};
+use skyward_mavlink::mavlink::error::MessageReadError;
+use tracing::error;
+
+use crate::{
+    error::ErrInstrument,
+    mavlink::{
+        MavHeader, MavMessage, MavlinkVersion, TimedMessage, peek_reader::PeekReader,
+        read_versioned_msg,
+    },
+};
+
+const MAX_STORED_MSGS: usize = 100; // 192 bytes each = 19.2 KB
+const SERIAL_PORT_TIMEOUT_MS: u64 = 100;
+
+/// Represents a candidate serial port device.
+#[derive(Debug, Clone)]
+pub struct SerialPortCandidate {
+    port_name: String,
+    info: SerialPortInfo,
+}
+
+impl PartialEq for SerialPortCandidate {
+    fn eq(&self, other: &Self) -> bool {
+        self.port_name == other.port_name
+    }
+}
+
+impl SerialPortCandidate {
+    /// Connects to the serial port with the given baud rate.
+    pub fn connect(self, baud_rate: u32) -> Result<SerialConnection, serialport::Error> {
+        let serial_port = serialport::new(&self.port_name, baud_rate)
+            .timeout(std::time::Duration::from_millis(SERIAL_PORT_TIMEOUT_MS))
+            .open()?;
+        Ok(SerialConnection {
+            serial_port_reader: Arc::new(Mutex::new(PeekReader::new(serial_port))),
+            stored_msgs: Arc::new(Mutex::new(VecDeque::with_capacity(MAX_STORED_MSGS))),
+            running_flag: Arc::new(AtomicBool::new(false)),
+            thread_handle: None,
+        })
+    }
+
+    /// Get a list of all serial USB ports available on the system
+    pub fn list_all_usb_ports() -> anyhow::Result<Vec<Self>> {
+        let ports = serialport::available_ports().context("No serial ports found!")?;
+        Ok(ports
+            .into_iter()
+            .filter(|p| matches!(p.port_type, SerialPortType::UsbPort(_)))
+            .map(|p| SerialPortCandidate {
+                port_name: p.port_name.clone(),
+                info: p,
+            })
+            .collect())
+    }
+
+    /// Finds the first USB serial port with "STM32" or "ST-LINK" in its product name.
+    /// Renamed from get_first_stm32_serial_port.
+    pub fn find_first_stm32_port() -> Option<Self> {
+        let ports = Self::list_all_usb_ports().log_unwrap();
+        for port in ports {
+            if let serialport::SerialPortType::UsbPort(info) = &port.info.port_type {
+                if let Some(p) = &info.product {
+                    if p.contains("STM32") || p.contains("ST-LINK") {
+                        return Some(port);
+                    }
+                }
+            }
+        }
+        None
+    }
+}
+
+impl AsRef<String> for SerialPortCandidate {
+    fn as_ref(&self) -> &String {
+        &self.port_name
+    }
+}
+
+/// Manages a connection to a serial port.
+pub struct SerialConnection {
+    serial_port_reader: Arc<Mutex<PeekReader<Box<dyn SerialPort>>>>,
+    stored_msgs: Arc<Mutex<VecDeque<TimedMessage>>>,
+    running_flag: Arc<AtomicBool>,
+    thread_handle: Option<JoinHandle<()>>,
+}
+
+impl SerialConnection {
+    /// Starts receiving messages asynchronously.
+    pub fn start_receiving(&mut self) {
+        let running_flag = self.running_flag.clone();
+        let serial_port = self.serial_port_reader.clone();
+        let stored_msgs = self.stored_msgs.clone();
+        let thread_handle = std::thread::spawn(move || {
+            while running_flag.load(Ordering::Relaxed) {
+                let res: Result<(MavHeader, MavMessage), MessageReadError> =
+                    read_versioned_msg(&mut serial_port.lock().log_unwrap(), MavlinkVersion::V1);
+                match res {
+                    Ok((_, msg)) => {
+                        // Store the message in the buffer.
+                        stored_msgs
+                            .lock()
+                            .log_unwrap()
+                            .push_back(TimedMessage::just_received(msg));
+                    }
+                    Err(MessageReadError::Io(e)) => {
+                        // Ignore timeouts.
+                        if e.kind() == std::io::ErrorKind::TimedOut {
+                            continue;
+                        } else {
+                            error!("Error reading message: {:?}", e);
+                            running_flag.store(false, Ordering::Relaxed);
+                        }
+                    }
+                    Err(e) => {
+                        error!("Error reading message: {:?}", e);
+                    }
+                };
+            }
+        });
+        self.thread_handle.replace(thread_handle);
+    }
+
+    /// Stops receiving messages.
+    pub fn stop_receiving(&mut self) {
+        self.running_flag.store(false, Ordering::Relaxed);
+        if let Some(handle) = self.thread_handle.take() {
+            handle.join().log_unwrap();
+        }
+    }
+
+    /// Retrieves and clears the stored messages.
+    pub fn retrieve_messages(&self) -> Vec<TimedMessage> {
+        self.stored_msgs.lock().log_unwrap().drain(..).collect()
+    }
+
+    /// Transmits a message over the serial connection.
+    pub fn transmit_message(&mut self, msg: &[u8]) {
+        todo!()
+    }
+}
+
+#[allow(clippy::unwrap_used)]
+#[cfg(test)]
+mod tests {
+    use std::{collections::VecDeque, io::Read};
+
+    use rand::prelude::*;
+    use skyward_mavlink::{mavlink::*, orion::*};
+
+    use super::*;
+
+    struct ChunkedMessageStreamGenerator {
+        rng: SmallRng,
+        buffer: VecDeque<u8>,
+    }
+
+    impl ChunkedMessageStreamGenerator {
+        const KINDS: [u32; 2] = [ACK_TM_DATA::ID, NACK_TM_DATA::ID];
+
+        fn new() -> Self {
+            Self {
+                rng: SmallRng::seed_from_u64(42),
+                buffer: VecDeque::new(),
+            }
+        }
+
+        fn msg_push(&mut self, msg: &MavMessage, header: MavHeader) -> std::io::Result<()> {
+            write_v1_msg(&mut self.buffer, header, msg).unwrap();
+            Ok(())
+        }
+
+        fn fill_buffer(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+            while buf.len() > self.buffer.len() {
+                self.add_next_rand();
+            }
+            let n = buf.len();
+            buf.iter_mut()
+                .zip(self.buffer.drain(..n))
+                .for_each(|(a, b)| *a = b);
+            Ok(n)
+        }
+
+        fn add_next_rand(&mut self) {
+            let i = self.rng.random_range(0..Self::KINDS.len());
+            let id = Self::KINDS[i];
+            let msg = MavMessage::default_message_from_id(id).unwrap();
+            let header = MavHeader {
+                system_id: 1,
+                component_id: 1,
+                sequence: 0,
+            };
+            self.msg_push(&msg, header).unwrap();
+        }
+    }
+
+    impl Read for ChunkedMessageStreamGenerator {
+        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+            // fill buffer with sequence of byte of random length
+            if buf.len() == 1 {
+                self.fill_buffer(&mut buf[..1])
+            } else if !buf.is_empty() {
+                let size = self.rng.random_range(1..buf.len());
+                self.fill_buffer(&mut buf[..size])
+            } else {
+                Ok(0)
+            }
+        }
+    }
+
+    #[test]
+    fn test_peek_reader_with_chunked_transmission() {
+        let mut gms = ChunkedMessageStreamGenerator::new();
+        let mut reader = PeekReader::new(&mut gms);
+        let mut msgs = Vec::new();
+        for _ in 0..100 {
+            let (_, msg): (MavHeader, MavMessage) = read_v1_msg(&mut reader).unwrap();
+            msgs.push(msg);
+        }
+        for msg in msgs {
+            assert!(msg.message_id() == ACK_TM_DATA::ID || msg.message_id() == NACK_TM_DATA::ID);
+        }
+    }
+}
diff --git a/src/main.rs b/src/main.rs
index 218319d..7db6c02 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,10 +2,10 @@
 #![warn(clippy::unwrap_used)]
 #![warn(clippy::panic)]
 
+mod communication;
 mod error;
 mod mavlink;
 mod message_broker;
-mod serial;
 mod ui;
 mod utils;
 
diff --git a/src/serial.rs b/src/serial.rs
deleted file mode 100644
index 29ffb2d..0000000
--- a/src/serial.rs
+++ /dev/null
@@ -1,28 +0,0 @@
-//! Serial port utilities
-//!
-//! This module provides utilities for working with serial ports, such as listing all available serial ports and finding the first serial port that contains "STM32" or "ST-LINK" in its product name.
-
-use anyhow::Context;
-
-use crate::error::ErrInstrument;
-
-/// Get the first serial port that contains "STM32" or "ST-LINK" in its product name
-pub fn get_first_stm32_serial_port() -> Option<String> {
-    let ports = serialport::available_ports().log_expect("Serial ports cannot be listed!");
-    for port in ports {
-        if let serialport::SerialPortType::UsbPort(info) = port.port_type {
-            if let Some(p) = info.product {
-                if p.contains("STM32") || p.contains("ST-LINK") {
-                    return Some(port.port_name);
-                }
-            }
-        }
-    }
-    None
-}
-
-/// Get a list of all serial ports available on the system
-pub fn list_all_serial_ports() -> anyhow::Result<Vec<String>> {
-    let ports = serialport::available_ports().context("No serial ports found!")?;
-    Ok(ports.iter().map(|p| p.port_name.clone()).collect())
-}
diff --git a/src/ui/app.rs b/src/ui/app.rs
index 0570632..5175a94 100644
--- a/src/ui/app.rs
+++ b/src/ui/app.rs
@@ -7,14 +7,14 @@ use super::{
     widgets::reception_led::ReceptionLed,
 };
 use crate::{
+    communication::SerialPortCandidate,
     error::ErrInstrument,
     mavlink,
     message_broker::{MessageBroker, MessageBundle},
-    serial::{get_first_stm32_serial_port, list_all_serial_ports},
     ui::panes::PaneKind,
 };
 use eframe::CreationContext;
-use egui::{Align2, Button, ComboBox, Key, Modifiers, Sides, Vec2};
+use egui::{Align2, Button, Color32, ComboBox, Key, Modifiers, RichText, Sides, Vec2};
 use egui_extras::{Size, StripBuilder};
 use egui_tiles::{Behavior, Container, Linear, LinearDir, Tile, TileId, Tiles, Tree};
 use serde::{Deserialize, Serialize};
@@ -24,7 +24,7 @@ use std::{
     path::{Path, PathBuf},
     time::{Duration, Instant},
 };
-use tracing::{debug, error, trace};
+use tracing::{debug, error, trace, warn};
 
 pub struct App {
     /// Persistent state of the app
@@ -374,33 +374,62 @@ impl AppState {
     }
 }
 
-#[derive(Debug, PartialEq, Eq, Default)]
+#[derive(Debug)]
 enum ConnectionKind {
-    #[default]
-    Ethernet,
-    Serial,
+    Ethernet {
+        port: u16,
+    },
+    Serial {
+        port: Option<SerialPortCandidate>,
+        baud_rate: u32,
+    },
 }
 
-#[derive(Debug)]
-enum ConnectionDetails {
-    Ethernet { port: u16 },
-    Serial { port: String, baud_rate: u32 },
+impl ConnectionKind {
+    fn default_ethernet() -> Self {
+        ConnectionKind::Ethernet {
+            port: mavlink::DEFAULT_ETHERNET_PORT,
+        }
+    }
+
+    fn default_serial() -> Self {
+        ConnectionKind::Serial {
+            port: SerialPortCandidate::find_first_stm32_port().or(
+                SerialPortCandidate::list_all_usb_ports()
+                    .ok()
+                    .and_then(|ports| ports.first().cloned()),
+            ),
+            baud_rate: 115200,
+        }
+    }
 }
 
-impl Default for ConnectionDetails {
+impl Default for ConnectionKind {
     fn default() -> Self {
-        ConnectionDetails::Ethernet {
+        ConnectionKind::Ethernet {
             port: mavlink::DEFAULT_ETHERNET_PORT,
         }
     }
 }
 
+// Implement PartialEq just for the variants, not the fields (for radio buttons)
+impl PartialEq for ConnectionKind {
+    fn eq(&self, other: &Self) -> bool {
+        matches!(
+            (self, other),
+            (
+                ConnectionKind::Ethernet { .. },
+                ConnectionKind::Ethernet { .. }
+            ) | (ConnectionKind::Serial { .. }, ConnectionKind::Serial { .. })
+        )
+    }
+}
+
 #[derive(Debug, Default)]
 struct SourceWindow {
     visible: bool,
     connected: bool,
     connection_kind: ConnectionKind,
-    connection_details: ConnectionDetails,
 }
 
 impl SourceWindow {
@@ -430,29 +459,24 @@ impl SourceWindow {
         let SourceWindow {
             connected,
             connection_kind,
-            connection_details,
             ..
         } = self;
         ui.label("Select Source:");
         ui.horizontal_top(|ui| {
-            ui.radio_value(connection_kind, ConnectionKind::Ethernet, "Ethernet");
-            ui.radio_value(connection_kind, ConnectionKind::Serial, "Serial");
+            ui.radio_value(
+                connection_kind,
+                ConnectionKind::default_ethernet(),
+                "Ethernet",
+            );
+            ui.radio_value(connection_kind, ConnectionKind::default_serial(), "Serial");
         });
 
         ui.separator();
 
-        match *connection_kind {
-            ConnectionKind::Ethernet => {
-                if !matches!(connection_details, ConnectionDetails::Ethernet { .. }) {
-                    *connection_details = ConnectionDetails::Ethernet {
-                        port: mavlink::DEFAULT_ETHERNET_PORT,
-                    };
-                }
-                let ConnectionDetails::Ethernet { port } = connection_details else {
-                    error!("UNREACHABLE: Connection kind is not Ethernet");
-                    unreachable!("Connection kind is not Ethernet");
-                };
-
+        // flag to check if the connection is valid
+        let mut connection_valid = true;
+        match connection_kind {
+            ConnectionKind::Ethernet { port } => {
                 egui::Grid::new("grid")
                     .num_columns(2)
                     .spacing([10.0, 5.0])
@@ -462,41 +486,41 @@ impl SourceWindow {
                         ui.end_row();
                     });
             }
-            ConnectionKind::Serial => {
-                if !matches!(connection_details, ConnectionDetails::Serial { .. }) {
-                    *connection_details = ConnectionDetails::Serial {
-                        // Default to the first STM32 serial port if available, otherwise
-                        // default to the first serial port available
-                        port: get_first_stm32_serial_port().unwrap_or(
-                            list_all_serial_ports()
-                                .ok()
-                                .and_then(|ports| ports.first().cloned())
-                                .unwrap_or_default(),
-                        ),
-                        baud_rate: 115200,
-                    };
-                }
-                let ConnectionDetails::Serial { port, baud_rate } = connection_details else {
-                    error!("UNREACHABLE: Connection kind is not Serial");
-                    unreachable!("Connection kind is not Serial");
-                };
-
+            ConnectionKind::Serial { port, baud_rate } => {
                 egui::Grid::new("grid")
                     .num_columns(2)
                     .spacing([10.0, 5.0])
                     .show(ui, |ui| {
                         ui.label("Serial Port:");
-                        ComboBox::from_id_salt("serial_port")
-                            .selected_text(port.clone())
-                            .show_ui(ui, |ui| {
-                                for available_port in list_all_serial_ports().unwrap_or_default() {
-                                    ui.selectable_value(
-                                        port,
-                                        available_port.clone(),
-                                        available_port,
-                                    );
-                                }
-                            });
+                        match port {
+                            Some(port) => {
+                                ComboBox::from_id_salt("serial_port")
+                                    .selected_text(port.as_ref())
+                                    .show_ui(ui, |ui| {
+                                        for available_port in
+                                            SerialPortCandidate::list_all_usb_ports().log_unwrap()
+                                        {
+                                            ui.selectable_value(
+                                                port,
+                                                available_port.clone(),
+                                                available_port.as_ref(),
+                                            );
+                                        }
+                                    });
+                            }
+                            None => {
+                                warn!("USER ERROR: No serial port found");
+                                ui.label(
+                                    RichText::new("No port found")
+                                        .color(Color32::RED)
+                                        .underline()
+                                        .strong(),
+                                );
+                                // invalid the connection
+                                connection_valid = false;
+                            }
+                        }
+
                         ui.end_row();
                         ui.label("Baud Rate:");
                         ui.add(
@@ -517,15 +541,17 @@ impl SourceWindow {
                 .horizontal(|mut strip| {
                     strip.cell(|ui| {
                         let btn1 = Button::new("Connect");
-                        ui.add_enabled_ui(!*connected, |ui| {
+                        ui.add_enabled_ui(!*connected & connection_valid, |ui| {
                             if ui.add_sized(ui.available_size(), btn1).clicked() {
-                                match connection_details {
-                                    ConnectionDetails::Ethernet { port } => {
+                                match connection_kind {
+                                    ConnectionKind::Ethernet { port } => {
                                         message_broker.listen_from_ethernet_port(*port);
                                     }
-                                    ConnectionDetails::Serial { port, baud_rate } => {
-                                        message_broker
-                                            .listen_from_serial_port(port.clone(), *baud_rate);
+                                    ConnectionKind::Serial { port, baud_rate } => {
+                                        message_broker.listen_from_serial_port(
+                                            port.as_ref().log_unwrap().as_ref().to_owned(),
+                                            *baud_rate,
+                                        );
                                     }
                                 }
                                 *can_be_closed = true;
diff --git a/src/utils.rs b/src/utils.rs
index d3699bb..4c96d92 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -1,58 +1,3 @@
-#[derive(Debug)]
-pub struct RingBuffer<const G: usize> {
-    buffer: Box<[u8; G]>,
-    write_cursor: usize,
-    read_cursor: usize,
-}
+mod ring_buffer;
 
-impl<const G: usize> std::io::Write for RingBuffer<G> {
-    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
-        let mut written = 0;
-        for byte in buf {
-            if self.is_full() {
-                Err(std::io::Error::new(
-                    std::io::ErrorKind::WriteZero,
-                    "Buffer full",
-                ))?;
-            }
-            self.buffer[self.write_cursor] = *byte;
-            self.write_cursor = (self.write_cursor + 1) % G;
-            written += 1;
-        }
-        Ok(written)
-    }
-
-    fn flush(&mut self) -> std::io::Result<()> {
-        Ok(())
-    }
-}
-
-impl<const G: usize> std::io::Read for RingBuffer<G> {
-    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
-        let mut read = 0;
-        while read < buf.len() {
-            if self.read_cursor == self.write_cursor {
-                break;
-            }
-            buf[read] = self.buffer[self.read_cursor];
-            self.read_cursor = (self.read_cursor + 1) % G;
-            read += 1;
-        }
-        Ok(read)
-    }
-}
-
-impl<const G: usize> RingBuffer<G> {
-    pub fn new() -> Self {
-        Self {
-            buffer: Box::new([0; G]),
-            write_cursor: 0,
-            read_cursor: 0,
-        }
-    }
-
-    #[inline]
-    pub fn is_full(&self) -> bool {
-        (self.write_cursor + 1) % G == self.read_cursor
-    }
-}
+pub use ring_buffer::{OverwritingRingBuffer, RingBuffer};
diff --git a/src/utils/ring_buffer.rs b/src/utils/ring_buffer.rs
new file mode 100644
index 0000000..b1a42fb
--- /dev/null
+++ b/src/utils/ring_buffer.rs
@@ -0,0 +1,258 @@
+use std::collections::binary_heap::PeekMut;
+
+use skyward_mavlink::mavlink::peek_reader::PeekReader;
+
+#[derive(Debug)]
+pub struct RingBuffer<const G: usize> {
+    buffer: Box<[u8; G]>,
+    write_cursor: usize,
+    read_cursor: usize,
+}
+
+impl<const G: usize> RingBuffer<G> {
+    pub fn new() -> Self {
+        Self {
+            buffer: Box::new([0; G]),
+            write_cursor: 0,
+            read_cursor: 0,
+        }
+    }
+
+    pub fn len(&self) -> usize {
+        if self.write_cursor >= self.read_cursor {
+            self.write_cursor - self.read_cursor
+        } else {
+            G - self.read_cursor + self.write_cursor
+        }
+    }
+
+    #[inline]
+    pub fn remaining_capacity(&self) -> usize {
+        G - self.len() - 1
+    }
+
+    #[inline]
+    pub fn is_full(&self) -> bool {
+        (self.write_cursor + 1) % G == self.read_cursor
+    }
+}
+
+impl<const G: usize> std::io::Write for RingBuffer<G> {
+    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+        if self.remaining_capacity() < buf.len() {
+            Err(std::io::Error::new(
+                std::io::ErrorKind::WriteZero,
+                "Buffer full",
+            ))?;
+        }
+        let mut written = 0;
+        for byte in buf {
+            self.buffer[self.write_cursor] = *byte;
+            self.write_cursor = (self.write_cursor + 1) % G;
+            written += 1;
+        }
+        Ok(written)
+    }
+
+    fn flush(&mut self) -> std::io::Result<()> {
+        Ok(())
+    }
+}
+
+impl<const G: usize> std::io::Read for RingBuffer<G> {
+    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+        let mut read = 0;
+        while read < buf.len() {
+            if self.read_cursor == self.write_cursor {
+                break;
+            }
+            buf[read] = self.buffer[self.read_cursor];
+            self.read_cursor = (self.read_cursor + 1) % G;
+            read += 1;
+        }
+        Ok(read)
+    }
+}
+
+pub struct OverwritingRingBuffer<const G: usize> {
+    buffer: Box<[u8; G]>,
+    write_cursor: usize,
+    read_cursor: usize,
+    full: bool, // new flag to indicate if buffer is full
+}
+
+impl<const G: usize> OverwritingRingBuffer<G> {
+    pub fn new() -> Self {
+        Self {
+            buffer: Box::new([0; G]),
+            write_cursor: 0,
+            read_cursor: 0,
+            full: false, // initialize full flag to false
+        }
+    }
+
+    // Updated len() using full flag
+    pub fn len(&self) -> usize {
+        if self.full {
+            G
+        } else if self.write_cursor >= self.read_cursor {
+            self.write_cursor - self.read_cursor
+        } else {
+            G - self.read_cursor + self.write_cursor
+        }
+    }
+}
+
+impl<const G: usize> std::io::Write for OverwritingRingBuffer<G> {
+    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+        let mut written = 0;
+        for &byte in buf {
+            self.buffer[self.write_cursor] = byte;
+            self.write_cursor = (self.write_cursor + 1) % G;
+            if self.full {
+                // Buffer full; advance read_cursor to overwrite oldest
+                self.read_cursor = (self.read_cursor + 1) % G;
+            }
+            // Set full flag if write_cursor catches up to read_cursor
+            self.full = self.write_cursor == self.read_cursor;
+            written += 1;
+        }
+        Ok(written)
+    }
+
+    fn flush(&mut self) -> std::io::Result<()> {
+        Ok(())
+    }
+}
+
+impl<const G: usize> std::io::Read for OverwritingRingBuffer<G> {
+    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+        let mut read = 0;
+        // If full, then buffer is not empty and we will clear full once reading starts.
+        while read < buf.len() {
+            // Buffer empty condition: not full and cursors equal
+            if !self.full && (self.read_cursor == self.write_cursor) {
+                break;
+            }
+            buf[read] = self.buffer[self.read_cursor];
+            self.read_cursor = (self.read_cursor + 1) % G;
+            self.full = false; // once reading, clear full flag
+            read += 1;
+        }
+        Ok(read)
+    }
+}
+
+#[allow(clippy::unwrap_used)]
+#[cfg(test)]
+mod tests {
+    use std::io::{Read, Write};
+
+    // Tests for RingBuffer
+    #[test]
+    fn test_ring_buffer_empty_read() {
+        let mut rb = super::RingBuffer::<8>::new();
+        let mut buf = [0; 4];
+        let n = rb.read(&mut buf).unwrap();
+        assert_eq!(n, 0);
+    }
+
+    #[test]
+    fn test_ring_buffer_write_then_read() {
+        let mut rb = super::RingBuffer::<8>::new();
+        let data = [10, 20, 30, 40];
+        let n = rb.write(&data).unwrap();
+        assert_eq!(n, data.len());
+        let mut buf = [0u8; 4];
+        let n = rb.read(&mut buf).unwrap();
+        assert_eq!(n, data.len());
+        assert_eq!(buf, data);
+    }
+
+    #[test]
+    fn test_ring_buffer_wraparound() {
+        let mut rb = super::RingBuffer::<8>::new();
+        // Write initial data.
+        let data1 = [1, 2, 3, 4, 5, 6];
+        let n = rb.write(&data1).unwrap();
+        assert_eq!(n, data1.len());
+        // Read a few bytes to free space.
+        let mut buf1 = [0u8; 3];
+        let n = rb.read(&mut buf1).unwrap();
+        assert_eq!(n, 3);
+        assert_eq!(buf1, [1, 2, 3]);
+        // Write additional data to wrap-around.
+        let data2 = [7, 8, 9];
+        let n = rb.write(&data2).unwrap();
+        assert_eq!(n, data2.len());
+        // Read remaining data.
+        let mut buf2 = [0u8; 6];
+        let n = rb.read(&mut buf2).unwrap();
+        // Expected order: remaining from data1 ([4,5,6]) then data2 ([7,8,9])
+        assert_eq!(n, 6);
+        assert_eq!(buf2, [4, 5, 6, 7, 8, 9]);
+    }
+
+    #[test]
+    fn test_ring_buffer_full_error() {
+        let mut rb = super::RingBuffer::<4>::new();
+        // Usable capacity is G - 1: 3 bytes.
+        let _ = rb.write(&[10, 20, 30]).unwrap();
+        let res = rb.write(&[40]);
+        assert!(res.is_err());
+    }
+
+    #[test]
+    fn test_ring_buffer_partial_write_when_full() {
+        let mut rb = super::RingBuffer::<6>::new();
+        // Usable capacity: 5 bytes.
+        let n = rb.write(&[1, 2, 3, 4]).unwrap();
+        assert_eq!(n, 4);
+        // Only one byte can be written before buffer is full.
+        // The write should error on the second byte.
+        let res = rb.write(&[5, 6]);
+        assert!(res.is_err());
+
+        // Read the first 5 bytes.
+        let mut buf = [0u8; 5];
+        let n = rb.read(&mut buf).unwrap();
+        assert_eq!(n, 4);
+        assert_eq!(buf, [1, 2, 3, 4, 0]);
+    }
+
+    // Tests for OverwritingRingBuffer
+    #[test]
+    fn test_overwriting_ring_buffer_empty_read() {
+        let mut orb = super::OverwritingRingBuffer::<8>::new();
+        let mut buf = [0; 4];
+        let n = orb.read(&mut buf).unwrap();
+        assert_eq!(n, 0);
+    }
+
+    #[test]
+    fn test_overwriting_ring_buffer_write_then_read() {
+        let mut orb = super::OverwritingRingBuffer::<8>::new();
+        let data = [10, 20, 30, 40];
+        let n = orb.write(&data).unwrap();
+        assert_eq!(n, data.len());
+        let mut buf = [0u8; 4];
+        let n = orb.read(&mut buf).unwrap();
+        assert_eq!(n, data.len());
+        assert_eq!(buf, data);
+    }
+
+    #[test]
+    fn test_overwriting_ring_buffer_overwrite() {
+        let mut orb = super::OverwritingRingBuffer::<6>::new();
+        // Write initial 5 elements (usable capacity = 5).
+        let _ = orb.write(&[1, 2, 3, 4, 5]).unwrap();
+        // Write two more elements to force overwrite.
+        let _ = orb.write(&[6, 7]).unwrap();
+        // After overwrite, expected order starts from index advanced by one overwrite
+        // Expected stored order: [2,3,4,5,6,7]
+        let mut buf = [0u8; 6];
+        let n = orb.read(&mut buf).unwrap();
+        assert_eq!(n, 6);
+        assert_eq!(buf, [2, 3, 4, 5, 6, 7]);
+    }
+}
-- 
GitLab


From 2b270d16117952050a00f88b71aab4827f84cee7 Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Thu, 6 Mar 2025 00:43:01 +0100
Subject: [PATCH 02/14] CHECKPOINT

---
 src/communication.rs          | 141 ++++++++++++++++++++++++-
 src/communication/error.rs    |  28 +++++
 src/communication/ethernet.rs |  72 +++++++++++++
 src/communication/serial.rs   | 187 +++++++++++++++-------------------
 4 files changed, 322 insertions(+), 106 deletions(-)
 create mode 100644 src/communication/error.rs
 create mode 100644 src/communication/ethernet.rs

diff --git a/src/communication.rs b/src/communication.rs
index 70fb136..acc25f7 100644
--- a/src/communication.rs
+++ b/src/communication.rs
@@ -1,3 +1,142 @@
+mod error;
+mod ethernet;
 mod serial;
 
-pub use serial::{SerialConnection, SerialPortCandidate};
+use std::{
+    num::NonZero,
+    sync::{
+        Arc,
+        atomic::{AtomicBool, Ordering},
+    },
+    thread::JoinHandle,
+};
+
+use enum_dispatch::enum_dispatch;
+use ring_channel::{RingReceiver, TryRecvError, ring_channel};
+use skyward_mavlink::mavlink::{
+    MavFrame,
+    error::{MessageReadError, MessageWriteError},
+};
+
+use crate::{
+    error::ErrInstrument,
+    mavlink::{MavMessage, TimedMessage},
+};
+
+use ethernet::EthernetTransceiver;
+use serial::SerialTransceiver;
+
+// Re-exports
+pub use error::{CommunicationError, ConnectionError};
+pub use ethernet::EthernetConfiguration;
+pub use serial::{SerialConfiguration, find_first_stm32_port, list_all_usb_ports};
+
+const MAX_STORED_MSGS: usize = 100; // 192 bytes each = 19.2 KB
+
+pub trait TransceiverConfigExt: Connectable + Sized {
+    fn open_connection(self) -> Result<Connection, ConnectionError> {
+        Ok(self.connect()?.open_connection())
+    }
+}
+
+trait Connectable {
+    type Connected: MessageTransceiver;
+
+    fn connect(self) -> Result<Self::Connected, ConnectionError>;
+}
+
+#[enum_dispatch(Transceivers)]
+trait MessageTransceiver: Send + Sync + Into<Transceivers> {
+    /// Reads a message from the serial port, blocking until a valid message is received.
+    /// This method ignores timeout errors and continues trying.
+    fn wait_for_message(&self) -> Result<TimedMessage, MessageReadError>;
+
+    /// Transmits a message over the serial connection.
+    fn transmit_message(&self, msg: MavFrame<MavMessage>) -> Result<usize, MessageWriteError>;
+
+    /// Opens a connection to the transceiver and returns a handle to it.
+    fn open_connection(self) -> Connection {
+        let running_flag = Arc::new(AtomicBool::new(true));
+        let (tx, rx) = ring_channel(NonZero::new(MAX_STORED_MSGS).unwrap());
+        let endpoint_inner = Arc::new(self.into());
+        let thread_handle;
+
+        {
+            let running_flag = running_flag.clone();
+            let endpoint_inner = endpoint_inner.clone();
+            thread_handle = std::thread::spawn(move || {
+                while running_flag.load(Ordering::Relaxed) {
+                    match endpoint_inner.wait_for_message() {
+                        Ok(msg) => {
+                            tx.send(msg);
+                        }
+                        Err(MessageReadError::Io(e)) => {
+                            tracing::error!("Failed to read message: {e:#?}");
+                            running_flag.store(false, Ordering::Relaxed);
+                            return Err(CommunicationError::Io(e));
+                        }
+                        Err(MessageReadError::Parse(e)) => {
+                            tracing::error!("Failed to read message: {e:#?}");
+                        }
+                    }
+                }
+                Ok(())
+            });
+        }
+
+        Connection {
+            endpoint: endpoint_inner,
+            rx_ring_channel: rx,
+            running_flag,
+            thread_handle,
+        }
+    }
+}
+
+#[enum_dispatch]
+enum Transceivers {
+    Serial(SerialTransceiver),
+    Ethernet(EthernetTransceiver),
+}
+
+pub struct Connection {
+    endpoint: Arc<Transceivers>,
+    rx_ring_channel: RingReceiver<TimedMessage>,
+    running_flag: Arc<AtomicBool>,
+    thread_handle: JoinHandle<Result<(), CommunicationError>>,
+}
+
+impl Connection {
+    /// Retrieves and clears the stored messages.
+    pub fn retrieve_messages(&self) -> Result<Vec<TimedMessage>, CommunicationError> {
+        // otherwise retrieve all messages from the buffer and return them
+        let mut stored_msgs = Vec::new();
+        loop {
+            match self.rx_ring_channel.try_recv() {
+                Ok(msg) => {
+                    // Store the message in the buffer.
+                    stored_msgs.push(msg);
+                }
+                Err(TryRecvError::Empty) => {
+                    break;
+                }
+                Err(TryRecvError::Disconnected) => {
+                    return Err(CommunicationError::ConnectionClosed);
+                }
+            }
+        }
+        Ok(stored_msgs)
+    }
+
+    /// Send a message over the serial connection.
+    pub fn send_message(&self, msg: MavFrame<MavMessage>) -> Result<(), CommunicationError> {
+        self.endpoint.transmit_message(msg)?;
+        Ok(())
+    }
+}
+
+impl Drop for Connection {
+    fn drop(&mut self) {
+        self.running_flag.store(false, Ordering::Relaxed);
+    }
+}
diff --git a/src/communication/error.rs b/src/communication/error.rs
new file mode 100644
index 0000000..50a12da
--- /dev/null
+++ b/src/communication/error.rs
@@ -0,0 +1,28 @@
+use skyward_mavlink::mavlink::error::MessageWriteError;
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+pub enum CommunicationError {
+    #[error("IO error: {0}")]
+    Io(#[from] std::io::Error),
+    #[error("Connection closed")]
+    ConnectionClosed,
+}
+
+#[derive(Debug, Error)]
+pub enum ConnectionError {
+    #[error("Wrong configuration: {0}")]
+    WrongConfiguration(String),
+    #[error("IO error: {0}")]
+    Io(#[from] std::io::Error),
+    #[error("Unknown error")]
+    Unknown(String),
+}
+
+impl From<MessageWriteError> for CommunicationError {
+    fn from(e: MessageWriteError) -> Self {
+        match e {
+            MessageWriteError::Io(e) => Self::Io(e),
+        }
+    }
+}
diff --git a/src/communication/ethernet.rs b/src/communication/ethernet.rs
new file mode 100644
index 0000000..de0e84c
--- /dev/null
+++ b/src/communication/ethernet.rs
@@ -0,0 +1,72 @@
+use std::{
+    collections::VecDeque,
+    io::Read,
+    net::UdpSocket,
+    sync::{
+        Arc, Mutex,
+        atomic::{AtomicBool, Ordering},
+    },
+    thread::JoinHandle,
+};
+
+use anyhow::Context;
+use ring_channel::{RingReceiver, RingSender};
+use skyward_mavlink::mavlink::{
+    MavFrame,
+    error::{MessageReadError, MessageWriteError},
+    read_v1_msg, write_v1_msg,
+};
+use tracing::{debug, error, trace};
+
+use crate::{
+    error::ErrInstrument,
+    mavlink::{
+        MavHeader, MavMessage, MavlinkVersion, TimedMessage, peek_reader::PeekReader,
+        read_versioned_msg,
+    },
+};
+
+use super::{Connectable, ConnectionError, MessageTransceiver, Transceivers};
+
+#[derive(Debug, Clone)]
+pub struct EthernetConfiguration {
+    pub port: u16,
+}
+
+impl Connectable for EthernetConfiguration {
+    type Connected = EthernetTransceiver;
+
+    fn connect(self) -> Result<Self::Connected, ConnectionError> {
+        let socket = std::net::UdpSocket::bind(format!("0.0.0.0:{}", self.port))?;
+        debug!("Connected to Ethernet port on port {}", self.port);
+        let reader = Mutex::new(PeekReader::new(VecDeque::new()));
+        Ok(EthernetTransceiver { socket, reader })
+    }
+}
+
+/// Manages a connection to a Ethernet port.
+pub struct EthernetTransceiver {
+    socket: UdpSocket,
+    reader: Mutex<PeekReader<VecDeque<u8>>>,
+}
+
+impl MessageTransceiver for EthernetTransceiver {
+    fn wait_for_message(&self) -> Result<TimedMessage, MessageReadError> {
+        let mut reader = self.reader.lock().log_unwrap();
+        let read = self.socket.recv(reader.reader_mut().make_contiguous())?;
+        trace!("Received {} bytes", read);
+        let (_, res) = read_v1_msg(&mut reader)?;
+        debug!("Received message: {:?}", res);
+        Ok(TimedMessage::just_received(res))
+    }
+
+    fn transmit_message(&self, msg: MavFrame<MavMessage>) -> Result<usize, MessageWriteError> {
+        let MavFrame { header, msg, .. } = msg;
+        let mut write_buf = Vec::new();
+        write_v1_msg(&mut write_buf, header, &msg)?;
+        let written = self.socket.send(&write_buf)?;
+        debug!("Sent message: {:?}", msg);
+        trace!("Sent {} bytes via Ethernet", written);
+        Ok(written)
+    }
+}
diff --git a/src/communication/serial.rs b/src/communication/serial.rs
index e71153e..7e9e59e 100644
--- a/src/communication/serial.rs
+++ b/src/communication/serial.rs
@@ -6,6 +6,7 @@
 
 use std::{
     collections::VecDeque,
+    io::Read,
     sync::{
         Arc, Mutex,
         atomic::{AtomicBool, Ordering},
@@ -14,9 +15,14 @@ use std::{
 };
 
 use anyhow::Context;
+use ring_channel::{RingReceiver, RingSender};
 use serialport::{SerialPort, SerialPortInfo, SerialPortType};
-use skyward_mavlink::mavlink::error::MessageReadError;
-use tracing::error;
+use skyward_mavlink::mavlink::{
+    MavFrame,
+    error::{MessageReadError, MessageWriteError},
+    read_v1_msg, write_v1_msg,
+};
+use tracing::{debug, error, trace};
 
 use crate::{
     error::ErrInstrument,
@@ -26,132 +32,103 @@ use crate::{
     },
 };
 
-const MAX_STORED_MSGS: usize = 100; // 192 bytes each = 19.2 KB
+use super::{Connectable, ConnectionError, MessageTransceiver, Transceivers};
+
 const SERIAL_PORT_TIMEOUT_MS: u64 = 100;
 
-/// Represents a candidate serial port device.
-#[derive(Debug, Clone)]
-pub struct SerialPortCandidate {
-    port_name: String,
-    info: SerialPortInfo,
+/// Get a list of all serial USB ports available on the system
+pub fn list_all_usb_ports() -> anyhow::Result<Vec<SerialPortInfo>> {
+    let ports = serialport::available_ports().context("No serial ports found!")?;
+    Ok(ports
+        .into_iter()
+        .filter(|p| matches!(p.port_type, SerialPortType::UsbPort(_)))
+        .collect())
 }
 
-impl PartialEq for SerialPortCandidate {
-    fn eq(&self, other: &Self) -> bool {
-        self.port_name == other.port_name
+/// Finds the first USB serial port with "STM32" or "ST-LINK" in its product name.
+/// Renamed from get_first_stm32_serial_port.
+pub fn find_first_stm32_port() -> Option<SerialPortInfo> {
+    let ports = list_all_usb_ports().log_unwrap();
+    for port in ports {
+        if let serialport::SerialPortType::UsbPort(info) = &port.port_type {
+            if let Some(p) = &info.product {
+                if p.contains("STM32") || p.contains("ST-LINK") {
+                    return Some(port);
+                }
+            }
+        }
     }
+    None
 }
 
-impl SerialPortCandidate {
-    /// Connects to the serial port with the given baud rate.
-    pub fn connect(self, baud_rate: u32) -> Result<SerialConnection, serialport::Error> {
-        let serial_port = serialport::new(&self.port_name, baud_rate)
+#[derive(Debug, Clone)]
+pub struct SerialConfiguration {
+    pub port_name: String,
+    pub baud_rate: u32,
+}
+
+impl Connectable for SerialConfiguration {
+    type Connected = SerialTransceiver;
+
+    fn connect(self) -> Result<Self::Connected, ConnectionError> {
+        let port = serialport::new(&self.port_name, self.baud_rate)
             .timeout(std::time::Duration::from_millis(SERIAL_PORT_TIMEOUT_MS))
             .open()?;
-        Ok(SerialConnection {
-            serial_port_reader: Arc::new(Mutex::new(PeekReader::new(serial_port))),
-            stored_msgs: Arc::new(Mutex::new(VecDeque::with_capacity(MAX_STORED_MSGS))),
-            running_flag: Arc::new(AtomicBool::new(false)),
-            thread_handle: None,
+        debug!(
+            "Connected to serial port {} with baud rate {}",
+            self.port_name, self.baud_rate
+        );
+        Ok(SerialTransceiver {
+            serial_reader: Mutex::new(PeekReader::new(port.try_clone()?)),
+            serial_writer: Mutex::new(port),
         })
     }
-
-    /// Get a list of all serial USB ports available on the system
-    pub fn list_all_usb_ports() -> anyhow::Result<Vec<Self>> {
-        let ports = serialport::available_ports().context("No serial ports found!")?;
-        Ok(ports
-            .into_iter()
-            .filter(|p| matches!(p.port_type, SerialPortType::UsbPort(_)))
-            .map(|p| SerialPortCandidate {
-                port_name: p.port_name.clone(),
-                info: p,
-            })
-            .collect())
-    }
-
-    /// Finds the first USB serial port with "STM32" or "ST-LINK" in its product name.
-    /// Renamed from get_first_stm32_serial_port.
-    pub fn find_first_stm32_port() -> Option<Self> {
-        let ports = Self::list_all_usb_ports().log_unwrap();
-        for port in ports {
-            if let serialport::SerialPortType::UsbPort(info) = &port.info.port_type {
-                if let Some(p) = &info.product {
-                    if p.contains("STM32") || p.contains("ST-LINK") {
-                        return Some(port);
-                    }
-                }
-            }
-        }
-        None
-    }
 }
 
-impl AsRef<String> for SerialPortCandidate {
-    fn as_ref(&self) -> &String {
-        &self.port_name
+impl From<serialport::Error> for ConnectionError {
+    fn from(e: serialport::Error) -> Self {
+        let serialport::Error { kind, description } = e.clone();
+        match kind {
+            serialport::ErrorKind::NoDevice => ConnectionError::WrongConfiguration(description),
+            serialport::ErrorKind::InvalidInput => ConnectionError::WrongConfiguration(description),
+            serialport::ErrorKind::Unknown => ConnectionError::Unknown(description),
+            serialport::ErrorKind::Io(e) => ConnectionError::Io(e.into()),
+        }
     }
 }
 
 /// Manages a connection to a serial port.
-pub struct SerialConnection {
-    serial_port_reader: Arc<Mutex<PeekReader<Box<dyn SerialPort>>>>,
-    stored_msgs: Arc<Mutex<VecDeque<TimedMessage>>>,
-    running_flag: Arc<AtomicBool>,
-    thread_handle: Option<JoinHandle<()>>,
+pub struct SerialTransceiver {
+    serial_reader: Mutex<PeekReader<Box<dyn SerialPort>>>,
+    serial_writer: Mutex<Box<dyn SerialPort>>,
 }
 
-impl SerialConnection {
-    /// Starts receiving messages asynchronously.
-    pub fn start_receiving(&mut self) {
-        let running_flag = self.running_flag.clone();
-        let serial_port = self.serial_port_reader.clone();
-        let stored_msgs = self.stored_msgs.clone();
-        let thread_handle = std::thread::spawn(move || {
-            while running_flag.load(Ordering::Relaxed) {
-                let res: Result<(MavHeader, MavMessage), MessageReadError> =
-                    read_versioned_msg(&mut serial_port.lock().log_unwrap(), MavlinkVersion::V1);
-                match res {
-                    Ok((_, msg)) => {
-                        // Store the message in the buffer.
-                        stored_msgs
-                            .lock()
-                            .log_unwrap()
-                            .push_back(TimedMessage::just_received(msg));
-                    }
-                    Err(MessageReadError::Io(e)) => {
-                        // Ignore timeouts.
-                        if e.kind() == std::io::ErrorKind::TimedOut {
-                            continue;
-                        } else {
-                            error!("Error reading message: {:?}", e);
-                            running_flag.store(false, Ordering::Relaxed);
-                        }
-                    }
-                    Err(e) => {
-                        error!("Error reading message: {:?}", e);
-                    }
-                };
+impl MessageTransceiver for SerialTransceiver {
+    fn wait_for_message(&self) -> Result<TimedMessage, MessageReadError> {
+        loop {
+            let res: Result<(_, MavMessage), MessageReadError> =
+                read_v1_msg(&mut self.serial_reader.lock().log_unwrap());
+            match res {
+                Ok((_, msg)) => {
+                    return Ok(TimedMessage::just_received(msg));
+                }
+                Err(MessageReadError::Io(e)) if e.kind() == std::io::ErrorKind::TimedOut => {
+                    // Ignore timeouts.
+                    continue;
+                }
+                Err(e) => {
+                    return Err(e);
+                }
             }
-        });
-        self.thread_handle.replace(thread_handle);
-    }
-
-    /// Stops receiving messages.
-    pub fn stop_receiving(&mut self) {
-        self.running_flag.store(false, Ordering::Relaxed);
-        if let Some(handle) = self.thread_handle.take() {
-            handle.join().log_unwrap();
         }
     }
 
-    /// Retrieves and clears the stored messages.
-    pub fn retrieve_messages(&self) -> Vec<TimedMessage> {
-        self.stored_msgs.lock().log_unwrap().drain(..).collect()
-    }
-
-    /// Transmits a message over the serial connection.
-    pub fn transmit_message(&mut self, msg: &[u8]) {
-        todo!()
+    fn transmit_message(&self, msg: MavFrame<MavMessage>) -> Result<usize, MessageWriteError> {
+        let MavFrame { header, msg, .. } = msg;
+        let written = write_v1_msg(&mut *self.serial_writer.lock().log_unwrap(), header, &msg)?;
+        debug!("Sent message: {:?}", msg);
+        trace!("Sent {} bytes via serial", written);
+        Ok(written)
     }
 }
 
-- 
GitLab


From 62bc12fa2397e3b1de5771a673092bae2eea3c98 Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Thu, 6 Mar 2025 01:58:43 +0100
Subject: [PATCH 03/14] CHECKPOINT

---
 src/communication.rs          |  23 ++--
 src/communication/ethernet.rs |  15 +--
 src/communication/serial.rs   |   3 +-
 src/mavlink.rs                |   2 +
 src/message_broker.rs         | 244 ++++++++++++++++++----------------
 src/ui/app.rs                 | 180 +++++++++++++------------
 6 files changed, 249 insertions(+), 218 deletions(-)

diff --git a/src/communication.rs b/src/communication.rs
index acc25f7..d699d2c 100644
--- a/src/communication.rs
+++ b/src/communication.rs
@@ -1,6 +1,6 @@
 mod error;
-mod ethernet;
-mod serial;
+pub mod ethernet;
+pub mod serial;
 
 use std::{
     num::NonZero,
@@ -28,21 +28,21 @@ use serial::SerialTransceiver;
 
 // Re-exports
 pub use error::{CommunicationError, ConnectionError};
-pub use ethernet::EthernetConfiguration;
-pub use serial::{SerialConfiguration, find_first_stm32_port, list_all_usb_ports};
 
 const MAX_STORED_MSGS: usize = 100; // 192 bytes each = 19.2 KB
 
-pub trait TransceiverConfigExt: Connectable + Sized {
-    fn open_connection(self) -> Result<Connection, ConnectionError> {
-        Ok(self.connect()?.open_connection())
+pub trait TransceiverConfigExt: Connectable {
+    fn open_connection(&self) -> Result<Connection, ConnectionError> {
+        Ok(self.connect()?.connect_transceiver())
     }
 }
 
+impl<T: Connectable> TransceiverConfigExt for T {}
+
 trait Connectable {
     type Connected: MessageTransceiver;
 
-    fn connect(self) -> Result<Self::Connected, ConnectionError>;
+    fn connect(&self) -> Result<Self::Connected, ConnectionError>;
 }
 
 #[enum_dispatch(Transceivers)]
@@ -55,9 +55,9 @@ trait MessageTransceiver: Send + Sync + Into<Transceivers> {
     fn transmit_message(&self, msg: MavFrame<MavMessage>) -> Result<usize, MessageWriteError>;
 
     /// Opens a connection to the transceiver and returns a handle to it.
-    fn open_connection(self) -> Connection {
+    fn connect_transceiver(self) -> Connection {
         let running_flag = Arc::new(AtomicBool::new(true));
-        let (tx, rx) = ring_channel(NonZero::new(MAX_STORED_MSGS).unwrap());
+        let (tx, rx) = ring_channel(NonZero::new(MAX_STORED_MSGS).log_unwrap());
         let endpoint_inner = Arc::new(self.into());
         let thread_handle;
 
@@ -68,7 +68,8 @@ trait MessageTransceiver: Send + Sync + Into<Transceivers> {
                 while running_flag.load(Ordering::Relaxed) {
                     match endpoint_inner.wait_for_message() {
                         Ok(msg) => {
-                            tx.send(msg);
+                            tx.send(msg)
+                                .map_err(|_| CommunicationError::ConnectionClosed)?;
                         }
                         Err(MessageReadError::Io(e)) => {
                             tracing::error!("Failed to read message: {e:#?}");
diff --git a/src/communication/ethernet.rs b/src/communication/ethernet.rs
index de0e84c..52e5bee 100644
--- a/src/communication/ethernet.rs
+++ b/src/communication/ethernet.rs
@@ -21,7 +21,7 @@ use tracing::{debug, error, trace};
 use crate::{
     error::ErrInstrument,
     mavlink::{
-        MavHeader, MavMessage, MavlinkVersion, TimedMessage, peek_reader::PeekReader,
+        MAX_MSG_SIZE, MavHeader, MavMessage, MavlinkVersion, TimedMessage, peek_reader::PeekReader,
         read_versioned_msg,
     },
 };
@@ -36,25 +36,24 @@ pub struct EthernetConfiguration {
 impl Connectable for EthernetConfiguration {
     type Connected = EthernetTransceiver;
 
-    fn connect(self) -> Result<Self::Connected, ConnectionError> {
-        let socket = std::net::UdpSocket::bind(format!("0.0.0.0:{}", self.port))?;
+    fn connect(&self) -> Result<Self::Connected, ConnectionError> {
+        let socket = UdpSocket::bind(format!("0.0.0.0:{}", self.port))?;
         debug!("Connected to Ethernet port on port {}", self.port);
-        let reader = Mutex::new(PeekReader::new(VecDeque::new()));
-        Ok(EthernetTransceiver { socket, reader })
+        Ok(EthernetTransceiver { socket })
     }
 }
 
 /// Manages a connection to a Ethernet port.
 pub struct EthernetTransceiver {
     socket: UdpSocket,
-    reader: Mutex<PeekReader<VecDeque<u8>>>,
 }
 
 impl MessageTransceiver for EthernetTransceiver {
     fn wait_for_message(&self) -> Result<TimedMessage, MessageReadError> {
-        let mut reader = self.reader.lock().log_unwrap();
-        let read = self.socket.recv(reader.reader_mut().make_contiguous())?;
+        let mut buf = [0; MAX_MSG_SIZE];
+        let read = self.socket.recv(&mut buf)?;
         trace!("Received {} bytes", read);
+        let mut reader = PeekReader::new(&buf[..read]);
         let (_, res) = read_v1_msg(&mut reader)?;
         debug!("Received message: {:?}", res);
         Ok(TimedMessage::just_received(res))
diff --git a/src/communication/serial.rs b/src/communication/serial.rs
index 7e9e59e..b49342b 100644
--- a/src/communication/serial.rs
+++ b/src/communication/serial.rs
@@ -35,6 +35,7 @@ use crate::{
 use super::{Connectable, ConnectionError, MessageTransceiver, Transceivers};
 
 const SERIAL_PORT_TIMEOUT_MS: u64 = 100;
+pub const DEFAULT_BAUD_RATE: u32 = 115200;
 
 /// Get a list of all serial USB ports available on the system
 pub fn list_all_usb_ports() -> anyhow::Result<Vec<SerialPortInfo>> {
@@ -70,7 +71,7 @@ pub struct SerialConfiguration {
 impl Connectable for SerialConfiguration {
     type Connected = SerialTransceiver;
 
-    fn connect(self) -> Result<Self::Connected, ConnectionError> {
+    fn connect(&self) -> Result<Self::Connected, ConnectionError> {
         let port = serialport::new(&self.port_name, self.baud_rate)
             .timeout(std::time::Duration::from_millis(SERIAL_PORT_TIMEOUT_MS))
             .open()?;
diff --git a/src/mavlink.rs b/src/mavlink.rs
index 9119346..9589a2f 100644
--- a/src/mavlink.rs
+++ b/src/mavlink.rs
@@ -13,3 +13,5 @@ pub use reflection::ReflectionContext;
 
 /// Default port for the Ethernet connection
 pub const DEFAULT_ETHERNET_PORT: u16 = 42069;
+/// Maximum size of a Mavlink message
+pub const MAX_MSG_SIZE: usize = 280;
diff --git a/src/message_broker.rs b/src/message_broker.rs
index 77932cf..f583074 100644
--- a/src/message_broker.rs
+++ b/src/message_broker.rs
@@ -11,11 +11,11 @@ pub use message_bundle::MessageBundle;
 use reception_queue::ReceptionQueue;
 
 use crate::{
+    communication::{Connection, ConnectionError, TransceiverConfigExt},
     error::ErrInstrument,
     mavlink::{Message, TimedMessage, byte_parser},
     utils::RingBuffer,
 };
-use anyhow::{Context, Result};
 use ring_channel::{RingReceiver, RingSender, ring_channel};
 use std::{
     collections::HashMap,
@@ -28,7 +28,7 @@ use std::{
     time::{Duration, Instant},
 };
 use tokio::{net::UdpSocket, task::JoinHandle};
-use tracing::{debug, trace};
+use tracing::{debug, error, trace};
 
 /// Maximum size of the UDP buffer
 const UDP_BUFFER_SIZE: usize = 65527;
@@ -37,20 +37,13 @@ const UDP_BUFFER_SIZE: usize = 65527;
 ///
 /// It is responsible for receiving messages from the Mavlink listener and
 /// dispatching them to the views that are interested in them.
-#[derive(Debug)]
 pub struct MessageBroker {
     /// A map of all messages received so far, indexed by message ID
     messages: HashMap<u32, Vec<TimedMessage>>,
     /// instant queue used for frequency calculation and reception time
     last_receptions: Arc<Mutex<ReceptionQueue>>,
-    /// Flag to stop the listener
-    running_flag: Arc<AtomicBool>,
-    /// Listener message sender
-    tx: RingSender<TimedMessage>,
-    /// Broker message receiver
-    rx: RingReceiver<TimedMessage>,
-    /// Task handle for the listener
-    task: Option<JoinHandle<Result<()>>>,
+    /// Connection to the Mavlink listener
+    connection: Option<Connection>,
     /// Egui context
     ctx: egui::Context,
 }
@@ -58,116 +51,123 @@ pub struct MessageBroker {
 impl MessageBroker {
     /// Creates a new `MessageBroker` with the given channel size and Egui context.
     pub fn new(channel_size: NonZeroUsize, ctx: egui::Context) -> Self {
-        let (tx, rx) = ring_channel(channel_size);
         Self {
             messages: HashMap::new(),
             // TODO: make this configurable
             last_receptions: Arc::new(Mutex::new(ReceptionQueue::new(Duration::from_secs(1)))),
-            tx,
-            rx,
+            connection: None,
             ctx,
-            running_flag: Arc::new(AtomicBool::new(false)),
-            task: None,
         }
     }
 
+    /// Start a listener task that listens to incoming messages from the given
+    /// medium (Serial or Ethernet) and stores them in a ring buffer.
+    pub fn open_connection(
+        &mut self,
+        config: impl TransceiverConfigExt,
+    ) -> Result<(), ConnectionError> {
+        self.connection = Some(config.open_connection()?);
+        Ok(())
+    }
+
     /// Stop the listener task from listening to incoming messages, if it is
     /// running.
-    pub fn stop_listening(&mut self) {
-        self.running_flag.store(false, Ordering::Relaxed);
-        if let Some(t) = self.task.take() {
-            t.abort()
-        }
+    pub fn close_connection(&mut self) {
+        self.connection.take();
     }
 
-    /// Start a listener task that listens to incoming messages from the given
-    /// Ethernet port, and accumulates them in a ring buffer, read only when
-    /// views request a refresh.
-    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 last_receptions = Arc::clone(&self.last_receptions);
-
-        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();
-
-        debug!("Spawning listener task at {}", bind_address);
-        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 byte_parser(&buf[..len]) {
-                    trace!("Received message: {:?}", mav_message);
-                    tx.send(TimedMessage::just_received(mav_message))
-                        .context("Failed to send message")?;
-                    last_receptions.lock().unwrap().push(Instant::now());
-                    ctx.request_repaint();
-                }
-            }
-
-            Ok::<(), anyhow::Error>(())
-        });
-        self.task = Some(handle);
+    pub fn is_connected(&self) -> bool {
+        self.connection.is_some()
     }
 
-    /// Start a listener task that listens to incoming messages from the given
-    /// serial port and stores them in a ring buffer.
-    pub fn listen_from_serial_port(&mut self, port: String, baud_rate: u32) {
-        // Stop the current listener if it exists
-        self.stop_listening();
-        self.running_flag.store(true, Ordering::Relaxed);
-        let last_receptions = Arc::clone(&self.last_receptions);
-
-        let tx = self.tx.clone();
-        let ctx = self.ctx.clone();
-
-        let running_flag = self.running_flag.clone();
-
-        debug!("Spawning listener task at {}", port);
-        let handle = tokio::task::spawn_blocking(move || {
-            let mut serial_port = serialport::new(port, baud_rate)
-                .timeout(std::time::Duration::from_millis(100))
-                .open()
-                .context("Failed to open serial port")?;
-            debug!("Listening on serial port");
-
-            let mut ring_buf = RingBuffer::<1024>::new();
-            let mut temp_buf = [0; 512];
-            // need to do a better error handling for this (need toast errors)
-            while running_flag.load(Ordering::Relaxed) {
-                let result = serial_port
-                    .read(&mut temp_buf)
-                    .log_expect("Failed to read from serial port");
-                debug!("Read {} bytes from serial port", result);
-                trace!("data read from serial: {:?}", &temp_buf[..result]);
-                ring_buf
-                    .write(&temp_buf[..result])
-                    .log_expect("Failed to write to ring buffer, check buffer size");
-                for (_, mav_message) in byte_parser(&mut ring_buf) {
-                    debug!("Received message: {:?}", mav_message);
-                    tx.send(TimedMessage::just_received(mav_message))
-                        .context("Failed to send message")?;
-                    last_receptions.lock().unwrap().push(Instant::now());
-                    ctx.request_repaint();
-                }
-            }
-
-            Ok::<(), anyhow::Error>(())
-        });
-        self.task = Some(handle);
-    }
+    // /// Start a listener task that listens to incoming messages from the given
+    // /// Ethernet port, and accumulates them in a ring buffer, read only when
+    // /// views request a refresh.
+    // 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 last_receptions = Arc::clone(&self.last_receptions);
+
+    //     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();
+
+    //     debug!("Spawning listener task at {}", bind_address);
+    //     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 byte_parser(&buf[..len]) {
+    //                 trace!("Received message: {:?}", mav_message);
+    //                 tx.send(TimedMessage::just_received(mav_message))
+    //                     .context("Failed to send message")?;
+    //                 last_receptions.lock().unwrap().push(Instant::now());
+    //                 ctx.request_repaint();
+    //             }
+    //         }
+
+    //         Ok::<(), anyhow::Error>(())
+    //     });
+    //     self.task = Some(handle);
+    // }
+
+    // /// Start a listener task that listens to incoming messages from the given
+    // /// serial port and stores them in a ring buffer.
+    // pub fn listen_from_serial_port(&mut self, port: String, baud_rate: u32) {
+    //     // Stop the current listener if it exists
+    //     self.stop_listening();
+    //     self.running_flag.store(true, Ordering::Relaxed);
+    //     let last_receptions = Arc::clone(&self.last_receptions);
+
+    //     let tx = self.tx.clone();
+    //     let ctx = self.ctx.clone();
+
+    //     let running_flag = self.running_flag.clone();
+
+    //     debug!("Spawning listener task at {}", port);
+    //     let handle = tokio::task::spawn_blocking(move || {
+    //         let mut serial_port = serialport::new(port, baud_rate)
+    //             .timeout(std::time::Duration::from_millis(100))
+    //             .open()
+    //             .context("Failed to open serial port")?;
+    //         debug!("Listening on serial port");
+
+    //         let mut ring_buf = RingBuffer::<1024>::new();
+    //         let mut temp_buf = [0; 512];
+    //         // need to do a better error handling for this (need toast errors)
+    //         while running_flag.load(Ordering::Relaxed) {
+    //             let result = serial_port
+    //                 .read(&mut temp_buf)
+    //                 .log_expect("Failed to read from serial port");
+    //             debug!("Read {} bytes from serial port", result);
+    //             trace!("data read from serial: {:?}", &temp_buf[..result]);
+    //             ring_buf
+    //                 .write(&temp_buf[..result])
+    //                 .log_expect("Failed to write to ring buffer, check buffer size");
+    //             for (_, mav_message) in byte_parser(&mut ring_buf) {
+    //                 debug!("Received message: {:?}", mav_message);
+    //                 tx.send(TimedMessage::just_received(mav_message))
+    //                     .context("Failed to send message")?;
+    //                 last_receptions.lock().unwrap().push(Instant::now());
+    //                 ctx.request_repaint();
+    //             }
+    //         }
+
+    //         Ok::<(), anyhow::Error>(())
+    //     });
+    //     self.task = Some(handle);
+    // }
 
     /// Returns the time since the last message was received.
     pub fn time_since_last_reception(&self) -> Option<Duration> {
@@ -189,14 +189,28 @@ impl MessageBroker {
     /// Processes incoming network messages. New messages are added to the
     /// given `MessageBundle`.
     pub fn process_messages(&mut self, bundle: &mut MessageBundle) {
-        while let Ok(message) = self.rx.try_recv() {
-            bundle.insert(message.clone());
-
-            // Store the message in the broker
-            self.messages
-                .entry(message.message.message_id())
-                .or_default()
-                .push(message);
+        // process messages only if the connection is open
+        if let Some(connection) = &self.connection {
+            // check for communication errors, and log them
+            match connection.retrieve_messages() {
+                Ok(messages) => {
+                    for message in messages {
+                        bundle.insert(message.clone());
+
+                        // Store the message in the broker
+                        self.messages
+                            .entry(message.message.message_id())
+                            .or_default()
+                            .push(message);
+                    }
+                    self.ctx.request_repaint();
+                }
+                Err(e) => {
+                    error!("Error while receiving messages: {:?}", e);
+                    // TODO: user error handling, until them silently close the connection
+                    self.close_connection();
+                }
+            }
         }
     }
 
diff --git a/src/ui/app.rs b/src/ui/app.rs
index 5175a94..df9ea13 100644
--- a/src/ui/app.rs
+++ b/src/ui/app.rs
@@ -7,9 +7,15 @@ use super::{
     widgets::reception_led::ReceptionLed,
 };
 use crate::{
-    communication::SerialPortCandidate,
+    communication::{
+        Connection, ConnectionError, TransceiverConfigExt,
+        ethernet::EthernetConfiguration,
+        serial::{
+            DEFAULT_BAUD_RATE, SerialConfiguration, find_first_stm32_port, list_all_usb_ports,
+        },
+    },
     error::ErrInstrument,
-    mavlink,
+    mavlink::{self, DEFAULT_ETHERNET_PORT},
     message_broker::{MessageBroker, MessageBundle},
     ui::panes::PaneKind,
 };
@@ -18,6 +24,7 @@ use egui::{Align2, Button, Color32, ComboBox, Key, Modifiers, RichText, Sides, V
 use egui_extras::{Size, StripBuilder};
 use egui_tiles::{Behavior, Container, Linear, LinearDir, Tile, TileId, Tiles, Tree};
 use serde::{Deserialize, Serialize};
+use serialport::SerialPortInfo;
 use std::{
     fs,
     num::NonZeroUsize,
@@ -375,61 +382,72 @@ impl AppState {
 }
 
 #[derive(Debug)]
-enum ConnectionKind {
-    Ethernet {
-        port: u16,
-    },
-    Serial {
-        port: Option<SerialPortCandidate>,
-        baud_rate: u32,
-    },
+enum ConnectionConfig {
+    Ethernet(EthernetConfiguration),
+    Serial(Option<SerialConfiguration>),
 }
 
-impl ConnectionKind {
+impl ConnectionConfig {
     fn default_ethernet() -> Self {
-        ConnectionKind::Ethernet {
-            port: mavlink::DEFAULT_ETHERNET_PORT,
-        }
+        Self::Ethernet(EthernetConfiguration {
+            port: DEFAULT_ETHERNET_PORT,
+        })
     }
 
     fn default_serial() -> Self {
-        ConnectionKind::Serial {
-            port: SerialPortCandidate::find_first_stm32_port().or(
-                SerialPortCandidate::list_all_usb_ports()
-                    .ok()
-                    .and_then(|ports| ports.first().cloned()),
-            ),
-            baud_rate: 115200,
+        let port_name = find_first_stm32_port()
+            .map(|port| port.port_name)
+            .or(list_all_usb_ports()
+                .ok()
+                .and_then(|ports| ports.first().map(|port| port.port_name.clone())));
+        let Some(port_name) = port_name else {
+            warn!("USER ERROR: No serial port found");
+            return Self::Serial(None);
+        };
+        Self::Serial(Some(SerialConfiguration {
+            port_name,
+            baud_rate: DEFAULT_BAUD_RATE,
+        }))
+    }
+
+    fn is_valid(&self) -> bool {
+        match self {
+            Self::Ethernet(_) => true,
+            Self::Serial(Some(_)) => true,
+            Self::Serial(None) => false,
+        }
+    }
+
+    fn open_connection(&self, msg_broker: &mut MessageBroker) -> Result<(), ConnectionError> {
+        match self {
+            Self::Ethernet(config) => msg_broker.open_connection(config.clone()),
+            Self::Serial(Some(config)) => msg_broker.open_connection(config.clone()),
+            Self::Serial(None) => Err(ConnectionError::WrongConfiguration(
+                "No serial port found".to_string(),
+            )),
         }
     }
 }
 
-impl Default for ConnectionKind {
+impl Default for ConnectionConfig {
     fn default() -> Self {
-        ConnectionKind::Ethernet {
-            port: mavlink::DEFAULT_ETHERNET_PORT,
-        }
+        Self::Ethernet(EthernetConfiguration {
+            port: DEFAULT_ETHERNET_PORT,
+        })
     }
 }
 
-// Implement PartialEq just for the variants, not the fields (for radio buttons)
-impl PartialEq for ConnectionKind {
+impl PartialEq for ConnectionConfig {
     fn eq(&self, other: &Self) -> bool {
-        matches!(
-            (self, other),
-            (
-                ConnectionKind::Ethernet { .. },
-                ConnectionKind::Ethernet { .. }
-            ) | (ConnectionKind::Serial { .. }, ConnectionKind::Serial { .. })
-        )
+        matches!(self, Self::Ethernet(_)) && matches!(other, Self::Ethernet(_))
+            || matches!(self, Self::Serial(_)) && matches!(other, Self::Serial(_))
     }
 }
 
 #[derive(Debug, Default)]
 struct SourceWindow {
     visible: bool,
-    connected: bool,
-    connection_kind: ConnectionKind,
+    connection_config: ConnectionConfig,
 }
 
 impl SourceWindow {
@@ -457,26 +475,26 @@ impl SourceWindow {
         message_broker: &mut MessageBroker,
     ) {
         let SourceWindow {
-            connected,
-            connection_kind,
-            ..
+            connection_config, ..
         } = self;
         ui.label("Select Source:");
         ui.horizontal_top(|ui| {
             ui.radio_value(
-                connection_kind,
-                ConnectionKind::default_ethernet(),
+                connection_config,
+                ConnectionConfig::default_ethernet(),
                 "Ethernet",
             );
-            ui.radio_value(connection_kind, ConnectionKind::default_serial(), "Serial");
+            ui.radio_value(
+                connection_config,
+                ConnectionConfig::default_serial(),
+                "Serial",
+            );
         });
 
         ui.separator();
 
-        // flag to check if the connection is valid
-        let mut connection_valid = true;
-        match connection_kind {
-            ConnectionKind::Ethernet { port } => {
+        match connection_config {
+            ConnectionConfig::Ethernet(EthernetConfiguration { port }) => {
                 egui::Grid::new("grid")
                     .num_columns(2)
                     .spacing([10.0, 5.0])
@@ -486,29 +504,39 @@ impl SourceWindow {
                         ui.end_row();
                     });
             }
-            ConnectionKind::Serial { port, baud_rate } => {
+            ConnectionConfig::Serial(opt) => {
                 egui::Grid::new("grid")
                     .num_columns(2)
                     .spacing([10.0, 5.0])
                     .show(ui, |ui| {
                         ui.label("Serial Port:");
-                        match port {
-                            Some(port) => {
+                        match opt {
+                            Some(SerialConfiguration {
+                                port_name,
+                                baud_rate,
+                            }) => {
                                 ComboBox::from_id_salt("serial_port")
-                                    .selected_text(port.as_ref())
+                                    .selected_text(port_name.as_str())
                                     .show_ui(ui, |ui| {
-                                        for available_port in
-                                            SerialPortCandidate::list_all_usb_ports().log_unwrap()
-                                        {
+                                        for available_port in list_all_usb_ports().log_unwrap() {
                                             ui.selectable_value(
-                                                port,
-                                                available_port.clone(),
-                                                available_port.as_ref(),
+                                                port_name,
+                                                available_port.port_name.clone(),
+                                                available_port.port_name,
                                             );
                                         }
                                     });
+
+                                ui.label("Baud Rate:");
+                                ui.add(
+                                    egui::DragValue::new(baud_rate)
+                                        .range(110..=256000)
+                                        .speed(100),
+                                );
+                                ui.end_row();
                             }
                             None => {
+                                // in case of a serial connection missing
                                 warn!("USER ERROR: No serial port found");
                                 ui.label(
                                     RichText::new("No port found")
@@ -516,19 +544,10 @@ impl SourceWindow {
                                         .underline()
                                         .strong(),
                                 );
-                                // invalid the connection
-                                connection_valid = false;
                             }
                         }
 
                         ui.end_row();
-                        ui.label("Baud Rate:");
-                        ui.add(
-                            egui::DragValue::new(baud_rate)
-                                .range(110..=256000)
-                                .speed(100),
-                        );
-                        ui.end_row();
                     });
             }
         };
@@ -541,30 +560,25 @@ impl SourceWindow {
                 .horizontal(|mut strip| {
                     strip.cell(|ui| {
                         let btn1 = Button::new("Connect");
-                        ui.add_enabled_ui(!*connected & connection_valid, |ui| {
-                            if ui.add_sized(ui.available_size(), btn1).clicked() {
-                                match connection_kind {
-                                    ConnectionKind::Ethernet { port } => {
-                                        message_broker.listen_from_ethernet_port(*port);
-                                    }
-                                    ConnectionKind::Serial { port, baud_rate } => {
-                                        message_broker.listen_from_serial_port(
-                                            port.as_ref().log_unwrap().as_ref().to_owned(),
-                                            *baud_rate,
-                                        );
+                        ui.add_enabled_ui(
+                            !message_broker.is_connected() & connection_config.is_valid(),
+                            |ui| {
+                                if ui.add_sized(ui.available_size(), btn1).clicked() {
+                                    if let Err(e) =
+                                        connection_config.open_connection(message_broker)
+                                    {
+                                        error!("Failed to open connection: {:?}", e); // TODO: handle user erros
                                     }
+                                    *can_be_closed = true;
                                 }
-                                *can_be_closed = true;
-                                *connected = true;
-                            }
-                        });
+                            },
+                        );
                     });
                     strip.cell(|ui| {
                         let btn2 = Button::new("Disconnect");
-                        ui.add_enabled_ui(*connected, |ui| {
+                        ui.add_enabled_ui(message_broker.is_connected(), |ui| {
                             if ui.add_sized(ui.available_size(), btn2).clicked() {
-                                message_broker.stop_listening();
-                                *connected = false;
+                                message_broker.close_connection();
                             }
                         });
                     });
-- 
GitLab


From 94a7441ee0752d4d024a0d1313470eb9f19af441 Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Thu, 6 Mar 2025 02:12:00 +0100
Subject: [PATCH 04/14] cargo fix & major fix for ethernet

---
 src/communication/ethernet.rs |  27 ++------
 src/communication/serial.rs   |  25 ++------
 src/mavlink/base.rs           |  10 ---
 src/message_broker.rs         | 117 +++-------------------------------
 src/ui/app.rs                 |  11 +---
 src/ui/panes/plot.rs          |   5 +-
 src/utils.rs                  |   1 -
 src/utils/ring_buffer.rs      |   2 -
 8 files changed, 29 insertions(+), 169 deletions(-)

diff --git a/src/communication/ethernet.rs b/src/communication/ethernet.rs
index 52e5bee..22ba29e 100644
--- a/src/communication/ethernet.rs
+++ b/src/communication/ethernet.rs
@@ -1,32 +1,17 @@
-use std::{
-    collections::VecDeque,
-    io::Read,
-    net::UdpSocket,
-    sync::{
-        Arc, Mutex,
-        atomic::{AtomicBool, Ordering},
-    },
-    thread::JoinHandle,
-};
+use std::net::UdpSocket;
 
-use anyhow::Context;
-use ring_channel::{RingReceiver, RingSender};
 use skyward_mavlink::mavlink::{
     MavFrame,
     error::{MessageReadError, MessageWriteError},
     read_v1_msg, write_v1_msg,
 };
-use tracing::{debug, error, trace};
+use tracing::{debug, trace};
 
-use crate::{
-    error::ErrInstrument,
-    mavlink::{
-        MAX_MSG_SIZE, MavHeader, MavMessage, MavlinkVersion, TimedMessage, peek_reader::PeekReader,
-        read_versioned_msg,
-    },
-};
+use crate::mavlink::{
+        MAX_MSG_SIZE, MavMessage, TimedMessage, peek_reader::PeekReader,
+    };
 
-use super::{Connectable, ConnectionError, MessageTransceiver, Transceivers};
+use super::{Connectable, ConnectionError, MessageTransceiver};
 
 #[derive(Debug, Clone)]
 pub struct EthernetConfiguration {
diff --git a/src/communication/serial.rs b/src/communication/serial.rs
index b49342b..fd7a374 100644
--- a/src/communication/serial.rs
+++ b/src/communication/serial.rs
@@ -4,35 +4,23 @@
 //! listing all available serial ports and finding the first serial port that
 //! contains "STM32" or "ST-LINK" in its product name.
 
-use std::{
-    collections::VecDeque,
-    io::Read,
-    sync::{
-        Arc, Mutex,
-        atomic::{AtomicBool, Ordering},
-    },
-    thread::JoinHandle,
-};
+use std::sync::Mutex;
 
 use anyhow::Context;
-use ring_channel::{RingReceiver, RingSender};
 use serialport::{SerialPort, SerialPortInfo, SerialPortType};
 use skyward_mavlink::mavlink::{
     MavFrame,
     error::{MessageReadError, MessageWriteError},
     read_v1_msg, write_v1_msg,
 };
-use tracing::{debug, error, trace};
+use tracing::{debug, trace};
 
 use crate::{
     error::ErrInstrument,
-    mavlink::{
-        MavHeader, MavMessage, MavlinkVersion, TimedMessage, peek_reader::PeekReader,
-        read_versioned_msg,
-    },
+    mavlink::{MavMessage, TimedMessage, peek_reader::PeekReader},
 };
 
-use super::{Connectable, ConnectionError, MessageTransceiver, Transceivers};
+use super::{Connectable, ConnectionError, MessageTransceiver};
 
 const SERIAL_PORT_TIMEOUT_MS: u64 = 100;
 pub const DEFAULT_BAUD_RATE: u32 = 115200;
@@ -80,7 +68,7 @@ impl Connectable for SerialConfiguration {
             self.port_name, self.baud_rate
         );
         Ok(SerialTransceiver {
-            serial_reader: Mutex::new(PeekReader::new(port.try_clone()?)),
+            serial_reader: Mutex::new(Box::new(PeekReader::new(port.try_clone()?))),
             serial_writer: Mutex::new(port),
         })
     }
@@ -100,7 +88,8 @@ impl From<serialport::Error> for ConnectionError {
 
 /// Manages a connection to a serial port.
 pub struct SerialTransceiver {
-    serial_reader: Mutex<PeekReader<Box<dyn SerialPort>>>,
+    serial_reader: Mutex<Box<PeekReader<Box<dyn SerialPort>>>>,
+    #[allow(dead_code)]
     serial_writer: Mutex<Box<dyn SerialPort>>,
 }
 
diff --git a/src/mavlink/base.rs b/src/mavlink/base.rs
index c10d9fe..fb64c4c 100644
--- a/src/mavlink/base.rs
+++ b/src/mavlink/base.rs
@@ -6,8 +6,6 @@
 
 use std::time::Instant;
 
-use skyward_mavlink::mavlink::peek_reader::PeekReader;
-
 // Re-export from the mavlink crate
 pub use skyward_mavlink::{
     mavlink::*, orion::*,
@@ -60,11 +58,3 @@ where
         })
         .collect())
 }
-
-/// Read a stream of bytes and return an iterator of MavLink messages
-pub fn byte_parser<'a>(
-    reader: impl std::io::Read + 'a,
-) -> impl Iterator<Item = (MavHeader, MavMessage)> + 'a {
-    let mut reader = PeekReader::new(reader);
-    std::iter::from_fn(move || read_v1_msg(&mut reader).ok())
-}
diff --git a/src/message_broker.rs b/src/message_broker.rs
index f583074..982222a 100644
--- a/src/message_broker.rs
+++ b/src/message_broker.rs
@@ -13,25 +13,14 @@ use reception_queue::ReceptionQueue;
 use crate::{
     communication::{Connection, ConnectionError, TransceiverConfigExt},
     error::ErrInstrument,
-    mavlink::{Message, TimedMessage, byte_parser},
-    utils::RingBuffer,
+    mavlink::{Message, TimedMessage},
 };
-use ring_channel::{RingReceiver, RingSender, ring_channel};
 use std::{
     collections::HashMap,
-    io::Write,
-    num::NonZeroUsize,
-    sync::{
-        Arc, Mutex,
-        atomic::{AtomicBool, Ordering},
-    },
-    time::{Duration, Instant},
+    sync::{Arc, Mutex},
+    time::Duration,
 };
-use tokio::{net::UdpSocket, task::JoinHandle};
-use tracing::{debug, error, trace};
-
-/// Maximum size of the UDP buffer
-const UDP_BUFFER_SIZE: usize = 65527;
+use tracing::error;
 
 /// The MessageBroker struct contains the state of the message broker.
 ///
@@ -50,7 +39,7 @@ pub struct MessageBroker {
 
 impl MessageBroker {
     /// Creates a new `MessageBroker` with the given channel size and Egui context.
-    pub fn new(channel_size: NonZeroUsize, ctx: egui::Context) -> Self {
+    pub fn new(ctx: egui::Context) -> Self {
         Self {
             messages: HashMap::new(),
             // TODO: make this configurable
@@ -80,106 +69,17 @@ impl MessageBroker {
         self.connection.is_some()
     }
 
-    // /// Start a listener task that listens to incoming messages from the given
-    // /// Ethernet port, and accumulates them in a ring buffer, read only when
-    // /// views request a refresh.
-    // 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 last_receptions = Arc::clone(&self.last_receptions);
-
-    //     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();
-
-    //     debug!("Spawning listener task at {}", bind_address);
-    //     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 byte_parser(&buf[..len]) {
-    //                 trace!("Received message: {:?}", mav_message);
-    //                 tx.send(TimedMessage::just_received(mav_message))
-    //                     .context("Failed to send message")?;
-    //                 last_receptions.lock().unwrap().push(Instant::now());
-    //                 ctx.request_repaint();
-    //             }
-    //         }
-
-    //         Ok::<(), anyhow::Error>(())
-    //     });
-    //     self.task = Some(handle);
-    // }
-
-    // /// Start a listener task that listens to incoming messages from the given
-    // /// serial port and stores them in a ring buffer.
-    // pub fn listen_from_serial_port(&mut self, port: String, baud_rate: u32) {
-    //     // Stop the current listener if it exists
-    //     self.stop_listening();
-    //     self.running_flag.store(true, Ordering::Relaxed);
-    //     let last_receptions = Arc::clone(&self.last_receptions);
-
-    //     let tx = self.tx.clone();
-    //     let ctx = self.ctx.clone();
-
-    //     let running_flag = self.running_flag.clone();
-
-    //     debug!("Spawning listener task at {}", port);
-    //     let handle = tokio::task::spawn_blocking(move || {
-    //         let mut serial_port = serialport::new(port, baud_rate)
-    //             .timeout(std::time::Duration::from_millis(100))
-    //             .open()
-    //             .context("Failed to open serial port")?;
-    //         debug!("Listening on serial port");
-
-    //         let mut ring_buf = RingBuffer::<1024>::new();
-    //         let mut temp_buf = [0; 512];
-    //         // need to do a better error handling for this (need toast errors)
-    //         while running_flag.load(Ordering::Relaxed) {
-    //             let result = serial_port
-    //                 .read(&mut temp_buf)
-    //                 .log_expect("Failed to read from serial port");
-    //             debug!("Read {} bytes from serial port", result);
-    //             trace!("data read from serial: {:?}", &temp_buf[..result]);
-    //             ring_buf
-    //                 .write(&temp_buf[..result])
-    //                 .log_expect("Failed to write to ring buffer, check buffer size");
-    //             for (_, mav_message) in byte_parser(&mut ring_buf) {
-    //                 debug!("Received message: {:?}", mav_message);
-    //                 tx.send(TimedMessage::just_received(mav_message))
-    //                     .context("Failed to send message")?;
-    //                 last_receptions.lock().unwrap().push(Instant::now());
-    //                 ctx.request_repaint();
-    //             }
-    //         }
-
-    //         Ok::<(), anyhow::Error>(())
-    //     });
-    //     self.task = Some(handle);
-    // }
-
     /// Returns the time since the last message was received.
     pub fn time_since_last_reception(&self) -> Option<Duration> {
         self.last_receptions
             .lock()
-            .unwrap()
+            .log_unwrap()
             .time_since_last_reception()
     }
 
     /// Returns the frequency of messages received in the last second.
     pub fn reception_frequency(&self) -> f64 {
-        self.last_receptions.lock().unwrap().frequency()
+        self.last_receptions.lock().log_unwrap().frequency()
     }
 
     pub fn get(&self, id: u32) -> &[TimedMessage] {
@@ -197,6 +97,9 @@ impl MessageBroker {
                     for message in messages {
                         bundle.insert(message.clone());
 
+                        // Update the last reception time
+                        self.last_receptions.lock().log_unwrap().push(message.time);
+
                         // Store the message in the broker
                         self.messages
                             .entry(message.message.message_id())
diff --git a/src/ui/app.rs b/src/ui/app.rs
index df9ea13..ac62583 100644
--- a/src/ui/app.rs
+++ b/src/ui/app.rs
@@ -8,14 +8,14 @@ use super::{
 };
 use crate::{
     communication::{
-        Connection, ConnectionError, TransceiverConfigExt,
+        ConnectionError,
         ethernet::EthernetConfiguration,
         serial::{
             DEFAULT_BAUD_RATE, SerialConfiguration, find_first_stm32_port, list_all_usb_ports,
         },
     },
     error::ErrInstrument,
-    mavlink::{self, DEFAULT_ETHERNET_PORT},
+    mavlink::DEFAULT_ETHERNET_PORT,
     message_broker::{MessageBroker, MessageBundle},
     ui::panes::PaneKind,
 };
@@ -24,10 +24,8 @@ use egui::{Align2, Button, Color32, ComboBox, Key, Modifiers, RichText, Sides, V
 use egui_extras::{Size, StripBuilder};
 use egui_tiles::{Behavior, Container, Linear, LinearDir, Tile, TileId, Tiles, Tree};
 use serde::{Deserialize, Serialize};
-use serialport::SerialPortInfo;
 use std::{
     fs,
-    num::NonZeroUsize,
     path::{Path, PathBuf},
     time::{Duration, Instant},
 };
@@ -283,10 +281,7 @@ impl App {
         Self {
             state,
             layout_manager,
-            message_broker: MessageBroker::new(
-                NonZeroUsize::new(50).log_unwrap(),
-                ctx.egui_ctx.clone(),
-            ),
+            message_broker: MessageBroker::new(ctx.egui_ctx.clone()),
             widget_gallery: WidgetGallery::default(),
             behavior: AppBehavior::default(),
             maximized_pane: None,
diff --git a/src/ui/panes/plot.rs b/src/ui/panes/plot.rs
index bfd2ca8..da202dd 100644
--- a/src/ui/panes/plot.rs
+++ b/src/ui/panes/plot.rs
@@ -2,6 +2,7 @@ mod source_window;
 
 use super::PaneBehavior;
 use crate::{
+    error::ErrInstrument,
     mavlink::{MessageData, ROCKET_FLIGHT_TM_DATA, TimedMessage, extract_from_message},
     ui::app::PaneResponse,
 };
@@ -98,8 +99,8 @@ impl PaneBehavior for Plot2DPane {
         } = &self.settings;
 
         for msg in messages {
-            let x: f64 = extract_from_message(&msg.message, [x_field]).unwrap()[0];
-            let ys: Vec<f64> = extract_from_message(&msg.message, y_fields).unwrap();
+            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();
 
             if self.line_data.len() < ys.len() {
                 self.line_data.resize(ys.len(), Vec::new());
diff --git a/src/utils.rs b/src/utils.rs
index 4c96d92..7e22ff5 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -1,3 +1,2 @@
 mod ring_buffer;
 
-pub use ring_buffer::{OverwritingRingBuffer, RingBuffer};
diff --git a/src/utils/ring_buffer.rs b/src/utils/ring_buffer.rs
index b1a42fb..c25df11 100644
--- a/src/utils/ring_buffer.rs
+++ b/src/utils/ring_buffer.rs
@@ -1,6 +1,4 @@
-use std::collections::binary_heap::PeekMut;
 
-use skyward_mavlink::mavlink::peek_reader::PeekReader;
 
 #[derive(Debug)]
 pub struct RingBuffer<const G: usize> {
-- 
GitLab


From c17ae80378e8290d9bef67ea271721446537bd58 Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Thu, 6 Mar 2025 10:52:31 +0100
Subject: [PATCH 05/14] added logging to file and removed uuid

---
 Cargo.lock  | 98 +++++++++++++++++++++++++++++++++++++++++++++++------
 Cargo.toml  | 10 +++---
 src/main.rs |  7 ++++
 3 files changed, 99 insertions(+), 16 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 8c97a46..9ba641e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -753,6 +753,15 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
+dependencies = [
+ "crossbeam-utils",
+]
+
 [[package]]
 name = "crossbeam-queue"
 version = "0.3.12"
@@ -784,6 +793,15 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
 
+[[package]]
+name = "deranged"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
+dependencies = [
+ "powerfmt",
+]
+
 [[package]]
 name = "derivative"
 version = "2.2.0"
@@ -2190,6 +2208,12 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
 [[package]]
 name = "num-derive"
 version = "0.4.2"
@@ -2619,6 +2643,12 @@ dependencies = [
  "windows-sys 0.59.0",
 ]
 
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
 [[package]]
 name = "ppv-lite86"
 version = "0.2.20"
@@ -2959,9 +2989,9 @@ dependencies = [
  "thiserror 2.0.11",
  "tokio",
  "tracing",
+ "tracing-appender",
  "tracing-subscriber",
  "tracing-tracy",
- "uuid",
 ]
 
 [[package]]
@@ -3394,6 +3424,37 @@ dependencies = [
  "weezl",
 ]
 
+[[package]]
+name = "time"
+version = "0.3.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef"
+
+[[package]]
+name = "time-macros"
+version = "0.2.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
 [[package]]
 name = "tiny-skia"
 version = "0.11.4"
@@ -3471,6 +3532,18 @@ dependencies = [
  "tracing-core",
 ]
 
+[[package]]
+name = "tracing-appender"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf"
+dependencies = [
+ "crossbeam-channel",
+ "thiserror 1.0.69",
+ "time",
+ "tracing-subscriber",
+]
+
 [[package]]
 name = "tracing-attributes"
 version = "0.1.28"
@@ -3503,6 +3576,16 @@ dependencies = [
  "tracing-core",
 ]
 
+[[package]]
+name = "tracing-serde"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
+dependencies = [
+ "serde",
+ "tracing-core",
+]
+
 [[package]]
 name = "tracing-subscriber"
 version = "0.3.19"
@@ -3513,12 +3596,15 @@ dependencies = [
  "nu-ansi-term",
  "once_cell",
  "regex",
+ "serde",
+ "serde_json",
  "sharded-slab",
  "smallvec",
  "thread_local",
  "tracing",
  "tracing-core",
  "tracing-log",
+ "tracing-serde",
 ]
 
 [[package]]
@@ -3647,16 +3733,6 @@ version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
 
-[[package]]
-name = "uuid"
-version = "1.15.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587"
-dependencies = [
- "getrandom 0.3.1",
- "serde",
-]
-
 [[package]]
 name = "valuable"
 version = "0.1.1"
diff --git a/Cargo.toml b/Cargo.toml
index 8c9d9fb..03ca628 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -28,9 +28,12 @@ serialport = "4.7.0"
 # ========= Persistency =========
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
-# =========== Logging ===========
+# =========== Tracing and profiling ===========
 tracing = "0.1"
-tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
+tracing-tracy = "0.11.4"
+profiling = { version = "1.0", features = ["profile-with-tracy"] }
+tracing-appender = "0.2"
 # =========== Utility ===========
 # for dynamic dispatch
 enum_dispatch = "0.3"
@@ -39,9 +42,6 @@ strum_macros = "0.26"
 anyhow = "1.0"
 ring-channel = "0.12.0"
 thiserror = "2.0.7"
-uuid = { version = "1.12.1", features = ["serde", "v7"] }
-profiling = { version = "1.0", features = ["profile-with-tracy"] }
-tracing-tracy = "0.11.4"
 
 [dev-dependencies]
 rand = "0.9.0"
diff --git a/src/main.rs b/src/main.rs
index 7db6c02..9093f87 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -26,8 +26,15 @@ static APP_NAME: &str = "segs";
 fn main() -> Result<(), eframe::Error> {
     // Set up logging (USE RUST_LOG=debug to see logs)
     let env_filter = EnvFilter::builder().from_env_lossy();
+    let file_appender = tracing_appender::rolling::daily("logs/", "segs.log");
+    let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
     tracing_subscriber::registry()
         .with(tracing_subscriber::fmt::layer().with_filter(env_filter))
+        .with(
+            tracing_subscriber::fmt::layer()
+                .json()
+                .with_writer(non_blocking),
+        )
         .with(tracing_tracy::TracyLayer::default())
         .init();
 
-- 
GitLab


From a80137d8578ab0053a962f7204267bc4b63ae13f Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Thu, 6 Mar 2025 11:40:28 +0100
Subject: [PATCH 06/14] CHECKPOINT

---
 .gitignore                                    |   3 +
 src/communication.rs                          |  11 +-
 src/ui.rs                                     |   1 +
 src/ui/app.rs                                 | 251 ++----------------
 src/ui/persistency.rs                         |   2 -
 src/ui/windows.rs                             |   5 +
 src/ui/windows/connections.rs                 | 219 +++++++++++++++
 .../layouts.rs}                               |   7 +-
 8 files changed, 256 insertions(+), 243 deletions(-)
 create mode 100644 src/ui/windows.rs
 create mode 100644 src/ui/windows/connections.rs
 rename src/ui/{persistency/layout_manager_window.rs => windows/layouts.rs} (99%)

diff --git a/.gitignore b/.gitignore
index f7cd28a..008d2b3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,6 @@
 
 # Build related
 /target
+
+# Logs
+/logs
diff --git a/src/communication.rs b/src/communication.rs
index d699d2c..01b5ca5 100644
--- a/src/communication.rs
+++ b/src/communication.rs
@@ -8,7 +8,6 @@ use std::{
         Arc,
         atomic::{AtomicBool, Ordering},
     },
-    thread::JoinHandle,
 };
 
 use enum_dispatch::enum_dispatch;
@@ -28,8 +27,10 @@ use serial::SerialTransceiver;
 
 // Re-exports
 pub use error::{CommunicationError, ConnectionError};
+pub use ethernet::EthernetConfiguration;
+pub use serial::SerialConfiguration;
 
-const MAX_STORED_MSGS: usize = 100; // 192 bytes each = 19.2 KB
+const MAX_STORED_MSGS: usize = 1000; // 192 bytes each = 192 KB
 
 pub trait TransceiverConfigExt: Connectable {
     fn open_connection(&self) -> Result<Connection, ConnectionError> {
@@ -59,12 +60,12 @@ trait MessageTransceiver: Send + Sync + Into<Transceivers> {
         let running_flag = Arc::new(AtomicBool::new(true));
         let (tx, rx) = ring_channel(NonZero::new(MAX_STORED_MSGS).log_unwrap());
         let endpoint_inner = Arc::new(self.into());
-        let thread_handle;
 
         {
             let running_flag = running_flag.clone();
             let endpoint_inner = endpoint_inner.clone();
-            thread_handle = std::thread::spawn(move || {
+            // detach the thread, to see errors rely on logs
+            let _ = std::thread::spawn(move || {
                 while running_flag.load(Ordering::Relaxed) {
                     match endpoint_inner.wait_for_message() {
                         Ok(msg) => {
@@ -89,7 +90,6 @@ trait MessageTransceiver: Send + Sync + Into<Transceivers> {
             endpoint: endpoint_inner,
             rx_ring_channel: rx,
             running_flag,
-            thread_handle,
         }
     }
 }
@@ -104,7 +104,6 @@ pub struct Connection {
     endpoint: Arc<Transceivers>,
     rx_ring_channel: RingReceiver<TimedMessage>,
     running_flag: Arc<AtomicBool>,
-    thread_handle: JoinHandle<Result<(), CommunicationError>>,
 }
 
 impl Connection {
diff --git a/src/ui.rs b/src/ui.rs
index 050d0cc..08be0bb 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -5,5 +5,6 @@ mod shortcuts;
 mod utils;
 mod widget_gallery;
 mod widgets;
+pub mod windows;
 
 pub use app::App;
diff --git a/src/ui/app.rs b/src/ui/app.rs
index ac62583..39bc126 100644
--- a/src/ui/app.rs
+++ b/src/ui/app.rs
@@ -1,27 +1,5 @@
-use super::{
-    panes::{Pane, PaneBehavior},
-    persistency::{LayoutManager, LayoutManagerWindow},
-    shortcuts,
-    utils::maximized_pane_ui,
-    widget_gallery::WidgetGallery,
-    widgets::reception_led::ReceptionLed,
-};
-use crate::{
-    communication::{
-        ConnectionError,
-        ethernet::EthernetConfiguration,
-        serial::{
-            DEFAULT_BAUD_RATE, SerialConfiguration, find_first_stm32_port, list_all_usb_ports,
-        },
-    },
-    error::ErrInstrument,
-    mavlink::DEFAULT_ETHERNET_PORT,
-    message_broker::{MessageBroker, MessageBundle},
-    ui::panes::PaneKind,
-};
 use eframe::CreationContext;
-use egui::{Align2, Button, Color32, ComboBox, Key, Modifiers, RichText, Sides, Vec2};
-use egui_extras::{Size, StripBuilder};
+use egui::{Button, Key, Modifiers, Sides};
 use egui_tiles::{Behavior, Container, Linear, LinearDir, Tile, TileId, Tiles, Tree};
 use serde::{Deserialize, Serialize};
 use std::{
@@ -29,7 +7,22 @@ use std::{
     path::{Path, PathBuf},
     time::{Duration, Instant},
 };
-use tracing::{debug, error, trace, warn};
+use tracing::{debug, error, trace};
+
+use crate::{
+    error::ErrInstrument,
+    message_broker::{MessageBroker, MessageBundle},
+};
+
+use super::{
+    panes::{Pane, PaneBehavior, PaneKind},
+    persistency::LayoutManager,
+    shortcuts,
+    utils::maximized_pane_ui,
+    widget_gallery::WidgetGallery,
+    widgets::reception_led::ReceptionLed,
+    windows::{ConnectionsWindow, LayoutManagerWindow},
+};
 
 pub struct App {
     /// Persistent state of the app
@@ -42,7 +35,7 @@ pub struct App {
     message_bundle: MessageBundle,
     // == Windows ==
     widget_gallery: WidgetGallery,
-    sources_window: SourceWindow,
+    sources_window: ConnectionsWindow,
     layout_manager_window: LayoutManagerWindow,
 }
 
@@ -286,7 +279,7 @@ impl App {
             behavior: AppBehavior::default(),
             maximized_pane: None,
             message_bundle: MessageBundle::default(),
-            sources_window: SourceWindow::default(),
+            sources_window: ConnectionsWindow::default(),
             layout_manager_window: LayoutManagerWindow::default(),
         }
     }
@@ -376,212 +369,6 @@ impl AppState {
     }
 }
 
-#[derive(Debug)]
-enum ConnectionConfig {
-    Ethernet(EthernetConfiguration),
-    Serial(Option<SerialConfiguration>),
-}
-
-impl ConnectionConfig {
-    fn default_ethernet() -> Self {
-        Self::Ethernet(EthernetConfiguration {
-            port: DEFAULT_ETHERNET_PORT,
-        })
-    }
-
-    fn default_serial() -> Self {
-        let port_name = find_first_stm32_port()
-            .map(|port| port.port_name)
-            .or(list_all_usb_ports()
-                .ok()
-                .and_then(|ports| ports.first().map(|port| port.port_name.clone())));
-        let Some(port_name) = port_name else {
-            warn!("USER ERROR: No serial port found");
-            return Self::Serial(None);
-        };
-        Self::Serial(Some(SerialConfiguration {
-            port_name,
-            baud_rate: DEFAULT_BAUD_RATE,
-        }))
-    }
-
-    fn is_valid(&self) -> bool {
-        match self {
-            Self::Ethernet(_) => true,
-            Self::Serial(Some(_)) => true,
-            Self::Serial(None) => false,
-        }
-    }
-
-    fn open_connection(&self, msg_broker: &mut MessageBroker) -> Result<(), ConnectionError> {
-        match self {
-            Self::Ethernet(config) => msg_broker.open_connection(config.clone()),
-            Self::Serial(Some(config)) => msg_broker.open_connection(config.clone()),
-            Self::Serial(None) => Err(ConnectionError::WrongConfiguration(
-                "No serial port found".to_string(),
-            )),
-        }
-    }
-}
-
-impl Default for ConnectionConfig {
-    fn default() -> Self {
-        Self::Ethernet(EthernetConfiguration {
-            port: DEFAULT_ETHERNET_PORT,
-        })
-    }
-}
-
-impl PartialEq for ConnectionConfig {
-    fn eq(&self, other: &Self) -> bool {
-        matches!(self, Self::Ethernet(_)) && matches!(other, Self::Ethernet(_))
-            || matches!(self, Self::Serial(_)) && matches!(other, Self::Serial(_))
-    }
-}
-
-#[derive(Debug, Default)]
-struct SourceWindow {
-    visible: bool,
-    connection_config: ConnectionConfig,
-}
-
-impl SourceWindow {
-    #[profiling::function]
-    fn show_window(&mut self, ui: &mut egui::Ui, message_broker: &mut MessageBroker) {
-        let mut window_is_open = self.visible;
-        let mut can_be_closed = false;
-        egui::Window::new("Sources")
-            .id(ui.id())
-            .anchor(Align2::CENTER_CENTER, [0.0, 0.0])
-            .max_width(200.0)
-            .collapsible(false)
-            .resizable(false)
-            .open(&mut window_is_open)
-            .show(ui.ctx(), |ui| {
-                self.ui(ui, &mut can_be_closed, message_broker);
-            });
-        self.visible = window_is_open && !can_be_closed;
-    }
-
-    fn ui(
-        &mut self,
-        ui: &mut egui::Ui,
-        can_be_closed: &mut bool,
-        message_broker: &mut MessageBroker,
-    ) {
-        let SourceWindow {
-            connection_config, ..
-        } = self;
-        ui.label("Select Source:");
-        ui.horizontal_top(|ui| {
-            ui.radio_value(
-                connection_config,
-                ConnectionConfig::default_ethernet(),
-                "Ethernet",
-            );
-            ui.radio_value(
-                connection_config,
-                ConnectionConfig::default_serial(),
-                "Serial",
-            );
-        });
-
-        ui.separator();
-
-        match connection_config {
-            ConnectionConfig::Ethernet(EthernetConfiguration { port }) => {
-                egui::Grid::new("grid")
-                    .num_columns(2)
-                    .spacing([10.0, 5.0])
-                    .show(ui, |ui| {
-                        ui.label("Ethernet Port:");
-                        ui.add(egui::DragValue::new(port).range(0..=65535).speed(10));
-                        ui.end_row();
-                    });
-            }
-            ConnectionConfig::Serial(opt) => {
-                egui::Grid::new("grid")
-                    .num_columns(2)
-                    .spacing([10.0, 5.0])
-                    .show(ui, |ui| {
-                        ui.label("Serial Port:");
-                        match opt {
-                            Some(SerialConfiguration {
-                                port_name,
-                                baud_rate,
-                            }) => {
-                                ComboBox::from_id_salt("serial_port")
-                                    .selected_text(port_name.as_str())
-                                    .show_ui(ui, |ui| {
-                                        for available_port in list_all_usb_ports().log_unwrap() {
-                                            ui.selectable_value(
-                                                port_name,
-                                                available_port.port_name.clone(),
-                                                available_port.port_name,
-                                            );
-                                        }
-                                    });
-
-                                ui.label("Baud Rate:");
-                                ui.add(
-                                    egui::DragValue::new(baud_rate)
-                                        .range(110..=256000)
-                                        .speed(100),
-                                );
-                                ui.end_row();
-                            }
-                            None => {
-                                // in case of a serial connection missing
-                                warn!("USER ERROR: No serial port found");
-                                ui.label(
-                                    RichText::new("No port found")
-                                        .color(Color32::RED)
-                                        .underline()
-                                        .strong(),
-                                );
-                            }
-                        }
-
-                        ui.end_row();
-                    });
-            }
-        };
-
-        ui.separator();
-
-        ui.allocate_ui(Vec2::new(ui.available_width(), 20.0), |ui| {
-            StripBuilder::new(ui)
-                .sizes(Size::remainder(), 2) // top cell
-                .horizontal(|mut strip| {
-                    strip.cell(|ui| {
-                        let btn1 = Button::new("Connect");
-                        ui.add_enabled_ui(
-                            !message_broker.is_connected() & connection_config.is_valid(),
-                            |ui| {
-                                if ui.add_sized(ui.available_size(), btn1).clicked() {
-                                    if let Err(e) =
-                                        connection_config.open_connection(message_broker)
-                                    {
-                                        error!("Failed to open connection: {:?}", e); // TODO: handle user erros
-                                    }
-                                    *can_be_closed = true;
-                                }
-                            },
-                        );
-                    });
-                    strip.cell(|ui| {
-                        let btn2 = Button::new("Disconnect");
-                        ui.add_enabled_ui(message_broker.is_connected(), |ui| {
-                            if ui.add_sized(ui.available_size(), btn2).clicked() {
-                                message_broker.close_connection();
-                            }
-                        });
-                    });
-                });
-        });
-    }
-}
-
 /// Behavior for the tree of panes in the app
 #[derive(Default)]
 pub struct AppBehavior {
diff --git a/src/ui/persistency.rs b/src/ui/persistency.rs
index 7998384..44bfc12 100644
--- a/src/ui/persistency.rs
+++ b/src/ui/persistency.rs
@@ -1,5 +1,3 @@
 mod layout_manager;
-mod layout_manager_window;
 
 pub use layout_manager::LayoutManager;
-pub use layout_manager_window::LayoutManagerWindow;
diff --git a/src/ui/windows.rs b/src/ui/windows.rs
new file mode 100644
index 0000000..557c348
--- /dev/null
+++ b/src/ui/windows.rs
@@ -0,0 +1,5 @@
+mod connections;
+mod layouts;
+
+pub use connections::ConnectionsWindow;
+pub use layouts::LayoutManagerWindow;
diff --git a/src/ui/windows/connections.rs b/src/ui/windows/connections.rs
new file mode 100644
index 0000000..bfa7293
--- /dev/null
+++ b/src/ui/windows/connections.rs
@@ -0,0 +1,219 @@
+use egui::{Align2, Button, Color32, ComboBox, RichText, Vec2};
+use egui_extras::{Size, StripBuilder};
+use tracing::{error, warn};
+
+use crate::{
+    communication::{
+        ConnectionError, EthernetConfiguration, SerialConfiguration,
+        serial::{DEFAULT_BAUD_RATE, find_first_stm32_port, list_all_usb_ports},
+    },
+    error::ErrInstrument,
+    mavlink::DEFAULT_ETHERNET_PORT,
+    message_broker::MessageBroker,
+};
+
+#[derive(Debug)]
+enum ConnectionConfig {
+    Ethernet(EthernetConfiguration),
+    Serial(Option<SerialConfiguration>),
+}
+
+impl ConnectionConfig {
+    fn default_ethernet() -> Self {
+        Self::Ethernet(EthernetConfiguration {
+            port: DEFAULT_ETHERNET_PORT,
+        })
+    }
+
+    fn default_serial() -> Self {
+        let port_name = find_first_stm32_port()
+            .map(|port| port.port_name)
+            .or(list_all_usb_ports()
+                .ok()
+                .and_then(|ports| ports.first().map(|port| port.port_name.clone())));
+        let Some(port_name) = port_name else {
+            warn!("USER ERROR: No serial port found");
+            return Self::Serial(None);
+        };
+        Self::Serial(Some(SerialConfiguration {
+            port_name,
+            baud_rate: DEFAULT_BAUD_RATE,
+        }))
+    }
+
+    fn is_valid(&self) -> bool {
+        match self {
+            Self::Ethernet(_) => true,
+            Self::Serial(Some(_)) => true,
+            Self::Serial(None) => false,
+        }
+    }
+
+    fn open_connection(&self, msg_broker: &mut MessageBroker) -> Result<(), ConnectionError> {
+        match self {
+            Self::Ethernet(config) => msg_broker.open_connection(config.clone()),
+            Self::Serial(Some(config)) => msg_broker.open_connection(config.clone()),
+            Self::Serial(None) => Err(ConnectionError::WrongConfiguration(
+                "No serial port found".to_string(),
+            )),
+        }
+    }
+}
+
+impl Default for ConnectionConfig {
+    fn default() -> Self {
+        Self::Ethernet(EthernetConfiguration {
+            port: DEFAULT_ETHERNET_PORT,
+        })
+    }
+}
+
+impl PartialEq for ConnectionConfig {
+    fn eq(&self, other: &Self) -> bool {
+        matches!(self, Self::Ethernet(_)) && matches!(other, Self::Ethernet(_))
+            || matches!(self, Self::Serial(_)) && matches!(other, Self::Serial(_))
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct ConnectionsWindow {
+    pub visible: bool,
+    connection_config: ConnectionConfig,
+}
+
+impl ConnectionsWindow {
+    #[profiling::function]
+    pub fn show_window(&mut self, ui: &mut egui::Ui, message_broker: &mut MessageBroker) {
+        let mut window_is_open = self.visible;
+        let mut can_be_closed = false;
+        egui::Window::new("Sources")
+            .id(ui.id())
+            .anchor(Align2::CENTER_CENTER, [0.0, 0.0])
+            .max_width(200.0)
+            .collapsible(false)
+            .resizable(false)
+            .open(&mut window_is_open)
+            .show(ui.ctx(), |ui| {
+                self.ui(ui, &mut can_be_closed, message_broker);
+            });
+        self.visible = window_is_open && !can_be_closed;
+    }
+
+    fn ui(
+        &mut self,
+        ui: &mut egui::Ui,
+        can_be_closed: &mut bool,
+        message_broker: &mut MessageBroker,
+    ) {
+        let ConnectionsWindow {
+            connection_config, ..
+        } = self;
+        ui.label("Select Source:");
+        ui.horizontal_top(|ui| {
+            ui.radio_value(
+                connection_config,
+                ConnectionConfig::default_ethernet(),
+                "Ethernet",
+            );
+            ui.radio_value(
+                connection_config,
+                ConnectionConfig::default_serial(),
+                "Serial",
+            );
+        });
+
+        ui.separator();
+
+        match connection_config {
+            ConnectionConfig::Ethernet(EthernetConfiguration { port }) => {
+                egui::Grid::new("grid")
+                    .num_columns(2)
+                    .spacing([10.0, 5.0])
+                    .show(ui, |ui| {
+                        ui.label("Ethernet Port:");
+                        ui.add(egui::DragValue::new(port).range(0..=65535).speed(10));
+                        ui.end_row();
+                    });
+            }
+            ConnectionConfig::Serial(opt) => {
+                egui::Grid::new("grid")
+                    .num_columns(2)
+                    .spacing([10.0, 5.0])
+                    .show(ui, |ui| {
+                        ui.label("Serial Port:");
+                        match opt {
+                            Some(SerialConfiguration {
+                                port_name,
+                                baud_rate,
+                            }) => {
+                                ComboBox::from_id_salt("serial_port")
+                                    .selected_text(port_name.as_str())
+                                    .show_ui(ui, |ui| {
+                                        for available_port in list_all_usb_ports().log_unwrap() {
+                                            ui.selectable_value(
+                                                port_name,
+                                                available_port.port_name.clone(),
+                                                available_port.port_name,
+                                            );
+                                        }
+                                    });
+
+                                ui.label("Baud Rate:");
+                                ui.add(
+                                    egui::DragValue::new(baud_rate)
+                                        .range(110..=256000)
+                                        .speed(100),
+                                );
+                                ui.end_row();
+                            }
+                            None => {
+                                // in case of a serial connection missing
+                                warn!("USER ERROR: No serial port found");
+                                ui.label(
+                                    RichText::new("No port found")
+                                        .color(Color32::RED)
+                                        .underline()
+                                        .strong(),
+                                );
+                            }
+                        }
+
+                        ui.end_row();
+                    });
+            }
+        };
+
+        ui.separator();
+
+        ui.allocate_ui(Vec2::new(ui.available_width(), 20.0), |ui| {
+            StripBuilder::new(ui)
+                .sizes(Size::remainder(), 2) // top cell
+                .horizontal(|mut strip| {
+                    strip.cell(|ui| {
+                        let btn1 = Button::new("Connect");
+                        ui.add_enabled_ui(
+                            !message_broker.is_connected() & connection_config.is_valid(),
+                            |ui| {
+                                if ui.add_sized(ui.available_size(), btn1).clicked() {
+                                    if let Err(e) =
+                                        connection_config.open_connection(message_broker)
+                                    {
+                                        error!("Failed to open connection: {:?}", e); // TODO: handle user erros
+                                    }
+                                    *can_be_closed = true;
+                                }
+                            },
+                        );
+                    });
+                    strip.cell(|ui| {
+                        let btn2 = Button::new("Disconnect");
+                        ui.add_enabled_ui(message_broker.is_connected(), |ui| {
+                            if ui.add_sized(ui.available_size(), btn2).clicked() {
+                                message_broker.close_connection();
+                            }
+                        });
+                    });
+                });
+        });
+    }
+}
diff --git a/src/ui/persistency/layout_manager_window.rs b/src/ui/windows/layouts.rs
similarity index 99%
rename from src/ui/persistency/layout_manager_window.rs
rename to src/ui/windows/layouts.rs
index d4be72e..0407cef 100644
--- a/src/ui/persistency/layout_manager_window.rs
+++ b/src/ui/windows/layouts.rs
@@ -8,9 +8,10 @@ use egui_extras::{Column, Size, StripBuilder, TableBuilder};
 use egui_file::FileDialog;
 use tracing::{debug, error};
 
-use crate::{error::ErrInstrument, ui::app::AppState};
-
-use super::LayoutManager;
+use crate::{
+    error::ErrInstrument,
+    ui::{app::AppState, persistency::LayoutManager},
+};
 
 #[derive(Default)]
 pub struct LayoutManagerWindow {
-- 
GitLab


From 81700d28c938cd741148a0c87bb0cf38aa01c948 Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Thu, 6 Mar 2025 16:08:50 +0100
Subject: [PATCH 07/14] Added cache for serial port listing

---
 src/communication.rs          |  11 +++
 src/communication/ethernet.rs |   7 +-
 src/communication/serial.rs   |  21 ++--
 src/ui.rs                     |   1 +
 src/ui/cache.rs               |  60 ++++++++++++
 src/ui/utils.rs               |   2 +
 src/ui/windows/connections.rs | 174 +++++++++++++++++-----------------
 7 files changed, 179 insertions(+), 97 deletions(-)
 create mode 100644 src/ui/cache.rs

diff --git a/src/communication.rs b/src/communication.rs
index 01b5ca5..366f9a1 100644
--- a/src/communication.rs
+++ b/src/communication.rs
@@ -32,6 +32,14 @@ pub use serial::SerialConfiguration;
 
 const MAX_STORED_MSGS: usize = 1000; // 192 bytes each = 192 KB
 
+pub trait TransceiverConfig: TransceiverConfigSealed {}
+
+trait TransceiverConfigSealed {}
+
+impl<T: TransceiverConfigSealed> TransceiverConfig for T {}
+
+impl<T: Connectable> TransceiverConfigSealed for T {}
+
 pub trait TransceiverConfigExt: Connectable {
     fn open_connection(&self) -> Result<Connection, ConnectionError> {
         Ok(self.connect()?.connect_transceiver())
@@ -56,6 +64,7 @@ trait MessageTransceiver: Send + Sync + Into<Transceivers> {
     fn transmit_message(&self, msg: MavFrame<MavMessage>) -> Result<usize, MessageWriteError>;
 
     /// Opens a connection to the transceiver and returns a handle to it.
+    #[profiling::function]
     fn connect_transceiver(self) -> Connection {
         let running_flag = Arc::new(AtomicBool::new(true));
         let (tx, rx) = ring_channel(NonZero::new(MAX_STORED_MSGS).log_unwrap());
@@ -108,6 +117,7 @@ pub struct Connection {
 
 impl Connection {
     /// Retrieves and clears the stored messages.
+    #[profiling::function]
     pub fn retrieve_messages(&self) -> Result<Vec<TimedMessage>, CommunicationError> {
         // otherwise retrieve all messages from the buffer and return them
         let mut stored_msgs = Vec::new();
@@ -129,6 +139,7 @@ impl Connection {
     }
 
     /// Send a message over the serial connection.
+    #[profiling::function]
     pub fn send_message(&self, msg: MavFrame<MavMessage>) -> Result<(), CommunicationError> {
         self.endpoint.transmit_message(msg)?;
         Ok(())
diff --git a/src/communication/ethernet.rs b/src/communication/ethernet.rs
index 22ba29e..cafc2df 100644
--- a/src/communication/ethernet.rs
+++ b/src/communication/ethernet.rs
@@ -7,9 +7,7 @@ use skyward_mavlink::mavlink::{
 };
 use tracing::{debug, trace};
 
-use crate::mavlink::{
-        MAX_MSG_SIZE, MavMessage, TimedMessage, peek_reader::PeekReader,
-    };
+use crate::mavlink::{MAX_MSG_SIZE, MavMessage, TimedMessage, peek_reader::PeekReader};
 
 use super::{Connectable, ConnectionError, MessageTransceiver};
 
@@ -21,6 +19,7 @@ pub struct EthernetConfiguration {
 impl Connectable for EthernetConfiguration {
     type Connected = EthernetTransceiver;
 
+    #[profiling::function]
     fn connect(&self) -> Result<Self::Connected, ConnectionError> {
         let socket = UdpSocket::bind(format!("0.0.0.0:{}", self.port))?;
         debug!("Connected to Ethernet port on port {}", self.port);
@@ -34,6 +33,7 @@ pub struct EthernetTransceiver {
 }
 
 impl MessageTransceiver for EthernetTransceiver {
+    #[profiling::function]
     fn wait_for_message(&self) -> Result<TimedMessage, MessageReadError> {
         let mut buf = [0; MAX_MSG_SIZE];
         let read = self.socket.recv(&mut buf)?;
@@ -44,6 +44,7 @@ impl MessageTransceiver for EthernetTransceiver {
         Ok(TimedMessage::just_received(res))
     }
 
+    #[profiling::function]
     fn transmit_message(&self, msg: MavFrame<MavMessage>) -> Result<usize, MessageWriteError> {
         let MavFrame { header, msg, .. } = msg;
         let mut write_buf = Vec::new();
diff --git a/src/communication/serial.rs b/src/communication/serial.rs
index fd7a374..27c240c 100644
--- a/src/communication/serial.rs
+++ b/src/communication/serial.rs
@@ -4,9 +4,9 @@
 //! listing all available serial ports and finding the first serial port that
 //! contains "STM32" or "ST-LINK" in its product name.
 
-use std::sync::Mutex;
+use std::{sync::Mutex, time::Duration};
 
-use anyhow::Context;
+use egui::Id;
 use serialport::{SerialPort, SerialPortInfo, SerialPortType};
 use skyward_mavlink::mavlink::{
     MavFrame,
@@ -26,8 +26,9 @@ const SERIAL_PORT_TIMEOUT_MS: u64 = 100;
 pub const DEFAULT_BAUD_RATE: u32 = 115200;
 
 /// Get a list of all serial USB ports available on the system
-pub fn list_all_usb_ports() -> anyhow::Result<Vec<SerialPortInfo>> {
-    let ports = serialport::available_ports().context("No serial ports found!")?;
+#[profiling::function]
+pub fn list_all_usb_ports() -> Result<Vec<SerialPortInfo>, serialport::Error> {
+    let ports = serialport::available_ports()?;
     Ok(ports
         .into_iter()
         .filter(|p| matches!(p.port_type, SerialPortType::UsbPort(_)))
@@ -36,18 +37,19 @@ pub fn list_all_usb_ports() -> anyhow::Result<Vec<SerialPortInfo>> {
 
 /// Finds the first USB serial port with "STM32" or "ST-LINK" in its product name.
 /// Renamed from get_first_stm32_serial_port.
-pub fn find_first_stm32_port() -> Option<SerialPortInfo> {
-    let ports = list_all_usb_ports().log_unwrap();
+#[profiling::function]
+pub fn find_first_stm32_port() -> Result<Option<SerialPortInfo>, serialport::Error> {
+    let ports = list_all_usb_ports()?;
     for port in ports {
         if let serialport::SerialPortType::UsbPort(info) = &port.port_type {
             if let Some(p) = &info.product {
                 if p.contains("STM32") || p.contains("ST-LINK") {
-                    return Some(port);
+                    return Ok(Some(port));
                 }
             }
         }
     }
-    None
+    Ok(None)
 }
 
 #[derive(Debug, Clone)]
@@ -59,6 +61,7 @@ pub struct SerialConfiguration {
 impl Connectable for SerialConfiguration {
     type Connected = SerialTransceiver;
 
+    #[profiling::function]
     fn connect(&self) -> Result<Self::Connected, ConnectionError> {
         let port = serialport::new(&self.port_name, self.baud_rate)
             .timeout(std::time::Duration::from_millis(SERIAL_PORT_TIMEOUT_MS))
@@ -94,6 +97,7 @@ pub struct SerialTransceiver {
 }
 
 impl MessageTransceiver for SerialTransceiver {
+    #[profiling::function]
     fn wait_for_message(&self) -> Result<TimedMessage, MessageReadError> {
         loop {
             let res: Result<(_, MavMessage), MessageReadError> =
@@ -113,6 +117,7 @@ impl MessageTransceiver for SerialTransceiver {
         }
     }
 
+    #[profiling::function]
     fn transmit_message(&self, msg: MavFrame<MavMessage>) -> Result<usize, MessageWriteError> {
         let MavFrame { header, msg, .. } = msg;
         let written = write_v1_msg(&mut *self.serial_writer.lock().log_unwrap(), header, &msg)?;
diff --git a/src/ui.rs b/src/ui.rs
index 08be0bb..383fabd 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -1,4 +1,5 @@
 mod app;
+mod cache;
 mod panes;
 mod persistency;
 mod shortcuts;
diff --git a/src/ui/cache.rs b/src/ui/cache.rs
new file mode 100644
index 0000000..fdf52be
--- /dev/null
+++ b/src/ui/cache.rs
@@ -0,0 +1,60 @@
+use std::time::{Duration, Instant};
+
+use egui::Context;
+use serialport::SerialPortInfo;
+
+use crate::{communication, error::ErrInstrument};
+
+const SERIAL_PORT_REFRESH_INTERVAL: Duration = Duration::from_millis(500);
+
+fn call<T, F>(ctx: &egui::Context, id: egui::Id, fun: F, expiration_duration: Duration) -> T
+where
+    F: Fn() -> T,
+    T: Clone + Send + Sync + 'static,
+{
+    ctx.memory_mut(|m| {
+        match m.data.get_temp::<(T, Instant)>(id) {
+            None => {
+                m.data.insert_temp(id, (fun(), Instant::now()));
+            }
+            Some((_, i)) if i.elapsed() >= expiration_duration => {
+                m.data.insert_temp(id, (fun(), Instant::now()));
+            }
+            _ => {}
+        }
+        m.data.get_temp::<(T, Instant)>(id).log_unwrap().0
+    })
+}
+
+pub trait CacheCall {
+    fn call_cached<F, T>(&self, id: egui::Id, fun: F, expiration_duration: Duration) -> T
+    where
+        F: Fn() -> T,
+        T: Clone + Send + Sync + 'static;
+}
+
+impl CacheCall for egui::Context {
+    fn call_cached<F, T>(&self, id: egui::Id, fun: F, expiration_duration: Duration) -> T
+    where
+        F: Fn() -> T,
+        T: Clone + Send + Sync + 'static,
+    {
+        call(&self, id, fun, expiration_duration)
+    }
+}
+
+pub fn cached_list_all_usb_ports(ctx: &Context) -> Result<Vec<SerialPortInfo>, serialport::Error> {
+    ctx.call_cached(
+        egui::Id::new("list_usb_ports"),
+        communication::serial::list_all_usb_ports,
+        SERIAL_PORT_REFRESH_INTERVAL,
+    )
+}
+
+pub fn cached_first_stm32_port(ctx: &Context) -> Result<Option<SerialPortInfo>, serialport::Error> {
+    ctx.call_cached(
+        egui::Id::new("list_usb_ports"),
+        communication::serial::find_first_stm32_port,
+        SERIAL_PORT_REFRESH_INTERVAL,
+    )
+}
diff --git a/src/ui/utils.rs b/src/ui/utils.rs
index 7ad8631..8315193 100644
--- a/src/ui/utils.rs
+++ b/src/ui/utils.rs
@@ -1,3 +1,5 @@
+use std::time::{Duration, Instant};
+
 use egui::containers::Frame;
 use egui::{Response, Shadow, Stroke, Style, Ui};
 use egui_tiles::TileId;
diff --git a/src/ui/windows/connections.rs b/src/ui/windows/connections.rs
index bfa7293..7a5458b 100644
--- a/src/ui/windows/connections.rs
+++ b/src/ui/windows/connections.rs
@@ -1,83 +1,21 @@
-use egui::{Align2, Button, Color32, ComboBox, RichText, Vec2};
+use egui::{Align2, Button, ComboBox, Context, RichText, Vec2};
 use egui_extras::{Size, StripBuilder};
 use tracing::{error, warn};
 
 use crate::{
     communication::{
-        ConnectionError, EthernetConfiguration, SerialConfiguration,
-        serial::{DEFAULT_BAUD_RATE, find_first_stm32_port, list_all_usb_ports},
+        ConnectionError, EthernetConfiguration, SerialConfiguration, serial::DEFAULT_BAUD_RATE,
     },
     error::ErrInstrument,
     mavlink::DEFAULT_ETHERNET_PORT,
     message_broker::MessageBroker,
+    ui::cache::{cached_first_stm32_port, cached_list_all_usb_ports},
 };
 
-#[derive(Debug)]
-enum ConnectionConfig {
-    Ethernet(EthernetConfiguration),
-    Serial(Option<SerialConfiguration>),
-}
-
-impl ConnectionConfig {
-    fn default_ethernet() -> Self {
-        Self::Ethernet(EthernetConfiguration {
-            port: DEFAULT_ETHERNET_PORT,
-        })
-    }
-
-    fn default_serial() -> Self {
-        let port_name = find_first_stm32_port()
-            .map(|port| port.port_name)
-            .or(list_all_usb_ports()
-                .ok()
-                .and_then(|ports| ports.first().map(|port| port.port_name.clone())));
-        let Some(port_name) = port_name else {
-            warn!("USER ERROR: No serial port found");
-            return Self::Serial(None);
-        };
-        Self::Serial(Some(SerialConfiguration {
-            port_name,
-            baud_rate: DEFAULT_BAUD_RATE,
-        }))
-    }
-
-    fn is_valid(&self) -> bool {
-        match self {
-            Self::Ethernet(_) => true,
-            Self::Serial(Some(_)) => true,
-            Self::Serial(None) => false,
-        }
-    }
-
-    fn open_connection(&self, msg_broker: &mut MessageBroker) -> Result<(), ConnectionError> {
-        match self {
-            Self::Ethernet(config) => msg_broker.open_connection(config.clone()),
-            Self::Serial(Some(config)) => msg_broker.open_connection(config.clone()),
-            Self::Serial(None) => Err(ConnectionError::WrongConfiguration(
-                "No serial port found".to_string(),
-            )),
-        }
-    }
-}
-
-impl Default for ConnectionConfig {
-    fn default() -> Self {
-        Self::Ethernet(EthernetConfiguration {
-            port: DEFAULT_ETHERNET_PORT,
-        })
-    }
-}
-
-impl PartialEq for ConnectionConfig {
-    fn eq(&self, other: &Self) -> bool {
-        matches!(self, Self::Ethernet(_)) && matches!(other, Self::Ethernet(_))
-            || matches!(self, Self::Serial(_)) && matches!(other, Self::Serial(_))
-    }
-}
-
-#[derive(Debug, Default)]
+#[derive(Default)]
 pub struct ConnectionsWindow {
     pub visible: bool,
+    connection_kind: ConnectionKind,
     connection_config: ConnectionConfig,
 }
 
@@ -106,24 +44,31 @@ impl ConnectionsWindow {
         message_broker: &mut MessageBroker,
     ) {
         let ConnectionsWindow {
-            connection_config, ..
+            connection_kind,
+            connection_config,
+            ..
         } = self;
         ui.label("Select Source:");
         ui.horizontal_top(|ui| {
-            ui.radio_value(
-                connection_config,
-                ConnectionConfig::default_ethernet(),
-                "Ethernet",
-            );
-            ui.radio_value(
-                connection_config,
-                ConnectionConfig::default_serial(),
-                "Serial",
-            );
+            ui.radio_value(connection_kind, ConnectionKind::Ethernet, "Ethernet");
+            ui.radio_value(connection_kind, ConnectionKind::Serial, "Serial");
         });
 
         ui.separator();
 
+        match (connection_kind, &connection_config) {
+            (ConnectionKind::Ethernet, ConnectionConfig::Ethernet(_)) => {}
+            (ConnectionKind::Serial, ConnectionConfig::Serial(_)) => {}
+            (ConnectionKind::Ethernet, _) => {
+                *connection_config = ConnectionConfig::Ethernet(default_ethernet());
+            }
+            (ConnectionKind::Serial, _) => {
+                *connection_config = ConnectionConfig::Serial(
+                    default_serial(ui.ctx()).log_expect("USER ERROR: issues with serail ports"),
+                );
+            }
+        }
+
         match connection_config {
             ConnectionConfig::Ethernet(EthernetConfiguration { port }) => {
                 egui::Grid::new("grid")
@@ -141,7 +86,7 @@ impl ConnectionsWindow {
                     .spacing([10.0, 5.0])
                     .show(ui, |ui| {
                         ui.label("Serial Port:");
-                        match opt {
+                        match opt.as_mut() {
                             Some(SerialConfiguration {
                                 port_name,
                                 baud_rate,
@@ -149,7 +94,9 @@ impl ConnectionsWindow {
                                 ComboBox::from_id_salt("serial_port")
                                     .selected_text(port_name.as_str())
                                     .show_ui(ui, |ui| {
-                                        for available_port in list_all_usb_ports().log_unwrap() {
+                                        for available_port in
+                                            cached_list_all_usb_ports(ui.ctx()).log_unwrap()
+                                        {
                                             ui.selectable_value(
                                                 port_name,
                                                 available_port.port_name.clone(),
@@ -169,12 +116,9 @@ impl ConnectionsWindow {
                             None => {
                                 // in case of a serial connection missing
                                 warn!("USER ERROR: No serial port found");
-                                ui.label(
-                                    RichText::new("No port found")
-                                        .color(Color32::RED)
-                                        .underline()
-                                        .strong(),
-                                );
+                                ui.label(RichText::new("No port found").underline().strong());
+                                *opt = default_serial(ui.ctx())
+                                    .log_expect("USER ERROR: issues with serial ports");
                             }
                         }
 
@@ -217,3 +161,61 @@ impl ConnectionsWindow {
         });
     }
 }
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+enum ConnectionKind {
+    #[default]
+    Ethernet,
+    Serial,
+}
+
+#[derive(Debug, Clone)]
+pub enum ConnectionConfig {
+    Ethernet(EthernetConfiguration),
+    Serial(Option<SerialConfiguration>),
+}
+
+fn default_ethernet() -> EthernetConfiguration {
+    EthernetConfiguration {
+        port: DEFAULT_ETHERNET_PORT,
+    }
+}
+
+fn default_serial(ctx: &Context) -> Result<Option<SerialConfiguration>, serialport::Error> {
+    let port_name =
+        cached_first_stm32_port(ctx)?
+            .map(|port| port.port_name)
+            .or(cached_list_all_usb_ports(ctx)
+                .ok()
+                .and_then(|ports| ports.first().map(|port| port.port_name.clone())));
+    Ok(port_name.map(|port_name| SerialConfiguration {
+        port_name,
+        baud_rate: DEFAULT_BAUD_RATE,
+    }))
+}
+
+impl ConnectionConfig {
+    fn is_valid(&self) -> bool {
+        match self {
+            ConnectionConfig::Ethernet(_) => true,
+            ConnectionConfig::Serial(Some(_)) => true,
+            ConnectionConfig::Serial(None) => false,
+        }
+    }
+
+    fn open_connection(&self, msg_broker: &mut MessageBroker) -> Result<(), ConnectionError> {
+        match self {
+            Self::Ethernet(config) => msg_broker.open_connection(config.clone()),
+            Self::Serial(Some(config)) => msg_broker.open_connection(config.clone()),
+            Self::Serial(None) => Err(ConnectionError::WrongConfiguration(
+                "No serial port found".to_string(),
+            )),
+        }
+    }
+}
+
+impl Default for ConnectionConfig {
+    fn default() -> Self {
+        ConnectionConfig::Ethernet(default_ethernet())
+    }
+}
-- 
GitLab


From 96287274534854a7d692a39afa5058b4bf95f47f Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Thu, 6 Mar 2025 16:40:24 +0100
Subject: [PATCH 08/14] Documented code

---
 src/communication.rs          | 60 +++++++++++++++++++----------------
 src/communication/error.rs    |  6 ++++
 src/communication/ethernet.rs | 11 ++++++-
 src/communication/serial.rs   | 26 +++++++++------
 src/ui/cache.rs               | 34 +++++++++++++++++++-
 src/ui/utils.rs               |  2 --
 6 files changed, 98 insertions(+), 41 deletions(-)

diff --git a/src/communication.rs b/src/communication.rs
index 366f9a1..48e8bc3 100644
--- a/src/communication.rs
+++ b/src/communication.rs
@@ -1,9 +1,15 @@
+//! Main communication module.
+//!
+//! Provides a unified interface for handling message transmission and reception
+//! through different physical connection types (e.g., serial, Ethernet).
+//! It also manages connections and message buffering.
+
 mod error;
 pub mod ethernet;
 pub mod serial;
 
 use std::{
-    num::NonZero,
+    num::NonZeroUsize,
     sync::{
         Arc,
         atomic::{AtomicBool, Ordering},
@@ -30,50 +36,54 @@ pub use error::{CommunicationError, ConnectionError};
 pub use ethernet::EthernetConfiguration;
 pub use serial::SerialConfiguration;
 
-const MAX_STORED_MSGS: usize = 1000; // 192 bytes each = 192 KB
+const MAX_STORED_MSGS: usize = 1000; // e.g., 192 bytes each = 192 KB
 
+/// Trait to abstract common configuration types.
 pub trait TransceiverConfig: TransceiverConfigSealed {}
-
 trait TransceiverConfigSealed {}
-
 impl<T: TransceiverConfigSealed> TransceiverConfig for T {}
-
 impl<T: Connectable> TransceiverConfigSealed for T {}
 
+/// Extension trait to open a connection directly from a configuration.
 pub trait TransceiverConfigExt: Connectable {
+    /// Opens a connection and returns a handle to it.
     fn open_connection(&self) -> Result<Connection, ConnectionError> {
-        Ok(self.connect()?.connect_transceiver())
+        Ok(self.connect()?.open_listening_connection())
     }
 }
-
 impl<T: Connectable> TransceiverConfigExt for T {}
 
+/// Trait representing an entity that can be connected.
 trait Connectable {
     type Connected: MessageTransceiver;
 
+    /// Establishes a connection based on the configuration.
     fn connect(&self) -> Result<Self::Connected, ConnectionError>;
 }
 
+/// Trait representing a message transceiver.
+/// This trait abstracts the common operations for message transmission and reception.
+/// It also provides a default implementation for opening a listening connection, while
+/// being transparent to the actual Transceiver type.
 #[enum_dispatch(Transceivers)]
 trait MessageTransceiver: Send + Sync + Into<Transceivers> {
-    /// Reads a message from the serial port, blocking until a valid message is received.
-    /// This method ignores timeout errors and continues trying.
+    /// Blocks until a valid message is received.
     fn wait_for_message(&self) -> Result<TimedMessage, MessageReadError>;
 
-    /// Transmits a message over the serial connection.
+    /// Transmits a message using the connection.
     fn transmit_message(&self, msg: MavFrame<MavMessage>) -> Result<usize, MessageWriteError>;
 
-    /// Opens a connection to the transceiver and returns a handle to it.
+    /// Opens a listening connection and spawns a thread for message handling.
     #[profiling::function]
-    fn connect_transceiver(self) -> Connection {
+    fn open_listening_connection(self) -> Connection {
         let running_flag = Arc::new(AtomicBool::new(true));
-        let (tx, rx) = ring_channel(NonZero::new(MAX_STORED_MSGS).log_unwrap());
+        let (tx, rx) = ring_channel(NonZeroUsize::new(MAX_STORED_MSGS).log_unwrap());
         let endpoint_inner = Arc::new(self.into());
 
         {
             let running_flag = running_flag.clone();
             let endpoint_inner = endpoint_inner.clone();
-            // detach the thread, to see errors rely on logs
+            // Detached thread for message handling; errors are logged.
             let _ = std::thread::spawn(move || {
                 while running_flag.load(Ordering::Relaxed) {
                     match endpoint_inner.wait_for_message() {
@@ -96,40 +106,36 @@ trait MessageTransceiver: Send + Sync + Into<Transceivers> {
         }
 
         Connection {
-            endpoint: endpoint_inner,
+            transceiver: endpoint_inner,
             rx_ring_channel: rx,
             running_flag,
         }
     }
 }
 
+/// Enum representing the different types of transceivers.
 #[enum_dispatch]
 enum Transceivers {
     Serial(SerialTransceiver),
     Ethernet(EthernetTransceiver),
 }
 
+/// Represents an active connection with buffered messages.
 pub struct Connection {
-    endpoint: Arc<Transceivers>,
+    transceiver: Arc<Transceivers>,
     rx_ring_channel: RingReceiver<TimedMessage>,
     running_flag: Arc<AtomicBool>,
 }
 
 impl Connection {
-    /// Retrieves and clears the stored messages.
+    /// Retrieves and clears stored messages.
     #[profiling::function]
     pub fn retrieve_messages(&self) -> Result<Vec<TimedMessage>, CommunicationError> {
-        // otherwise retrieve all messages from the buffer and return them
         let mut stored_msgs = Vec::new();
         loop {
             match self.rx_ring_channel.try_recv() {
-                Ok(msg) => {
-                    // Store the message in the buffer.
-                    stored_msgs.push(msg);
-                }
-                Err(TryRecvError::Empty) => {
-                    break;
-                }
+                Ok(msg) => stored_msgs.push(msg),
+                Err(TryRecvError::Empty) => break,
                 Err(TryRecvError::Disconnected) => {
                     return Err(CommunicationError::ConnectionClosed);
                 }
@@ -138,10 +144,10 @@ impl Connection {
         Ok(stored_msgs)
     }
 
-    /// Send a message over the serial connection.
+    /// Sends a message over the connection.
     #[profiling::function]
     pub fn send_message(&self, msg: MavFrame<MavMessage>) -> Result<(), CommunicationError> {
-        self.endpoint.transmit_message(msg)?;
+        self.transceiver.transmit_message(msg)?;
         Ok(())
     }
 }
diff --git a/src/communication/error.rs b/src/communication/error.rs
index 50a12da..42b0f1c 100644
--- a/src/communication/error.rs
+++ b/src/communication/error.rs
@@ -1,6 +1,11 @@
+//! Error handling for communication modules.
+//!
+//! Contains definitions for errors that can occur during serial or Ethernet communication.
+
 use skyward_mavlink::mavlink::error::MessageWriteError;
 use thiserror::Error;
 
+/// Represents communication errors.
 #[derive(Debug, Error)]
 pub enum CommunicationError {
     #[error("IO error: {0}")]
@@ -9,6 +14,7 @@ pub enum CommunicationError {
     ConnectionClosed,
 }
 
+/// Represents errors during connection setup.
 #[derive(Debug, Error)]
 pub enum ConnectionError {
     #[error("Wrong configuration: {0}")]
diff --git a/src/communication/ethernet.rs b/src/communication/ethernet.rs
index cafc2df..f9316b8 100644
--- a/src/communication/ethernet.rs
+++ b/src/communication/ethernet.rs
@@ -1,3 +1,8 @@
+//! Ethernet utilities module.
+//!
+//! Provides functionality to connect via Ethernet using UDP, allowing message
+//! transmission and reception over a network.
+
 use std::net::UdpSocket;
 
 use skyward_mavlink::mavlink::{
@@ -11,6 +16,7 @@ use crate::mavlink::{MAX_MSG_SIZE, MavMessage, TimedMessage, peek_reader::PeekRe
 
 use super::{Connectable, ConnectionError, MessageTransceiver};
 
+/// Configuration for an Ethernet connection.
 #[derive(Debug, Clone)]
 pub struct EthernetConfiguration {
     pub port: u16,
@@ -19,6 +25,7 @@ pub struct EthernetConfiguration {
 impl Connectable for EthernetConfiguration {
     type Connected = EthernetTransceiver;
 
+    /// Binds to the specified UDP port to create a network connection.
     #[profiling::function]
     fn connect(&self) -> Result<Self::Connected, ConnectionError> {
         let socket = UdpSocket::bind(format!("0.0.0.0:{}", self.port))?;
@@ -27,12 +34,13 @@ impl Connectable for EthernetConfiguration {
     }
 }
 
-/// Manages a connection to a Ethernet port.
+/// Manages a connection over Ethernet.
 pub struct EthernetTransceiver {
     socket: UdpSocket,
 }
 
 impl MessageTransceiver for EthernetTransceiver {
+    /// Waits for a message over Ethernet, blocking until a valid message arrives.
     #[profiling::function]
     fn wait_for_message(&self) -> Result<TimedMessage, MessageReadError> {
         let mut buf = [0; MAX_MSG_SIZE];
@@ -44,6 +52,7 @@ impl MessageTransceiver for EthernetTransceiver {
         Ok(TimedMessage::just_received(res))
     }
 
+    /// Transmits a message using the UDP socket.
     #[profiling::function]
     fn transmit_message(&self, msg: MavFrame<MavMessage>) -> Result<usize, MessageWriteError> {
         let MavFrame { header, msg, .. } = msg;
diff --git a/src/communication/serial.rs b/src/communication/serial.rs
index 27c240c..d36b82c 100644
--- a/src/communication/serial.rs
+++ b/src/communication/serial.rs
@@ -1,12 +1,10 @@
-//! Serial port utilities
+//! Serial port utilities module.
 //!
-//! This module provides utilities for working with serial ports, such as
-//! listing all available serial ports and finding the first serial port that
-//! contains "STM32" or "ST-LINK" in its product name.
+//! Provides functions for listing USB serial ports, finding a STM32 port,
+//! and handling serial connections including message transmission and reception.
 
-use std::{sync::Mutex, time::Duration};
+use std::sync::Mutex;
 
-use egui::Id;
 use serialport::{SerialPort, SerialPortInfo, SerialPortType};
 use skyward_mavlink::mavlink::{
     MavFrame,
@@ -25,7 +23,10 @@ use super::{Connectable, ConnectionError, MessageTransceiver};
 const SERIAL_PORT_TIMEOUT_MS: u64 = 100;
 pub const DEFAULT_BAUD_RATE: u32 = 115200;
 
-/// Get a list of all serial USB ports available on the system
+/// Returns a list of all USB serial ports available on the system.
+///
+/// # Returns
+/// * `Ok(Vec<SerialPortInfo>)` if ports are found or an error otherwise.
 #[profiling::function]
 pub fn list_all_usb_ports() -> Result<Vec<SerialPortInfo>, serialport::Error> {
     let ports = serialport::available_ports()?;
@@ -35,8 +36,10 @@ pub fn list_all_usb_ports() -> Result<Vec<SerialPortInfo>, serialport::Error> {
         .collect())
 }
 
-/// Finds the first USB serial port with "STM32" or "ST-LINK" in its product name.
-/// Renamed from get_first_stm32_serial_port.
+/// Finds the first USB serial port whose product name contains "STM32" or "ST-LINK".
+///
+/// # Returns
+/// * `Ok(Some(SerialPortInfo))` if a matching port is found, `Ok(None)` otherwise.
 #[profiling::function]
 pub fn find_first_stm32_port() -> Result<Option<SerialPortInfo>, serialport::Error> {
     let ports = list_all_usb_ports()?;
@@ -52,6 +55,7 @@ pub fn find_first_stm32_port() -> Result<Option<SerialPortInfo>, serialport::Err
     Ok(None)
 }
 
+/// Configuration for a serial connection.
 #[derive(Debug, Clone)]
 pub struct SerialConfiguration {
     pub port_name: String,
@@ -61,6 +65,7 @@ pub struct SerialConfiguration {
 impl Connectable for SerialConfiguration {
     type Connected = SerialTransceiver;
 
+    /// Connects using the serial port configuration.
     #[profiling::function]
     fn connect(&self) -> Result<Self::Connected, ConnectionError> {
         let port = serialport::new(&self.port_name, self.baud_rate)
@@ -97,6 +102,7 @@ pub struct SerialTransceiver {
 }
 
 impl MessageTransceiver for SerialTransceiver {
+    /// Blocks until a valid message is received from the serial port.
     #[profiling::function]
     fn wait_for_message(&self) -> Result<TimedMessage, MessageReadError> {
         loop {
@@ -107,7 +113,6 @@ impl MessageTransceiver for SerialTransceiver {
                     return Ok(TimedMessage::just_received(msg));
                 }
                 Err(MessageReadError::Io(e)) if e.kind() == std::io::ErrorKind::TimedOut => {
-                    // Ignore timeouts.
                     continue;
                 }
                 Err(e) => {
@@ -117,6 +122,7 @@ impl MessageTransceiver for SerialTransceiver {
         }
     }
 
+    /// Transmits a message via the serial connection.
     #[profiling::function]
     fn transmit_message(&self, msg: MavFrame<MavMessage>) -> Result<usize, MessageWriteError> {
         let MavFrame { header, msg, .. } = msg;
diff --git a/src/ui/cache.rs b/src/ui/cache.rs
index fdf52be..46b29cc 100644
--- a/src/ui/cache.rs
+++ b/src/ui/cache.rs
@@ -1,3 +1,6 @@
+//! Module for caching expensive UI calls using egui's temporary memory storage.
+//! It provides utilities for caching the results of functions to avoid frequent recalculations.
+
 use std::time::{Duration, Instant};
 
 use egui::Context;
@@ -7,6 +10,13 @@ use crate::{communication, error::ErrInstrument};
 
 const SERIAL_PORT_REFRESH_INTERVAL: Duration = Duration::from_millis(500);
 
+/// Internal helper function that caches the result of a given function call for a specified duration.
+///
+/// # Arguments
+/// * `ctx` - The egui context used for caching.
+/// * `id` - The unique identifier for the cached item.
+/// * `fun` - The function whose return value is to be cached.
+/// * `expiration_duration` - The duration after which the cache should be refreshed.
 fn call<T, F>(ctx: &egui::Context, id: egui::Id, fun: F, expiration_duration: Duration) -> T
 where
     F: Fn() -> T,
@@ -26,7 +36,14 @@ where
     })
 }
 
+/// A trait to extend egui's Context with a caching function.
 pub trait CacheCall {
+    /// Calls the provided function and caches its result.
+    ///
+    /// # Arguments
+    /// * `id` - A unique identifier for the cached value.
+    /// * `fun` - The function to be cached.
+    /// * `expiration_duration` - The cache expiration duration.
     fn call_cached<F, T>(&self, id: egui::Id, fun: F, expiration_duration: Duration) -> T
     where
         F: Fn() -> T,
@@ -34,15 +51,23 @@ pub trait CacheCall {
 }
 
 impl CacheCall for egui::Context {
+    /// Implements the caching call using the internal `call` function.
     fn call_cached<F, T>(&self, id: egui::Id, fun: F, expiration_duration: Duration) -> T
     where
         F: Fn() -> T,
         T: Clone + Send + Sync + 'static,
     {
-        call(&self, id, fun, expiration_duration)
+        call(self, id, fun, expiration_duration)
     }
 }
 
+/// Returns a cached list of all available USB ports.
+///
+/// # Arguments
+/// * `ctx` - The egui context used for caching.
+///
+/// # Returns
+/// * A Result containing a vector of `SerialPortInfo` or a `serialport::Error`.
 pub fn cached_list_all_usb_ports(ctx: &Context) -> Result<Vec<SerialPortInfo>, serialport::Error> {
     ctx.call_cached(
         egui::Id::new("list_usb_ports"),
@@ -51,6 +76,13 @@ pub fn cached_list_all_usb_ports(ctx: &Context) -> Result<Vec<SerialPortInfo>, s
     )
 }
 
+/// Returns the first cached STM32 port found, if any.
+///
+/// # Arguments
+/// * `ctx` - The egui context used for caching.
+///
+/// # Returns
+/// * A Result containing an Option of `SerialPortInfo` or a `serialport::Error`.
 pub fn cached_first_stm32_port(ctx: &Context) -> Result<Option<SerialPortInfo>, serialport::Error> {
     ctx.call_cached(
         egui::Id::new("list_usb_ports"),
diff --git a/src/ui/utils.rs b/src/ui/utils.rs
index 8315193..7ad8631 100644
--- a/src/ui/utils.rs
+++ b/src/ui/utils.rs
@@ -1,5 +1,3 @@
-use std::time::{Duration, Instant};
-
 use egui::containers::Frame;
 use egui::{Response, Shadow, Stroke, Style, Ui};
 use egui_tiles::TileId;
-- 
GitLab


From 12558c28466f2122cd42750392c2b42471dd38dd Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Thu, 6 Mar 2025 16:55:47 +0100
Subject: [PATCH 09/14] Added sealed module to suppress warnings

---
 src/communication.rs          | 198 ++++++++++++++++++----------------
 src/communication/ethernet.rs |   5 +-
 src/communication/serial.rs   |   5 +-
 3 files changed, 115 insertions(+), 93 deletions(-)

diff --git a/src/communication.rs b/src/communication.rs
index 48e8bc3..110853d 100644
--- a/src/communication.rs
+++ b/src/communication.rs
@@ -8,28 +8,16 @@ mod error;
 pub mod ethernet;
 pub mod serial;
 
-use std::{
-    num::NonZeroUsize,
-    sync::{
-        Arc,
-        atomic::{AtomicBool, Ordering},
-    },
+use std::sync::{
+    Arc,
+    atomic::{AtomicBool, Ordering},
 };
 
-use enum_dispatch::enum_dispatch;
-use ring_channel::{RingReceiver, TryRecvError, ring_channel};
-use skyward_mavlink::mavlink::{
-    MavFrame,
-    error::{MessageReadError, MessageWriteError},
-};
-
-use crate::{
-    error::ErrInstrument,
-    mavlink::{MavMessage, TimedMessage},
-};
+use ring_channel::{RingReceiver, TryRecvError};
+use sealed::MessageTransceiver;
+use skyward_mavlink::mavlink::MavFrame;
 
-use ethernet::EthernetTransceiver;
-use serial::SerialTransceiver;
+use crate::mavlink::{MavMessage, TimedMessage};
 
 // Re-exports
 pub use error::{CommunicationError, ConnectionError};
@@ -38,91 +26,119 @@ pub use serial::SerialConfiguration;
 
 const MAX_STORED_MSGS: usize = 1000; // e.g., 192 bytes each = 192 KB
 
-/// Trait to abstract common configuration types.
-pub trait TransceiverConfig: TransceiverConfigSealed {}
-trait TransceiverConfigSealed {}
-impl<T: TransceiverConfigSealed> TransceiverConfig for T {}
-impl<T: Connectable> TransceiverConfigSealed for T {}
-
-/// Extension trait to open a connection directly from a configuration.
-pub trait TransceiverConfigExt: Connectable {
-    /// Opens a connection and returns a handle to it.
-    fn open_connection(&self) -> Result<Connection, ConnectionError> {
-        Ok(self.connect()?.open_listening_connection())
+mod sealed {
+    use std::{
+        num::NonZeroUsize,
+        sync::{
+            Arc,
+            atomic::{AtomicBool, Ordering},
+        },
+    };
+
+    use enum_dispatch::enum_dispatch;
+    use ring_channel::ring_channel;
+    use skyward_mavlink::mavlink::{
+        MavFrame,
+        error::{MessageReadError, MessageWriteError},
+    };
+
+    use crate::{
+        error::ErrInstrument,
+        mavlink::{MavMessage, TimedMessage},
+    };
+
+    use super::{
+        CommunicationError, Connection, ConnectionError, MAX_STORED_MSGS,
+        ethernet::EthernetTransceiver, serial::SerialTransceiver,
+    };
+
+    pub trait TransceiverConfigSealed {}
+
+    /// Trait representing an entity that can be connected.
+    pub trait Connectable {
+        type Connected: MessageTransceiver;
+
+        /// Establishes a connection based on the configuration.
+        fn connect(&self) -> Result<Self::Connected, ConnectionError>;
     }
-}
-impl<T: Connectable> TransceiverConfigExt for T {}
-
-/// Trait representing an entity that can be connected.
-trait Connectable {
-    type Connected: MessageTransceiver;
-
-    /// Establishes a connection based on the configuration.
-    fn connect(&self) -> Result<Self::Connected, ConnectionError>;
-}
-
-/// Trait representing a message transceiver.
-/// This trait abstracts the common operations for message transmission and reception.
-/// It also provides a default implementation for opening a listening connection, while
-/// being transparent to the actual Transceiver type.
-#[enum_dispatch(Transceivers)]
-trait MessageTransceiver: Send + Sync + Into<Transceivers> {
-    /// Blocks until a valid message is received.
-    fn wait_for_message(&self) -> Result<TimedMessage, MessageReadError>;
 
-    /// Transmits a message using the connection.
-    fn transmit_message(&self, msg: MavFrame<MavMessage>) -> Result<usize, MessageWriteError>;
-
-    /// Opens a listening connection and spawns a thread for message handling.
-    #[profiling::function]
-    fn open_listening_connection(self) -> Connection {
-        let running_flag = Arc::new(AtomicBool::new(true));
-        let (tx, rx) = ring_channel(NonZeroUsize::new(MAX_STORED_MSGS).log_unwrap());
-        let endpoint_inner = Arc::new(self.into());
-
-        {
-            let running_flag = running_flag.clone();
-            let endpoint_inner = endpoint_inner.clone();
-            // Detached thread for message handling; errors are logged.
-            let _ = std::thread::spawn(move || {
-                while running_flag.load(Ordering::Relaxed) {
-                    match endpoint_inner.wait_for_message() {
-                        Ok(msg) => {
-                            tx.send(msg)
-                                .map_err(|_| CommunicationError::ConnectionClosed)?;
-                        }
-                        Err(MessageReadError::Io(e)) => {
-                            tracing::error!("Failed to read message: {e:#?}");
-                            running_flag.store(false, Ordering::Relaxed);
-                            return Err(CommunicationError::Io(e));
-                        }
-                        Err(MessageReadError::Parse(e)) => {
-                            tracing::error!("Failed to read message: {e:#?}");
+    /// Trait representing a message transceiver.
+    /// This trait abstracts the common operations for message transmission and reception.
+    /// It also provides a default implementation for opening a listening connection, while
+    /// being transparent to the actual Transceiver type.
+    #[enum_dispatch(Transceivers)]
+    pub trait MessageTransceiver: Send + Sync + Into<Transceivers> {
+        /// Blocks until a valid message is received.
+        fn wait_for_message(&self) -> Result<TimedMessage, MessageReadError>;
+
+        /// Transmits a message using the connection.
+        fn transmit_message(&self, msg: MavFrame<MavMessage>) -> Result<usize, MessageWriteError>;
+
+        /// Opens a listening connection and spawns a thread for message handling.
+        #[profiling::function]
+        fn open_listening_connection(self) -> Connection {
+            let running_flag = Arc::new(AtomicBool::new(true));
+            let (tx, rx) = ring_channel(NonZeroUsize::new(MAX_STORED_MSGS).log_unwrap());
+            let endpoint_inner = Arc::new(self.into());
+
+            {
+                let running_flag = running_flag.clone();
+                let endpoint_inner = endpoint_inner.clone();
+                // Detached thread for message handling; errors are logged.
+                let _ = std::thread::spawn(move || {
+                    while running_flag.load(Ordering::Relaxed) {
+                        match endpoint_inner.wait_for_message() {
+                            Ok(msg) => {
+                                tx.send(msg)
+                                    .map_err(|_| CommunicationError::ConnectionClosed)?;
+                            }
+                            Err(MessageReadError::Io(e)) => {
+                                tracing::error!("Failed to read message: {e:#?}");
+                                running_flag.store(false, Ordering::Relaxed);
+                                return Err(CommunicationError::Io(e));
+                            }
+                            Err(MessageReadError::Parse(e)) => {
+                                tracing::error!("Failed to read message: {e:#?}");
+                            }
                         }
                     }
-                }
-                Ok(())
-            });
-        }
+                    Ok(())
+                });
+            }
 
-        Connection {
-            transceiver: endpoint_inner,
-            rx_ring_channel: rx,
-            running_flag,
+            Connection {
+                transceiver: endpoint_inner,
+                rx_ring_channel: rx,
+                running_flag,
+            }
         }
     }
+
+    /// Enum representing the different types of transceivers.
+    #[enum_dispatch]
+    pub(super) enum Transceivers {
+        Serial(SerialTransceiver),
+        Ethernet(EthernetTransceiver),
+    }
 }
 
-/// Enum representing the different types of transceivers.
-#[enum_dispatch]
-enum Transceivers {
-    Serial(SerialTransceiver),
-    Ethernet(EthernetTransceiver),
+/// Trait to abstract common configuration types.
+pub trait TransceiverConfig: sealed::TransceiverConfigSealed {}
+impl<T: sealed::TransceiverConfigSealed> TransceiverConfig for T {}
+impl<T: sealed::Connectable> sealed::TransceiverConfigSealed for T {}
+
+/// Extension trait to open a connection directly from a configuration.
+pub trait TransceiverConfigExt: sealed::Connectable {
+    /// Opens a connection and returns a handle to it.
+    fn open_connection(&self) -> Result<Connection, ConnectionError> {
+        Ok(self.connect()?.open_listening_connection())
+    }
 }
+impl<T: sealed::Connectable> TransceiverConfigExt for T {}
 
 /// Represents an active connection with buffered messages.
 pub struct Connection {
-    transceiver: Arc<Transceivers>,
+    transceiver: Arc<sealed::Transceivers>,
     rx_ring_channel: RingReceiver<TimedMessage>,
     running_flag: Arc<AtomicBool>,
 }
diff --git a/src/communication/ethernet.rs b/src/communication/ethernet.rs
index f9316b8..f999ff8 100644
--- a/src/communication/ethernet.rs
+++ b/src/communication/ethernet.rs
@@ -14,7 +14,10 @@ use tracing::{debug, trace};
 
 use crate::mavlink::{MAX_MSG_SIZE, MavMessage, TimedMessage, peek_reader::PeekReader};
 
-use super::{Connectable, ConnectionError, MessageTransceiver};
+use super::{
+    ConnectionError,
+    sealed::{Connectable, MessageTransceiver},
+};
 
 /// Configuration for an Ethernet connection.
 #[derive(Debug, Clone)]
diff --git a/src/communication/serial.rs b/src/communication/serial.rs
index d36b82c..3a9d798 100644
--- a/src/communication/serial.rs
+++ b/src/communication/serial.rs
@@ -18,7 +18,10 @@ use crate::{
     mavlink::{MavMessage, TimedMessage, peek_reader::PeekReader},
 };
 
-use super::{Connectable, ConnectionError, MessageTransceiver};
+use super::{
+    ConnectionError,
+    sealed::{Connectable, MessageTransceiver},
+};
 
 const SERIAL_PORT_TIMEOUT_MS: u64 = 100;
 pub const DEFAULT_BAUD_RATE: u32 = 115200;
-- 
GitLab


From 1f0ca358935b5d9a6a4f70800d9fe4cc28da788c Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Thu, 6 Mar 2025 17:49:57 +0100
Subject: [PATCH 10/14] [plot] moved window creation at the end of the loop

---
 src/ui/panes/plot.rs | 26 +++++++++++++-------------
 1 file changed, 13 insertions(+), 13 deletions(-)

diff --git a/src/ui/panes/plot.rs b/src/ui/panes/plot.rs
index da202dd..e83bd2f 100644
--- a/src/ui/panes/plot.rs
+++ b/src/ui/panes/plot.rs
@@ -42,19 +42,6 @@ impl PaneBehavior for Plot2DPane {
     fn ui(&mut self, ui: &mut egui::Ui, _: TileId) -> PaneResponse {
         let mut response = PaneResponse::default();
 
-        let mut settings = SourceSettings::new(&mut self.settings, &mut self.line_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));
-
-        if settings.are_sources_changed() {
-            self.state_valid = false;
-        }
-
         let ctrl_pressed = ui.input(|i| i.modifiers.ctrl);
 
         egui_plot::Plot::new("plot")
@@ -81,6 +68,19 @@ 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);
+        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));
+
+        if settings.are_sources_changed() {
+            self.state_valid = false;
+        }
+
         response
     }
 
-- 
GitLab


From fa3b205b23aba5128cdd7452b1feac5d880b4c61 Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Thu, 6 Mar 2025 17:50:12 +0100
Subject: [PATCH 11/14] implemented send message mechanism

---
 src/communication.rs          | 11 ++++++++-
 src/communication/ethernet.rs | 12 +++++++---
 src/message_broker.rs         | 45 +++++++++++++++++++++++++++++------
 src/ui/app.rs                 | 30 ++++++++++++++++++++---
 src/ui/panes.rs               | 11 ++++++++-
 src/ui/windows/connections.rs | 15 ++++++++----
 6 files changed, 105 insertions(+), 19 deletions(-)

diff --git a/src/communication.rs b/src/communication.rs
index 110853d..f4d74ab 100644
--- a/src/communication.rs
+++ b/src/communication.rs
@@ -114,6 +114,8 @@ mod sealed {
         }
     }
 
+    impl<T: Connectable> TransceiverConfigSealed for T {}
+
     /// Enum representing the different types of transceivers.
     #[enum_dispatch]
     pub(super) enum Transceivers {
@@ -125,7 +127,6 @@ mod sealed {
 /// Trait to abstract common configuration types.
 pub trait TransceiverConfig: sealed::TransceiverConfigSealed {}
 impl<T: sealed::TransceiverConfigSealed> TransceiverConfig for T {}
-impl<T: sealed::Connectable> sealed::TransceiverConfigSealed for T {}
 
 /// Extension trait to open a connection directly from a configuration.
 pub trait TransceiverConfigExt: sealed::Connectable {
@@ -168,6 +169,14 @@ impl Connection {
     }
 }
 
+impl std::fmt::Debug for Connection {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("Connection")
+            .field("running_flag", &self.running_flag)
+            .finish()
+    }
+}
+
 impl Drop for Connection {
     fn drop(&mut self) {
         self.running_flag.store(false, Ordering::Relaxed);
diff --git a/src/communication/ethernet.rs b/src/communication/ethernet.rs
index f999ff8..17ace52 100644
--- a/src/communication/ethernet.rs
+++ b/src/communication/ethernet.rs
@@ -22,7 +22,8 @@ use super::{
 /// Configuration for an Ethernet connection.
 #[derive(Debug, Clone)]
 pub struct EthernetConfiguration {
-    pub port: u16,
+    pub recv_port: u16,
+    pub send_port: u16,
 }
 
 impl Connectable for EthernetConfiguration {
@@ -31,8 +32,13 @@ impl Connectable for EthernetConfiguration {
     /// Binds to the specified UDP port to create a network connection.
     #[profiling::function]
     fn connect(&self) -> Result<Self::Connected, ConnectionError> {
-        let socket = UdpSocket::bind(format!("0.0.0.0:{}", self.port))?;
-        debug!("Connected to Ethernet port on port {}", self.port);
+        let recv_addr = format!("0.0.0.0:{}", self.recv_port);
+        let send_addr = format!("255.255.255.255:{}", self.send_port);
+        let socket = UdpSocket::bind(recv_addr)?;
+        debug!("Bound to Ethernet port on port {}", self.recv_port);
+        socket.set_broadcast(true)?;
+        socket.connect(send_addr)?;
+        debug!("Connected to Ethernet port on port {}", self.send_port);
         Ok(EthernetTransceiver { socket })
     }
 }
diff --git a/src/message_broker.rs b/src/message_broker.rs
index 982222a..7cf10e7 100644
--- a/src/message_broker.rs
+++ b/src/message_broker.rs
@@ -10,18 +10,24 @@ mod reception_queue;
 pub use message_bundle::MessageBundle;
 use reception_queue::ReceptionQueue;
 
-use crate::{
-    communication::{Connection, ConnectionError, TransceiverConfigExt},
-    error::ErrInstrument,
-    mavlink::{Message, TimedMessage},
-};
 use std::{
     collections::HashMap,
     sync::{Arc, Mutex},
     time::Duration,
 };
+
 use tracing::error;
 
+use crate::{
+    communication::{Connection, ConnectionError, TransceiverConfigExt},
+    error::ErrInstrument,
+    mavlink::{MavFrame, MavHeader, MavMessage, MavlinkVersion, Message, TimedMessage},
+};
+
+const RECEPTION_QUEUE_INTERVAL: Duration = Duration::from_secs(1);
+const SEGS_SYSTEM_ID: u8 = 1;
+const SEGS_COMPONENT_ID: u8 = 1;
+
 /// The MessageBroker struct contains the state of the message broker.
 ///
 /// It is responsible for receiving messages from the Mavlink listener and
@@ -43,7 +49,7 @@ impl MessageBroker {
         Self {
             messages: HashMap::new(),
             // TODO: make this configurable
-            last_receptions: Arc::new(Mutex::new(ReceptionQueue::new(Duration::from_secs(1)))),
+            last_receptions: Arc::new(Mutex::new(ReceptionQueue::new(RECEPTION_QUEUE_INTERVAL))),
             connection: None,
             ctx,
         }
@@ -88,7 +94,8 @@ impl MessageBroker {
 
     /// Processes incoming network messages. New messages are added to the
     /// given `MessageBundle`.
-    pub fn process_messages(&mut self, bundle: &mut MessageBundle) {
+    #[profiling::function]
+    pub fn process_incoming_messages(&mut self, bundle: &mut MessageBundle) {
         // process messages only if the connection is open
         if let Some(connection) = &self.connection {
             // check for communication errors, and log them
@@ -117,6 +124,30 @@ impl MessageBroker {
         }
     }
 
+    /// Processes outgoing messages.
+    /// WARNING: This methods blocks the UI, thus a detailed profiling is needed.
+    /// FIXME
+    #[profiling::function]
+    pub fn process_outgoing_messages(&mut self, messages: Vec<MavMessage>) {
+        if let Some(connection) = &self.connection {
+            for msg in messages {
+                let header = MavHeader {
+                    system_id: SEGS_SYSTEM_ID,
+                    component_id: SEGS_COMPONENT_ID,
+                    ..Default::default()
+                };
+                let frame = MavFrame {
+                    header,
+                    msg,
+                    protocol_version: MavlinkVersion::V1,
+                };
+                if let Err(e) = connection.send_message(frame) {
+                    error!("Error while transmitting message: {:?}", e);
+                }
+            }
+        }
+    }
+
     // 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)
 }
diff --git a/src/ui/app.rs b/src/ui/app.rs
index 39bc126..632d259 100644
--- a/src/ui/app.rs
+++ b/src/ui/app.rs
@@ -11,6 +11,7 @@ use tracing::{debug, error, trace};
 
 use crate::{
     error::ErrInstrument,
+    mavlink::MavMessage,
     message_broker::{MessageBroker, MessageBundle},
 };
 
@@ -43,7 +44,7 @@ pub struct App {
 impl eframe::App for App {
     // The update function is called each time the UI needs repainting!
     fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
-        self.process_messages();
+        self.process_incoming_messages();
 
         let panes_tree = &mut self.state.panes_tree;
 
@@ -240,6 +241,9 @@ impl eframe::App for App {
             self.behavior.action = Some(action);
         }
 
+        // Process outgoing messages
+        self.process_outgoing_messages();
+
         // Used for the profiler
         profiling::finish_frame!();
 
@@ -286,11 +290,11 @@ impl App {
 
     /// Retrieves new messages from the message broker and dispatches them to the panes.
     #[profiling::function]
-    fn process_messages(&mut self) {
+    fn process_incoming_messages(&mut self) {
         let start = Instant::now();
 
         self.message_broker
-            .process_messages(&mut self.message_bundle);
+            .process_incoming_messages(&mut self.message_bundle);
 
         // Skip updating the panes if there are no messages
         let count = self.message_bundle.count();
@@ -325,6 +329,26 @@ impl App {
         );
         self.message_bundle.reset();
     }
+
+    /// Sends outgoing messages from the panes to the message broker.
+    #[profiling::function]
+    fn process_outgoing_messages(&mut self) {
+        let outgoing: Vec<MavMessage> = self
+            .state
+            .panes_tree
+            .tiles
+            .iter_mut()
+            .filter_map(|(_, tile)| {
+                if let Tile::Pane(pane) = tile {
+                    Some(pane.drain_outgoing_messages())
+                } else {
+                    None
+                }
+            })
+            .flatten()
+            .collect();
+        self.message_broker.process_outgoing_messages(outgoing);
+    }
 }
 
 #[derive(Serialize, Deserialize, Clone, PartialEq)]
diff --git a/src/ui/panes.rs b/src/ui/panes.rs
index 17993c8..5a13f02 100644
--- a/src/ui/panes.rs
+++ b/src/ui/panes.rs
@@ -7,7 +7,7 @@ use enum_dispatch::enum_dispatch;
 use serde::{Deserialize, Serialize};
 use strum_macros::{self, EnumIter, EnumMessage};
 
-use crate::mavlink::TimedMessage;
+use crate::mavlink::{MavMessage, TimedMessage};
 
 use super::app::PaneResponse;
 
@@ -37,6 +37,11 @@ pub trait PaneBehavior {
     fn get_message_subscription(&self) -> Option<u32>;
     /// Checks whether the full message history should be sent to the pane.
     fn should_send_message_history(&self) -> bool;
+
+    /// Drains the outgoing messages from the pane.
+    fn drain_outgoing_messages(&mut self) -> Vec<MavMessage> {
+        Vec::new()
+    }
 }
 
 impl PaneBehavior for Pane {
@@ -59,6 +64,10 @@ impl PaneBehavior for Pane {
     fn should_send_message_history(&self) -> bool {
         self.pane.should_send_message_history()
     }
+
+    fn drain_outgoing_messages(&mut self) -> Vec<MavMessage> {
+        self.pane.drain_outgoing_messages()
+    }
 }
 
 // An enum to represent the diffent kinds of widget available to the user.
diff --git a/src/ui/windows/connections.rs b/src/ui/windows/connections.rs
index 7a5458b..e818ddb 100644
--- a/src/ui/windows/connections.rs
+++ b/src/ui/windows/connections.rs
@@ -70,13 +70,19 @@ impl ConnectionsWindow {
         }
 
         match connection_config {
-            ConnectionConfig::Ethernet(EthernetConfiguration { port }) => {
+            ConnectionConfig::Ethernet(EthernetConfiguration {
+                recv_port,
+                send_port,
+            }) => {
                 egui::Grid::new("grid")
                     .num_columns(2)
                     .spacing([10.0, 5.0])
                     .show(ui, |ui| {
-                        ui.label("Ethernet Port:");
-                        ui.add(egui::DragValue::new(port).range(0..=65535).speed(10));
+                        ui.label("Ethernet Receiving Port:");
+                        ui.add(egui::DragValue::new(recv_port).range(0..=65535).speed(10));
+                        ui.end_row();
+                        ui.label("Ethernet Sending Port:");
+                        ui.add(egui::DragValue::new(send_port).range(0..=65535).speed(10));
                         ui.end_row();
                     });
             }
@@ -177,7 +183,8 @@ pub enum ConnectionConfig {
 
 fn default_ethernet() -> EthernetConfiguration {
     EthernetConfiguration {
-        port: DEFAULT_ETHERNET_PORT,
+        recv_port: DEFAULT_ETHERNET_PORT,
+        send_port: DEFAULT_ETHERNET_PORT,
     }
 }
 
-- 
GitLab


From 11cb52b98ecaac325507815efa4bbf1849c0eb03 Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Thu, 6 Mar 2025 18:04:36 +0100
Subject: [PATCH 12/14] added default methods to pane_behavior

---
 src/ui/panes.rs                 | 12 +++++++++---
 src/ui/panes/messages_viewer.rs | 10 ----------
 2 files changed, 9 insertions(+), 13 deletions(-)

diff --git a/src/ui/panes.rs b/src/ui/panes.rs
index 5a13f02..34c5650 100644
--- a/src/ui/panes.rs
+++ b/src/ui/panes.rs
@@ -26,17 +26,23 @@ impl Pane {
 pub trait PaneBehavior {
     /// Renders the UI of the pane.
     fn ui(&mut self, ui: &mut egui::Ui, tile_id: TileId) -> PaneResponse;
+
     /// Whether the pane contains the pointer.
     fn contains_pointer(&self) -> bool;
 
     /// Updates the pane state. This method is called before `ui` to allow the
     /// pane to update its state based on the messages received.
-    fn update(&mut self, messages: &[TimedMessage]);
+    fn update(&mut self, _messages: &[TimedMessage]) {}
 
     /// Returns the ID of the messages this pane is interested in, if any.
-    fn get_message_subscription(&self) -> Option<u32>;
+    fn get_message_subscription(&self) -> Option<u32> {
+        None
+    }
+
     /// Checks whether the full message history should be sent to the pane.
-    fn should_send_message_history(&self) -> bool;
+    fn should_send_message_history(&self) -> bool {
+        false
+    }
 
     /// Drains the outgoing messages from the pane.
     fn drain_outgoing_messages(&mut self) -> Vec<MavMessage> {
diff --git a/src/ui/panes/messages_viewer.rs b/src/ui/panes/messages_viewer.rs
index 23a1f04..2c8a54f 100644
--- a/src/ui/panes/messages_viewer.rs
+++ b/src/ui/panes/messages_viewer.rs
@@ -32,14 +32,4 @@ impl PaneBehavior for MessagesViewerPane {
     fn contains_pointer(&self) -> bool {
         self.contains_pointer
     }
-
-    fn update(&mut self, _messages: &[crate::mavlink::TimedMessage]) {}
-
-    fn get_message_subscription(&self) -> Option<u32> {
-        None
-    }
-
-    fn should_send_message_history(&self) -> bool {
-        false
-    }
 }
-- 
GitLab


From c499a533b3c7cb3a0352110d175e9c47ac2b333f Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Thu, 6 Mar 2025 23:36:05 +0100
Subject: [PATCH 13/14] fixed issue with ethernet on macos and removed double
 port specs

---
 src/communication/ethernet.rs | 31 ++++++++++++++++++-------------
 src/ui/windows/connections.rs | 13 +++----------
 2 files changed, 21 insertions(+), 23 deletions(-)

diff --git a/src/communication/ethernet.rs b/src/communication/ethernet.rs
index 17ace52..2506fa9 100644
--- a/src/communication/ethernet.rs
+++ b/src/communication/ethernet.rs
@@ -22,8 +22,7 @@ use super::{
 /// Configuration for an Ethernet connection.
 #[derive(Debug, Clone)]
 pub struct EthernetConfiguration {
-    pub recv_port: u16,
-    pub send_port: u16,
+    pub port: u16,
 }
 
 impl Connectable for EthernetConfiguration {
@@ -32,20 +31,26 @@ impl Connectable for EthernetConfiguration {
     /// Binds to the specified UDP port to create a network connection.
     #[profiling::function]
     fn connect(&self) -> Result<Self::Connected, ConnectionError> {
-        let recv_addr = format!("0.0.0.0:{}", self.recv_port);
-        let send_addr = format!("255.255.255.255:{}", self.send_port);
-        let socket = UdpSocket::bind(recv_addr)?;
-        debug!("Bound to Ethernet port on port {}", self.recv_port);
-        socket.set_broadcast(true)?;
-        socket.connect(send_addr)?;
-        debug!("Connected to Ethernet port on port {}", self.send_port);
-        Ok(EthernetTransceiver { socket })
+        let recv_addr = format!("0.0.0.0:{}", self.port);
+        let server_socket = UdpSocket::bind(recv_addr)?;
+        debug!("Bound to Ethernet port on port {}", self.port);
+        let send_addr = "0.0.0.0:0";
+        let cast_addr = format!("255.255.255.255:{}", self.port);
+        let client_socket = UdpSocket::bind(send_addr)?;
+        client_socket.set_broadcast(true)?;
+        client_socket.connect(&cast_addr)?;
+        debug!("Created Ethernet connection to {}", cast_addr);
+        Ok(EthernetTransceiver {
+            server_socket,
+            client_socket,
+        })
     }
 }
 
 /// Manages a connection over Ethernet.
 pub struct EthernetTransceiver {
-    socket: UdpSocket,
+    server_socket: UdpSocket,
+    client_socket: UdpSocket,
 }
 
 impl MessageTransceiver for EthernetTransceiver {
@@ -53,7 +58,7 @@ impl MessageTransceiver for EthernetTransceiver {
     #[profiling::function]
     fn wait_for_message(&self) -> Result<TimedMessage, MessageReadError> {
         let mut buf = [0; MAX_MSG_SIZE];
-        let read = self.socket.recv(&mut buf)?;
+        let read = self.server_socket.recv(&mut buf)?;
         trace!("Received {} bytes", read);
         let mut reader = PeekReader::new(&buf[..read]);
         let (_, res) = read_v1_msg(&mut reader)?;
@@ -67,7 +72,7 @@ impl MessageTransceiver for EthernetTransceiver {
         let MavFrame { header, msg, .. } = msg;
         let mut write_buf = Vec::new();
         write_v1_msg(&mut write_buf, header, &msg)?;
-        let written = self.socket.send(&write_buf)?;
+        let written = self.client_socket.send(&write_buf)?;
         debug!("Sent message: {:?}", msg);
         trace!("Sent {} bytes via Ethernet", written);
         Ok(written)
diff --git a/src/ui/windows/connections.rs b/src/ui/windows/connections.rs
index e818ddb..ebc34c3 100644
--- a/src/ui/windows/connections.rs
+++ b/src/ui/windows/connections.rs
@@ -70,20 +70,14 @@ impl ConnectionsWindow {
         }
 
         match connection_config {
-            ConnectionConfig::Ethernet(EthernetConfiguration {
-                recv_port,
-                send_port,
-            }) => {
+            ConnectionConfig::Ethernet(EthernetConfiguration { port: recv_port }) => {
                 egui::Grid::new("grid")
                     .num_columns(2)
                     .spacing([10.0, 5.0])
                     .show(ui, |ui| {
-                        ui.label("Ethernet Receiving Port:");
+                        ui.label("Ethernet Port:");
                         ui.add(egui::DragValue::new(recv_port).range(0..=65535).speed(10));
                         ui.end_row();
-                        ui.label("Ethernet Sending Port:");
-                        ui.add(egui::DragValue::new(send_port).range(0..=65535).speed(10));
-                        ui.end_row();
                     });
             }
             ConnectionConfig::Serial(opt) => {
@@ -183,8 +177,7 @@ pub enum ConnectionConfig {
 
 fn default_ethernet() -> EthernetConfiguration {
     EthernetConfiguration {
-        recv_port: DEFAULT_ETHERNET_PORT,
-        send_port: DEFAULT_ETHERNET_PORT,
+        port: DEFAULT_ETHERNET_PORT,
     }
 }
 
-- 
GitLab


From 7b64b07018f94b4a39c60abd7bcdbb39af1859b8 Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Fri, 7 Mar 2025 00:14:36 +0100
Subject: [PATCH 14/14] moved from custom implemented to library provided

moved from custom implemented read and write algorithms to methods
provided inside the mavlink_core library (off-the-shelf)
---
 src/communication.rs          |   5 +-
 src/communication/ethernet.rs |  48 +++++------
 src/communication/serial.rs   | 146 +++-------------------------------
 src/mavlink.rs                |   2 -
 4 files changed, 34 insertions(+), 167 deletions(-)

diff --git a/src/communication.rs b/src/communication.rs
index f4d74ab..cf0bbcc 100644
--- a/src/communication.rs
+++ b/src/communication.rs
@@ -15,9 +15,8 @@ use std::sync::{
 
 use ring_channel::{RingReceiver, TryRecvError};
 use sealed::MessageTransceiver;
-use skyward_mavlink::mavlink::MavFrame;
 
-use crate::mavlink::{MavMessage, TimedMessage};
+use crate::mavlink::{MavConnection, MavFrame, MavMessage, TimedMessage};
 
 // Re-exports
 pub use error::{CommunicationError, ConnectionError};
@@ -26,6 +25,8 @@ pub use serial::SerialConfiguration;
 
 const MAX_STORED_MSGS: usize = 1000; // e.g., 192 bytes each = 192 KB
 
+pub(super) type BoxedConnection = Box<dyn MavConnection<MavMessage> + Send + Sync>;
+
 mod sealed {
     use std::{
         num::NonZeroUsize,
diff --git a/src/communication/ethernet.rs b/src/communication/ethernet.rs
index 2506fa9..629887c 100644
--- a/src/communication/ethernet.rs
+++ b/src/communication/ethernet.rs
@@ -3,19 +3,16 @@
 //! Provides functionality to connect via Ethernet using UDP, allowing message
 //! transmission and reception over a network.
 
-use std::net::UdpSocket;
-
 use skyward_mavlink::mavlink::{
-    MavFrame,
+    self,
     error::{MessageReadError, MessageWriteError},
-    read_v1_msg, write_v1_msg,
 };
 use tracing::{debug, trace};
 
-use crate::mavlink::{MAX_MSG_SIZE, MavMessage, TimedMessage, peek_reader::PeekReader};
+use crate::mavlink::{MavFrame, MavMessage, MavlinkVersion, TimedMessage};
 
 use super::{
-    ConnectionError,
+    BoxedConnection, ConnectionError,
     sealed::{Connectable, MessageTransceiver},
 };
 
@@ -31,48 +28,39 @@ impl Connectable for EthernetConfiguration {
     /// Binds to the specified UDP port to create a network connection.
     #[profiling::function]
     fn connect(&self) -> Result<Self::Connected, ConnectionError> {
-        let recv_addr = format!("0.0.0.0:{}", self.port);
-        let server_socket = UdpSocket::bind(recv_addr)?;
-        debug!("Bound to Ethernet port on port {}", self.port);
-        let send_addr = "0.0.0.0:0";
-        let cast_addr = format!("255.255.255.255:{}", self.port);
-        let client_socket = UdpSocket::bind(send_addr)?;
-        client_socket.set_broadcast(true)?;
-        client_socket.connect(&cast_addr)?;
-        debug!("Created Ethernet connection to {}", cast_addr);
+        let incoming_addr = format!("udpin:0.0.0.0:{}", self.port);
+        let outgoing_addr = format!("udpbcast: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);
+        outgoing_conn.set_protocol_version(MavlinkVersion::V1);
+        debug!("Ethernet connections set up on port {}", self.port);
         Ok(EthernetTransceiver {
-            server_socket,
-            client_socket,
+            incoming_conn,
+            outgoing_conn,
         })
     }
 }
 
 /// Manages a connection over Ethernet.
 pub struct EthernetTransceiver {
-    server_socket: UdpSocket,
-    client_socket: UdpSocket,
+    incoming_conn: BoxedConnection,
+    outgoing_conn: BoxedConnection,
 }
 
 impl MessageTransceiver for EthernetTransceiver {
     /// Waits for a message over Ethernet, blocking until a valid message arrives.
     #[profiling::function]
     fn wait_for_message(&self) -> Result<TimedMessage, MessageReadError> {
-        let mut buf = [0; MAX_MSG_SIZE];
-        let read = self.server_socket.recv(&mut buf)?;
-        trace!("Received {} bytes", read);
-        let mut reader = PeekReader::new(&buf[..read]);
-        let (_, res) = read_v1_msg(&mut reader)?;
-        debug!("Received message: {:?}", res);
-        Ok(TimedMessage::just_received(res))
+        let (_, msg) = self.incoming_conn.recv()?;
+        debug!("Received message: {:?}", &msg);
+        Ok(TimedMessage::just_received(msg))
     }
 
     /// Transmits a message using the UDP socket.
     #[profiling::function]
     fn transmit_message(&self, msg: MavFrame<MavMessage>) -> Result<usize, MessageWriteError> {
-        let MavFrame { header, msg, .. } = msg;
-        let mut write_buf = Vec::new();
-        write_v1_msg(&mut write_buf, header, &msg)?;
-        let written = self.client_socket.send(&write_buf)?;
+        let written = self.outgoing_conn.send_frame(&msg)?;
         debug!("Sent message: {:?}", msg);
         trace!("Sent {} bytes via Ethernet", written);
         Ok(written)
diff --git a/src/communication/serial.rs b/src/communication/serial.rs
index 3a9d798..8f1c6fa 100644
--- a/src/communication/serial.rs
+++ b/src/communication/serial.rs
@@ -3,27 +3,20 @@
 //! Provides functions for listing USB serial ports, finding a STM32 port,
 //! and handling serial connections including message transmission and reception.
 
-use std::sync::Mutex;
-
-use serialport::{SerialPort, SerialPortInfo, SerialPortType};
+use serialport::{SerialPortInfo, SerialPortType};
 use skyward_mavlink::mavlink::{
-    MavFrame,
+    self,
     error::{MessageReadError, MessageWriteError},
-    read_v1_msg, write_v1_msg,
 };
 use tracing::{debug, trace};
 
-use crate::{
-    error::ErrInstrument,
-    mavlink::{MavMessage, TimedMessage, peek_reader::PeekReader},
-};
+use crate::mavlink::{MavFrame, MavMessage, MavlinkVersion, TimedMessage};
 
 use super::{
-    ConnectionError,
+    BoxedConnection, ConnectionError,
     sealed::{Connectable, MessageTransceiver},
 };
 
-const SERIAL_PORT_TIMEOUT_MS: u64 = 100;
 pub const DEFAULT_BAUD_RATE: u32 = 115200;
 
 /// Returns a list of all USB serial ports available on the system.
@@ -71,150 +64,37 @@ impl Connectable for SerialConfiguration {
     /// Connects using the serial port configuration.
     #[profiling::function]
     fn connect(&self) -> Result<Self::Connected, ConnectionError> {
-        let port = serialport::new(&self.port_name, self.baud_rate)
-            .timeout(std::time::Duration::from_millis(SERIAL_PORT_TIMEOUT_MS))
-            .open()?;
+        let serial_edpoint = format!("serial:{}:{}", self.port_name, self.baud_rate);
+        let mut mav_connection: BoxedConnection = mavlink::connect(&serial_edpoint)?;
+        mav_connection.set_protocol_version(MavlinkVersion::V1);
         debug!(
             "Connected to serial port {} with baud rate {}",
             self.port_name, self.baud_rate
         );
-        Ok(SerialTransceiver {
-            serial_reader: Mutex::new(Box::new(PeekReader::new(port.try_clone()?))),
-            serial_writer: Mutex::new(port),
-        })
-    }
-}
-
-impl From<serialport::Error> for ConnectionError {
-    fn from(e: serialport::Error) -> Self {
-        let serialport::Error { kind, description } = e.clone();
-        match kind {
-            serialport::ErrorKind::NoDevice => ConnectionError::WrongConfiguration(description),
-            serialport::ErrorKind::InvalidInput => ConnectionError::WrongConfiguration(description),
-            serialport::ErrorKind::Unknown => ConnectionError::Unknown(description),
-            serialport::ErrorKind::Io(e) => ConnectionError::Io(e.into()),
-        }
+        Ok(SerialTransceiver { mav_connection })
     }
 }
 
 /// Manages a connection to a serial port.
 pub struct SerialTransceiver {
-    serial_reader: Mutex<Box<PeekReader<Box<dyn SerialPort>>>>,
-    #[allow(dead_code)]
-    serial_writer: Mutex<Box<dyn SerialPort>>,
+    mav_connection: BoxedConnection,
 }
 
 impl MessageTransceiver for SerialTransceiver {
     /// Blocks until a valid message is received from the serial port.
     #[profiling::function]
     fn wait_for_message(&self) -> Result<TimedMessage, MessageReadError> {
-        loop {
-            let res: Result<(_, MavMessage), MessageReadError> =
-                read_v1_msg(&mut self.serial_reader.lock().log_unwrap());
-            match res {
-                Ok((_, msg)) => {
-                    return Ok(TimedMessage::just_received(msg));
-                }
-                Err(MessageReadError::Io(e)) if e.kind() == std::io::ErrorKind::TimedOut => {
-                    continue;
-                }
-                Err(e) => {
-                    return Err(e);
-                }
-            }
-        }
+        let (_, msg) = self.mav_connection.recv()?;
+        debug!("Received message: {:?}", &msg);
+        Ok(TimedMessage::just_received(msg))
     }
 
     /// Transmits a message via the serial connection.
     #[profiling::function]
     fn transmit_message(&self, msg: MavFrame<MavMessage>) -> Result<usize, MessageWriteError> {
-        let MavFrame { header, msg, .. } = msg;
-        let written = write_v1_msg(&mut *self.serial_writer.lock().log_unwrap(), header, &msg)?;
+        let written = self.mav_connection.send_frame(&msg)?;
         debug!("Sent message: {:?}", msg);
         trace!("Sent {} bytes via serial", written);
         Ok(written)
     }
 }
-
-#[allow(clippy::unwrap_used)]
-#[cfg(test)]
-mod tests {
-    use std::{collections::VecDeque, io::Read};
-
-    use rand::prelude::*;
-    use skyward_mavlink::{mavlink::*, orion::*};
-
-    use super::*;
-
-    struct ChunkedMessageStreamGenerator {
-        rng: SmallRng,
-        buffer: VecDeque<u8>,
-    }
-
-    impl ChunkedMessageStreamGenerator {
-        const KINDS: [u32; 2] = [ACK_TM_DATA::ID, NACK_TM_DATA::ID];
-
-        fn new() -> Self {
-            Self {
-                rng: SmallRng::seed_from_u64(42),
-                buffer: VecDeque::new(),
-            }
-        }
-
-        fn msg_push(&mut self, msg: &MavMessage, header: MavHeader) -> std::io::Result<()> {
-            write_v1_msg(&mut self.buffer, header, msg).unwrap();
-            Ok(())
-        }
-
-        fn fill_buffer(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
-            while buf.len() > self.buffer.len() {
-                self.add_next_rand();
-            }
-            let n = buf.len();
-            buf.iter_mut()
-                .zip(self.buffer.drain(..n))
-                .for_each(|(a, b)| *a = b);
-            Ok(n)
-        }
-
-        fn add_next_rand(&mut self) {
-            let i = self.rng.random_range(0..Self::KINDS.len());
-            let id = Self::KINDS[i];
-            let msg = MavMessage::default_message_from_id(id).unwrap();
-            let header = MavHeader {
-                system_id: 1,
-                component_id: 1,
-                sequence: 0,
-            };
-            self.msg_push(&msg, header).unwrap();
-        }
-    }
-
-    impl Read for ChunkedMessageStreamGenerator {
-        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
-            // fill buffer with sequence of byte of random length
-            if buf.len() == 1 {
-                self.fill_buffer(&mut buf[..1])
-            } else if !buf.is_empty() {
-                let size = self.rng.random_range(1..buf.len());
-                self.fill_buffer(&mut buf[..size])
-            } else {
-                Ok(0)
-            }
-        }
-    }
-
-    #[test]
-    fn test_peek_reader_with_chunked_transmission() {
-        let mut gms = ChunkedMessageStreamGenerator::new();
-        let mut reader = PeekReader::new(&mut gms);
-        let mut msgs = Vec::new();
-        for _ in 0..100 {
-            let (_, msg): (MavHeader, MavMessage) = read_v1_msg(&mut reader).unwrap();
-            msgs.push(msg);
-        }
-        for msg in msgs {
-            assert!(msg.message_id() == ACK_TM_DATA::ID || msg.message_id() == NACK_TM_DATA::ID);
-        }
-    }
-}
diff --git a/src/mavlink.rs b/src/mavlink.rs
index 9589a2f..9119346 100644
--- a/src/mavlink.rs
+++ b/src/mavlink.rs
@@ -13,5 +13,3 @@ pub use reflection::ReflectionContext;
 
 /// Default port for the Ethernet connection
 pub const DEFAULT_ETHERNET_PORT: u16 = 42069;
-/// Maximum size of a Mavlink message
-pub const MAX_MSG_SIZE: usize = 280;
-- 
GitLab