From 72a1207dc940ca14beaeba3dd3bb16b32ac50bcc Mon Sep 17 00:00:00 2001
From: Federico Lolli <federico.lolli@skywarder.eu>
Date: Wed, 8 Jan 2025 10:08:41 +0000
Subject: [PATCH] Implemented message broker for incoming MavLink messages from
 an UDP socket

---
 Cargo.lock                         | 594 ++++++++++++++++++++++-------
 Cargo.toml                         |  26 +-
 justfile                           |  11 +
 src/main.rs                        |  29 +-
 src/mavlink.rs                     |  10 +
 src/mavlink/base.rs                |  57 +++
 src/mavlink/message_broker.rs      | 153 ++++++++
 src/mavlink/reflection.rs          |  98 +++++
 src/ui/composable_view.rs          |  64 ++++
 src/ui/panes.rs                    |  14 +-
 src/ui/panes/default.rs            |  73 ++--
 src/ui/panes/plot.rs               | 248 ++++++++++++
 src/ui/panes/plot/source_window.rs | 185 +++++++++
 src/ui/panes/plot_2d.rs            | 119 ------
 src/ui/utils.rs                    |  31 +-
 15 files changed, 1404 insertions(+), 308 deletions(-)
 create mode 100644 justfile
 create mode 100644 src/mavlink.rs
 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 a6a4c77..0ea4525 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"
