From ea6a7075954b50595d2b38834beddecabf281f07 Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Thu, 14 Nov 2024 18:50:48 +0100
Subject: [PATCH 01/17] [ui] Added utils.rs with vertical_centering helper
 function with double pass

---
 Cargo.lock              | 70 ++++++++++++++++++++---------------------
 justfile                | 11 +++++++
 src/ui/panes/default.rs | 66 +++++++++++++++-----------------------
 src/ui/utils.rs         | 31 +++++++++++++++++-
 4 files changed, 101 insertions(+), 77 deletions(-)
 create mode 100644 justfile

diff --git a/Cargo.lock b/Cargo.lock
index a6a4c77..844231a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,6 +1,6 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
-version = 3
+version = 4
 
 [[package]]
 name = "ab_glyph"
@@ -143,9 +143,9 @@ dependencies = [
 
 [[package]]
 name = "allocator-api2"
-version = "0.2.18"
+version = "0.2.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
+checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9"
 
 [[package]]
 name = "android-activity"
@@ -324,9 +324,9 @@ dependencies = [
 
 [[package]]
 name = "async-io"
-version = "2.3.4"
+version = "2.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8"
+checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
 dependencies = [
  "async-lock",
  "cfg-if",
@@ -625,9 +625,9 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.1.36"
+version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baee610e9452a8f6f0a1b6194ec09ff9e2d85dea54432acdae41aa0761c95d70"
+checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47"
 dependencies = [
  "jobserver",
  "libc",
@@ -794,9 +794,9 @@ dependencies = [
 
 [[package]]
 name = "cpufeatures"
-version = "0.2.14"
+version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0"
+checksum = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6"
 dependencies = [
  "libc",
 ]
@@ -1234,9 +1234,9 @@ dependencies = [
 
 [[package]]
 name = "fastrand"
-version = "2.1.1"
+version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
+checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
 
 [[package]]
 name = "fdeflate"
@@ -1249,9 +1249,9 @@ dependencies = [
 
 [[package]]
 name = "flate2"
-version = "1.0.34"
+version = "1.0.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0"
+checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
 dependencies = [
  "crc32fast",
  "miniz_oxide",
@@ -1307,9 +1307,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
 
 [[package]]
 name = "futures-lite"
-version = "2.4.0"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f1fa2f9765705486b33fd2acf1577f8ec449c2ba1f318ae5447697b7c08d210"
+checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1"
 dependencies = [
  "fastrand",
  "futures-core",
@@ -1856,9 +1856,9 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
 
 [[package]]
 name = "libc"
-version = "0.2.161"
+version = "0.2.162"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
+checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
 
 [[package]]
 name = "libloading"
@@ -2449,9 +2449,9 @@ dependencies = [
 
 [[package]]
 name = "polling"
-version = "3.7.3"
+version = "3.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511"
+checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
 dependencies = [
  "cfg-if",
  "concurrent-queue",
@@ -2597,9 +2597,9 @@ dependencies = [
 
 [[package]]
 name = "regex-automata"
-version = "0.4.8"
+version = "0.4.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -2638,9 +2638,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
 
 [[package]]
 name = "rustix"
-version = "0.38.39"
+version = "0.38.40"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee"
+checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0"
 dependencies = [
  "bitflags 2.6.0",
  "errno",
@@ -2708,18 +2708,18 @@ dependencies = [
 
 [[package]]
 name = "serde"
-version = "1.0.214"
+version = "1.0.215"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
+checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.214"
+version = "1.0.215"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
+checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2728,9 +2728,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.132"
+version = "1.0.133"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
+checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
 dependencies = [
  "itoa",
  "memchr",
@@ -2912,9 +2912,9 @@ dependencies = [
 
 [[package]]
 name = "tempfile"
-version = "3.13.0"
+version = "3.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b"
+checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
 dependencies = [
  "cfg-if",
  "fastrand",
@@ -2934,18 +2934,18 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "1.0.68"
+version = "1.0.69"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.68"
+version = "1.0.69"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
 dependencies = [
  "proc-macro2",
  "quote",
diff --git a/justfile b/justfile
new file mode 100644
index 0000000..613e908
--- /dev/null
+++ b/justfile
@@ -0,0 +1,11 @@
+alias r := run
+alias d := doc
+
+default:
+    just run
+
+run LEVEL="debug":
+    RUST_LOG=segs={{LEVEL}} cargo r
+
+doc:
+    cargo doc --no-deps --open
diff --git a/src/ui/panes/default.rs b/src/ui/panes/default.rs
index 2d1b51a..26eb63c 100644
--- a/src/ui/panes/default.rs
+++ b/src/ui/panes/default.rs
@@ -1,13 +1,17 @@
 use super::{plot_2d::Plot2DPane, Pane, PaneBehavior, PaneKind};
 use serde::{Deserialize, Serialize};
 
-use crate::ui::composable_view::{PaneAction, PaneResponse};
+use crate::ui::{
+    composable_view::{PaneAction, PaneResponse},
+    utils::{vertically_centered, SizingMemo},
+};
 
 #[derive(Clone, Debug, Serialize, Deserialize)]
 pub struct DefaultPane {
     occupied: f32,
     fixed: bool,
-
+    #[serde(skip)]
+    centering_memo: SizingMemo,
     #[serde(skip)]
     contains_pointer: bool,
 }
@@ -15,8 +19,7 @@ pub struct DefaultPane {
 impl Default for DefaultPane {
     fn default() -> Self {
         DefaultPane {
-            occupied: 0.0,
-            fixed: false,
+            centering_memo: SizingMemo::default(),
             contains_pointer: false,
         }
     }
@@ -31,48 +34,29 @@ impl PartialEq for DefaultPane {
 impl PaneBehavior for DefaultPane {
     fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse {
         let mut response = PaneResponse::default();
-        let pane_rect = ui.max_rect();
 
-        let parent = ui.vertical_centered(|ui| {
-            let hpad = (pane_rect.height() - self.occupied) / 2.0;
-            if self.fixed {
-                ui.add_space(hpad);
-            }
-            let mut height_occupied = 0.0;
-            let btn = ui.button("Vertical Split");
-            if btn.clicked() {
-                response.set_action(PaneAction::SplitV);
-                log::debug!("Vertical Split button clicked");
-            }
-            height_occupied += btn.rect.height();
-            let btn = ui.button("Horizontal Split");
-            if btn.clicked() {
-                response.set_action(PaneAction::SplitH);
-                log::debug!("Horizontal Split button clicked");
-            }
-            height_occupied += btn.rect.height();
-            let btn = ui.button("Plot");
-            if btn.clicked() {
-                response.set_action(PaneAction::Replace(Pane::boxed(PaneKind::Plot2D(
-                    Plot2DPane::default(),
-                ))));
-            }
-            height_occupied += btn.rect.height();
-            if !self.fixed {
-                self.occupied = height_occupied;
-                ui.ctx().request_discard("test");
-                self.fixed = true;
-            }
-            if self.fixed {
-                ui.add_space(hpad);
-            }
-            ui.set_min_height(pane_rect.height());
+        let parent = vertically_centered(ui, &mut self.centering_memo, |ui| {
+            ui.vertical_centered(|ui| {
+                if ui.button("Vertical Split").clicked() {
+                    response.set_action(PaneAction::SplitV);
+                    log::debug!("Vertical Split button clicked");
+                }
+                if ui.button("Horizontal Split").clicked() {
+                    response.set_action(PaneAction::SplitH);
+                    log::debug!("Horizontal Split button clicked");
+                }
+                if ui.button("Plot").clicked() {
+                    response.set_action(PaneAction::Replace(Pane::boxed(PaneKind::Plot2D(
+                        Plot2DPane::default(),
+                    ))));
+                }
+            })
+            .response
         });
 
-        self.contains_pointer = parent.response.contains_pointer();
+        self.contains_pointer = parent.contains_pointer();
 
         if parent
-            .response
             .interact(egui::Sense::click_and_drag())
             .on_hover_cursor(egui::CursorIcon::Grab)
             .dragged()
diff --git a/src/ui/utils.rs b/src/ui/utils.rs
index b9f510b..3172463 100644
--- a/src/ui/utils.rs
+++ b/src/ui/utils.rs
@@ -1,5 +1,5 @@
 use egui::containers::Frame;
-use egui::{Shadow, Stroke, Style, Ui};
+use egui::{Response, Shadow, Stroke, Style, Ui};
 
 use super::panes::{Pane, PaneBehavior};
 
@@ -12,3 +12,32 @@ pub fn maximized_pane_ui(ui: &mut Ui, pane: &mut Pane) {
         .stroke(Stroke::NONE)
         .show(ui, |ui| pane.ui(ui));
 }
+
+#[derive(Debug, Default, Clone)]
+pub struct SizingMemo {
+    occupied_height: f32,
+    sizing_pass_done: bool,
+}
+
+pub fn vertically_centered(
+    ui: &mut Ui,
+    memo: &mut SizingMemo,
+    add_contents: impl FnOnce(&mut Ui) -> Response,
+) -> egui::Response {
+    if !memo.sizing_pass_done {
+        let r = add_contents(ui);
+        memo.occupied_height = r.rect.height();
+        memo.sizing_pass_done = true;
+        ui.ctx()
+            .request_discard("horizontally_centered requires a sizing pass");
+        r
+    } else {
+        let spacing = (ui.available_height() - memo.occupied_height) / 2.0;
+        ui.vertical_centered(|ui| {
+            ui.add_space(spacing);
+            add_contents(ui);
+            ui.add_space(spacing);
+        })
+        .response
+    }
+}
-- 
GitLab


From 10c1268ac4aaaf8d8fbcfd554f843dcd8b781d39 Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Thu, 14 Nov 2024 19:31:07 +0100
Subject: [PATCH 02/17] [Cargo] Added Tokio and moved log to tracing

---
 Cargo.lock              | 257 +++++++++++++++++++++++++---------------
 Cargo.toml              |   6 +-
 src/main.rs             |  10 +-
 src/ui/panes/default.rs |   5 +-
 4 files changed, 175 insertions(+), 103 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 844231a..7c49f2a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -112,6 +112,15 @@ dependencies = [
  "winit",
 ]
 
+[[package]]
+name = "addr2line"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+dependencies = [
+ "gimli",
+]
+
 [[package]]
 name = "adler2"
 version = "2.0.0"
@@ -183,55 +192,6 @@ dependencies = [
  "libc",
 ]
 
-[[package]]
-name = "anstream"
-version = "0.6.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
-dependencies = [
- "anstyle",
- "anstyle-parse",
- "anstyle-query",
- "anstyle-wincon",
- "colorchoice",
- "is_terminal_polyfill",
- "utf8parse",
-]
-
-[[package]]
-name = "anstyle"
-version = "1.0.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
-
-[[package]]
-name = "anstyle-parse"
-version = "0.2.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
-dependencies = [
- "utf8parse",
-]
-
-[[package]]
-name = "anstyle-query"
-version = "1.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
-dependencies = [
- "windows-sys 0.59.0",
-]
-
-[[package]]
-name = "anstyle-wincon"
-version = "3.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
-dependencies = [
- "anstyle",
- "windows-sys 0.59.0",
-]
-
 [[package]]
 name = "arboard"
 version = "3.4.1"
@@ -480,6 +440,21 @@ version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
 
+[[package]]
+name = "backtrace"
+version = "0.3.74"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-targets 0.52.6",
+]
+
 [[package]]
 name = "base64"
 version = "0.21.7"
@@ -686,12 +661,6 @@ dependencies = [
  "unicode-width",
 ]
 
-[[package]]
-name = "colorchoice"
-version = "1.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
-
 [[package]]
 name = "com"
 version = "0.6.0"
@@ -1142,29 +1111,6 @@ dependencies = [
  "syn 2.0.87",
 ]
 
-[[package]]
-name = "env_filter"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab"
-dependencies = [
- "log",
- "regex",
-]
-
-[[package]]
-name = "env_logger"
-version = "0.11.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d"
-dependencies = [
- "anstream",
- "anstyle",
- "env_filter",
- "humantime",
- "log",
-]
-
 [[package]]
 name = "epaint"
 version = "0.29.1"
@@ -1389,6 +1335,12 @@ dependencies = [
  "wasi",
 ]
 
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+
 [[package]]
 name = "gl_generator"
 version = "0.14.0"
@@ -1600,12 +1552,6 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
-[[package]]
-name = "humantime"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
-
 [[package]]
 name = "icu_collections"
 version = "1.5.0"
@@ -1776,12 +1722,6 @@ dependencies = [
  "hashbrown 0.15.1",
 ]
 
-[[package]]
-name = "is_terminal_polyfill"
-version = "1.70.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
-
 [[package]]
 name = "itertools"
 version = "0.13.0"
@@ -1854,6 +1794,12 @@ version = "3.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
 
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
 [[package]]
 name = "libc"
 version = "0.2.162"
@@ -1924,6 +1870,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "matchers"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
+dependencies = [
+ "regex-automata 0.1.10",
+]
+
 [[package]]
 name = "memchr"
 version = "2.7.4"
@@ -2068,6 +2023,16 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
 
+[[package]]
+name = "nu-ansi-term"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi",
+]
+
 [[package]]
 name = "num-traits"
 version = "0.2.19"
@@ -2310,6 +2275,15 @@ dependencies = [
  "objc2-foundation",
 ]
 
+[[package]]
+name = "object"
+version = "0.36.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "once_cell"
 version = "1.20.2"
@@ -2335,6 +2309,12 @@ dependencies = [
  "pin-project-lite",
 ]
 
+[[package]]
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
 [[package]]
 name = "owned_ttf_parser"
 version = "0.25.0"
@@ -2591,8 +2571,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
 dependencies = [
  "aho-corasick",
  "memchr",
- "regex-automata",
- "regex-syntax",
+ "regex-automata 0.4.9",
+ "regex-syntax 0.8.5",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+dependencies = [
+ "regex-syntax 0.6.29",
 ]
 
 [[package]]
@@ -2603,9 +2592,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
 dependencies = [
  "aho-corasick",
  "memchr",
- "regex-syntax",
+ "regex-syntax 0.8.5",
 ]
 
+[[package]]
+name = "regex-syntax"
+version = "0.6.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
+
 [[package]]
 name = "regex-syntax"
 version = "0.8.5"
@@ -2630,6 +2625,12 @@ dependencies = [
  "serde_derive",
 ]
 
+[[package]]
+name = "rustc-demangle"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+
 [[package]]
 name = "rustc-hash"
 version = "1.1.0"
@@ -2700,10 +2701,11 @@ dependencies = [
  "egui_plot",
  "egui_tiles",
  "enum_dispatch",
- "env_logger",
- "log",
  "serde",
  "serde_json",
+ "tokio",
+ "tracing",
+ "tracing-subscriber",
 ]
 
 [[package]]
@@ -2760,6 +2762,15 @@ dependencies = [
  "digest",
 ]
 
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
 [[package]]
 name = "shlex"
 version = "1.3.0"
@@ -2952,6 +2963,16 @@ dependencies = [
  "syn 2.0.87",
 ]
 
+[[package]]
+name = "thread_local"
+version = "1.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
 [[package]]
 name = "tiny-skia"
 version = "0.11.4"
@@ -2987,6 +3008,16 @@ dependencies = [
  "zerovec",
 ]
 
+[[package]]
+name = "tokio"
+version = "1.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
+dependencies = [
+ "backtrace",
+ "pin-project-lite",
+]
+
 [[package]]
 name = "toml_datetime"
 version = "0.6.8"
@@ -3033,6 +3064,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
 dependencies = [
  "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
 ]
 
 [[package]]
@@ -3121,10 +3182,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
 
 [[package]]
-name = "utf8parse"
-version = "0.2.2"
+name = "valuable"
+version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
 
 [[package]]
 name = "version_check"
diff --git a/Cargo.toml b/Cargo.toml
index 9a6f56e..6748631 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,12 +14,14 @@ eframe = { version = "0.29", features = ["persistence"] }
 egui = { version = "0.29", features = ["log"] }
 egui_plot = "0.29"
 egui_file = "0.19"
+# =========== Asynchronous ===========
+tokio = { version = "1.41", features = ["rt-multi-thread"] }
 # ========= Persistency =========
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
 # =========== Logging ===========
-env_logger = "0.11"
-log = "0.4"
+tracing = "0.1"
+tracing-subscriber = { version = "0.3", features = ["env-filter"] }
 # =========== Utility ===========
 # for dynamic dispatch
 enum_dispatch = "0.3"
diff --git a/src/main.rs b/src/main.rs
index c988cd7..1bab97b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,5 @@
+use tokio::runtime::Runtime;
+use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer};
 use ui::ComposableView;
 
 mod ui;
@@ -6,7 +8,13 @@ static APP_NAME: &str = "segs";
 
 fn main() -> Result<(), eframe::Error> {
     // set up logging (USE RUST_LOG=debug to see logs)
-    env_logger::init();
+    let env_filter = EnvFilter::builder().from_env_lossy();
+    tracing_subscriber::registry()
+        .with(tracing_subscriber::fmt::layer().with_filter(env_filter))
+        .init();
+
+    let rt = Runtime::new().expect("Unable to create Runtime");
+    let _enter = rt.enter();
 
     let native_options = eframe::NativeOptions {
         // By modifying the viewport, we can change things like the windows size
diff --git a/src/ui/panes/default.rs b/src/ui/panes/default.rs
index 26eb63c..250e35e 100644
--- a/src/ui/panes/default.rs
+++ b/src/ui/panes/default.rs
@@ -1,5 +1,6 @@
 use super::{plot_2d::Plot2DPane, Pane, PaneBehavior, PaneKind};
 use serde::{Deserialize, Serialize};
+use tracing::debug;
 
 use crate::ui::{
     composable_view::{PaneAction, PaneResponse},
@@ -39,11 +40,11 @@ impl PaneBehavior for DefaultPane {
             ui.vertical_centered(|ui| {
                 if ui.button("Vertical Split").clicked() {
                     response.set_action(PaneAction::SplitV);
-                    log::debug!("Vertical Split button clicked");
+                    debug!("Vertical Split button clicked");
                 }
                 if ui.button("Horizontal Split").clicked() {
                     response.set_action(PaneAction::SplitH);
-                    log::debug!("Horizontal Split button clicked");
+                    debug!("Horizontal Split button clicked");
                 }
                 if ui.button("Plot").clicked() {
                     response.set_action(PaneAction::Replace(Pane::boxed(PaneKind::Plot2D(
-- 
GitLab


From 174dc881afbd4a25ab287ad16087ef5d5d04665a Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Tue, 19 Nov 2024 01:38:20 +0100
Subject: [PATCH 03/17] [Mavlink] Added Mavlink support in async way

---
 Cargo.lock                | 213 +++++++++++++++++++++++++++++++++++++-
 Cargo.toml                |  20 +++-
 src/main.rs               |  12 ++-
 src/mavlink.rs            | 128 +++++++++++++++++++++++
 src/ui/composable_view.rs |  76 ++++++++++++++
 5 files changed, 445 insertions(+), 4 deletions(-)
 create mode 100644 src/mavlink.rs

diff --git a/Cargo.lock b/Cargo.lock
index 7c49f2a..6fe75a4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -192,6 +192,12 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "anyhow"
+version = "1.0.93"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
+
 [[package]]
 name = "arboard"
 version = "3.4.1"
@@ -770,6 +776,12 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "crc-any"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a62ec9ff5f7965e4d7280bd5482acd20aadb50d632cf6c1d74493856b011fa73"
+
 [[package]]
 name = "crc32fast"
 version = "1.4.2"
@@ -779,6 +791,15 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
+dependencies = [
+ "crossbeam-utils",
+]
+
 [[package]]
 name = "crossbeam-utils"
 version = "0.8.20"
@@ -1525,6 +1546,18 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
+
 [[package]]
 name = "hermit-abi"
 version = "0.4.0"
@@ -1722,6 +1755,15 @@ dependencies = [
  "hashbrown 0.15.1",
 ]
 
+[[package]]
+name = "ioctl-rs"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7970510895cee30b3e9128319f2cefd4bde883a39f38baa279567ba3a7eb97d"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "itertools"
 version = "0.13.0"
@@ -1879,6 +1921,31 @@ dependencies = [
  "regex-automata 0.1.10",
 ]
 
+[[package]]
+name = "mavlink-bindgen"
+version = "0.13.2"
+source = "git+https://github.com/federico123579/rust-mavlink.git?branch=strum-integration#02a3eebe16ac1aacb93c2c61696dcd731f6f2ef4"
+dependencies = [
+ "crc-any",
+ "lazy_static",
+ "proc-macro2",
+ "quick-xml 0.36.2",
+ "quote",
+ "thiserror",
+]
+
+[[package]]
+name = "mavlink-core"
+version = "0.13.2"
+source = "git+https://github.com/federico123579/rust-mavlink.git?branch=strum-integration#02a3eebe16ac1aacb93c2c61696dcd731f6f2ef4"
+dependencies = [
+ "byteorder",
+ "crc-any",
+ "serde",
+ "serde_arrays",
+ "serial",
+]
+
 [[package]]
 name = "memchr"
 version = "2.7.4"
@@ -1944,6 +2011,18 @@ dependencies = [
  "simd-adler32",
 ]
 
+[[package]]
+name = "mio"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
+dependencies = [
+ "hermit-abi 0.3.9",
+ "libc",
+ "wasi",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "naga"
 version = "22.1.0"
@@ -2033,6 +2112,17 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "num-derive"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.87",
+]
+
 [[package]]
 name = "num-traits"
 version = "0.2.19"
@@ -2435,7 +2525,7 @@ checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
 dependencies = [
  "cfg-if",
  "concurrent-queue",
- "hermit-abi",
+ "hermit-abi 0.4.0",
  "pin-project-lite",
  "rustix",
  "tracing",
@@ -2650,6 +2740,12 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
+[[package]]
+name = "rustversion"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
+
 [[package]]
 name = "ryu"
 version = "1.0.18"
@@ -2694,6 +2790,8 @@ dependencies = [
 name = "segs"
 version = "0.1.0"
 dependencies = [
+ "anyhow",
+ "crossbeam-channel",
  "eframe",
  "egui",
  "egui_extras",
@@ -2701,8 +2799,11 @@ dependencies = [
  "egui_plot",
  "egui_tiles",
  "enum_dispatch",
+ "parking_lot",
  "serde",
  "serde_json",
+ "skyward_mavlink",
+ "strum",
  "tokio",
  "tracing",
  "tracing-subscriber",
@@ -2717,6 +2818,15 @@ dependencies = [
  "serde_derive",
 ]
 
+[[package]]
+name = "serde_arrays"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38636132857f68ec3d5f3eb121166d2af33cb55174c4d5ff645db6165cbef0fd"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "serde_derive"
 version = "1.0.215"
@@ -2751,6 +2861,48 @@ dependencies = [
  "syn 2.0.87",
 ]
 
+[[package]]
+name = "serial"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1237a96570fc377c13baa1b88c7589ab66edced652e43ffb17088f003db3e86"
+dependencies = [
+ "serial-core",
+ "serial-unix",
+ "serial-windows",
+]
+
+[[package]]
+name = "serial-core"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f46209b345401737ae2125fe5b19a77acce90cd53e1658cda928e4fe9a64581"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "serial-unix"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f03fbca4c9d866e24a459cbca71283f545a37f8e3e002ad8c70593871453cab7"
+dependencies = [
+ "ioctl-rs",
+ "libc",
+ "serial-core",
+ "termios",
+]
+
+[[package]]
+name = "serial-windows"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15c6d3b776267a75d31bbdfd5d36c0ca051251caafc285827052bc53bcdc8162"
+dependencies = [
+ "libc",
+ "serial-core",
+]
+
 [[package]]
 name = "sha1"
 version = "0.10.6"
@@ -2792,6 +2944,22 @@ version = "0.3.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
 
+[[package]]
+name = "skyward_mavlink"
+version = "0.1.0"
+source = "git+https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git?branch=rust-strum#5b515ef056e5f783dc1cbc1c73eadd2f08a5652d"
+dependencies = [
+ "bitflags 2.6.0",
+ "mavlink-bindgen",
+ "mavlink-core",
+ "num-derive",
+ "num-traits",
+ "serde",
+ "serde_arrays",
+ "strum",
+ "strum_macros",
+]
+
 [[package]]
 name = "slab"
 version = "0.4.9"
@@ -2861,6 +3029,16 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "socket2"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "spirv"
 version = "0.3.0+sdk-1.3.268.0"
@@ -2888,6 +3066,25 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
 
+[[package]]
+name = "strum"
+version = "0.26.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
+
+[[package]]
+name = "strum_macros"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn 2.0.87",
+]
+
 [[package]]
 name = "syn"
 version = "1.0.109"
@@ -2943,6 +3140,15 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "termios"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "thiserror"
 version = "1.0.69"
@@ -3015,7 +3221,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
 dependencies = [
  "backtrace",
+ "libc",
+ "mio",
+ "parking_lot",
  "pin-project-lite",
+ "socket2",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 6748631..6855548 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,15 +14,31 @@ eframe = { version = "0.29", features = ["persistence"] }
 egui = { version = "0.29", features = ["log"] }
 egui_plot = "0.29"
 egui_file = "0.19"
-# =========== Asynchronous ===========
-tokio = { version = "1.41", features = ["rt-multi-thread"] }
 # ========= Persistency =========
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
 # =========== Logging ===========
 tracing = "0.1"
 tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+# =========== Performance ===========
+# for fast mutexes
+parking_lot = "0.12"
+# for fast channels
+crossbeam-channel = "0.5"
 # =========== Utility ===========
 # for dynamic dispatch
 enum_dispatch = "0.3"
 egui_extras = "0.29.1"
+strum = "0.26"
+anyhow = "1.0"
+
+# =========== Asynchronous ===========
+[dependencies.tokio]
+version = "1.41"
+features = ["rt-multi-thread", "net", "parking_lot", "sync"]
+
+# =========== Mavlink ===========
+[dependencies.skyward_mavlink]
+git = "https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git"
+branch = "rust-strum"
+features = ["lyra", "serde", "strum"]
diff --git a/src/main.rs b/src/main.rs
index 1bab97b..9076b22 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,8 +1,15 @@
+mod mavlink;
+mod ui;
+
+use std::sync::OnceLock;
+
+use mavlink::MessageManager;
+use parking_lot::Mutex;
 use tokio::runtime::Runtime;
 use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer};
 use ui::ComposableView;
 
-mod ui;
+static MSG_MANAGER: OnceLock<Mutex<MessageManager>> = OnceLock::new();
 
 static APP_NAME: &str = "segs";
 
@@ -33,6 +40,9 @@ fn main() -> Result<(), eframe::Error> {
         APP_NAME, // This is the app id, used for example by Wayland
         native_options,
         Box::new(|ctx| {
+            MSG_MANAGER
+                .set(Mutex::new(MessageManager::new(50, ctc.egui_ctx.clone())))
+                .expect("Unable to set MessageManager");
             let app = ctx
                 .storage
                 .map(|storage| ComposableView::new(APP_NAME, storage))
diff --git a/src/mavlink.rs b/src/mavlink.rs
new file mode 100644
index 0000000..6f04270
--- /dev/null
+++ b/src/mavlink.rs
@@ -0,0 +1,128 @@
+use std::{
+    collections::HashMap,
+    sync::{
+        atomic::{AtomicBool, Ordering},
+        Arc,
+    },
+    time::Instant,
+};
+
+use anyhow::{Context, Result};
+use crossbeam_channel::{Receiver, Sender};
+use skyward_mavlink::{
+    lyra::MavMessage,
+    mavlink::{peek_reader::PeekReader, read_v1_msg, MavHeader, Message},
+};
+use strum::VariantNames;
+use tokio::{net::UdpSocket, task::JoinHandle};
+use tracing::{debug, info};
+
+pub const DEFAULT_ETHERNET_PORT: u16 = 42069;
+const UDP_BUFFER_SIZE: usize = 65527;
+
+#[derive(Debug)]
+pub struct MessageManager {
+    messages: HashMap<u32, Vec<TimedMessage>>,
+    tx: Sender<MavMessage>,
+    rx: Receiver<MavMessage>,
+    ctx: egui::Context,
+    running_flag: Arc<AtomicBool>,
+    task: Option<JoinHandle<Result<()>>>,
+}
+
+impl MessageManager {
+    pub fn new(channel_size: usize, ctx: egui::Context) -> Self {
+        let (tx, rx) = crossbeam_channel::bounded(channel_size);
+        Self {
+            messages: HashMap::new(),
+            tx,
+            rx,
+            ctx,
+            running_flag: Arc::new(AtomicBool::new(false)),
+            task: None,
+        }
+    }
+
+    pub fn get_message(&mut self, message_id: u32) -> Option<&[TimedMessage]> {
+        while let Ok(message) = self.rx.try_recv() {
+            info!("Received message: {:?}", message);
+            self.add_message(message);
+        }
+        self.messages.get(&message_id).map(|v| v.as_slice())
+    }
+
+    pub fn stop_listening(&mut self) {
+        self.running_flag.store(false, Ordering::Relaxed);
+        self.task.take().map(|t| t.abort());
+    }
+
+    pub fn listen_from_ethernet_port(&mut self, port: u16) {
+        // Stop the current listener if it exists
+        self.stop_listening();
+        self.running_flag.store(true, Ordering::Relaxed);
+
+        let tx = self.tx.clone();
+        let ctx = self.ctx.clone();
+
+        let bind_address = format!("0.0.0.0:{}", port);
+        let mut buf = Box::new([0; UDP_BUFFER_SIZE]);
+        let running_flag = self.running_flag.clone();
+
+        let handle = tokio::spawn(async move {
+            let socket = UdpSocket::bind(bind_address)
+                .await
+                .context("Failed to bind socket")?;
+            debug!("Listening on UDP");
+
+            while running_flag.load(Ordering::Relaxed) {
+                let (len, _) = socket
+                    .recv_from(buf.as_mut_slice())
+                    .await
+                    .context("Failed to receive message")?;
+                for (_, mav_message) in iter_messages(&buf[..len]) {
+                    tx.send(mav_message).context("Failed to send message")?;
+                    ctx.request_repaint();
+                }
+                // buf.iter_mut().for_each(|b| *b = 0);
+            }
+
+            Ok::<(), anyhow::Error>(())
+        });
+        self.task = Some(handle);
+    }
+
+    pub fn clear(&mut self) {
+        self.messages.clear();
+    }
+
+    fn add_message(&mut self, message: MavMessage) {
+        self.messages
+            .entry(message.message_id())
+            .or_default()
+            .push(TimedMessage::just_received(message));
+    }
+
+    // TODO: Implement a scheduler removal of old messages (configurable, must not hurt performance)
+    // TODO: Add a Dashmap if performance is a problem (Personally don't think it will be)
+}
+
+#[derive(Debug, Clone)]
+pub struct TimedMessage {
+    message: MavMessage,
+    time: Instant,
+}
+
+impl TimedMessage {
+    fn just_received(message: MavMessage) -> Self {
+        Self {
+            message,
+            time: Instant::now(),
+        }
+    }
+}
+
+/// Helper function to read a stream of bytes and return an iterator of MavLink messages
+fn iter_messages(buf: &[u8]) -> impl Iterator<Item = (MavHeader, MavMessage)> + '_ {
+    let mut reader = PeekReader::new(buf);
+    std::iter::from_fn(move || read_v1_msg(&mut reader).ok())
+}
diff --git a/src/ui/composable_view.rs b/src/ui/composable_view.rs
index 7351601..0070e24 100644
--- a/src/ui/composable_view.rs
+++ b/src/ui/composable_view.rs
@@ -1,3 +1,5 @@
+use crate::{mavlink, MSG_MANAGER};
+
 use super::{
     layout_manager::LayoutManager,
     panes::{Pane, PaneBehavior, PaneKind},
@@ -21,6 +23,23 @@ pub struct ComposableView {
     pub layout_manager: LayoutManager,
     behavior: ComposableBehavior,
     maximized_pane: Option<TileId>,
+    sources_window: SourceWindow,
+}
+
+// Implementing the default trait allows us to define a default configuration for our app
+impl Default for ComposableView {
+    fn default() -> Self {
+        let mut tiles = Tiles::default();
+        let root = tiles.insert_pane(Pane::default());
+        let panes_tree = egui_tiles::Tree::new("my_tree", root, tiles);
+
+        Self {
+            panes_tree,
+            behavior: Default::default(),
+            maximized_pane: None,
+            sources_window: Default::default(),
+        }
+    }
 }
 
 // An app must implement the `App` trait to define how the ui is built
@@ -126,9 +145,16 @@ impl eframe::App for ComposableView {
 
         // Show a panel at the bottom of the screen with few global controls
         egui::TopBottomPanel::bottom("bottom_control").show(ctx, |ui| {
+            // Horizontal belt of controls
             ui.horizontal(|ui| {
                 egui::global_theme_preference_switch(ui);
 
+                // Window for the sources
+                self.sources_window.show_window(ui);
+
+                if ui.button("Sources").clicked() {
+                    self.sources_window.visible = !self.sources_window.visible;
+                }
                 if ui.button("Layout Manager").clicked() {
                     self.layout_manager.toggle_open_state();
                 }
@@ -233,6 +259,56 @@ impl ComposableViewState {
     }
 }
 
+struct SourceWindow {
+    port: u16,
+    visible: bool,
+}
+
+impl Default for SourceWindow {
+    fn default() -> Self {
+        Self {
+            port: mavlink::DEFAULT_ETHERNET_PORT,
+            visible: false,
+        }
+    }
+}
+
+impl SourceWindow {
+    fn show_window(&mut self, ui: &mut egui::Ui) {
+        let mut window_is_open = self.visible;
+        let mut can_be_closed = false;
+        egui::Window::new("Sources")
+            .id(ui.id())
+            .auto_sized()
+            .collapsible(true)
+            .movable(true)
+            .open(&mut window_is_open)
+            .show(ui.ctx(), |ui| {
+                self.ui(ui, &mut can_be_closed);
+            });
+        self.visible = window_is_open && !can_be_closed;
+    }
+
+    fn ui(&mut self, ui: &mut egui::Ui, can_be_closed: &mut bool) {
+        egui::Grid::new(ui.id())
+            .num_columns(2)
+            .spacing([10.0, 5.0])
+            .show(ui, |ui| {
+                ui.label("Ethernet Port:");
+                ui.add(egui::Slider::new(&mut self.port, 1024..=65535).text("Port"));
+                ui.end_row();
+            });
+        if ui.button("Connect").clicked() {
+            MSG_MANAGER
+                .get()
+                .unwrap()
+                .lock()
+                .listen_from_ethernet_port(self.port);
+            *can_be_closed = true;
+        }
+    }
+}
+
 /// Behavior for the tree of panes in the composable view
 #[derive(Default)]
 pub struct ComposableBehavior {
-- 
GitLab


From 1badc6d562863d97361dc14c322f7854e362aaf5 Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Tue, 19 Nov 2024 01:38:29 +0100
Subject: [PATCH 04/17] [Mavlink] Updated Plot pane to support
 reconfigurability on mavlink msgs

---
 Cargo.lock              |  53 ++++------
 Cargo.toml              |  30 ++++--
 src/main.rs             |   5 +-
 src/mavlink.rs          |  98 ++++++++++++++++-
 src/ui/panes/plot_2d.rs | 225 ++++++++++++++++++++++++++++++++++------
 5 files changed, 328 insertions(+), 83 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 6fe75a4..bdc8fd3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1546,12 +1546,6 @@ dependencies = [
  "winapi",
 ]
 
-[[package]]
-name = "heck"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
-
 [[package]]
 name = "hermit-abi"
 version = "0.3.9"
@@ -1923,21 +1917,24 @@ dependencies = [
 
 [[package]]
 name = "mavlink-bindgen"
-version = "0.13.2"
-source = "git+https://github.com/federico123579/rust-mavlink.git?branch=strum-integration#02a3eebe16ac1aacb93c2c61696dcd731f6f2ef4"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83b15a4ad504e29cabfb03fdc97250a22d2354a5404d80fc48dbf02e06acf5f"
 dependencies = [
  "crc-any",
  "lazy_static",
  "proc-macro2",
- "quick-xml 0.36.2",
+ "quick-xml 0.26.0",
  "quote",
+ "serde",
  "thiserror",
 ]
 
 [[package]]
 name = "mavlink-core"
-version = "0.13.2"
-source = "git+https://github.com/federico123579/rust-mavlink.git?branch=strum-integration#02a3eebe16ac1aacb93c2c61696dcd731f6f2ef4"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e64d975ca3cf0ad8a7c278553f91d77de15fcde9b79bf6bc542e209dd0c7dee"
 dependencies = [
  "byteorder",
  "crc-any",
@@ -2571,6 +2568,15 @@ version = "1.0.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d"
 
+[[package]]
+name = "quick-xml"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "quick-xml"
 version = "0.30.0"
@@ -2740,12 +2746,6 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
-[[package]]
-name = "rustversion"
-version = "1.0.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
-
 [[package]]
 name = "ryu"
 version = "1.0.18"
@@ -2799,6 +2799,7 @@ dependencies = [
  "egui_plot",
  "egui_tiles",
  "enum_dispatch",
+ "mavlink-bindgen",
  "parking_lot",
  "serde",
  "serde_json",
@@ -2947,17 +2948,16 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
 [[package]]
 name = "skyward_mavlink"
 version = "0.1.0"
-source = "git+https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git?branch=rust-strum#5b515ef056e5f783dc1cbc1c73eadd2f08a5652d"
 dependencies = [
  "bitflags 2.6.0",
  "mavlink-bindgen",
  "mavlink-core",
  "num-derive",
  "num-traits",
+ "paste",
  "serde",
  "serde_arrays",
- "strum",
- "strum_macros",
+ "serde_json",
 ]
 
 [[package]]
@@ -3072,19 +3072,6 @@ version = "0.26.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
 
-[[package]]
-name = "strum_macros"
-version = "0.26.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
-dependencies = [
- "heck",
- "proc-macro2",
- "quote",
- "rustversion",
- "syn 2.0.87",
-]
-
 [[package]]
 name = "syn"
 version = "1.0.109"
diff --git a/Cargo.toml b/Cargo.toml
index 6855548..e3c6d53 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,6 +14,25 @@ eframe = { version = "0.29", features = ["persistence"] }
 egui = { version = "0.29", features = ["log"] }
 egui_plot = "0.29"
 egui_file = "0.19"
+# =========== Asynchronous ===========
+tokio = { version = "1.41", features = [
+    "rt-multi-thread",
+    "net",
+    "parking_lot",
+    "sync",
+] }
+# =========== Mavlink ===========
+# skyward_mavlink = { git = "https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git", branch = "rust-strum", features = [
+#     "reflection",
+#     "lyra",
+#     "serde",
+# ] }
+mavlink-bindgen = { version = "0.13.1", features = ["serde"] }
+skyward_mavlink = { path = "../mavlink-skyward-lib/mavlink_rust", features = [
+    "reflection",
+    "lyra",
+    "serde",
+] }
 # ========= Persistency =========
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
@@ -31,14 +50,3 @@ enum_dispatch = "0.3"
 egui_extras = "0.29.1"
 strum = "0.26"
 anyhow = "1.0"
-
-# =========== Asynchronous ===========
-[dependencies.tokio]
-version = "1.41"
-features = ["rt-multi-thread", "net", "parking_lot", "sync"]
-
-# =========== Mavlink ===========
-[dependencies.skyward_mavlink]
-git = "https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git"
-branch = "rust-strum"
-features = ["lyra", "serde", "strum"]
diff --git a/src/main.rs b/src/main.rs
index 9076b22..4af7995 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,15 +1,16 @@
 mod mavlink;
 mod ui;
 
-use std::sync::OnceLock;
+use std::sync::{LazyLock, OnceLock};
 
-use mavlink::MessageManager;
+use mavlink::{MessageManager, ReflectionContext};
 use parking_lot::Mutex;
 use tokio::runtime::Runtime;
 use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer};
 use ui::ComposableView;
 
 static MSG_MANAGER: OnceLock<Mutex<MessageManager>> = OnceLock::new();
+static MAVLINK_PROFILE: LazyLock<ReflectionContext> = LazyLock::new(|| ReflectionContext::new());
 
 static APP_NAME: &str = "segs";
 
diff --git a/src/mavlink.rs b/src/mavlink.rs
index 6f04270..5b2214f 100644
--- a/src/mavlink.rs
+++ b/src/mavlink.rs
@@ -9,13 +9,13 @@ use std::{
 
 use anyhow::{Context, Result};
 use crossbeam_channel::{Receiver, Sender};
+use mavlink_bindgen::parser::{MavProfile, MavType};
 use skyward_mavlink::{
     lyra::MavMessage,
     mavlink::{peek_reader::PeekReader, read_v1_msg, MavHeader, Message},
 };
-use strum::VariantNames;
 use tokio::{net::UdpSocket, task::JoinHandle};
-use tracing::{debug, info};
+use tracing::debug;
 
 pub const DEFAULT_ETHERNET_PORT: u16 = 42069;
 const UDP_BUFFER_SIZE: usize = 65527;
@@ -45,7 +45,6 @@ impl MessageManager {
 
     pub fn get_message(&mut self, message_id: u32) -> Option<&[TimedMessage]> {
         while let Ok(message) = self.rx.try_recv() {
-            info!("Received message: {:?}", message);
             self.add_message(message);
         }
         self.messages.get(&message_id).map(|v| v.as_slice())
@@ -108,8 +107,8 @@ impl MessageManager {
 
 #[derive(Debug, Clone)]
 pub struct TimedMessage {
-    message: MavMessage,
-    time: Instant,
+    pub message: MavMessage,
+    pub time: Instant,
 }
 
 impl TimedMessage {
@@ -126,3 +125,92 @@ fn iter_messages(buf: &[u8]) -> impl Iterator<Item = (MavHeader, MavMessage)> +
     let mut reader = PeekReader::new(buf);
     std::iter::from_fn(move || read_v1_msg(&mut reader).ok())
 }
+
+pub struct ReflectionContext {
+    mavlink_profile: MavProfile,
+    id_name_map: HashMap<u32, String>,
+}
+
+impl ReflectionContext {
+    pub fn new() -> Self {
+        let profile: MavProfile =
+            serde_json::from_str(skyward_mavlink::reflection::LYRA_MAVLINK_PROFILE_SERIALIZED)
+                .expect("Failed to deserialize MavProfile");
+        let id_name_map = profile
+            .messages
+            .iter()
+            .map(|(name, m)| (m.id, name.clone()))
+            .collect();
+        Self {
+            mavlink_profile: profile,
+            id_name_map,
+        }
+    }
+
+    pub fn get_name_from_id(&self, message_id: u32) -> Option<&str> {
+        self.id_name_map.get(&message_id).map(|s| s.as_str())
+    }
+
+    pub fn messages(&self) -> Vec<&str> {
+        self.mavlink_profile
+            .messages
+            .keys()
+            .map(|s| s.as_str())
+            .collect()
+    }
+
+    pub fn get_fields_by_id(&self, message_id: u32) -> Vec<&str> {
+        self.mavlink_profile
+            .messages
+            .iter()
+            .find(|(_, m)| m.id == message_id)
+            .map(|(_, m)| &m.fields)
+            .unwrap_or_else(|| {
+                panic!("Message ID {} not found in profile", message_id);
+            })
+            .into_iter()
+            .map(|f| f.name.as_str())
+            .collect()
+    }
+
+    pub fn get_plottable_fields_by_id(&self, message_id: u32) -> Vec<&str> {
+        self.mavlink_profile
+            .messages
+            .iter()
+            .find(|(_, m)| m.id == message_id)
+            .map(|(_, m)| &m.fields)
+            .unwrap_or_else(|| {
+                panic!("Message ID {} not found in profile", message_id);
+            })
+            .into_iter()
+            .filter(|f| match f.mavtype {
+                MavType::UInt8
+                | MavType::UInt16
+                | MavType::UInt32
+                | MavType::UInt64
+                | MavType::Int8
+                | MavType::Int16
+                | MavType::Int32
+                | MavType::Int64
+                | MavType::Float
+                | MavType::Double => true,
+                _ => false,
+            })
+            .map(|f| f.name.as_str())
+            .collect()
+    }
+
+    pub fn get_fields_by_name(&self, message_name: &str) -> Vec<&str> {
+        self.mavlink_profile
+            .messages
+            .iter()
+            .find(|(_, m)| m.name == message_name)
+            .map(|(_, m)| &m.fields)
+            .unwrap_or_else(|| {
+                panic!("Message {} not found in profile", message_name);
+            })
+            .into_iter()
+            .map(|f| f.name.as_str())
+            .collect()
+    }
+}
diff --git a/src/ui/panes/plot_2d.rs b/src/ui/panes/plot_2d.rs
index 6ac0658..d3ec757 100644
--- a/src/ui/panes/plot_2d.rs
+++ b/src/ui/panes/plot_2d.rs
@@ -1,22 +1,31 @@
-use crate::ui::composable_view::PaneResponse;
+use crate::{ui::composable_view::PaneResponse, MAVLINK_PROFILE, MSG_MANAGER};
 
 use super::PaneBehavior;
 
+use egui::Color32;
 use egui_plot::{Line, PlotPoints};
 use serde::{Deserialize, Serialize};
+use skyward_mavlink::{
+    lyra::{MavMessage, ROCKET_FLIGHT_TM_DATA},
+    mavlink::{Message, MessageData},
+};
 
 #[derive(Clone, Debug, Serialize, Deserialize)]
 pub struct Plot2DPane {
-    n_points: u32,
-    frequency: f64,
-    width: f32,
-    color: egui::Color32,
-
+    // UI settings
     #[serde(skip)]
     pub contains_pointer: bool,
-
     #[serde(skip)]
     settings_visible: bool,
+    sources_visible: bool,
+    // Mavlink settings
+    msg_id: u32,
+    field_x: String,
+    fields_y: Vec<String>,
+    plot_active: bool,
+    // Plot specific settings
+    width: f32,
+    color: Color32,
 }
 
 impl Default for Plot2DPane {
@@ -24,10 +33,13 @@ impl Default for Plot2DPane {
         Self {
             contains_pointer: false,
             settings_visible: false,
-            n_points: 2,
-            frequency: 1.0,
+            sources_visible: false,
+            msg_id: ROCKET_FLIGHT_TM_DATA::ID,
+            field_x: "timestamp".to_owned(),
+            fields_y: vec![],
+            plot_active: false,
             width: 1.0,
-            color: egui::Color32::from_rgb(0, 120, 240),
+            color: Color32::from_rgb(0, 120, 240),
         }
     }
 }
@@ -45,34 +57,80 @@ impl PaneBehavior for Plot2DPane {
     fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse {
         let mut response = PaneResponse::default();
 
-        let mut window_visible = self.settings_visible;
+        // Spawn windows
+        let mut settings_window_visible = self.settings_visible;
         egui::Window::new("Plot Settings")
-            .id(ui.id())
+            .id(ui.make_persistent_id("plot_settings"))
             .auto_sized()
             .collapsible(true)
             .movable(true)
-            .open(&mut window_visible)
+            .open(&mut settings_window_visible)
             .show(ui.ctx(), |ui| self.settings_window(ui));
-        self.settings_visible = window_visible;
+        self.settings_visible = settings_window_visible;
+
+        let mut sources_window_visible = self.sources_visible;
+        egui::Window::new("Plot Sources")
+            .id(ui.make_persistent_id("plot_sources"))
+            .auto_sized()
+            .collapsible(true)
+            .movable(true)
+            .open(&mut sources_window_visible)
+            .show(ui.ctx(), |ui| self.sources_window(ui));
+        self.sources_visible = sources_window_visible;
 
         let ctrl_pressed = ui.input(|i| i.modifiers.ctrl);
 
-        let plot = egui_plot::Plot::new("plot");
+        let mut plot_lines = Vec::new();
+        if self.plot_active {
+            let acc_points = MSG_MANAGER
+                .get()
+                .unwrap()
+                .lock()
+                .get_message(self.msg_id)
+                .map(|msg| {
+                    msg.into_iter()
+                        .map(|msg| {
+                            let value: serde_json::Value =
+                                serde_json::to_value(msg.message.clone()).unwrap();
+
+                            let x = value.get(&self.field_x).unwrap();
+                            let x = serde_json::from_value::<f64>(x.clone()).unwrap();
+                            let mut ys = Vec::new();
+                            for field in self.fields_y.iter() {
+                                let y = value.get(field).unwrap();
+                                ys.push(serde_json::from_value::<f64>(y.clone()).unwrap());
+                            }
+                            (x, ys)
+                        })
+                        .collect::<Vec<(f64, Vec<f64>)>>()
+                })
+                .unwrap_or_default();
+
+            if !acc_points.is_empty() {
+                for i in 0..self.fields_y.len() {
+                    let plot_line: Vec<[f64; 2]> = acc_points
+                        .iter()
+                        .map(|(timestamp, acc)| [*timestamp as f64, acc[i] as f64])
+                        .collect();
+                    plot_lines.push(plot_line);
+                }
+            }
+        }
+
+        let plot = egui_plot::Plot::new("plot").auto_bounds([true, true].into());
         plot.show(ui, |plot_ui| {
             self.contains_pointer = plot_ui.response().contains_pointer();
             if plot_ui.response().dragged() && ctrl_pressed {
                 println!("ctrl + drag");
                 response.set_drag_started();
             }
-            let points: Vec<[f64; 2]> = (0..self.n_points)
-                .map(|i| i as f64 * 100.0 / (self.n_points - 1) as f64)
-                .map(|i| [i, (i * std::f64::consts::PI * 2.0 * self.frequency).sin()])
-                .collect();
-            plot_ui.line(
-                Line::new(PlotPoints::from(points))
-                    .color(self.color)
-                    .width(self.width),
-            );
+            for plot_line in plot_lines {
+                plot_ui.line(
+                    Line::new(PlotPoints::from(plot_line))
+                        .color(self.color)
+                        .width(self.width),
+                );
+            }
             plot_ui.response().context_menu(|ui| self.menu(ui));
         });
 
@@ -92,6 +150,11 @@ impl Plot2DPane {
             self.settings_visible = true;
             ui.close_menu();
         }
+
+        if ui.button("Sources…").clicked() {
+            self.sources_visible = true;
+            ui.close_menu();
+        }
     }
 
     fn settings_window(&mut self, ui: &mut egui::Ui) {
@@ -99,14 +162,6 @@ impl Plot2DPane {
             .num_columns(2)
             .spacing([10.0, 5.0])
             .show(ui, |ui| {
-                ui.label("Size:");
-                ui.add(egui::Slider::new(&mut self.n_points, 2..=1000).text("Points"));
-                ui.end_row();
-
-                ui.label("Frequency:");
-                ui.add(egui::Slider::new(&mut self.frequency, 0.1..=10.0).text("Hz"));
-                ui.end_row();
-
                 ui.label("Color:");
                 ui.color_edit_button_srgba(&mut self.color);
                 ui.end_row();
@@ -116,4 +171,110 @@ impl Plot2DPane {
                 ui.end_row();
             });
     }
+
+    fn sources_window(&mut self, ui: &mut egui::Ui) {
+        let old_msg_id = self.msg_id;
+        let msg_name = MAVLINK_PROFILE
+            .get_name_from_id(self.msg_id)
+            .unwrap_or_default();
+        egui::ComboBox::from_label("Message Kind")
+            .selected_text(msg_name)
+            .show_ui(ui, |ui| {
+                for msg in MAVLINK_PROFILE.messages() {
+                    ui.selectable_value(
+                        &mut self.msg_id,
+                        MavMessage::message_id_from_name(msg).unwrap(),
+                        msg,
+                    );
+                }
+            });
+
+        // reset fields if the message is changed
+        if self.msg_id != old_msg_id {
+            self.fields_y.truncate(1);
+        }
+
+        // check fields and assing a default field_x and field_y once the msg is changed
+        let fields = MAVLINK_PROFILE.get_plottable_fields_by_id(self.msg_id);
+        // get the first field that is in the list of fields or the previous if valid
+        let mut field_x = fields
+            .contains(&self.field_x.as_str())
+            .then(|| self.field_x.clone())
+            .or(fields.get(0).map(|s| s.to_string()));
+        // get the second field that is in the list of fields or the previous if valid
+        let mut field_y = self
+            .fields_y
+            .get(0)
+            .map(|s| fields.contains(&s.as_str()).then_some(s.to_owned()))
+            .flatten()
+            .or(fields.get(1).map(|s| s.to_string()));
+
+        // if fields are valid, show the combo boxes for the x_axis
+        if field_x.is_some() {
+            let field_x = field_x.as_mut().unwrap();
+            egui::ComboBox::from_label("X Axis")
+                .selected_text(field_x.as_str())
+                .show_ui(ui, |ui| {
+                    for msg in fields.iter() {
+                        ui.selectable_value(field_x, (*msg).to_owned(), *msg);
+                    }
+                });
+        }
+        // if fields are more than 1, show the combo boxes for the y_axis
+        if field_y.is_some() {
+            let field_y = field_y.as_mut().unwrap();
+            let widget_label = if self.fields_y.len() > 1 {
+                "Y Axis 1"
+            } else {
+                "Y Axis"
+            };
+            egui::ComboBox::from_label(widget_label)
+                .selected_text(field_y.as_str())
+                .show_ui(ui, |ui| {
+                    for msg in fields.iter() {
+                        ui.selectable_value(field_y, (*msg).to_owned(), *msg);
+                    }
+                });
+        }
+        // check how many fields are left and how many are selected
+        let fields_selected = self.fields_y.len() + 1;
+        let fields_left_to_draw = fields.len().saturating_sub(2);
+        for i in 0..fields_left_to_draw.min(fields_selected.saturating_sub(2)) {
+            let field = self.fields_y.get_mut(1 + i).unwrap();
+            let widget_label = format!("Y Axis {}", i + 2);
+            egui::ComboBox::from_label(widget_label)
+                .selected_text(field.as_str())
+                .show_ui(ui, |ui| {
+                    for msg in fields.iter() {
+                        ui.selectable_value(field, (*msg).to_owned(), *msg);
+                    }
+                });
+            self.fields_y[1 + i] = field.clone();
+        }
+
+        // if we have fields left, show the add button
+        let fields_left_to_draw = fields.len().saturating_sub(fields_selected);
+        if fields_left_to_draw > 0 {
+            if ui
+                .button("Add Y Axis")
+                .on_hover_text("Add another Y axis")
+                .clicked()
+            {
+                self.fields_y.push(fields[fields_selected].to_string());
+            }
+        }
+
+        // update fields and flag for active plot
+        self.field_x = field_x.unwrap_or_default();
+        if field_y.is_some() {
+            if self.fields_y.get(0).is_none() {
+                self.fields_y.push(field_y.unwrap());
+            } else {
+                self.fields_y[0] = field_y.unwrap();
+            }
+            self.plot_active = true;
+        } else {
+            self.plot_active = false;
+        }
+    }
 }
-- 
GitLab


From 027ea6ab7a26a5b5630b50b86a9ef7ad21b133fa Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Sat, 23 Nov 2024 14:51:44 +0100
Subject: [PATCH 05/17] [Mavlink] Fixed few warnings

---
 src/main.rs             |  2 +-
 src/mavlink.rs          | 36 ++++++++++++++++++++----------------
 src/ui/panes.rs         | 10 +---------
 src/ui/panes/default.rs |  2 +-
 src/ui/panes/plot_2d.rs | 22 ++++++++++------------
 5 files changed, 33 insertions(+), 39 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 4af7995..852ee0f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -10,7 +10,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilte
 use ui::ComposableView;
 
 static MSG_MANAGER: OnceLock<Mutex<MessageManager>> = OnceLock::new();
-static MAVLINK_PROFILE: LazyLock<ReflectionContext> = LazyLock::new(|| ReflectionContext::new());
+static MAVLINK_PROFILE: LazyLock<ReflectionContext> = LazyLock::new(ReflectionContext::new);
 
 static APP_NAME: &str = "segs";
 
diff --git a/src/mavlink.rs b/src/mavlink.rs
index 5b2214f..b0ce8e5 100644
--- a/src/mavlink.rs
+++ b/src/mavlink.rs
@@ -52,7 +52,9 @@ impl MessageManager {
 
     pub fn stop_listening(&mut self) {
         self.running_flag.store(false, Ordering::Relaxed);
-        self.task.take().map(|t| t.abort());
+        if let Some(t) = self.task.take() {
+            t.abort()
+        }
     }
 
     pub fn listen_from_ethernet_port(&mut self, port: u16) {
@@ -168,7 +170,7 @@ impl ReflectionContext {
             .unwrap_or_else(|| {
                 panic!("Message ID {} not found in profile", message_id);
             })
-            .into_iter()
+            .iter()
             .map(|f| f.name.as_str())
             .collect()
     }
@@ -182,19 +184,21 @@ impl ReflectionContext {
             .unwrap_or_else(|| {
                 panic!("Message ID {} not found in profile", message_id);
             })
-            .into_iter()
-            .filter(|f| match f.mavtype {
-                MavType::UInt8
-                | MavType::UInt16
-                | MavType::UInt32
-                | MavType::UInt64
-                | MavType::Int8
-                | MavType::Int16
-                | MavType::Int32
-                | MavType::Int64
-                | MavType::Float
-                | MavType::Double => true,
-                _ => false,
+            .iter()
+            .filter(|f| {
+                matches!(
+                    f.mavtype,
+                    MavType::UInt8
+                        | MavType::UInt16
+                        | MavType::UInt32
+                        | MavType::UInt64
+                        | MavType::Int8
+                        | MavType::Int16
+                        | MavType::Int32
+                        | MavType::Int64
+                        | MavType::Float
+                        | MavType::Double
+                )
             })
             .map(|f| f.name.as_str())
             .collect()
@@ -209,7 +213,7 @@ impl ReflectionContext {
             .unwrap_or_else(|| {
                 panic!("Message {} not found in profile", message_name);
             })
-            .into_iter()
+            .iter()
             .map(|f| f.name.as_str())
             .collect()
     }
diff --git a/src/ui/panes.rs b/src/ui/panes.rs
index 50c4f90..0ebd401 100644
--- a/src/ui/panes.rs
+++ b/src/ui/panes.rs
@@ -7,19 +7,11 @@ use serde::{Deserialize, Serialize};
 
 use super::composable_view::PaneResponse;
 
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
 pub struct Pane {
     pub pane: PaneKind,
 }
 
-impl Default for Pane {
-    fn default() -> Self {
-        Self {
-            pane: PaneKind::default(),
-        }
-    }
-}
-
 impl Pane {
     pub fn boxed(pane: PaneKind) -> Box<Self> {
         Box::new(Self { pane })
diff --git a/src/ui/panes/default.rs b/src/ui/panes/default.rs
index 250e35e..31fa83a 100644
--- a/src/ui/panes/default.rs
+++ b/src/ui/panes/default.rs
@@ -7,7 +7,7 @@ use crate::ui::{
     utils::{vertically_centered, SizingMemo},
 };
 
-#[derive(Clone, Debug, Serialize, Deserialize)]
+#[derive(Clone, Debug, Default, Serialize, Deserialize)]
 pub struct DefaultPane {
     occupied: f32,
     fixed: bool,
diff --git a/src/ui/panes/plot_2d.rs b/src/ui/panes/plot_2d.rs
index d3ec757..30b2d55 100644
--- a/src/ui/panes/plot_2d.rs
+++ b/src/ui/panes/plot_2d.rs
@@ -88,7 +88,7 @@ impl PaneBehavior for Plot2DPane {
                 .lock()
                 .get_message(self.msg_id)
                 .map(|msg| {
-                    msg.into_iter()
+                    msg.iter()
                         .map(|msg| {
                             let value: serde_json::Value =
                                 serde_json::to_value(msg.message.clone()).unwrap();
@@ -110,7 +110,7 @@ impl PaneBehavior for Plot2DPane {
                 for i in 0..self.fields_y.len() {
                     let plot_line: Vec<[f64; 2]> = acc_points
                         .iter()
-                        .map(|(timestamp, acc)| [*timestamp as f64, acc[i] as f64])
+                        .map(|(timestamp, acc)| [{ *timestamp }, acc[i]])
                         .collect();
                     plot_lines.push(plot_line);
                 }
@@ -200,13 +200,12 @@ impl Plot2DPane {
         let mut field_x = fields
             .contains(&self.field_x.as_str())
             .then(|| self.field_x.clone())
-            .or(fields.get(0).map(|s| s.to_string()));
+            .or(fields.first().map(|s| s.to_string()));
         // get the second field that is in the list of fields or the previous if valid
         let mut field_y = self
             .fields_y
-            .get(0)
-            .map(|s| fields.contains(&s.as_str()).then_some(s.to_owned()))
-            .flatten()
+            .first()
+            .and_then(|s| fields.contains(&s.as_str()).then_some(s.to_owned()))
             .or(fields.get(1).map(|s| s.to_string()));
 
         // if fields are valid, show the combo boxes for the x_axis
@@ -254,20 +253,19 @@ impl Plot2DPane {
 
         // if we have fields left, show the add button
         let fields_left_to_draw = fields.len().saturating_sub(fields_selected);
-        if fields_left_to_draw > 0 {
-            if ui
+        if fields_left_to_draw > 0
+            && ui
                 .button("Add Y Axis")
                 .on_hover_text("Add another Y axis")
                 .clicked()
-            {
-                self.fields_y.push(fields[fields_selected].to_string());
-            }
+        {
+            self.fields_y.push(fields[fields_selected].to_string());
         }
 
         // update fields and flag for active plot
         self.field_x = field_x.unwrap_or_default();
         if field_y.is_some() {
-            if self.fields_y.get(0).is_none() {
+            if self.fields_y.first().is_none() {
                 self.fields_y.push(field_y.unwrap());
             } else {
                 self.fields_y[0] = field_y.unwrap();
-- 
GitLab


From 760a58e49cb2e6f6b1cd1710dc39eb9d4fe2bad2 Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Sun, 24 Nov 2024 12:30:13 +0100
Subject: [PATCH 06/17] [Plot2D] Updated settings window

---
 src/ui/panes/plot_2d.rs | 97 ++++++++++++++++++++++++++---------------
 1 file changed, 62 insertions(+), 35 deletions(-)

diff --git a/src/ui/panes/plot_2d.rs b/src/ui/panes/plot_2d.rs
index 30b2d55..0f5eb81 100644
--- a/src/ui/panes/plot_2d.rs
+++ b/src/ui/panes/plot_2d.rs
@@ -10,6 +10,32 @@ use skyward_mavlink::{
     mavlink::{Message, MessageData},
 };
 
+#[derive(Clone, Debug, Serialize, Deserialize)]
+struct PlotLineSettings {
+    field_y: String,
+    width: f32,
+    color: Color32,
+}
+
+impl Default for PlotLineSettings {
+    fn default() -> Self {
+        Self {
+            field_y: "".to_owned(),
+            width: 1.0,
+            color: Color32::BLUE,
+        }
+    }
+}
+
+impl PlotLineSettings {
+    fn new(field_y: String) -> Self {
+        Self {
+            field_y,
+            ..Default::default()
+        }
+    }
+}
+
 #[derive(Clone, Debug, Serialize, Deserialize)]
 pub struct Plot2DPane {
     // UI settings
@@ -21,11 +47,8 @@ pub struct Plot2DPane {
     // Mavlink settings
     msg_id: u32,
     field_x: String,
-    fields_y: Vec<String>,
+    plot_lines: Vec<PlotLineSettings>,
     plot_active: bool,
-    // Plot specific settings
-    width: f32,
-    color: Color32,
 }
 
 impl Default for Plot2DPane {
@@ -36,10 +59,8 @@ impl Default for Plot2DPane {
             sources_visible: false,
             msg_id: ROCKET_FLIGHT_TM_DATA::ID,
             field_x: "timestamp".to_owned(),
-            fields_y: vec![],
+            plot_lines: vec![],
             plot_active: false,
-            width: 1.0,
-            color: Color32::from_rgb(0, 120, 240),
         }
     }
 }
@@ -96,8 +117,8 @@ impl PaneBehavior for Plot2DPane {
                             let x = value.get(&self.field_x).unwrap();
                             let x = serde_json::from_value::<f64>(x.clone()).unwrap();
                             let mut ys = Vec::new();
-                            for field in self.fields_y.iter() {
-                                let y = value.get(field).unwrap();
+                            for field in self.plot_lines.iter() {
+                                let y = value.get(field.field_y.as_str()).unwrap();
                                 ys.push(serde_json::from_value::<f64>(y.clone()).unwrap());
                             }
                             (x, ys)
@@ -107,12 +128,12 @@ impl PaneBehavior for Plot2DPane {
                 .unwrap_or_default();
 
             if !acc_points.is_empty() {
-                for i in 0..self.fields_y.len() {
-                    let plot_line: Vec<[f64; 2]> = acc_points
+                for (i, plot_line) in self.plot_lines.iter().enumerate() {
+                    let points: Vec<[f64; 2]> = acc_points
                         .iter()
                         .map(|(timestamp, acc)| [{ *timestamp }, acc[i]])
                         .collect();
-                    plot_lines.push(plot_line);
+                    plot_lines.push((plot_line.clone(), points));
                 }
             }
         }
@@ -124,11 +145,11 @@ impl PaneBehavior for Plot2DPane {
                 println!("ctrl + drag");
                 response.set_drag_started();
             }
-            for plot_line in plot_lines {
+            for (plot_settings, data_points) in plot_lines {
                 plot_ui.line(
-                    Line::new(PlotPoints::from(plot_line))
-                        .color(self.color)
-                        .width(self.width),
+                    Line::new(PlotPoints::from(data_points))
+                        .color(plot_settings.color)
+                        .width(plot_settings.width),
                 );
             }
             plot_ui.response().context_menu(|ui| self.menu(ui));
@@ -159,16 +180,16 @@ impl Plot2DPane {
 
     fn settings_window(&mut self, ui: &mut egui::Ui) {
         egui::Grid::new(ui.id())
-            .num_columns(2)
+            .num_columns(4)
             .spacing([10.0, 5.0])
             .show(ui, |ui| {
-                ui.label("Color:");
-                ui.color_edit_button_srgba(&mut self.color);
-                ui.end_row();
-
-                ui.label("Width:");
-                ui.add(egui::Slider::new(&mut self.width, 0.1..=10.0).text("pt"));
-                ui.end_row();
+                for plot_line in self.plot_lines.iter_mut() {
+                    ui.label(&plot_line.field_y);
+                    ui.color_edit_button_srgba(&mut plot_line.color);
+                    ui.label("Width:");
+                    ui.add(egui::Slider::new(&mut plot_line.width, 0.1..=10.0).text("pt"));
+                    ui.end_row();
+                }
             });
     }
 
@@ -191,7 +212,7 @@ impl Plot2DPane {
 
         // reset fields if the message is changed
         if self.msg_id != old_msg_id {
-            self.fields_y.truncate(1);
+            self.plot_lines.truncate(1);
         }
 
         // check fields and assing a default field_x and field_y once the msg is changed
@@ -203,9 +224,13 @@ impl Plot2DPane {
             .or(fields.first().map(|s| s.to_string()));
         // get the second field that is in the list of fields or the previous if valid
         let mut field_y = self
-            .fields_y
+            .plot_lines
             .first()
-            .and_then(|s| fields.contains(&s.as_str()).then_some(s.to_owned()))
+            .and_then(|s| {
+                fields
+                    .contains(&s.field_y.as_str())
+                    .then_some(s.field_y.to_owned())
+            })
             .or(fields.get(1).map(|s| s.to_string()));
 
         // if fields are valid, show the combo boxes for the x_axis
@@ -222,7 +247,7 @@ impl Plot2DPane {
         // if fields are more than 1, show the combo boxes for the y_axis
         if field_y.is_some() {
             let field_y = field_y.as_mut().unwrap();
-            let widget_label = if self.fields_y.len() > 1 {
+            let widget_label = if self.plot_lines.len() > 1 {
                 "Y Axis 1"
             } else {
                 "Y Axis"
@@ -236,10 +261,10 @@ impl Plot2DPane {
                 });
         }
         // check how many fields are left and how many are selected
-        let fields_selected = self.fields_y.len() + 1;
+        let fields_selected = self.plot_lines.len() + 1;
         let fields_left_to_draw = fields.len().saturating_sub(2);
         for i in 0..fields_left_to_draw.min(fields_selected.saturating_sub(2)) {
-            let field = self.fields_y.get_mut(1 + i).unwrap();
+            let field = &mut self.plot_lines.get_mut(1 + i).unwrap().field_y;
             let widget_label = format!("Y Axis {}", i + 2);
             egui::ComboBox::from_label(widget_label)
                 .selected_text(field.as_str())
@@ -248,7 +273,7 @@ impl Plot2DPane {
                         ui.selectable_value(field, (*msg).to_owned(), *msg);
                     }
                 });
-            self.fields_y[1 + i] = field.clone();
+            self.plot_lines[1 + i].field_y = field.clone();
         }
 
         // if we have fields left, show the add button
@@ -259,16 +284,18 @@ impl Plot2DPane {
                 .on_hover_text("Add another Y axis")
                 .clicked()
         {
-            self.fields_y.push(fields[fields_selected].to_string());
+            self.plot_lines
+                .push(PlotLineSettings::new(fields[fields_selected].to_string()));
         }
 
         // update fields and flag for active plot
         self.field_x = field_x.unwrap_or_default();
         if field_y.is_some() {
-            if self.fields_y.first().is_none() {
-                self.fields_y.push(field_y.unwrap());
+            if self.plot_lines.first().is_none() {
+                self.plot_lines
+                    .push(PlotLineSettings::new(field_y.unwrap()));
             } else {
-                self.fields_y[0] = field_y.unwrap();
+                self.plot_lines[0].field_y = field_y.unwrap();
             }
             self.plot_active = true;
         } else {
-- 
GitLab


From 1ec99eedb01b02e331c6bb6f7b4d874468bd9dca Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Sun, 24 Nov 2024 20:28:42 +0100
Subject: [PATCH 07/17] [Plot2D] Refactored code added

---
 src/mavlink.rs          |   9 +-
 src/ui/panes/plot_2d.rs | 305 +++++++++++++++++++++-------------------
 2 files changed, 167 insertions(+), 147 deletions(-)

diff --git a/src/mavlink.rs b/src/mavlink.rs
index b0ce8e5..20ceffe 100644
--- a/src/mavlink.rs
+++ b/src/mavlink.rs
@@ -153,12 +153,15 @@ impl ReflectionContext {
         self.id_name_map.get(&message_id).map(|s| s.as_str())
     }
 
-    pub fn messages(&self) -> Vec<&str> {
-        self.mavlink_profile
+    pub fn sorted_messages(&self) -> Vec<&str> {
+        let mut msgs: Vec<&str> = self
+            .mavlink_profile
             .messages
             .keys()
             .map(|s| s.as_str())
-            .collect()
+            .collect();
+        msgs.sort();
+        msgs
     }
 
     pub fn get_fields_by_id(&self, message_id: u32) -> Vec<&str> {
diff --git a/src/ui/panes/plot_2d.rs b/src/ui/panes/plot_2d.rs
index 0f5eb81..0547ef9 100644
--- a/src/ui/panes/plot_2d.rs
+++ b/src/ui/panes/plot_2d.rs
@@ -2,8 +2,8 @@ use crate::{ui::composable_view::PaneResponse, MAVLINK_PROFILE, MSG_MANAGER};
 
 use super::PaneBehavior;
 
-use egui::Color32;
-use egui_plot::{Line, PlotPoints};
+use egui::{Color32, Vec2b};
+use egui_plot::{Legend, Line, PlotPoints};
 use serde::{Deserialize, Serialize};
 use skyward_mavlink::{
     lyra::{MavMessage, ROCKET_FLIGHT_TM_DATA},
@@ -12,7 +12,7 @@ use skyward_mavlink::{
 
 #[derive(Clone, Debug, Serialize, Deserialize)]
 struct PlotLineSettings {
-    field_y: String,
+    field: String,
     width: f32,
     color: Color32,
 }
@@ -20,7 +20,7 @@ struct PlotLineSettings {
 impl Default for PlotLineSettings {
     fn default() -> Self {
         Self {
-            field_y: "".to_owned(),
+            field: "".to_owned(),
             width: 1.0,
             color: Color32::BLUE,
         }
@@ -30,7 +30,7 @@ impl Default for PlotLineSettings {
 impl PlotLineSettings {
     fn new(field_y: String) -> Self {
         Self {
-            field_y,
+            field: field_y,
             ..Default::default()
         }
     }
@@ -78,26 +78,34 @@ impl PaneBehavior for Plot2DPane {
     fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse {
         let mut response = PaneResponse::default();
 
+        let Self {
+            settings_visible,
+            sources_visible,
+            plot_lines,
+            msg_id,
+            field_x,
+            plot_active,
+            ..
+        } = self;
+
         // Spawn windows
-        let mut settings_window_visible = self.settings_visible;
         egui::Window::new("Plot Settings")
             .id(ui.make_persistent_id("plot_settings"))
             .auto_sized()
             .collapsible(true)
             .movable(true)
-            .open(&mut settings_window_visible)
-            .show(ui.ctx(), |ui| self.settings_window(ui));
-        self.settings_visible = settings_window_visible;
+            .open(settings_visible)
+            .show(ui.ctx(), |ui| settings_window(ui, plot_lines));
 
-        let mut sources_window_visible = self.sources_visible;
         egui::Window::new("Plot Sources")
             .id(ui.make_persistent_id("plot_sources"))
             .auto_sized()
             .collapsible(true)
             .movable(true)
-            .open(&mut sources_window_visible)
-            .show(ui.ctx(), |ui| self.sources_window(ui));
-        self.sources_visible = sources_window_visible;
+            .open(sources_visible)
+            .show(ui.ctx(), |ui| {
+                sources_window(ui, msg_id, field_x, plot_lines, plot_active)
+            });
 
         let ctrl_pressed = ui.input(|i| i.modifiers.ctrl);
 
@@ -118,7 +126,7 @@ impl PaneBehavior for Plot2DPane {
                             let x = serde_json::from_value::<f64>(x.clone()).unwrap();
                             let mut ys = Vec::new();
                             for field in self.plot_lines.iter() {
-                                let y = value.get(field.field_y.as_str()).unwrap();
+                                let y = value.get(field.field.as_str()).unwrap();
                                 ys.push(serde_json::from_value::<f64>(y.clone()).unwrap());
                             }
                             (x, ys)
@@ -138,7 +146,9 @@ impl PaneBehavior for Plot2DPane {
             }
         }
 
-        let plot = egui_plot::Plot::new("plot").auto_bounds([true, true].into());
+        let plot = egui_plot::Plot::new("plot")
+            .auto_bounds(Vec2b::TRUE)
+            .legend(Legend::default());
         plot.show(ui, |plot_ui| {
             self.contains_pointer = plot_ui.response().contains_pointer();
             if plot_ui.response().dragged() && ctrl_pressed {
@@ -152,7 +162,9 @@ impl PaneBehavior for Plot2DPane {
                         .width(plot_settings.width),
                 );
             }
-            plot_ui.response().context_menu(|ui| self.menu(ui));
+            plot_ui
+                .response()
+                .context_menu(|ui| show_menu(ui, settings_visible, sources_visible));
         });
 
         response
@@ -163,143 +175,148 @@ impl PaneBehavior for Plot2DPane {
     }
 }
 
-impl Plot2DPane {
-    fn menu(&mut self, ui: &mut egui::Ui) {
-        ui.set_max_width(200.0); // To make sure we wrap long text
+fn settings_window(ui: &mut egui::Ui, plot_lines: &mut [PlotLineSettings]) {
+    egui::Grid::new(ui.id())
+        .num_columns(4)
+        .spacing([10.0, 5.0])
+        .show(ui, |ui| {
+            for plot_line in plot_lines.iter_mut() {
+                ui.label(&plot_line.field);
+                ui.color_edit_button_srgba(&mut plot_line.color);
+                ui.add(
+                    egui::DragValue::new(&mut plot_line.width)
+                        .speed(0.1)
+                        .suffix(" pt"),
+                )
+                .on_hover_text("Width of the line in points");
+                ui.end_row();
+            }
+        });
+}
 
-        if ui.button("Settings…").clicked() {
-            self.settings_visible = true;
-            ui.close_menu();
-        }
+fn sources_window(
+    ui: &mut egui::Ui,
+    msg_id: &mut u32,
+    field_x: &mut String,
+    plot_lines: &mut Vec<PlotLineSettings>,
+    plot_active: &mut bool,
+) {
+    // record msg id to check if it has changed
+    let old_msg_id = *msg_id;
+    // extract the msg name from the id to show it in the combo box
+    let msg_name = MAVLINK_PROFILE
+        .get_name_from_id(*msg_id)
+        .unwrap_or_default();
 
-        if ui.button("Sources…").clicked() {
-            self.sources_visible = true;
-            ui.close_menu();
-        }
-    }
+    // show the first combo box with the message name selection
+    egui::ComboBox::from_label("Message Kind")
+        .selected_text(msg_name)
+        .show_ui(ui, |ui| {
+            for msg in MAVLINK_PROFILE.sorted_messages() {
+                ui.selectable_value(msg_id, MavMessage::message_id_from_name(msg).unwrap(), msg);
+            }
+        });
 
-    fn settings_window(&mut self, ui: &mut egui::Ui) {
-        egui::Grid::new(ui.id())
-            .num_columns(4)
-            .spacing([10.0, 5.0])
-            .show(ui, |ui| {
-                for plot_line in self.plot_lines.iter_mut() {
-                    ui.label(&plot_line.field_y);
-                    ui.color_edit_button_srgba(&mut plot_line.color);
-                    ui.label("Width:");
-                    ui.add(egui::Slider::new(&mut plot_line.width, 0.1..=10.0).text("pt"));
-                    ui.end_row();
-                }
-            });
+    // reset fields if the message is changed
+    if *msg_id != old_msg_id {
+        plot_lines.truncate(1);
     }
 
-    fn sources_window(&mut self, ui: &mut egui::Ui) {
-        let old_msg_id = self.msg_id;
-        let msg_name = MAVLINK_PROFILE
-            .get_name_from_id(self.msg_id)
-            .unwrap_or_default();
-        egui::ComboBox::from_label("Message Kind")
-            .selected_text(msg_name)
-            .show_ui(ui, |ui| {
-                for msg in MAVLINK_PROFILE.messages() {
-                    ui.selectable_value(
-                        &mut self.msg_id,
-                        MavMessage::message_id_from_name(msg).unwrap(),
-                        msg,
-                    );
-                }
-            });
+    // check fields and assing a default field_x and field_y once the msg is changed
+    let fields = MAVLINK_PROFILE.get_plottable_fields_by_id(*msg_id);
+    // get the first field that is in the list of fields or the previous if valid
+    let new_field_x = fields
+        .contains(&field_x.as_str())
+        .then(|| field_x.to_owned())
+        .or(fields.first().map(|s| s.to_string()));
 
-        // reset fields if the message is changed
-        if self.msg_id != old_msg_id {
-            self.plot_lines.truncate(1);
-        }
+    // if there are no fields, reset the field_x and plot_lines
+    let Some(new_field_x) = new_field_x else {
+        *field_x = "".to_owned();
+        plot_lines.clear();
+        *plot_active = false;
+        return;
+    };
+    // update the field_x
+    *field_x = new_field_x;
 
-        // check fields and assing a default field_x and field_y once the msg is changed
-        let fields = MAVLINK_PROFILE.get_plottable_fields_by_id(self.msg_id);
-        // get the first field that is in the list of fields or the previous if valid
-        let mut field_x = fields
-            .contains(&self.field_x.as_str())
-            .then(|| self.field_x.clone())
-            .or(fields.first().map(|s| s.to_string()));
-        // get the second field that is in the list of fields or the previous if valid
-        let mut field_y = self
-            .plot_lines
-            .first()
-            .and_then(|s| {
-                fields
-                    .contains(&s.field_y.as_str())
-                    .then_some(s.field_y.to_owned())
-            })
-            .or(fields.get(1).map(|s| s.to_string()));
-
-        // if fields are valid, show the combo boxes for the x_axis
-        if field_x.is_some() {
-            let field_x = field_x.as_mut().unwrap();
-            egui::ComboBox::from_label("X Axis")
-                .selected_text(field_x.as_str())
-                .show_ui(ui, |ui| {
-                    for msg in fields.iter() {
-                        ui.selectable_value(field_x, (*msg).to_owned(), *msg);
-                    }
-                });
-        }
-        // if fields are more than 1, show the combo boxes for the y_axis
-        if field_y.is_some() {
-            let field_y = field_y.as_mut().unwrap();
-            let widget_label = if self.plot_lines.len() > 1 {
-                "Y Axis 1"
-            } else {
-                "Y Axis"
-            };
-            egui::ComboBox::from_label(widget_label)
-                .selected_text(field_y.as_str())
-                .show_ui(ui, |ui| {
-                    for msg in fields.iter() {
-                        ui.selectable_value(field_y, (*msg).to_owned(), *msg);
-                    }
-                });
-        }
-        // check how many fields are left and how many are selected
-        let fields_selected = self.plot_lines.len() + 1;
-        let fields_left_to_draw = fields.len().saturating_sub(2);
-        for i in 0..fields_left_to_draw.min(fields_selected.saturating_sub(2)) {
-            let field = &mut self.plot_lines.get_mut(1 + i).unwrap().field_y;
-            let widget_label = format!("Y Axis {}", i + 2);
-            egui::ComboBox::from_label(widget_label)
-                .selected_text(field.as_str())
-                .show_ui(ui, |ui| {
-                    for msg in fields.iter() {
-                        ui.selectable_value(field, (*msg).to_owned(), *msg);
-                    }
-                });
-            self.plot_lines[1 + i].field_y = field.clone();
-        }
+    // if fields are valid, show the combo boxes for the x_axis
+    egui::ComboBox::from_label("X Axis")
+        .selected_text(field_x.as_str())
+        .show_ui(ui, |ui| {
+            for msg in fields.iter() {
+                ui.selectable_value(field_x, (*msg).to_owned(), *msg);
+            }
+        });
 
-        // if we have fields left, show the add button
-        let fields_left_to_draw = fields.len().saturating_sub(fields_selected);
-        if fields_left_to_draw > 0
-            && ui
-                .button("Add Y Axis")
-                .on_hover_text("Add another Y axis")
-                .clicked()
-        {
-            self.plot_lines
-                .push(PlotLineSettings::new(fields[fields_selected].to_string()));
-        }
+    // populate the plot_lines with the first field if it is empty and there are more than 1 fields
+    if plot_lines.is_empty() && fields.len() > 1 {
+        plot_lines.push(PlotLineSettings::new(fields[1].to_string()));
+    }
 
-        // update fields and flag for active plot
-        self.field_x = field_x.unwrap_or_default();
-        if field_y.is_some() {
-            if self.plot_lines.first().is_none() {
-                self.plot_lines
-                    .push(PlotLineSettings::new(field_y.unwrap()));
-            } else {
-                self.plot_lines[0].field_y = field_y.unwrap();
+    // check how many fields are left and how many are selected
+    // let fields_selected = plot_lines.len() + 1;
+    // let fields_left_to_draw = fields.len().saturating_sub(2);
+    // fields_left_to_draw.min(fields_selected.saturating_sub(2)) {
+    let plot_lines_len = plot_lines.len();
+    egui::Grid::new(ui.auto_id_with("y_axis"))
+        .num_columns(3)
+        .spacing([10.0, 5.0])
+        .show(ui, |ui| {
+            for (i, line_settings) in plot_lines.iter_mut().enumerate() {
+                // let line_settings = &mut plot_lines.get_mut(1 + i).unwrap();
+                let PlotLineSettings {
+                    field,
+                    width,
+                    color,
+                } = line_settings;
+                let widget_label = if plot_lines_len > 1 {
+                    format!("Y Axis {}", i + 1)
+                } else {
+                    "Y Axis".to_owned()
+                };
+                egui::ComboBox::from_label(widget_label)
+                    .selected_text(field.as_str())
+                    .show_ui(ui, |ui| {
+                        for msg in fields.iter() {
+                            ui.selectable_value(field, (*msg).to_owned(), *msg);
+                        }
+                    });
+                ui.color_edit_button_srgba(color);
+                ui.add(egui::DragValue::new(width).speed(0.1).suffix(" pt"))
+                    .on_hover_text("Width of the line in points");
+                ui.end_row();
             }
-            self.plot_active = true;
-        } else {
-            self.plot_active = false;
-        }
+        });
+
+    // if we have fields left, show the add button
+    if fields.len().saturating_sub(plot_lines_len + 1) > 0
+        && ui
+            .button("Add Y Axis")
+            .on_hover_text("Add another Y axis")
+            .clicked()
+    {
+        let next_field = fields
+            .iter()
+            .find(|f| !plot_lines.iter().any(|l| l.field == **f))
+            .unwrap();
+        plot_lines.push(PlotLineSettings::new(next_field.to_string()));
+    }
+
+    // update fields and flag for active plot
+    *plot_active = !plot_lines.is_empty();
+}
+
+fn show_menu(ui: &mut egui::Ui, settings_visible: &mut bool, sources_visible: &mut bool) {
+    ui.set_max_width(200.0); // To make sure we wrap long text
+
+    if ui.button("Settings…").clicked() {
+        *settings_visible = true;
+        ui.close_menu();
+    }
+
+    if ui.button("Sources…").clicked() {
+        *sources_visible = true;
+        ui.close_menu();
     }
 }
-- 
GitLab


From 06271797f15e051888c9b0278290627f784337f1 Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Sun, 24 Nov 2024 20:36:12 +0100
Subject: [PATCH 08/17] [Plot2D] Updated widget with legend and labels

---
 src/ui/panes/plot_2d.rs | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/ui/panes/plot_2d.rs b/src/ui/panes/plot_2d.rs
index 0547ef9..5561181 100644
--- a/src/ui/panes/plot_2d.rs
+++ b/src/ui/panes/plot_2d.rs
@@ -148,7 +148,8 @@ impl PaneBehavior for Plot2DPane {
 
         let plot = egui_plot::Plot::new("plot")
             .auto_bounds(Vec2b::TRUE)
-            .legend(Legend::default());
+            .legend(Legend::default())
+            .label_formatter(|name, value| format!("{} - x:{:.2} y:{:.2}", name, value.x, value.y));
         plot.show(ui, |plot_ui| {
             self.contains_pointer = plot_ui.response().contains_pointer();
             if plot_ui.response().dragged() && ctrl_pressed {
@@ -159,7 +160,8 @@ impl PaneBehavior for Plot2DPane {
                 plot_ui.line(
                     Line::new(PlotPoints::from(data_points))
                         .color(plot_settings.color)
-                        .width(plot_settings.width),
+                        .width(plot_settings.width)
+                        .name(&plot_settings.field),
                 );
             }
             plot_ui
-- 
GitLab


From d73393aad730be81055feff3308c51f586155803 Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Sun, 24 Nov 2024 20:58:41 +0100
Subject: [PATCH 09/17] [Plot2D] Improved timestamp display in plot and grid
 spacing

---
 src/ui/panes/plot_2d.rs | 18 +++++++++++-------
 1 file changed, 11 insertions(+), 7 deletions(-)

diff --git a/src/ui/panes/plot_2d.rs b/src/ui/panes/plot_2d.rs
index 5561181..162dd05 100644
--- a/src/ui/panes/plot_2d.rs
+++ b/src/ui/panes/plot_2d.rs
@@ -115,14 +115,14 @@ impl PaneBehavior for Plot2DPane {
                 .get()
                 .unwrap()
                 .lock()
-                .get_message(self.msg_id)
+                .get_message(*msg_id)
                 .map(|msg| {
                     msg.iter()
                         .map(|msg| {
                             let value: serde_json::Value =
                                 serde_json::to_value(msg.message.clone()).unwrap();
 
-                            let x = value.get(&self.field_x).unwrap();
+                            let x = value.get(&*field_x).unwrap();
                             let x = serde_json::from_value::<f64>(x.clone()).unwrap();
                             let mut ys = Vec::new();
                             for field in self.plot_lines.iter() {
@@ -137,10 +137,14 @@ impl PaneBehavior for Plot2DPane {
 
             if !acc_points.is_empty() {
                 for (i, plot_line) in self.plot_lines.iter().enumerate() {
-                    let points: Vec<[f64; 2]> = acc_points
-                        .iter()
-                        .map(|(timestamp, acc)| [{ *timestamp }, acc[i]])
-                        .collect();
+                    let points: Vec<[f64; 2]> = {
+                        let iter = acc_points.iter();
+                        if field_x == "timestamp" {
+                            iter.map(|(x, ys)| [x / 1e6, ys[i]]).collect()
+                        } else {
+                            iter.map(|(x, ys)| [*x, ys[i]]).collect()
+                        }
+                    };
                     plot_lines.push((plot_line.clone(), points));
                 }
             }
@@ -263,7 +267,7 @@ fn sources_window(
     let plot_lines_len = plot_lines.len();
     egui::Grid::new(ui.auto_id_with("y_axis"))
         .num_columns(3)
-        .spacing([10.0, 5.0])
+        .spacing([10.0, 2.5])
         .show(ui, |ui| {
             for (i, line_settings) in plot_lines.iter_mut().enumerate() {
                 // let line_settings = &mut plot_lines.get_mut(1 + i).unwrap();
-- 
GitLab


From a4d3fa3af12fb9dec0270c0197720eabf155f70d Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Tue, 26 Nov 2024 18:36:03 +0100
Subject: [PATCH 10/17] [Cargo.toml] fixed issue with relative path

---
 Cargo.toml | 9 ++-------
 1 file changed, 2 insertions(+), 7 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index e3c6d53..c0f8fd2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,17 +22,12 @@ tokio = { version = "1.41", features = [
     "sync",
 ] }
 # =========== Mavlink ===========
-# skyward_mavlink = { git = "https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git", branch = "rust-strum", features = [
-#     "reflection",
-#     "lyra",
-#     "serde",
-# ] }
-mavlink-bindgen = { version = "0.13.1", features = ["serde"] }
-skyward_mavlink = { path = "../mavlink-skyward-lib/mavlink_rust", features = [
+skyward_mavlink = { git = "https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git", branch = "rust-strum", features = [
     "reflection",
     "lyra",
     "serde",
 ] }
+mavlink-bindgen = { version = "0.13.1", features = ["serde"] }
 # ========= Persistency =========
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
-- 
GitLab


From 7c200ed4214ca25d190596134ae9a67b82f3f81d Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Mon, 2 Dec 2024 23:17:33 +0100
Subject: [PATCH 11/17] [layout] Replaced Slider with DragValue for port
 selection

---
 src/ui/composable_view.rs | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/ui/composable_view.rs b/src/ui/composable_view.rs
index 0070e24..d89d70f 100644
--- a/src/ui/composable_view.rs
+++ b/src/ui/composable_view.rs
@@ -295,7 +295,11 @@ impl SourceWindow {
             .spacing([10.0, 5.0])
             .show(ui, |ui| {
                 ui.label("Ethernet Port:");
-                ui.add(egui::Slider::new(&mut self.port, 1024..=65535).text("Port"));
+                ui.add(
+                    egui::DragValue::new(&mut self.port)
+                        .range(0..=65535)
+                        .speed(10),
+                );
                 ui.end_row();
             });
         if ui.button("Connect").clicked() {
-- 
GitLab


From b4ed9f3704865eabd0b89bbcbd03f42377da61ad Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Mon, 2 Dec 2024 23:25:30 +0100
Subject: [PATCH 12/17] [Plot2D] Replaced old settings window with old sources

---
 src/ui/panes/plot_2d.rs | 39 ++-------------------------------------
 1 file changed, 2 insertions(+), 37 deletions(-)

diff --git a/src/ui/panes/plot_2d.rs b/src/ui/panes/plot_2d.rs
index 162dd05..9aba865 100644
--- a/src/ui/panes/plot_2d.rs
+++ b/src/ui/panes/plot_2d.rs
@@ -43,7 +43,6 @@ pub struct Plot2DPane {
     pub contains_pointer: bool,
     #[serde(skip)]
     settings_visible: bool,
-    sources_visible: bool,
     // Mavlink settings
     msg_id: u32,
     field_x: String,
@@ -56,7 +55,6 @@ impl Default for Plot2DPane {
         Self {
             contains_pointer: false,
             settings_visible: false,
-            sources_visible: false,
             msg_id: ROCKET_FLIGHT_TM_DATA::ID,
             field_x: "timestamp".to_owned(),
             plot_lines: vec![],
@@ -80,7 +78,6 @@ impl PaneBehavior for Plot2DPane {
 
         let Self {
             settings_visible,
-            sources_visible,
             plot_lines,
             msg_id,
             field_x,
@@ -95,14 +92,6 @@ impl PaneBehavior for Plot2DPane {
             .collapsible(true)
             .movable(true)
             .open(settings_visible)
-            .show(ui.ctx(), |ui| settings_window(ui, plot_lines));
-
-        egui::Window::new("Plot Sources")
-            .id(ui.make_persistent_id("plot_sources"))
-            .auto_sized()
-            .collapsible(true)
-            .movable(true)
-            .open(sources_visible)
             .show(ui.ctx(), |ui| {
                 sources_window(ui, msg_id, field_x, plot_lines, plot_active)
             });
@@ -170,7 +159,7 @@ impl PaneBehavior for Plot2DPane {
             }
             plot_ui
                 .response()
-                .context_menu(|ui| show_menu(ui, settings_visible, sources_visible));
+                .context_menu(|ui| show_menu(ui, settings_visible));
         });
 
         response
@@ -181,25 +170,6 @@ impl PaneBehavior for Plot2DPane {
     }
 }
 
-fn settings_window(ui: &mut egui::Ui, plot_lines: &mut [PlotLineSettings]) {
-    egui::Grid::new(ui.id())
-        .num_columns(4)
-        .spacing([10.0, 5.0])
-        .show(ui, |ui| {
-            for plot_line in plot_lines.iter_mut() {
-                ui.label(&plot_line.field);
-                ui.color_edit_button_srgba(&mut plot_line.color);
-                ui.add(
-                    egui::DragValue::new(&mut plot_line.width)
-                        .speed(0.1)
-                        .suffix(" pt"),
-                )
-                .on_hover_text("Width of the line in points");
-                ui.end_row();
-            }
-        });
-}
-
 fn sources_window(
     ui: &mut egui::Ui,
     msg_id: &mut u32,
@@ -313,16 +283,11 @@ fn sources_window(
     *plot_active = !plot_lines.is_empty();
 }
 
-fn show_menu(ui: &mut egui::Ui, settings_visible: &mut bool, sources_visible: &mut bool) {
+fn show_menu(ui: &mut egui::Ui, settings_visible: &mut bool) {
     ui.set_max_width(200.0); // To make sure we wrap long text
 
     if ui.button("Settings…").clicked() {
         *settings_visible = true;
         ui.close_menu();
     }
-
-    if ui.button("Sources…").clicked() {
-        *sources_visible = true;
-        ui.close_menu();
-    }
 }
-- 
GitLab


From b92753ab28875afc91b17ca51c9a30304dd3f42c Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Mon, 9 Dec 2024 19:09:59 +0100
Subject: [PATCH 13/17] [Mavlink & Plot] Updated MessageBroker with optimized
 handling of messages and refactore Plot

---
 Cargo.lock                         |  81 +++++++-
 Cargo.toml                         |   3 +-
 src/main.rs                        |  14 +-
 src/mavlink.rs                     | 227 +---------------------
 src/mavlink/base.rs                |  57 ++++++
 src/mavlink/message_broker.rs      | 152 +++++++++++++++
 src/mavlink/reflection.rs          |  98 ++++++++++
 src/ui/panes.rs                    |   4 +-
 src/ui/panes/default.rs            |   4 +-
 src/ui/panes/plot.rs               | 239 +++++++++++++++++++++++
 src/ui/panes/plot/source_window.rs | 185 ++++++++++++++++++
 src/ui/panes/plot_2d.rs            | 293 -----------------------------
 12 files changed, 828 insertions(+), 529 deletions(-)
 create mode 100644 src/mavlink/base.rs
 create mode 100644 src/mavlink/message_broker.rs
 create mode 100644 src/mavlink/reflection.rs
 create mode 100644 src/ui/panes/plot.rs
 create mode 100644 src/ui/panes/plot/source_window.rs
 delete mode 100644 src/ui/panes/plot_2d.rs

diff --git a/Cargo.lock b/Cargo.lock
index bdc8fd3..32b9503 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -800,6 +800,15 @@ dependencies = [
  "crossbeam-utils",
 ]
 
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35"
+dependencies = [
+ "crossbeam-utils",
+]
+
 [[package]]
 name = "crossbeam-utils"
 version = "0.8.20"
@@ -822,6 +831,17 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
 
+[[package]]
+name = "derivative"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "digest"
 version = "0.10.7"
@@ -1260,12 +1280,48 @@ dependencies = [
  "percent-encoding",
 ]
 
+[[package]]
+name = "futures"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
 [[package]]
 name = "futures-core"
 version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
 
+[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
 [[package]]
 name = "futures-io"
 version = "0.3.31"
@@ -1314,6 +1370,7 @@ version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
 dependencies = [
+ "futures-channel",
  "futures-core",
  "futures-io",
  "futures-macro",
@@ -2710,15 +2767,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
 
 [[package]]
-name = "ron"
-version = "0.8.1"
+name = "ring-channel"
+version = "0.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
+checksum = "10c5f5a2d656b9018cc447cae1e23c028bff5faae491fe61fd0777c57cfe0706"
 dependencies = [
- "base64",
- "bitflags 2.6.0",
- "serde",
- "serde_derive",
+ "crossbeam-queue",
+ "crossbeam-utils",
+ "derivative",
+ "futures",
+ "slotmap",
+ "spin",
 ]
 
 [[package]]
@@ -2801,6 +2860,7 @@ dependencies = [
  "enum_dispatch",
  "mavlink-bindgen",
  "parking_lot",
+ "ring-channel",
  "serde",
  "serde_json",
  "skyward_mavlink",
@@ -2948,6 +3008,7 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
 [[package]]
 name = "skyward_mavlink"
 version = "0.1.0"
+source = "git+https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git?branch=rust-strum#0a67d0afc508c38faecc611a58819479686bee27"
 dependencies = [
  "bitflags 2.6.0",
  "mavlink-bindgen",
@@ -3039,6 +3100,12 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+
 [[package]]
 name = "spirv"
 version = "0.3.0+sdk-1.3.268.0"
diff --git a/Cargo.toml b/Cargo.toml
index c0f8fd2..975dee2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -24,7 +24,7 @@ tokio = { version = "1.41", features = [
 # =========== Mavlink ===========
 skyward_mavlink = { git = "https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git", branch = "rust-strum", features = [
     "reflection",
-    "lyra",
+    "orion",
     "serde",
 ] }
 mavlink-bindgen = { version = "0.13.1", features = ["serde"] }
@@ -45,3 +45,4 @@ enum_dispatch = "0.3"
 egui_extras = "0.29.1"
 strum = "0.26"
 anyhow = "1.0"
+ring-channel = "0.12.0"
diff --git a/src/main.rs b/src/main.rs
index 852ee0f..b211995 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,15 +1,18 @@
 mod mavlink;
 mod ui;
 
-use std::sync::{LazyLock, OnceLock};
+use std::{
+    num::NonZeroUsize,
+    sync::{LazyLock, OnceLock},
+};
 
-use mavlink::{MessageManager, ReflectionContext};
+use mavlink::{MessageBroker, ReflectionContext};
 use parking_lot::Mutex;
 use tokio::runtime::Runtime;
 use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer};
 use ui::ComposableView;
 
-static MSG_MANAGER: OnceLock<Mutex<MessageManager>> = OnceLock::new();
+static MSG_MANAGER: OnceLock<Mutex<MessageBroker>> = OnceLock::new();
 static MAVLINK_PROFILE: LazyLock<ReflectionContext> = LazyLock::new(ReflectionContext::new);
 
 static APP_NAME: &str = "segs";
@@ -42,7 +45,10 @@ fn main() -> Result<(), eframe::Error> {
         native_options,
         Box::new(|ctx| {
             MSG_MANAGER
-                .set(Mutex::new(MessageManager::new(50, ctc.egui_ctx.clone())))
+                .set(Mutex::new(MessageBroker::new(
+                    NonZeroUsize::new(50).unwrap(),
+                    cc.egui_ctx.clone(),
+                )))
                 .expect("Unable to set MessageManager");
             let app = ctx
                 .storage
diff --git a/src/mavlink.rs b/src/mavlink.rs
index 20ceffe..116f305 100644
--- a/src/mavlink.rs
+++ b/src/mavlink.rs
@@ -1,223 +1,10 @@
-use std::{
-    collections::HashMap,
-    sync::{
-        atomic::{AtomicBool, Ordering},
-        Arc,
-    },
-    time::Instant,
-};
+mod base;
+mod message_broker;
+mod reflection;
 
-use anyhow::{Context, Result};
-use crossbeam_channel::{Receiver, Sender};
-use mavlink_bindgen::parser::{MavProfile, MavType};
-use skyward_mavlink::{
-    lyra::MavMessage,
-    mavlink::{peek_reader::PeekReader, read_v1_msg, MavHeader, Message},
-};
-use tokio::{net::UdpSocket, task::JoinHandle};
-use tracing::debug;
+// Export all the types from the base module as if they were defined in this module
+pub use base::*;
+pub use message_broker::{MessageBroker, MessageView};
+pub use reflection::ReflectionContext;
 
 pub const DEFAULT_ETHERNET_PORT: u16 = 42069;
-const UDP_BUFFER_SIZE: usize = 65527;
-
-#[derive(Debug)]
-pub struct MessageManager {
-    messages: HashMap<u32, Vec<TimedMessage>>,
-    tx: Sender<MavMessage>,
-    rx: Receiver<MavMessage>,
-    ctx: egui::Context,
-    running_flag: Arc<AtomicBool>,
-    task: Option<JoinHandle<Result<()>>>,
-}
-
-impl MessageManager {
-    pub fn new(channel_size: usize, ctx: egui::Context) -> Self {
-        let (tx, rx) = crossbeam_channel::bounded(channel_size);
-        Self {
-            messages: HashMap::new(),
-            tx,
-            rx,
-            ctx,
-            running_flag: Arc::new(AtomicBool::new(false)),
-            task: None,
-        }
-    }
-
-    pub fn get_message(&mut self, message_id: u32) -> Option<&[TimedMessage]> {
-        while let Ok(message) = self.rx.try_recv() {
-            self.add_message(message);
-        }
-        self.messages.get(&message_id).map(|v| v.as_slice())
-    }
-
-    pub fn stop_listening(&mut self) {
-        self.running_flag.store(false, Ordering::Relaxed);
-        if let Some(t) = self.task.take() {
-            t.abort()
-        }
-    }
-
-    pub fn listen_from_ethernet_port(&mut self, port: u16) {
-        // Stop the current listener if it exists
-        self.stop_listening();
-        self.running_flag.store(true, Ordering::Relaxed);
-
-        let tx = self.tx.clone();
-        let ctx = self.ctx.clone();
-
-        let bind_address = format!("0.0.0.0:{}", port);
-        let mut buf = Box::new([0; UDP_BUFFER_SIZE]);
-        let running_flag = self.running_flag.clone();
-
-        let handle = tokio::spawn(async move {
-            let socket = UdpSocket::bind(bind_address)
-                .await
-                .context("Failed to bind socket")?;
-            debug!("Listening on UDP");
-
-            while running_flag.load(Ordering::Relaxed) {
-                let (len, _) = socket
-                    .recv_from(buf.as_mut_slice())
-                    .await
-                    .context("Failed to receive message")?;
-                for (_, mav_message) in iter_messages(&buf[..len]) {
-                    tx.send(mav_message).context("Failed to send message")?;
-                    ctx.request_repaint();
-                }
-                // buf.iter_mut().for_each(|b| *b = 0);
-            }
-
-            Ok::<(), anyhow::Error>(())
-        });
-        self.task = Some(handle);
-    }
-
-    pub fn clear(&mut self) {
-        self.messages.clear();
-    }
-
-    fn add_message(&mut self, message: MavMessage) {
-        self.messages
-            .entry(message.message_id())
-            .or_default()
-            .push(TimedMessage::just_received(message));
-    }
-
-    // TODO: Implement a scheduler removal of old messages (configurable, must not hurt performance)
-    // TODO: Add a Dashmap if performance is a problem (Personally don't think it will be)
-}
-
-#[derive(Debug, Clone)]
-pub struct TimedMessage {
-    pub message: MavMessage,
-    pub time: Instant,
-}
-
-impl TimedMessage {
-    fn just_received(message: MavMessage) -> Self {
-        Self {
-            message,
-            time: Instant::now(),
-        }
-    }
-}
-
-/// Helper function to read a stream of bytes and return an iterator of MavLink messages
-fn iter_messages(buf: &[u8]) -> impl Iterator<Item = (MavHeader, MavMessage)> + '_ {
-    let mut reader = PeekReader::new(buf);
-    std::iter::from_fn(move || read_v1_msg(&mut reader).ok())
-}
-
-pub struct ReflectionContext {
-    mavlink_profile: MavProfile,
-    id_name_map: HashMap<u32, String>,
-}
-
-impl ReflectionContext {
-    pub fn new() -> Self {
-        let profile: MavProfile =
-            serde_json::from_str(skyward_mavlink::reflection::LYRA_MAVLINK_PROFILE_SERIALIZED)
-                .expect("Failed to deserialize MavProfile");
-        let id_name_map = profile
-            .messages
-            .iter()
-            .map(|(name, m)| (m.id, name.clone()))
-            .collect();
-        Self {
-            mavlink_profile: profile,
-            id_name_map,
-        }
-    }
-
-    pub fn get_name_from_id(&self, message_id: u32) -> Option<&str> {
-        self.id_name_map.get(&message_id).map(|s| s.as_str())
-    }
-
-    pub fn sorted_messages(&self) -> Vec<&str> {
-        let mut msgs: Vec<&str> = self
-            .mavlink_profile
-            .messages
-            .keys()
-            .map(|s| s.as_str())
-            .collect();
-        msgs.sort();
-        msgs
-    }
-
-    pub fn get_fields_by_id(&self, message_id: u32) -> Vec<&str> {
-        self.mavlink_profile
-            .messages
-            .iter()
-            .find(|(_, m)| m.id == message_id)
-            .map(|(_, m)| &m.fields)
-            .unwrap_or_else(|| {
-                panic!("Message ID {} not found in profile", message_id);
-            })
-            .iter()
-            .map(|f| f.name.as_str())
-            .collect()
-    }
-
-    pub fn get_plottable_fields_by_id(&self, message_id: u32) -> Vec<&str> {
-        self.mavlink_profile
-            .messages
-            .iter()
-            .find(|(_, m)| m.id == message_id)
-            .map(|(_, m)| &m.fields)
-            .unwrap_or_else(|| {
-                panic!("Message ID {} not found in profile", message_id);
-            })
-            .iter()
-            .filter(|f| {
-                matches!(
-                    f.mavtype,
-                    MavType::UInt8
-                        | MavType::UInt16
-                        | MavType::UInt32
-                        | MavType::UInt64
-                        | MavType::Int8
-                        | MavType::Int16
-                        | MavType::Int32
-                        | MavType::Int64
-                        | MavType::Float
-                        | MavType::Double
-                )
-            })
-            .map(|f| f.name.as_str())
-            .collect()
-    }
-
-    pub fn get_fields_by_name(&self, message_name: &str) -> Vec<&str> {
-        self.mavlink_profile
-            .messages
-            .iter()
-            .find(|(_, m)| m.name == message_name)
-            .map(|(_, m)| &m.fields)
-            .unwrap_or_else(|| {
-                panic!("Message {} not found in profile", message_name);
-            })
-            .iter()
-            .map(|f| f.name.as_str())
-            .collect()
-    }
-}
diff --git a/src/mavlink/base.rs b/src/mavlink/base.rs
new file mode 100644
index 0000000..f80c2a1
--- /dev/null
+++ b/src/mavlink/base.rs
@@ -0,0 +1,57 @@
+//! This module is a wrapper around the `skyward_mavlink` crate, facilitates
+//! rapid switching between different mavlink versions and profiles.
+//!
+//! In addition, it provides few utility functions to work with mavlink messages.
+
+use std::time::Instant;
+
+use skyward_mavlink::mavlink::peek_reader::PeekReader;
+
+// Re-export from the mavlink crate
+pub use skyward_mavlink::{
+    mavlink::*, orion::*,
+    reflection::ORION_MAVLINK_PROFILE_SERIALIZED as MAVLINK_PROFILE_SERIALIZED,
+};
+
+/// A wrapper around the `MavMessage` struct, adding a received time field.
+#[derive(Debug, Clone)]
+pub struct TimedMessage {
+    /// The underlying mavlink message
+    pub message: MavMessage,
+    /// The time instant at which the message was received
+    pub time: Instant,
+}
+
+impl TimedMessage {
+    pub fn just_received(message: MavMessage) -> Self {
+        Self {
+            message,
+            time: Instant::now(),
+        }
+    }
+}
+
+pub fn extract_from_message<K, T>(
+    message: &MavMessage,
+    fields: impl IntoIterator<Item = K>,
+) -> Vec<T>
+where
+    K: AsRef<str>,
+    T: serde::de::DeserializeOwned + Default,
+{
+    let value: serde_json::Value = serde_json::to_value(message).unwrap();
+    fields
+        .into_iter()
+        .map(|field| {
+            let field = field.as_ref();
+            let value = value.get(field).unwrap();
+            serde_json::from_value::<T>(value.clone()).unwrap_or_default()
+        })
+        .collect()
+}
+
+/// Helper function to read a stream of bytes and return an iterator of MavLink messages
+pub fn byte_parser(buf: &[u8]) -> impl Iterator<Item = (MavHeader, MavMessage)> + '_ {
+    let mut reader = PeekReader::new(buf);
+    std::iter::from_fn(move || read_v1_msg(&mut reader).ok())
+}
diff --git a/src/mavlink/message_broker.rs b/src/mavlink/message_broker.rs
new file mode 100644
index 0000000..17b80ab
--- /dev/null
+++ b/src/mavlink/message_broker.rs
@@ -0,0 +1,152 @@
+use std::{
+    collections::{HashMap, VecDeque},
+    num::NonZeroUsize,
+    sync::{
+        atomic::{AtomicBool, Ordering},
+        Arc,
+    },
+};
+
+use anyhow::{Context, Result};
+use egui::{ahash::HashMapExt, IdMap};
+use ring_channel::{ring_channel, RingReceiver, RingSender};
+use tokio::{net::UdpSocket, task::JoinHandle};
+use tracing::debug;
+
+use crate::mavlink::byte_parser;
+
+use super::{MavMessage, Message, TimedMessage};
+
+const UDP_BUFFER_SIZE: usize = 65527;
+
+pub trait MessageView {
+    fn widget_id(&self) -> &egui::Id;
+    fn id_of_interest(&self) -> u32;
+    fn is_valid(&self) -> bool;
+    fn populate_view(&mut self, msg_slice: &[TimedMessage]);
+    fn update_view(&mut self, msg_slice: &[TimedMessage]);
+}
+
+#[derive(Debug)]
+pub struct MessageBroker {
+    // == Messages ==
+    /// map(message ID -> vector of messages received so far)
+    messages: HashMap<u32, Vec<TimedMessage>>,
+    /// map(widget ID -> queue of messages left for update)
+    update_queues: IdMap<(u32, VecDeque<TimedMessage>)>,
+    // == Internal ==
+    /// Flag to stop the listener
+    running_flag: Arc<AtomicBool>,
+    /// Listener message sender
+    tx: RingSender<MavMessage>,
+    /// Broker message receiver
+    rx: RingReceiver<MavMessage>,
+    /// Task handle for the listener
+    task: Option<JoinHandle<Result<()>>>,
+    /// Egui context
+    ctx: egui::Context,
+}
+
+impl MessageBroker {
+    pub fn new(channel_size: NonZeroUsize, ctx: egui::Context) -> Self {
+        let (tx, rx) = ring_channel(channel_size);
+        Self {
+            messages: HashMap::new(),
+            update_queues: IdMap::new(),
+            tx,
+            rx,
+            ctx,
+            running_flag: Arc::new(AtomicBool::new(false)),
+            task: None,
+        }
+    }
+
+    pub fn refresh_view<V: MessageView>(&mut self, view: &mut V) {
+        self.process_incoming_msgs();
+        if !view.is_valid() {
+            self.init_view(view);
+        } else {
+            self.update_view(view);
+        }
+    }
+
+    pub fn stop_listening(&mut self) {
+        self.running_flag.store(false, Ordering::Relaxed);
+        if let Some(t) = self.task.take() {
+            t.abort()
+        }
+    }
+
+    pub fn listen_from_ethernet_port(&mut self, port: u16) {
+        // Stop the current listener if it exists
+        self.stop_listening();
+        self.running_flag.store(true, Ordering::Relaxed);
+
+        let tx = self.tx.clone();
+        let ctx = self.ctx.clone();
+
+        let bind_address = format!("0.0.0.0:{}", port);
+        let mut buf = Box::new([0; UDP_BUFFER_SIZE]);
+        let running_flag = self.running_flag.clone();
+
+        let handle = tokio::spawn(async move {
+            let socket = UdpSocket::bind(bind_address)
+                .await
+                .context("Failed to bind socket")?;
+            debug!("Listening on UDP");
+
+            while running_flag.load(Ordering::Relaxed) {
+                let (len, _) = socket
+                    .recv_from(buf.as_mut_slice())
+                    .await
+                    .context("Failed to receive message")?;
+                for (_, mav_message) in byte_parser(&buf[..len]) {
+                    tx.send(mav_message).context("Failed to send message")?;
+                    ctx.request_repaint();
+                }
+            }
+
+            Ok::<(), anyhow::Error>(())
+        });
+        self.task = Some(handle);
+    }
+
+    pub fn clear(&mut self) {
+        self.messages.clear();
+    }
+
+    fn init_view<V: MessageView>(&mut self, view: &mut V) {
+        if let Some(messages) = self.messages.get(&view.id_of_interest()) {
+            view.populate_view(messages);
+        }
+        self.update_queues
+            .insert(*view.widget_id(), (view.id_of_interest(), VecDeque::new()));
+    }
+
+    fn update_view<V: MessageView>(&mut self, view: &mut V) {
+        if let Some((_, queue)) = self.update_queues.get_mut(view.widget_id()) {
+            while let Some(msg) = queue.pop_front() {
+                view.update_view(&[msg]);
+            }
+        }
+    }
+
+    fn process_incoming_msgs(&mut self) {
+        while let Ok(message) = self.rx.try_recv() {
+            // first update the update queues
+            for (_, (id, queue)) in self.update_queues.iter_mut() {
+                if *id == message.message_id() {
+                    queue.push_back(TimedMessage::just_received(message.clone()));
+                }
+            }
+            // then store the message in the messages map
+            self.messages
+                .entry(message.message_id())
+                .or_default()
+                .push(TimedMessage::just_received(message));
+        }
+    }
+
+    // TODO: Implement a scheduler removal of old messages (configurable, must not hurt performance)
+    // TODO: Add a Dashmap if performance is a problem (Personally don't think it will be)
+}
diff --git a/src/mavlink/reflection.rs b/src/mavlink/reflection.rs
new file mode 100644
index 0000000..ae2394a
--- /dev/null
+++ b/src/mavlink/reflection.rs
@@ -0,0 +1,98 @@
+use std::collections::HashMap;
+
+use mavlink_bindgen::parser::{MavProfile, MavType};
+
+use super::MAVLINK_PROFILE_SERIALIZED;
+
+pub struct ReflectionContext {
+    mavlink_profile: MavProfile,
+    id_name_map: HashMap<u32, String>,
+}
+
+impl ReflectionContext {
+    pub fn new() -> Self {
+        let profile: MavProfile = serde_json::from_str(MAVLINK_PROFILE_SERIALIZED)
+            .expect("Failed to deserialize MavProfile");
+        let id_name_map = profile
+            .messages
+            .iter()
+            .map(|(name, m)| (m.id, name.clone()))
+            .collect();
+        Self {
+            mavlink_profile: profile,
+            id_name_map,
+        }
+    }
+
+    pub fn get_name_from_id(&self, message_id: u32) -> Option<&str> {
+        self.id_name_map.get(&message_id).map(|s| s.as_str())
+    }
+
+    pub fn sorted_messages(&self) -> Vec<&str> {
+        let mut msgs: Vec<&str> = self
+            .mavlink_profile
+            .messages
+            .keys()
+            .map(|s| s.as_str())
+            .collect();
+        msgs.sort();
+        msgs
+    }
+
+    pub fn get_fields_by_id(&self, message_id: u32) -> Vec<&str> {
+        self.mavlink_profile
+            .messages
+            .iter()
+            .find(|(_, m)| m.id == message_id)
+            .map(|(_, m)| &m.fields)
+            .unwrap_or_else(|| {
+                panic!("Message ID {} not found in profile", message_id);
+            })
+            .iter()
+            .map(|f| f.name.as_str())
+            .collect()
+    }
+
+    pub fn get_plottable_fields_by_id(&self, message_id: u32) -> Vec<&str> {
+        self.mavlink_profile
+            .messages
+            .iter()
+            .find(|(_, m)| m.id == message_id)
+            .map(|(_, m)| &m.fields)
+            .unwrap_or_else(|| {
+                panic!("Message ID {} not found in profile", message_id);
+            })
+            .iter()
+            .filter(|f| {
+                matches!(
+                    f.mavtype,
+                    MavType::UInt8
+                        | MavType::UInt16
+                        | MavType::UInt32
+                        | MavType::UInt64
+                        | MavType::Int8
+                        | MavType::Int16
+                        | MavType::Int32
+                        | MavType::Int64
+                        | MavType::Float
+                        | MavType::Double
+                )
+            })
+            .map(|f| f.name.as_str())
+            .collect()
+    }
+
+    pub fn get_fields_by_name(&self, message_name: &str) -> Vec<&str> {
+        self.mavlink_profile
+            .messages
+            .iter()
+            .find(|(_, m)| m.name == message_name)
+            .map(|(_, m)| &m.fields)
+            .unwrap_or_else(|| {
+                panic!("Message {} not found in profile", message_name);
+            })
+            .iter()
+            .map(|f| f.name.as_str())
+            .collect()
+    }
+}
diff --git a/src/ui/panes.rs b/src/ui/panes.rs
index 0ebd401..c0ae378 100644
--- a/src/ui/panes.rs
+++ b/src/ui/panes.rs
@@ -1,6 +1,6 @@
 mod default;
 mod messages_viewer;
-mod plot_2d;
+mod plot;
 
 use enum_dispatch::enum_dispatch;
 use serde::{Deserialize, Serialize};
@@ -40,7 +40,7 @@ impl PaneBehavior for Pane {
 pub enum PaneKind {
     Default(default::DefaultPane),
     MessagesViewer(messages_viewer::MessagesViewerPane),
-    Plot2D(plot_2d::Plot2DPane),
+    Plot2D(plot::Plot2DPane),
 }
 
 impl Default for PaneKind {
diff --git a/src/ui/panes/default.rs b/src/ui/panes/default.rs
index 31fa83a..42e6667 100644
--- a/src/ui/panes/default.rs
+++ b/src/ui/panes/default.rs
@@ -1,4 +1,4 @@
-use super::{plot_2d::Plot2DPane, Pane, PaneBehavior, PaneKind};
+use super::{plot::Plot2DPane, Pane, PaneBehavior, PaneKind};
 use serde::{Deserialize, Serialize};
 use tracing::debug;
 
@@ -48,7 +48,7 @@ impl PaneBehavior for DefaultPane {
                 }
                 if ui.button("Plot").clicked() {
                     response.set_action(PaneAction::Replace(Pane::boxed(PaneKind::Plot2D(
-                        Plot2DPane::default(),
+                        Plot2DPane::new(ui.auto_id_with("plot_2d")),
                     ))));
                 }
             })
diff --git a/src/ui/panes/plot.rs b/src/ui/panes/plot.rs
new file mode 100644
index 0000000..029981e
--- /dev/null
+++ b/src/ui/panes/plot.rs
@@ -0,0 +1,239 @@
+mod source_window;
+
+use egui::{Color32, Vec2b};
+use egui_plot::{Legend, Line, PlotPoints};
+use serde::{Deserialize, Serialize};
+use source_window::{sources_window, SourceSettings};
+
+use crate::{
+    mavlink::{
+        extract_from_message, MessageData, MessageView, TimedMessage, ROCKET_FLIGHT_TM_DATA,
+    },
+    ui::composable_view::PaneResponse,
+    MSG_MANAGER,
+};
+
+use super::PaneBehavior;
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct Plot2DPane {
+    // UI settings
+    #[serde(skip)]
+    pub contains_pointer: bool,
+    settings_visible: bool,
+    line_settings: Vec<LineSettings>,
+    plot_active: bool,
+    view: PlotMessageView,
+}
+
+impl Plot2DPane {
+    pub fn new(id: egui::Id) -> Self {
+        Self {
+            contains_pointer: false,
+            settings_visible: false,
+            line_settings: vec![],
+            plot_active: false,
+            view: PlotMessageView::new(id),
+        }
+    }
+}
+
+impl PaneBehavior for Plot2DPane {
+    fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse {
+        let mut response = PaneResponse::default();
+
+        let Self {
+            line_settings: plot_lines,
+            settings_visible,
+            plot_active,
+            view,
+            ..
+        } = self;
+
+        let mut settings = SourceSettings::new(&mut view.settings, plot_lines);
+        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(settings_visible)
+            .show(ui.ctx(), |ui| sources_window(ui, &mut settings));
+        // if settings are changed, invalidate the cache
+        view.cache_valid = !settings.are_sources_changed();
+        // if there are no fields, do not plot
+        *plot_active = !settings.fields_empty();
+
+        let ctrl_pressed = ui.input(|i| i.modifiers.ctrl);
+
+        let mut plot_lines = Vec::new();
+        if self.plot_active {
+            MSG_MANAGER.get().unwrap().lock().refresh_view(view);
+            let acc_points = &view.points;
+
+            let field_x = &view.settings.x_field;
+            if !acc_points.is_empty() {
+                for (i, plot_line) in self.line_settings.iter().enumerate() {
+                    let points: Vec<[f64; 2]> = {
+                        let iter = acc_points.iter();
+                        if field_x == "timestamp" {
+                            iter.map(|(x, ys)| [x / 1e6, ys[i]]).collect()
+                        } else {
+                            iter.map(|(x, ys)| [*x, ys[i]]).collect()
+                        }
+                    };
+                    plot_lines.push((plot_line.clone(), points));
+                }
+            }
+        }
+
+        let plot = egui_plot::Plot::new("plot")
+            .auto_bounds(Vec2b::TRUE)
+            .legend(Legend::default())
+            .label_formatter(|name, value| format!("{} - x:{:.2} y:{:.2}", name, value.x, value.y));
+        plot.show(ui, |plot_ui| {
+            self.contains_pointer = plot_ui.response().contains_pointer();
+            if plot_ui.response().dragged() && ctrl_pressed {
+                println!("ctrl + drag");
+                response.set_drag_started();
+            }
+            for (plot_settings, data_points) in plot_lines {
+                plot_ui.line(
+                    Line::new(PlotPoints::from(data_points))
+                        .color(plot_settings.color)
+                        .width(plot_settings.width)
+                        .name(&plot_settings.field),
+                );
+            }
+            plot_ui
+                .response()
+                .context_menu(|ui| show_menu(ui, settings_visible));
+        });
+
+        response
+    }
+
+    fn contains_pointer(&self) -> bool {
+        self.contains_pointer
+    }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct PlotMessageView {
+    // == Settings from the UI ==
+    settings: MsgSources,
+    // == Data ==
+    #[serde(skip)]
+    points: Vec<(f64, Vec<f64>)>,
+    // == Internal ==
+    id: egui::Id,
+    #[serde(skip)]
+    cache_valid: bool,
+}
+
+impl PlotMessageView {
+    fn new(id: egui::Id) -> Self {
+        Self {
+            settings: Default::default(),
+            points: Vec::new(),
+            id,
+            cache_valid: false,
+        }
+    }
+}
+
+impl MessageView for PlotMessageView {
+    fn widget_id(&self) -> &egui::Id {
+        &self.id
+    }
+
+    fn id_of_interest(&self) -> u32 {
+        self.settings.msg_id
+    }
+
+    fn is_valid(&self) -> bool {
+        self.cache_valid
+    }
+
+    fn populate_view(&mut self, msg_slice: &[TimedMessage]) {
+        self.points.clear();
+        let MsgSources {
+            x_field, y_fields, ..
+        } = &self.settings;
+        for msg in msg_slice {
+            let x: f64 = extract_from_message(&msg.message, [x_field])[0];
+            let ys: Vec<f64> = extract_from_message(&msg.message, y_fields);
+            self.points.push((x, ys));
+        }
+    }
+
+    fn update_view(&mut self, msg_slice: &[TimedMessage]) {
+        let MsgSources {
+            x_field, y_fields, ..
+        } = &self.settings;
+        for msg in msg_slice {
+            let x: f64 = extract_from_message(&msg.message, [x_field])[0];
+            let ys: Vec<f64> = extract_from_message(&msg.message, y_fields);
+            self.points.push((x, ys));
+        }
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+struct MsgSources {
+    msg_id: u32,
+    x_field: String,
+    y_fields: Vec<String>,
+}
+
+impl Default for MsgSources {
+    fn default() -> Self {
+        Self {
+            msg_id: ROCKET_FLIGHT_TM_DATA::ID,
+            x_field: "timestamp".to_owned(),
+            y_fields: Vec::new(),
+        }
+    }
+}
+
+impl PartialEq for MsgSources {
+    fn eq(&self, other: &Self) -> bool {
+        self.msg_id == other.msg_id
+            && self.x_field == other.x_field
+            && self.y_fields == other.y_fields
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+struct LineSettings {
+    field: String,
+    width: f32,
+    color: Color32,
+}
+
+impl Default for LineSettings {
+    fn default() -> Self {
+        Self {
+            field: "".to_owned(),
+            width: 1.0,
+            color: Color32::BLUE,
+        }
+    }
+}
+
+impl LineSettings {
+    fn new(field_y: String) -> Self {
+        Self {
+            field: field_y,
+            ..Default::default()
+        }
+    }
+}
+
+fn show_menu(ui: &mut egui::Ui, settings_visible: &mut bool) {
+    ui.set_max_width(200.0); // To make sure we wrap long text
+
+    if ui.button("Settings…").clicked() {
+        *settings_visible = true;
+        ui.close_menu();
+    }
+}
diff --git a/src/ui/panes/plot/source_window.rs b/src/ui/panes/plot/source_window.rs
new file mode 100644
index 0000000..ecb4d61
--- /dev/null
+++ b/src/ui/panes/plot/source_window.rs
@@ -0,0 +1,185 @@
+use crate::{
+    mavlink::{MavMessage, Message},
+    MAVLINK_PROFILE,
+};
+
+use super::{LineSettings, MsgSources};
+
+pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut SourceSettings) {
+    // extract the msg name from the id to show it in the combo box
+    let msg_name = MAVLINK_PROFILE
+        .get_name_from_id(*plot_settings.get_msg_id())
+        .unwrap_or_default();
+
+    // show the first combo box with the message name selection
+    egui::ComboBox::from_label("Message Kind")
+        .selected_text(msg_name)
+        .show_ui(ui, |ui| {
+            for msg in MAVLINK_PROFILE.sorted_messages() {
+                ui.selectable_value(
+                    plot_settings.get_mut_msg_id(),
+                    MavMessage::message_id_from_name(msg).unwrap(),
+                    msg,
+                );
+            }
+        });
+
+    // reset fields if the message is changed
+    if plot_settings.is_msg_id_changed() {
+        plot_settings.clear_fields();
+    }
+
+    // check fields and assign a default field_x and field_y once the msg is changed
+    let fields = MAVLINK_PROFILE.get_plottable_fields_by_id(*plot_settings.get_msg_id());
+    // get the first field that is in the list of fields or the previous if valid
+    let x_field = plot_settings.get_x_field();
+    let new_field_x = fields
+        .contains(&x_field)
+        .then(|| x_field.to_owned())
+        .or(fields.first().map(|s| s.to_string()));
+
+    // if there are no fields, reset the field_x and plot_lines
+    let Some(new_field_x) = new_field_x else {
+        plot_settings.clear_fields();
+        return;
+    };
+    // update the field_x
+    plot_settings.set_x_field(new_field_x);
+    let x_field = plot_settings.get_mut_x_field();
+
+    // if fields are valid, show the combo boxes for the x_axis
+    egui::ComboBox::from_label("X Axis")
+        .selected_text(x_field.as_str())
+        .show_ui(ui, |ui| {
+            for msg in fields.iter() {
+                ui.selectable_value(x_field, (*msg).to_owned(), *msg);
+            }
+        });
+
+    // populate the plot_lines with the first field if it is empty and there are more than 1 fields
+    if plot_settings.fields_empty() && fields.len() > 1 {
+        plot_settings.add_field(fields[1].to_string());
+    }
+
+    // check how many fields are left and how many are selected
+    let plot_lines_len = plot_settings.fields_len();
+    egui::Grid::new(ui.auto_id_with("y_axis"))
+        .num_columns(3)
+        .spacing([10.0, 2.5])
+        .show(ui, |ui| {
+            for (i, line_settings) in plot_settings.line_settings.iter_mut().enumerate() {
+                let LineSettings {
+                    field,
+                    width,
+                    color,
+                } = line_settings;
+                let widget_label = if plot_lines_len > 1 {
+                    format!("Y Axis {}", i + 1)
+                } else {
+                    "Y Axis".to_owned()
+                };
+                egui::ComboBox::from_label(widget_label)
+                    .selected_text(field.as_str())
+                    .show_ui(ui, |ui| {
+                        for msg in fields.iter() {
+                            ui.selectable_value(field, (*msg).to_owned(), *msg);
+                        }
+                    });
+                ui.color_edit_button_srgba(color);
+                ui.add(egui::DragValue::new(width).speed(0.1).suffix(" pt"))
+                    .on_hover_text("Width of the line in points");
+                ui.end_row();
+            }
+        });
+    // Sync changes applied to line_settings with msg_sources
+    plot_settings.sync_fields_with_lines();
+
+    // if we have fields left, show the add button
+    if fields.len().saturating_sub(plot_lines_len + 1) > 0
+        && ui
+            .button("Add Y Axis")
+            .on_hover_text("Add another Y axis")
+            .clicked()
+    {
+        let next_field = fields
+            .iter()
+            .find(|f| !plot_settings.contains_field(f))
+            .unwrap();
+        plot_settings.add_field(next_field.to_string());
+    }
+}
+
+pub struct SourceSettings<'a> {
+    msg_sources: &'a mut MsgSources,
+    old_msg_sources: MsgSources,
+    line_settings: &'a mut Vec<LineSettings>,
+}
+
+impl<'a> SourceSettings<'a> {
+    pub fn new(msg_sources: &'a mut MsgSources, line_settings: &'a mut Vec<LineSettings>) -> Self {
+        Self {
+            old_msg_sources: msg_sources.clone(),
+            msg_sources,
+            line_settings,
+        }
+    }
+
+    pub fn are_sources_changed(&self) -> bool {
+        self.msg_sources != &self.old_msg_sources
+    }
+
+    pub fn fields_empty(&self) -> bool {
+        self.msg_sources.y_fields.is_empty()
+    }
+
+    fn get_msg_id(&self) -> &u32 {
+        &self.msg_sources.msg_id
+    }
+
+    fn get_x_field(&self) -> &str {
+        &self.msg_sources.x_field
+    }
+
+    fn get_mut_msg_id(&mut self) -> &mut u32 {
+        &mut self.msg_sources.msg_id
+    }
+
+    fn get_mut_x_field(&mut self) -> &mut String {
+        &mut self.msg_sources.x_field
+    }
+
+    fn set_x_field(&mut self, field: String) {
+        self.msg_sources.x_field = field;
+    }
+
+    fn fields_len(&self) -> usize {
+        self.msg_sources.y_fields.len()
+    }
+
+    fn is_msg_id_changed(&self) -> bool {
+        self.msg_sources.msg_id != self.old_msg_sources.msg_id
+    }
+
+    fn contains_field(&self, field: &str) -> bool {
+        self.msg_sources.y_fields.contains(&field.to_owned())
+    }
+
+    fn sync_fields_with_lines(&mut self) {
+        self.msg_sources.y_fields = self
+            .line_settings
+            .iter()
+            .map(|ls| ls.field.clone())
+            .collect();
+    }
+
+    fn add_field(&mut self, field: String) {
+        self.line_settings.push(LineSettings::new(field.clone()));
+        self.msg_sources.y_fields.push(field);
+    }
+
+    fn clear_fields(&mut self) {
+        self.msg_sources.y_fields.clear();
+        self.line_settings.clear();
+        self.msg_sources.x_field = "".to_owned();
+    }
+}
diff --git a/src/ui/panes/plot_2d.rs b/src/ui/panes/plot_2d.rs
deleted file mode 100644
index 9aba865..0000000
--- a/src/ui/panes/plot_2d.rs
+++ /dev/null
@@ -1,293 +0,0 @@
-use crate::{ui::composable_view::PaneResponse, MAVLINK_PROFILE, MSG_MANAGER};
-
-use super::PaneBehavior;
-
-use egui::{Color32, Vec2b};
-use egui_plot::{Legend, Line, PlotPoints};
-use serde::{Deserialize, Serialize};
-use skyward_mavlink::{
-    lyra::{MavMessage, ROCKET_FLIGHT_TM_DATA},
-    mavlink::{Message, MessageData},
-};
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-struct PlotLineSettings {
-    field: String,
-    width: f32,
-    color: Color32,
-}
-
-impl Default for PlotLineSettings {
-    fn default() -> Self {
-        Self {
-            field: "".to_owned(),
-            width: 1.0,
-            color: Color32::BLUE,
-        }
-    }
-}
-
-impl PlotLineSettings {
-    fn new(field_y: String) -> Self {
-        Self {
-            field: field_y,
-            ..Default::default()
-        }
-    }
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct Plot2DPane {
-    // UI settings
-    #[serde(skip)]
-    pub contains_pointer: bool,
-    #[serde(skip)]
-    settings_visible: bool,
-    // Mavlink settings
-    msg_id: u32,
-    field_x: String,
-    plot_lines: Vec<PlotLineSettings>,
-    plot_active: bool,
-}
-
-impl Default for Plot2DPane {
-    fn default() -> Self {
-        Self {
-            contains_pointer: false,
-            settings_visible: false,
-            msg_id: ROCKET_FLIGHT_TM_DATA::ID,
-            field_x: "timestamp".to_owned(),
-            plot_lines: vec![],
-            plot_active: false,
-        }
-    }
-}
-
-impl PartialEq for Plot2DPane {
-    fn eq(&self, other: &Self) -> bool {
-        self.n_points == other.n_points
-            && self.frequency == other.frequency
-            && self.width == other.width
-            && self.color == other.color
-    }
-}
-
-impl PaneBehavior for Plot2DPane {
-    fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse {
-        let mut response = PaneResponse::default();
-
-        let Self {
-            settings_visible,
-            plot_lines,
-            msg_id,
-            field_x,
-            plot_active,
-            ..
-        } = self;
-
-        // Spawn windows
-        egui::Window::new("Plot Settings")
-            .id(ui.make_persistent_id("plot_settings"))
-            .auto_sized()
-            .collapsible(true)
-            .movable(true)
-            .open(settings_visible)
-            .show(ui.ctx(), |ui| {
-                sources_window(ui, msg_id, field_x, plot_lines, plot_active)
-            });
-
-        let ctrl_pressed = ui.input(|i| i.modifiers.ctrl);
-
-        let mut plot_lines = Vec::new();
-        if self.plot_active {
-            let acc_points = MSG_MANAGER
-                .get()
-                .unwrap()
-                .lock()
-                .get_message(*msg_id)
-                .map(|msg| {
-                    msg.iter()
-                        .map(|msg| {
-                            let value: serde_json::Value =
-                                serde_json::to_value(msg.message.clone()).unwrap();
-
-                            let x = value.get(&*field_x).unwrap();
-                            let x = serde_json::from_value::<f64>(x.clone()).unwrap();
-                            let mut ys = Vec::new();
-                            for field in self.plot_lines.iter() {
-                                let y = value.get(field.field.as_str()).unwrap();
-                                ys.push(serde_json::from_value::<f64>(y.clone()).unwrap());
-                            }
-                            (x, ys)
-                        })
-                        .collect::<Vec<(f64, Vec<f64>)>>()
-                })
-                .unwrap_or_default();
-
-            if !acc_points.is_empty() {
-                for (i, plot_line) in self.plot_lines.iter().enumerate() {
-                    let points: Vec<[f64; 2]> = {
-                        let iter = acc_points.iter();
-                        if field_x == "timestamp" {
-                            iter.map(|(x, ys)| [x / 1e6, ys[i]]).collect()
-                        } else {
-                            iter.map(|(x, ys)| [*x, ys[i]]).collect()
-                        }
-                    };
-                    plot_lines.push((plot_line.clone(), points));
-                }
-            }
-        }
-
-        let plot = egui_plot::Plot::new("plot")
-            .auto_bounds(Vec2b::TRUE)
-            .legend(Legend::default())
-            .label_formatter(|name, value| format!("{} - x:{:.2} y:{:.2}", name, value.x, value.y));
-        plot.show(ui, |plot_ui| {
-            self.contains_pointer = plot_ui.response().contains_pointer();
-            if plot_ui.response().dragged() && ctrl_pressed {
-                println!("ctrl + drag");
-                response.set_drag_started();
-            }
-            for (plot_settings, data_points) in plot_lines {
-                plot_ui.line(
-                    Line::new(PlotPoints::from(data_points))
-                        .color(plot_settings.color)
-                        .width(plot_settings.width)
-                        .name(&plot_settings.field),
-                );
-            }
-            plot_ui
-                .response()
-                .context_menu(|ui| show_menu(ui, settings_visible));
-        });
-
-        response
-    }
-
-    fn contains_pointer(&self) -> bool {
-        self.contains_pointer
-    }
-}
-
-fn sources_window(
-    ui: &mut egui::Ui,
-    msg_id: &mut u32,
-    field_x: &mut String,
-    plot_lines: &mut Vec<PlotLineSettings>,
-    plot_active: &mut bool,
-) {
-    // record msg id to check if it has changed
-    let old_msg_id = *msg_id;
-    // extract the msg name from the id to show it in the combo box
-    let msg_name = MAVLINK_PROFILE
-        .get_name_from_id(*msg_id)
-        .unwrap_or_default();
-
-    // show the first combo box with the message name selection
-    egui::ComboBox::from_label("Message Kind")
-        .selected_text(msg_name)
-        .show_ui(ui, |ui| {
-            for msg in MAVLINK_PROFILE.sorted_messages() {
-                ui.selectable_value(msg_id, MavMessage::message_id_from_name(msg).unwrap(), msg);
-            }
-        });
-
-    // reset fields if the message is changed
-    if *msg_id != old_msg_id {
-        plot_lines.truncate(1);
-    }
-
-    // check fields and assing a default field_x and field_y once the msg is changed
-    let fields = MAVLINK_PROFILE.get_plottable_fields_by_id(*msg_id);
-    // get the first field that is in the list of fields or the previous if valid
-    let new_field_x = fields
-        .contains(&field_x.as_str())
-        .then(|| field_x.to_owned())
-        .or(fields.first().map(|s| s.to_string()));
-
-    // if there are no fields, reset the field_x and plot_lines
-    let Some(new_field_x) = new_field_x else {
-        *field_x = "".to_owned();
-        plot_lines.clear();
-        *plot_active = false;
-        return;
-    };
-    // update the field_x
-    *field_x = new_field_x;
-
-    // if fields are valid, show the combo boxes for the x_axis
-    egui::ComboBox::from_label("X Axis")
-        .selected_text(field_x.as_str())
-        .show_ui(ui, |ui| {
-            for msg in fields.iter() {
-                ui.selectable_value(field_x, (*msg).to_owned(), *msg);
-            }
-        });
-
-    // populate the plot_lines with the first field if it is empty and there are more than 1 fields
-    if plot_lines.is_empty() && fields.len() > 1 {
-        plot_lines.push(PlotLineSettings::new(fields[1].to_string()));
-    }
-
-    // check how many fields are left and how many are selected
-    // let fields_selected = plot_lines.len() + 1;
-    // let fields_left_to_draw = fields.len().saturating_sub(2);
-    // fields_left_to_draw.min(fields_selected.saturating_sub(2)) {
-    let plot_lines_len = plot_lines.len();
-    egui::Grid::new(ui.auto_id_with("y_axis"))
-        .num_columns(3)
-        .spacing([10.0, 2.5])
-        .show(ui, |ui| {
-            for (i, line_settings) in plot_lines.iter_mut().enumerate() {
-                // let line_settings = &mut plot_lines.get_mut(1 + i).unwrap();
-                let PlotLineSettings {
-                    field,
-                    width,
-                    color,
-                } = line_settings;
-                let widget_label = if plot_lines_len > 1 {
-                    format!("Y Axis {}", i + 1)
-                } else {
-                    "Y Axis".to_owned()
-                };
-                egui::ComboBox::from_label(widget_label)
-                    .selected_text(field.as_str())
-                    .show_ui(ui, |ui| {
-                        for msg in fields.iter() {
-                            ui.selectable_value(field, (*msg).to_owned(), *msg);
-                        }
-                    });
-                ui.color_edit_button_srgba(color);
-                ui.add(egui::DragValue::new(width).speed(0.1).suffix(" pt"))
-                    .on_hover_text("Width of the line in points");
-                ui.end_row();
-            }
-        });
-
-    // if we have fields left, show the add button
-    if fields.len().saturating_sub(plot_lines_len + 1) > 0
-        && ui
-            .button("Add Y Axis")
-            .on_hover_text("Add another Y axis")
-            .clicked()
-    {
-        let next_field = fields
-            .iter()
-            .find(|f| !plot_lines.iter().any(|l| l.field == **f))
-            .unwrap();
-        plot_lines.push(PlotLineSettings::new(next_field.to_string()));
-    }
-
-    // update fields and flag for active plot
-    *plot_active = !plot_lines.is_empty();
-}
-
-fn show_menu(ui: &mut egui::Ui, settings_visible: &mut bool) {
-    ui.set_max_width(200.0); // To make sure we wrap long text
-
-    if ui.button("Settings…").clicked() {
-        *settings_visible = true;
-        ui.close_menu();
-    }
-}
-- 
GitLab


From c93ffcc3a0abe7117e21d37d534677b74d169e92 Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Mon, 9 Dec 2024 20:27:54 +0100
Subject: [PATCH 14/17] fixed some issues

---
 Cargo.lock                | 12 ++++++++++++
 src/main.rs               |  2 +-
 src/ui/composable_view.rs | 16 ----------------
 src/ui/panes/default.rs   |  6 ++----
 src/ui/panes/plot.rs      | 11 ++++++++++-
 5 files changed, 25 insertions(+), 22 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 32b9503..0ea4525 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2780,6 +2780,18 @@ dependencies = [
  "spin",
 ]
 
+[[package]]
+name = "ron"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
+dependencies = [
+ "base64",
+ "bitflags 2.6.0",
+ "serde",
+ "serde_derive",
+]
+
 [[package]]
 name = "rustc-demangle"
 version = "0.1.24"
diff --git a/src/main.rs b/src/main.rs
index b211995..b4b269e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -47,7 +47,7 @@ fn main() -> Result<(), eframe::Error> {
             MSG_MANAGER
                 .set(Mutex::new(MessageBroker::new(
                     NonZeroUsize::new(50).unwrap(),
-                    cc.egui_ctx.clone(),
+                    ctx.egui_ctx.clone(),
                 )))
                 .expect("Unable to set MessageManager");
             let app = ctx
diff --git a/src/ui/composable_view.rs b/src/ui/composable_view.rs
index d89d70f..7a74460 100644
--- a/src/ui/composable_view.rs
+++ b/src/ui/composable_view.rs
@@ -26,22 +26,6 @@ pub struct ComposableView {
     sources_window: SourceWindow,
 }
 
-// Implementing the default trait allows us to define a default configuration for our app
-impl Default for ComposableView {
-    fn default() -> Self {
-        let mut tiles = Tiles::default();
-        let root = tiles.insert_pane(Pane::default());
-        let panes_tree = egui_tiles::Tree::new("my_tree", root, tiles);
-
-        Self {
-            panes_tree,
-            behavior: Default::default(),
-            maximized_pane: None,
-            sources_window: Default::default(),
-        }
-    }
-}
-
 // An app must implement the `App` trait to define how the ui is built
 impl eframe::App for ComposableView {
     // The update function is called each time the UI needs repainting!
diff --git a/src/ui/panes/default.rs b/src/ui/panes/default.rs
index 42e6667..3cfd31c 100644
--- a/src/ui/panes/default.rs
+++ b/src/ui/panes/default.rs
@@ -7,10 +7,8 @@ use crate::ui::{
     utils::{vertically_centered, SizingMemo},
 };
 
-#[derive(Clone, Debug, Default, Serialize, Deserialize)]
+#[derive(Clone, Debug, Serialize, Deserialize)]
 pub struct DefaultPane {
-    occupied: f32,
-    fixed: bool,
     #[serde(skip)]
     centering_memo: SizingMemo,
     #[serde(skip)]
@@ -28,7 +26,7 @@ impl Default for DefaultPane {
 
 impl PartialEq for DefaultPane {
     fn eq(&self, other: &Self) -> bool {
-        self.occupied == other.occupied && self.fixed == other.fixed
+        true
     }
 }
 
diff --git a/src/ui/panes/plot.rs b/src/ui/panes/plot.rs
index 029981e..16b9033 100644
--- a/src/ui/panes/plot.rs
+++ b/src/ui/panes/plot.rs
@@ -20,6 +20,7 @@ pub struct Plot2DPane {
     // UI settings
     #[serde(skip)]
     pub contains_pointer: bool,
+    #[serde(skip)]
     settings_visible: bool,
     line_settings: Vec<LineSettings>,
     plot_active: bool,
@@ -38,6 +39,14 @@ impl Plot2DPane {
     }
 }
 
+impl PartialEq for Plot2DPane {
+    fn eq(&self, other: &Self) -> bool {
+        self.view.settings == other.view.settings
+            && self.line_settings == other.line_settings
+            && self.plot_active == other.plot_active
+    }
+}
+
 impl PaneBehavior for Plot2DPane {
     fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse {
         let mut response = PaneResponse::default();
@@ -203,7 +212,7 @@ impl PartialEq for MsgSources {
     }
 }
 
-#[derive(Clone, Debug, Serialize, Deserialize)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
 struct LineSettings {
     field: String,
     width: f32,
-- 
GitLab


From e9db281ea2ed2772eb6e14e8a47f173beab8c528 Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Mon, 16 Dec 2024 20:39:53 +0100
Subject: [PATCH 15/17] [Mavlink] Fixed issue with layout saving of plot and
 MessageView initial populate

---
 src/mavlink/message_broker.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/mavlink/message_broker.rs b/src/mavlink/message_broker.rs
index 17b80ab..862cdc1 100644
--- a/src/mavlink/message_broker.rs
+++ b/src/mavlink/message_broker.rs
@@ -63,7 +63,7 @@ impl MessageBroker {
 
     pub fn refresh_view<V: MessageView>(&mut self, view: &mut V) {
         self.process_incoming_msgs();
-        if !view.is_valid() {
+        if !view.is_valid() || !self.update_queues.contains_key(view.widget_id()) {
             self.init_view(view);
         } else {
             self.update_view(view);
-- 
GitLab


From fda91a24a2102fc90b95dd0c11941cbaac2259b1 Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Tue, 7 Jan 2025 11:28:17 +0100
Subject: [PATCH 16/17] [MessageBroker] Moved TimedMessage conversion up in
 receiving pipeline

---
 src/mavlink/message_broker.rs | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/src/mavlink/message_broker.rs b/src/mavlink/message_broker.rs
index 862cdc1..0a70820 100644
--- a/src/mavlink/message_broker.rs
+++ b/src/mavlink/message_broker.rs
@@ -15,7 +15,7 @@ use tracing::debug;
 
 use crate::mavlink::byte_parser;
 
-use super::{MavMessage, Message, TimedMessage};
+use super::{Message, TimedMessage};
 
 const UDP_BUFFER_SIZE: usize = 65527;
 
@@ -38,9 +38,9 @@ pub struct MessageBroker {
     /// Flag to stop the listener
     running_flag: Arc<AtomicBool>,
     /// Listener message sender
-    tx: RingSender<MavMessage>,
+    tx: RingSender<TimedMessage>,
     /// Broker message receiver
-    rx: RingReceiver<MavMessage>,
+    rx: RingReceiver<TimedMessage>,
     /// Task handle for the listener
     task: Option<JoinHandle<Result<()>>>,
     /// Egui context
@@ -101,7 +101,8 @@ impl MessageBroker {
                     .await
                     .context("Failed to receive message")?;
                 for (_, mav_message) in byte_parser(&buf[..len]) {
-                    tx.send(mav_message).context("Failed to send message")?;
+                    tx.send(TimedMessage::just_received(mav_message))
+                        .context("Failed to send message")?;
                     ctx.request_repaint();
                 }
             }
@@ -135,15 +136,15 @@ impl MessageBroker {
         while let Ok(message) = self.rx.try_recv() {
             // first update the update queues
             for (_, (id, queue)) in self.update_queues.iter_mut() {
-                if *id == message.message_id() {
-                    queue.push_back(TimedMessage::just_received(message.clone()));
+                if *id == message.message.message_id() {
+                    queue.push_back(message.clone());
                 }
             }
             // then store the message in the messages map
             self.messages
-                .entry(message.message_id())
+                .entry(message.message.message_id())
                 .or_default()
-                .push(TimedMessage::just_received(message));
+                .push(message);
         }
     }
 
-- 
GitLab


From 83f485b82e441152547982ef51f4e10e840d65d9 Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Tue, 7 Jan 2025 21:29:36 +0100
Subject: [PATCH 17/17] [SourceWindow] Made the source window not collapsible

---
 src/ui/composable_view.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/ui/composable_view.rs b/src/ui/composable_view.rs
index 7a74460..7b71f6b 100644
--- a/src/ui/composable_view.rs
+++ b/src/ui/composable_view.rs
@@ -264,7 +264,7 @@ impl SourceWindow {
         egui::Window::new("Sources")
             .id(ui.id())
             .auto_sized()
-            .collapsible(true)
+            .collapsible(false)
             .movable(true)
             .open(&mut window_is_open)
             .show(ui.ctx(), |ui| {
-- 
GitLab