@@ -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"
@@ -143,9 +152,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"
@@ -184,53 +193,10 @@ dependencies = [
 ]
 
 [[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"
+name = "anyhow"
+version = "1.0.93"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
-dependencies = [
- "anstyle",
- "windows-sys 0.59.0",
-]
+checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
 
 [[package]]
 name = "arboard"
@@ -324,9 +290,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",
@@ -480,6 +446,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"
@@ -625,9 +606,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",
@@ -686,12 +667,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"
@@ -794,13 +769,19 @@ 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",
 ]
 
+[[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"
@@ -810,6 +791,24 @@ 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-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"
@@ -832,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"
@@ -1142,29 +1152,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"
@@ -1234,9 +1221,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 +1236,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",
@@ -1293,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"
@@ -1307,9 +1330,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",
@@ -1347,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",
@@ -1389,6 +1413,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"
@@ -1573,6 +1603,12 @@ dependencies = [
  "winapi",
 ]
 
+[[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"
@@ -1600,12 +1636,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"
@@ -1777,10 +1807,13 @@ dependencies = [
 ]
 
 [[package]]
-name = "is_terminal_polyfill"
-version = "1.70.1"
+name = "ioctl-rs"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+checksum = "f7970510895cee30b3e9128319f2cefd4bde883a39f38baa279567ba3a7eb97d"
+dependencies = [
+ "libc",
+]
 
 [[package]]
 name = "itertools"
@@ -1854,11 +1887,17 @@ 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.161"
+version = "0.2.162"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
+checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
 
 [[package]]
 name = "libloading"
@@ -1924,6 +1963,43 @@ 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 = "mavlink-bindgen"
+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.26.0",
+ "quote",
+ "serde",
+ "thiserror",
+]
+
+[[package]]
+name = "mavlink-core"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e64d975ca3cf0ad8a7c278553f91d77de15fcde9b79bf6bc542e209dd0c7dee"
+dependencies = [
+ "byteorder",
+ "crc-any",
+ "serde",
+ "serde_arrays",
+ "serial",
+]
+
 [[package]]
 name = "memchr"
 version = "2.7.4"
@@ -1989,6 +2065,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"
@@ -2068,6 +2156,27 @@ 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-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"
@@ -2310,6 +2419,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 +2453,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"
@@ -2449,13 +2573,13 @@ 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",
- "hermit-abi",
+ "hermit-abi 0.4.0",
  "pin-project-lite",
  "rustix",
  "tracing",
@@ -2501,6 +2625,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"
@@ -2591,21 +2724,36 @@ 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]]
 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",
- "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"
@@ -2618,6 +2766,20 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
 
+[[package]]
+name = "ring-channel"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10c5f5a2d656b9018cc447cae1e23c028bff5faae491fe61fd0777c57cfe0706"
+dependencies = [
+ "crossbeam-queue",
+ "crossbeam-utils",
+ "derivative",
+ "futures",
+ "slotmap",
+ "spin",
+]
+
 [[package]]
 name = "ron"
 version = "0.8.1"
@@ -2630,6 +2792,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"
@@ -2638,9 +2806,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",
@@ -2693,6 +2861,8 @@ dependencies = [
 name = "segs"
 version = "0.1.0"
 dependencies = [
+ "anyhow",
+ "crossbeam-channel",
  "eframe",
  "egui",
  "egui_extras",
@@ -2700,26 +2870,41 @@ dependencies = [
  "egui_plot",
  "egui_tiles",
  "enum_dispatch",
- "env_logger",
- "log",
+ "mavlink-bindgen",
+ "parking_lot",
+ "ring-channel",
  "serde",
  "serde_json",
+ "skyward_mavlink",
+ "strum",
+ "tokio",
+ "tracing",
+ "tracing-subscriber",
 ]
 
 [[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_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.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 +2913,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",
@@ -2749,6 +2934,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"
@@ -2760,6 +2987,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"
@@ -2781,6 +3017,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#0a67d0afc508c38faecc611a58819479686bee27"
+dependencies = [
+ "bitflags 2.6.0",
+ "mavlink-bindgen",
+ "mavlink-core",
+ "num-derive",
+ "num-traits",
+ "paste",
+ "serde",
+ "serde_arrays",
+ "serde_json",
+]
+
 [[package]]
 name = "slab"
 version = "0.4.9"
@@ -2850,6 +3102,22 @@ 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 = "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"
@@ -2877,6 +3145,12 @@ 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 = "syn"
 version = "1.0.109"
@@ -2912,9 +3186,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",
@@ -2932,26 +3206,45 @@ 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.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",
  "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 +3280,21 @@ dependencies = [
  "zerovec",
 ]
 
+[[package]]
+name = "tokio"
+version = "1.42.0"
+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]]
 name = "toml_datetime"
 version = "0.6.8"
@@ -3033,6 +3341,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 +3459,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..975dee2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,13 +14,35 @@ 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",
+    "orion",
+    "serde",
+] }
+mavlink-bindgen = { version = "0.13.1", features = ["serde"] }
 # ========= 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"] }
+# =========== 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"
+ring-channel = "0.12.0"
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/main.rs b/src/main.rs
index c988cd7..b4b269e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,12 +1,31 @@
+mod mavlink;
+mod ui;
+
+use std::{
+    num::NonZeroUsize,
+    sync::{LazyLock, OnceLock},
+};
+
+use mavlink::{MessageBroker, ReflectionContext};
+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<MessageBroker>> = OnceLock::new();
+static MAVLINK_PROFILE: LazyLock<ReflectionContext> = LazyLock::new(ReflectionContext::new);
 
 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
@@ -25,6 +44,12 @@ 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(MessageBroker::new(
+                    NonZeroUsize::new(50).unwrap(),
+                    ctx.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..116f305
--- /dev/null
+++ b/src/mavlink.rs
@@ -0,0 +1,10 @@
+mod base;
+mod message_broker;
+mod reflection;
+
+// 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;
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..0a70820
--- /dev/null
+++ b/src/mavlink/message_broker.rs
@@ -0,0 +1,153 @@
+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::{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<TimedMessage>,
+    /// Broker message receiver
+    rx: RingReceiver<TimedMessage>,
+    /// 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.update_queues.contains_key(view.widget_id()) {
+            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(TimedMessage::just_received(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.message_id() {
+                    queue.push_back(message.clone());
+                }
+            }
+            // then store the message in the messages map
+            self.messages
+                .entry(message.message.message_id())
+                .or_default()
+                .push(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/composable_view.rs b/src/ui/composable_view.rs
index 7351601..7b71f6b 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,7 @@ pub struct ComposableView {
     pub layout_manager: LayoutManager,
     behavior: ComposableBehavior,
     maximized_pane: Option<TileId>,
+    sources_window: SourceWindow,
 }
 
 // An app must implement the `App` trait to define how the ui is built
@@ -126,9 +129,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 +243,60 @@ 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(false)
+            .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::DragValue::new(&mut self.port)
+                        .range(0..=65535)
+                        .speed(10),
+                );
+                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 {
diff --git a/src/ui/panes.rs b/src/ui/panes.rs
index 50c4f90..c0ae378 100644
--- a/src/ui/panes.rs
+++ b/src/ui/panes.rs
@@ -1,25 +1,17 @@
 mod default;
 mod messages_viewer;
-mod plot_2d;
+mod plot;
 
 use enum_dispatch::enum_dispatch;
 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 })
@@ -48,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 2d1b51a..3cfd31c 100644
--- a/src/ui/panes/default.rs
+++ b/src/ui/panes/default.rs
@@ -1,13 +1,16 @@
-use super::{plot_2d::Plot2DPane, Pane, PaneBehavior, PaneKind};
+use super::{plot::Plot2DPane, Pane, PaneBehavior, PaneKind};
 use serde::{Deserialize, Serialize};
+use tracing::debug;
 
-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 +18,7 @@ pub struct DefaultPane {
 impl Default for DefaultPane {
     fn default() -> Self {
         DefaultPane {
-            occupied: 0.0,
-            fixed: false,
+            centering_memo: SizingMemo::default(),
             contains_pointer: false,
         }
     }
@@ -24,55 +26,36 @@ impl Default for DefaultPane {
 
 impl PartialEq for DefaultPane {
     fn eq(&self, other: &Self) -> bool {
-        self.occupied == other.occupied && self.fixed == other.fixed
+        true
     }
 }
 
 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);
+                    debug!("Vertical Split button clicked");
+                }
+                if ui.button("Horizontal Split").clicked() {
+                    response.set_action(PaneAction::SplitH);
+                    debug!("Horizontal Split button clicked");
+                }
+                if ui.button("Plot").clicked() {
+                    response.set_action(PaneAction::Replace(Pane::boxed(PaneKind::Plot2D(
+                        Plot2DPane::new(ui.auto_id_with("plot_2d")),
+                    ))));
+                }
+            })
+            .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/panes/plot.rs b/src/ui/panes/plot.rs
new file mode 100644
index 0000000..16b9033
--- /dev/null
+++ b/src/ui/panes/plot.rs
@@ -0,0 +1,248 @@
+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,
+    #[serde(skip)]
+    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 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();
+
+        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, PartialEq, 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 6ac0658..0000000
--- a/src/ui/panes/plot_2d.rs
+++ /dev/null
@@ -1,119 +0,0 @@
-use crate::ui::composable_view::PaneResponse;
-
-use super::PaneBehavior;
-
-use egui_plot::{Line, PlotPoints};
-use serde::{Deserialize, Serialize};
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct Plot2DPane {
-    n_points: u32,
-    frequency: f64,
-    width: f32,
-    color: egui::Color32,
-
-    #[serde(skip)]
-    pub contains_pointer: bool,
-
-    #[serde(skip)]
-    settings_visible: bool,
-}
-
-impl Default for Plot2DPane {
-    fn default() -> Self {
-        Self {
-            contains_pointer: false,
-            settings_visible: false,
-            n_points: 2,
-            frequency: 1.0,
-            width: 1.0,
-            color: egui::Color32::from_rgb(0, 120, 240),
-        }
-    }
-}
-
-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 mut window_visible = self.settings_visible;
-        egui::Window::new("Plot Settings")
-            .id(ui.id())
-            .auto_sized()
-            .collapsible(true)
-            .movable(true)
-            .open(&mut window_visible)
-            .show(ui.ctx(), |ui| self.settings_window(ui));
-        self.settings_visible = window_visible;
-
-        let ctrl_pressed = ui.input(|i| i.modifiers.ctrl);
-
-        let plot = egui_plot::Plot::new("plot");
-        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),
-            );
-            plot_ui.response().context_menu(|ui| self.menu(ui));
-        });
-
-        response
-    }
-
-    fn contains_pointer(&self) -> bool {
-        self.contains_pointer
-    }
-}
-
-impl Plot2DPane {
-    fn menu(&mut self, ui: &mut egui::Ui) {
-        ui.set_max_width(200.0); // To make sure we wrap long text
-
-        if ui.button("Settings…").clicked() {
-            self.settings_visible = true;
-            ui.close_menu();
-        }
-    }
-
-    fn settings_window(&mut self, ui: &mut egui::Ui) {
-        egui::Grid::new(ui.id())
-            .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();
-
-                ui.label("Width:");
-                ui.add(egui::Slider::new(&mut self.width, 0.1..=10.0).text("pt"));
-                ui.end_row();
-            });
-    }
-}
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