diff --git a/Cargo.lock b/Cargo.lock
index 9ba641ebdda989d53134b402ded2883bdfdedcce..14c6a43485ce14feaf75e20ba9bc0e9d0cc45445 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -190,9 +190,9 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.96"
+version = "1.0.97"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
+checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
 
 [[package]]
 name = "arboard"
@@ -300,7 +300,7 @@ dependencies = [
  "futures-lite",
  "parking",
  "polling",
- "rustix",
+ "rustix 0.38.44",
  "slab",
  "tracing",
  "windows-sys 0.59.0",
@@ -332,7 +332,7 @@ dependencies = [
  "cfg-if",
  "event-listener",
  "futures-lite",
- "rustix",
+ "rustix 0.38.44",
  "tracing",
 ]
 
@@ -344,7 +344,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
@@ -359,7 +359,7 @@ dependencies = [
  "cfg-if",
  "futures-core",
  "futures-io",
- "rustix",
+ "rustix 0.38.44",
  "signal-hook-registry",
  "slab",
  "windows-sys 0.59.0",
@@ -373,13 +373,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
 
 [[package]]
 name = "async-trait"
-version = "0.1.86"
+version = "0.1.87"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d"
+checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
@@ -541,9 +541,9 @@ checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
 
 [[package]]
 name = "bytemuck"
-version = "1.21.0"
+version = "1.22.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
+checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540"
 dependencies = [
  "bytemuck_derive",
 ]
@@ -556,7 +556,7 @@ checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
@@ -573,9 +573,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
 
 [[package]]
 name = "bytes"
-version = "1.10.0"
+version = "1.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
 
 [[package]]
 name = "calloop"
@@ -586,7 +586,7 @@ dependencies = [
  "bitflags 2.9.0",
  "log",
  "polling",
- "rustix",
+ "rustix 0.38.44",
  "slab",
  "thiserror 1.0.69",
 ]
@@ -598,7 +598,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
 dependencies = [
  "calloop",
- "rustix",
+ "rustix 0.38.44",
  "wayland-backend",
  "wayland-client",
 ]
@@ -837,7 +837,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
@@ -872,15 +872,15 @@ checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53"
 
 [[package]]
 name = "dyn-clone"
-version = "1.0.18"
+version = "1.0.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35"
+checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
 
 [[package]]
 name = "ecolor"
-version = "0.31.0"
+version = "0.31.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "878e9005799dd739e5d5d89ff7480491c12d0af571d44399bcaefa1ee172dd76"
+checksum = "bc4feb366740ded31a004a0e4452fbf84e80ef432ecf8314c485210229672fd1"
 dependencies = [
  "bytemuck",
  "emath",
@@ -889,9 +889,9 @@ dependencies = [
 
 [[package]]
 name = "eframe"
-version = "0.31.0"
+version = "0.31.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eba4c50d905804fe9ec4e159fde06b9d38f9440228617ab64a03d7a2091ece63"
+checksum = "d0dfe0859f3fb1bc6424c57d41e10e9093fe938f426b691e42272c2f336d915c"
 dependencies = [
  "ahash",
  "bytemuck",
@@ -928,9 +928,9 @@ dependencies = [
 
 [[package]]
 name = "egui"
-version = "0.31.0"
+version = "0.31.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d2768eaa6d5c80a6e2a008da1f0e062dff3c83eb2b28605ea2d0732d46e74d6"
+checksum = "25dd34cec49ab55d85ebf70139cb1ccd29c977ef6b6ba4fe85489d6877ee9ef3"
 dependencies = [
  "accesskit",
  "ahash",
@@ -946,9 +946,9 @@ dependencies = [
 
 [[package]]
 name = "egui-wgpu"
-version = "0.31.0"
+version = "0.31.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d8151704bcef6271bec1806c51544d70e79ef20e8616e5eac01facfd9c8c54a"
+checksum = "d319dfef570f699b6e9114e235e862a2ddcf75f0d1a061de9e1328d92146d820"
 dependencies = [
  "ahash",
  "bytemuck",
@@ -966,9 +966,9 @@ dependencies = [
 
 [[package]]
 name = "egui-winit"
-version = "0.31.0"
+version = "0.31.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ace791b367c1f63e6044aef2f3834904509d1d1a6912fd23ebf3f6a9af92cd84"
+checksum = "7d9dfbb78fe4eb9c3a39ad528b90ee5915c252e77bbab9d4ebc576541ab67e13"
 dependencies = [
  "accesskit_winit",
  "ahash",
@@ -987,9 +987,9 @@ dependencies = [
 
 [[package]]
 name = "egui_extras"
-version = "0.31.0"
+version = "0.31.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5b5cf69510eb3d19211fc0c062fb90524f43fe8e2c012967dcf0e2d81cb040f"
+checksum = "624659a2e972a46f4d5f646557906c55f1cd5a0836eddbe610fdf1afba1b4226"
 dependencies = [
  "ahash",
  "egui",
@@ -1011,9 +1011,9 @@ dependencies = [
 
 [[package]]
 name = "egui_glow"
-version = "0.31.0"
+version = "0.31.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a53e2374a964c3c793cb0b8ead81bca631f24974bc0b747d1a5622f4e39fdd0"
+checksum = "910906e3f042ea6d2378ec12a6fd07698e14ddae68aed2d819ffe944a73aab9e"
 dependencies = [
  "ahash",
  "bytemuck",
@@ -1053,15 +1053,15 @@ dependencies = [
 
 [[package]]
 name = "either"
-version = "1.14.0"
+version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
 
 [[package]]
 name = "emath"
-version = "0.31.0"
+version = "0.31.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55b7b6be5ad1d247f11738b0e4699d9c20005ed366f2c29f5ec1f8e1de180bc2"
+checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b"
 dependencies = [
  "bytemuck",
  "serde",
@@ -1091,7 +1091,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
@@ -1103,7 +1103,7 @@ dependencies = [
  "once_cell",
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
@@ -1124,7 +1124,7 @@ checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
@@ -1135,14 +1135,14 @@ checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
 name = "epaint"
-version = "0.31.0"
+version = "0.31.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "275b665a7b9611d8317485187e5458750850f9e64604d3c58434bb3fc1d22915"
+checksum = "41fcc0f5a7c613afd2dee5e4b30c3e6acafb8ad6f0edb06068811f708a67c562"
 dependencies = [
  "ab_glyph",
  "ahash",
@@ -1159,9 +1159,9 @@ dependencies = [
 
 [[package]]
 name = "epaint_default_fonts"
-version = "0.31.0"
+version = "0.31.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9343d356d7cac894dacafc161b4654e0881301097bdf32a122ed503d97cb94b6"
+checksum = "fc7e7a64c02cf7a5b51e745a9e45f60660a286f151c238b9d397b3e923f5082f"
 
 [[package]]
 name = "equivalent"
@@ -1255,7 +1255,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
@@ -1342,7 +1342,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
@@ -1722,7 +1722,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
@@ -1808,9 +1808,9 @@ dependencies = [
 
 [[package]]
 name = "itoa"
-version = "1.0.14"
+version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
 
 [[package]]
 name = "jni"
@@ -1906,7 +1906,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
 dependencies = [
  "bitflags 2.9.0",
  "libc",
- "redox_syscall 0.5.9",
+ "redox_syscall 0.5.10",
 ]
 
 [[package]]
@@ -1935,6 +1935,12 @@ version = "0.4.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
 
+[[package]]
+name = "linux-raw-sys"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9"
+
 [[package]]
 name = "litemap"
 version = "0.7.5"
@@ -2005,14 +2011,13 @@ dependencies = [
 
 [[package]]
 name = "mavlink-bindgen"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f83b15a4ad504e29cabfb03fdc97250a22d2354a5404d80fc48dbf02e06acf5f"
+version = "0.14.0"
+source = "git+https://git.skywarder.eu/avn/swd/mavlink/rust-mavlink.git?rev=da4add3de8243d3b8194b9793677e4c950686ddc#da4add3de8243d3b8194b9793677e4c950686ddc"
 dependencies = [
  "crc-any",
  "lazy_static",
  "proc-macro2",
- "quick-xml 0.26.0",
+ "quick-xml 0.36.2",
  "quote",
  "serde",
  "thiserror 1.0.69",
@@ -2020,9 +2025,8 @@ dependencies = [
 
 [[package]]
 name = "mavlink-core"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e64d975ca3cf0ad8a7c278553f91d77de15fcde9b79bf6bc542e209dd0c7dee"
+version = "0.14.0"
+source = "git+https://git.skywarder.eu/avn/swd/mavlink/rust-mavlink.git?rev=b7446436b3c96ca4c40d28b54eeed346e7bf021e#b7446436b3c96ca4c40d28b54eeed346e7bf021e"
 dependencies = [
  "byteorder",
  "crc-any",
@@ -2125,7 +2129,7 @@ dependencies = [
  "spirv",
  "strum",
  "termcolor",
- "thiserror 2.0.11",
+ "thiserror 2.0.12",
  "unicode-xid",
 ]
 
@@ -2222,7 +2226,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
@@ -2252,7 +2256,7 @@ dependencies = [
  "proc-macro-crate",
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
@@ -2549,7 +2553,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
 dependencies = [
  "cfg-if",
  "libc",
- "redox_syscall 0.5.9",
+ "redox_syscall 0.5.10",
  "smallvec",
  "windows-targets 0.52.6",
 ]
@@ -2568,22 +2572,22 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
 
 [[package]]
 name = "pin-project"
-version = "1.1.9"
+version = "1.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d"
+checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
 dependencies = [
  "pin-project-internal",
 ]
 
 [[package]]
 name = "pin-project-internal"
-version = "1.1.9"
+version = "1.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67"
+checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
@@ -2611,9 +2615,9 @@ dependencies = [
 
 [[package]]
 name = "pkg-config"
-version = "0.3.31"
+version = "0.3.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
 
 [[package]]
 name = "png"
@@ -2638,7 +2642,7 @@ dependencies = [
  "concurrent-queue",
  "hermit-abi",
  "pin-project-lite",
- "rustix",
+ "rustix 0.38.44",
  "tracing",
  "windows-sys 0.59.0",
 ]
@@ -2660,18 +2664,18 @@ dependencies = [
 
 [[package]]
 name = "proc-macro-crate"
-version = "3.2.0"
+version = "3.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
+checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
 dependencies = [
  "toml_edit",
 ]
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.93"
+version = "1.0.94"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
+checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
 dependencies = [
  "unicode-ident",
 ]
@@ -2683,7 +2687,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d"
 dependencies = [
  "profiling-procmacros",
- "tracy-client",
+ "tracy-client 0.17.6",
 ]
 
 [[package]]
@@ -2693,26 +2697,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30"
 dependencies = [
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
 name = "quick-xml"
-version = "0.26.0"
+version = "0.30.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd"
+checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
 dependencies = [
  "memchr",
+ "serde",
 ]
 
 [[package]]
 name = "quick-xml"
-version = "0.30.0"
+version = "0.36.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
+checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"
 dependencies = [
  "memchr",
- "serde",
 ]
 
 [[package]]
@@ -2726,9 +2730,9 @@ dependencies = [
 
 [[package]]
 name = "quote"
-version = "1.0.38"
+version = "1.0.39"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
+checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801"
 dependencies = [
  "proc-macro2",
 ]
@@ -2752,7 +2756,7 @@ checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
 dependencies = [
  "rand_chacha 0.9.0",
  "rand_core 0.9.3",
- "zerocopy 0.8.21",
+ "zerocopy 0.8.23",
 ]
 
 [[package]]
@@ -2810,9 +2814,9 @@ dependencies = [
 
 [[package]]
 name = "redox_syscall"
-version = "0.5.9"
+version = "0.5.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f"
+checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1"
 dependencies = [
  "bitflags 2.9.0",
 ]
@@ -2914,21 +2918,34 @@ dependencies = [
  "bitflags 2.9.0",
  "errno",
  "libc",
- "linux-raw-sys",
+ "linux-raw-sys 0.4.15",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustix"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dade4812df5c384711475be5fcd8c162555352945401aed22a35bffeab61f657"
+dependencies = [
+ "bitflags 2.9.0",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.9.2",
  "windows-sys 0.59.0",
 ]
 
 [[package]]
 name = "rustversion"
-version = "1.0.19"
+version = "1.0.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
+checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
 
 [[package]]
 name = "ryu"
-version = "1.0.19"
+version = "1.0.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
 
 [[package]]
 name = "same-file"
@@ -2986,7 +3003,7 @@ dependencies = [
  "skyward_mavlink",
  "strum",
  "strum_macros",
- "thiserror 2.0.11",
+ "thiserror 2.0.12",
  "tokio",
  "tracing",
  "tracing-appender",
@@ -3020,14 +3037,14 @@ checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.139"
+version = "1.0.140"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
 dependencies = [
  "itoa",
  "memchr",
@@ -3037,13 +3054,13 @@ dependencies = [
 
 [[package]]
 name = "serde_repr"
-version = "0.1.19"
+version = "0.1.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
+checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
@@ -3150,8 +3167,8 @@ 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"
+version = "0.1.1"
+source = "git+https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git?rev=03d37888f7b5a84b5032ca1af392a16da7f39df2#03d37888f7b5a84b5032ca1af392a16da7f39df2"
 dependencies = [
  "bitflags 2.9.0",
  "mavlink-bindgen",
@@ -3201,7 +3218,7 @@ dependencies = [
  "libc",
  "log",
  "memmap2",
- "rustix",
+ "rustix 0.38.44",
  "thiserror 1.0.69",
  "wayland-backend",
  "wayland-client",
@@ -3295,7 +3312,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "rustversion",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
@@ -3311,9 +3328,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "2.0.98"
+version = "2.0.99"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
+checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -3328,20 +3345,20 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
 name = "tempfile"
-version = "3.17.1"
+version = "3.18.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230"
+checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567"
 dependencies = [
  "cfg-if",
  "fastrand",
  "getrandom 0.3.1",
  "once_cell",
- "rustix",
+ "rustix 1.0.1",
  "windows-sys 0.59.0",
 ]
 
@@ -3374,11 +3391,11 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "2.0.11"
+version = "2.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
 dependencies = [
- "thiserror-impl 2.0.11",
+ "thiserror-impl 2.0.12",
 ]
 
 [[package]]
@@ -3389,18 +3406,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "2.0.11"
+version = "2.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
@@ -3492,9 +3509,9 @@ dependencies = [
 
 [[package]]
 name = "tokio"
-version = "1.43.0"
+version = "1.44.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
+checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a"
 dependencies = [
  "backtrace",
  "libc",
@@ -3552,7 +3569,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
@@ -3615,7 +3632,7 @@ checksum = "0eaa1852afa96e0fe9e44caa53dc0bd2d9d05e0f2611ce09f97f8677af56e4ba"
 dependencies = [
  "tracing-core",
  "tracing-subscriber",
- "tracy-client",
+ "tracy-client 0.18.0",
 ]
 
 [[package]]
@@ -3629,6 +3646,17 @@ dependencies = [
  "tracy-client-sys",
 ]
 
+[[package]]
+name = "tracy-client"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d90a2c01305b02b76fdd89ac8608bae27e173c829a35f7d76a345ab5d33836db"
+dependencies = [
+ "loom",
+ "once_cell",
+ "tracy-client-sys",
+]
+
 [[package]]
 name = "tracy-client-sys"
 version = "0.24.3"
@@ -3688,9 +3716,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
 
 [[package]]
 name = "unicode-ident"
-version = "1.0.17"
+version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
 
 [[package]]
 name = "unicode-segmentation"
@@ -3792,7 +3820,7 @@ dependencies = [
  "log",
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
  "wasm-bindgen-shared",
 ]
 
@@ -3827,7 +3855,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
@@ -3849,7 +3877,7 @@ checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf"
 dependencies = [
  "cc",
  "downcast-rs",
- "rustix",
+ "rustix 0.38.44",
  "scoped-tls",
  "smallvec",
  "wayland-sys",
@@ -3862,7 +3890,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f"
 dependencies = [
  "bitflags 2.9.0",
- "rustix",
+ "rustix 0.38.44",
  "wayland-backend",
  "wayland-scanner",
 ]
@@ -3884,7 +3912,7 @@ version = "0.31.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a93029cbb6650748881a00e4922b076092a6a08c11e7fbdb923f064b23968c5d"
 dependencies = [
- "rustix",
+ "rustix 0.38.44",
  "wayland-client",
  "xcursor",
 ]
@@ -4039,7 +4067,7 @@ dependencies = [
  "raw-window-handle",
  "rustc-hash",
  "smallvec",
- "thiserror 2.0.11",
+ "thiserror 2.0.12",
  "wgpu-hal",
  "wgpu-types",
 ]
@@ -4078,7 +4106,7 @@ dependencies = [
  "renderdoc-sys",
  "rustc-hash",
  "smallvec",
- "thiserror 2.0.11",
+ "thiserror 2.0.12",
  "wasm-bindgen",
  "web-sys",
  "wgpu-types",
@@ -4159,7 +4187,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
@@ -4170,7 +4198,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
@@ -4438,7 +4466,7 @@ dependencies = [
  "pin-project",
  "raw-window-handle",
  "redox_syscall 0.4.1",
- "rustix",
+ "rustix 0.38.44",
  "sctk-adwaita",
  "smithay-client-toolkit",
  "smol_str",
@@ -4510,7 +4538,7 @@ dependencies = [
  "libc",
  "libloading",
  "once_cell",
- "rustix",
+ "rustix 0.38.44",
  "x11rb-protocol",
 ]
 
@@ -4581,7 +4609,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
  "synstructure",
 ]
 
@@ -4641,7 +4669,7 @@ checksum = "709ab20fc57cb22af85be7b360239563209258430bccf38d8b979c5a2ae3ecce"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
  "zbus-lockstep",
  "zbus_xml",
  "zvariant",
@@ -4656,7 +4684,7 @@ dependencies = [
  "proc-macro-crate",
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
  "zvariant_utils",
 ]
 
@@ -4696,11 +4724,11 @@ dependencies = [
 
 [[package]]
 name = "zerocopy"
-version = "0.8.21"
+version = "0.8.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcf01143b2dd5d134f11f545cf9f1431b13b749695cb33bcce051e7568f99478"
+checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6"
 dependencies = [
- "zerocopy-derive 0.8.21",
+ "zerocopy-derive 0.8.23",
 ]
 
 [[package]]
@@ -4711,18 +4739,18 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
 name = "zerocopy-derive"
-version = "0.8.21"
+version = "0.8.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712c8386f4f4299382c9abee219bee7084f78fb939d88b6840fcc1320d5f6da2"
+checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
@@ -4742,7 +4770,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
  "synstructure",
 ]
 
@@ -4765,7 +4793,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
 
 [[package]]
@@ -4790,7 +4818,7 @@ dependencies = [
  "proc-macro-crate",
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
  "zvariant_utils",
 ]
 
@@ -4802,5 +4830,5 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.98",
+ "syn 2.0.99",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 03ca6287fe321ae6cf764427956b606d6cdd3e9e..0d667b73bd61dccb69a207164a57c19fe0034891 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,12 +18,6 @@ egui_file = "0.22"
 # =========== Asynchronous ===========
 tokio = { version = "1.41", features = ["rt-multi-thread", "net", "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"] }
 serialport = "4.7.0"
 # ========= Persistency =========
 serde = { version = "1.0", features = ["derive"] }
@@ -43,5 +37,15 @@ anyhow = "1.0"
 ring-channel = "0.12.0"
 thiserror = "2.0.7"
 
+[dependencies.skyward_mavlink]
+git = "https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git"
+rev = "03d37888f7b5a84b5032ca1af392a16da7f39df2"
+features = ["reflection", "orion", "serde"]
+
+[dependencies.mavlink-bindgen]
+git = "https://git.skywarder.eu/avn/swd/mavlink/rust-mavlink.git"
+rev = "da4add3de8243d3b8194b9793677e4c950686ddc"
+features = ["serde"]
+
 [dev-dependencies]
 rand = "0.9.0"
diff --git a/src/communication/error.rs b/src/communication/error.rs
index 42b0f1c36326ec31e6f4f39cd1d78b6885142aae..d688e8650fc77e53e4117eabb53b6156382751e0 100644
--- a/src/communication/error.rs
+++ b/src/communication/error.rs
@@ -21,8 +21,6 @@ pub enum ConnectionError {
     WrongConfiguration(String),
     #[error("IO error: {0}")]
     Io(#[from] std::io::Error),
-    #[error("Unknown error")]
-    Unknown(String),
 }
 
 impl From<MessageWriteError> for CommunicationError {
diff --git a/src/communication/ethernet.rs b/src/communication/ethernet.rs
index 629887cd034a5f8a5553d70267de702861f23bd2..00331ed7ab0ab31816d316be2d668a03bbcc8fea 100644
--- a/src/communication/ethernet.rs
+++ b/src/communication/ethernet.rs
@@ -29,7 +29,7 @@ impl Connectable for EthernetConfiguration {
     #[profiling::function]
     fn connect(&self) -> Result<Self::Connected, ConnectionError> {
         let incoming_addr = format!("udpin:0.0.0.0:{}", self.port);
-        let outgoing_addr = format!("udpbcast:255.255.255.255:{}", self.port);
+        let outgoing_addr = format!("udpcast:255.255.255.255:{}", self.port);
         let mut incoming_conn: BoxedConnection = mavlink::connect(&incoming_addr)?;
         let mut outgoing_conn: BoxedConnection = mavlink::connect(&outgoing_addr)?;
         incoming_conn.set_protocol_version(MavlinkVersion::V1);
diff --git a/src/communication/serial.rs b/src/communication/serial.rs
index 8f1c6fa73b6c4c924357f0050857d371f02d4762..a27efdbfdaece9ed755f0d0594032c2aaddda3f5 100644
--- a/src/communication/serial.rs
+++ b/src/communication/serial.rs
@@ -51,6 +51,40 @@ pub fn find_first_stm32_port() -> Result<Option<SerialPortInfo>, serialport::Err
     Ok(None)
 }
 
+pub mod cached {
+    use egui::Context;
+
+    use crate::ui::cache::RecentCallCache;
+
+    use super::*;
+
+    /// Returns a cached list of all available USB ports.
+    ///
+    /// # Arguments
+    /// * `ctx` - The egui context used for caching.
+    ///
+    /// # Returns
+    /// * A Result containing a vector of `SerialPortInfo` or a `serialport::Error`.
+    pub fn cached_list_all_usb_ports(
+        ctx: &Context,
+    ) -> Result<Vec<SerialPortInfo>, serialport::Error> {
+        ctx.call_cached_short(&"list_usb_ports", list_all_usb_ports)
+    }
+
+    /// Returns the first cached STM32 port found, if any.
+    ///
+    /// # Arguments
+    /// * `ctx` - The egui context used for caching.
+    ///
+    /// # Returns
+    /// * A Result containing an Option of `SerialPortInfo` or a `serialport::Error`.
+    pub fn cached_first_stm32_port(
+        ctx: &Context,
+    ) -> Result<Option<SerialPortInfo>, serialport::Error> {
+        ctx.call_cached_short(&"find_first_stm32_port", find_first_stm32_port)
+    }
+}
+
 /// Configuration for a serial connection.
 #[derive(Debug, Clone)]
 pub struct SerialConfiguration {
diff --git a/src/main.rs b/src/main.rs
index 9093f877687fd8cc2c26b394476e95f6fad98fce..87495b96d8d51d8b6e7ece615f396ab1cef08fbb 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -15,7 +15,7 @@ use tokio::runtime::Runtime;
 use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::SubscriberInitExt};
 
 use error::ErrInstrument;
-use mavlink::ReflectionContext;
+use mavlink::reflection::ReflectionContext;
 use ui::App;
 
 /// ReflectionContext singleton, used to get access to the Mavlink message definitions
diff --git a/src/mavlink.rs b/src/mavlink.rs
index 91193466aa0254f7dbe1e0415be3421d18e4ab09..f1517ccf525029cb9782ba848669ab65611fb369 100644
--- a/src/mavlink.rs
+++ b/src/mavlink.rs
@@ -3,13 +3,35 @@
 //! It serves also as an abstraction wrapper around the `skyward_mavlink` crate, facilitating
 //! rapid switching between different mavlink versions and profiles (_dialects_).
 
-mod base;
 mod error;
-mod reflection;
+pub mod reflection;
 
-// Export all the types from the base module as if they were defined in this module
-pub use base::*;
-pub use reflection::ReflectionContext;
+use std::time::Instant;
+
+// Re-export from the mavlink crate
+pub use skyward_mavlink::{
+    mavlink::*, orion::*,
+    reflection::ORION_MAVLINK_PROFILE_SERIALIZED as MAVLINK_PROFILE_SERIALIZED,
+};
 
 /// Default port for the Ethernet connection
 pub const DEFAULT_ETHERNET_PORT: u16 = 42069;
+
+/// 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 {
+    /// Create a new `TimedMessage` instance with the given message and the current time
+    pub fn just_received(message: MavMessage) -> Self {
+        Self {
+            message,
+            time: Instant::now(),
+        }
+    }
+}
diff --git a/src/mavlink/base.rs b/src/mavlink/base.rs
deleted file mode 100644
index fb64c4cca761dc752c63498d8f5464cc5c475042..0000000000000000000000000000000000000000
--- a/src/mavlink/base.rs
+++ /dev/null
@@ -1,60 +0,0 @@
-//! Wrapper around the `skyward_mavlink` crate
-//!
-//! This 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;
-
-// Re-export from the mavlink crate
-pub use skyward_mavlink::{
-    mavlink::*, orion::*,
-    reflection::ORION_MAVLINK_PROFILE_SERIALIZED as MAVLINK_PROFILE_SERIALIZED,
-};
-
-use crate::error::ErrInstrument;
-
-use super::error::{MavlinkError, Result};
-
-/// 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 {
-    /// Create a new `TimedMessage` instance with the given message and the current time
-    pub fn just_received(message: MavMessage) -> Self {
-        Self {
-            message,
-            time: Instant::now(),
-        }
-    }
-}
-
-/// Extract fields from a MavLink message using string keys
-#[profiling::function]
-pub fn extract_from_message<K, T>(
-    message: &MavMessage,
-    fields: impl IntoIterator<Item = K>,
-) -> Result<Vec<T>>
-where
-    K: AsRef<str>,
-    T: serde::de::DeserializeOwned + Default,
-{
-    let value: serde_json::Value =
-        serde_json::to_value(message).log_expect("MavMessage should be serializable");
-    Ok(fields
-        .into_iter()
-        .flat_map(|field| {
-            let field = field.as_ref();
-            let value = value
-                .get(field)
-                .ok_or(MavlinkError::UnknownField(field.to_string()))?;
-            serde_json::from_value::<T>(value.clone()).map_err(MavlinkError::from)
-        })
-        .collect())
-}
diff --git a/src/mavlink/error.rs b/src/mavlink/error.rs
index 23975eb59b7384774098dacb2e31f5c5b7f1c878..b0aca6e4114deca47fb0c777683cc36b826924e9 100644
--- a/src/mavlink/error.rs
+++ b/src/mavlink/error.rs
@@ -1,11 +1,7 @@
 use thiserror::Error;
 
-pub type Result<T> = std::result::Result<T, MavlinkError>;
-
 #[derive(Debug, Error)]
 pub enum MavlinkError {
-    #[error("Error parsing field: {0}")]
-    UnknownField(String),
     #[error("Error parsing message: {0}")]
     ParseError(#[from] serde_json::Error),
 }
diff --git a/src/mavlink/reflection.rs b/src/mavlink/reflection.rs
index 528ffde5bb9979465bb760f8da74e80d5f620c3c..e3ed70e723913efad04c48e64e1bf43777ec1ad0 100644
--- a/src/mavlink/reflection.rs
+++ b/src/mavlink/reflection.rs
@@ -6,19 +6,22 @@
 
 use std::collections::HashMap;
 
-use anyhow::anyhow;
 use mavlink_bindgen::parser::{MavProfile, MavType};
+use serde::ser::SerializeStruct;
+use skyward_mavlink::mavlink::Message;
 
-use crate::error::ErrInstrument;
+use crate::{MAVLINK_PROFILE, error::ErrInstrument};
 
 use super::MAVLINK_PROFILE_SERIALIZED;
 
+pub use mavlink_bindgen::parser::{MavField, MavMessage};
+
 /// Reflection context for MAVLink messages.
 ///
 /// This struct provides methods to query information about MAVLink messages and their fields.
 pub struct ReflectionContext {
     mavlink_profile: MavProfile,
-    id_name_map: HashMap<u32, String>,
+    id_msg_map: HashMap<u32, MavMessage>,
 }
 
 impl ReflectionContext {
@@ -26,57 +29,56 @@ impl ReflectionContext {
     pub fn new() -> Self {
         let profile: MavProfile = serde_json::from_str(MAVLINK_PROFILE_SERIALIZED)
             .log_expect("Failed to deserialize MavProfile");
-        let id_name_map = profile
+        let id_msg_map = profile
             .messages
-            .iter()
-            .map(|(name, m)| (m.id, name.clone()))
+            .values()
+            .map(|m| (m.id, m.clone()))
             .collect();
         Self {
             mavlink_profile: profile,
-            id_name_map,
+            id_msg_map,
         }
     }
 
     /// Get the name of a message by its ID.
-    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 get_msg(&'static self, msg: impl MessageLike) -> Option<&'static MavMessage> {
+        msg.to_mav_message(self).ok()
     }
 
-    /// Get all message names in a sorted vector.
-    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
+    /// Get all field names for a message by its ID.
+    pub fn get_fields(&'static self, message_id: impl MessageLike) -> Option<Vec<IndexedField>> {
+        message_id.to_mav_message(self).ok().map(|msg| {
+            msg.fields
+                .iter()
+                .enumerate()
+                .map(|(i, f)| IndexedField {
+                    id: i,
+                    msg,
+                    field: f,
+                })
+                .collect()
+        })
     }
 
-    /// Get all field names for a message by its ID.
-    pub fn get_fields_by_id(&self, message_id: u32) -> anyhow::Result<Vec<&str>> {
-        Ok(self
+    /// Get all message names in a sorted vector.
+    pub fn get_sorted_msgs(&self) -> Vec<&MavMessage> {
+        let mut msgs: Vec<(&str, &MavMessage)> = self
             .mavlink_profile
             .messages
             .iter()
-            .find(|(_, m)| m.id == message_id)
-            .map(|(_, m)| &m.fields)
-            .ok_or(anyhow!("Message ID {} not found in profile", message_id))?
-            .iter()
-            .map(|f| f.name.as_str())
-            .collect())
+            .map(|(k, m)| (k.as_str(), m))
+            .collect();
+        msgs.sort_by_cached_key(|(k, _)| *k);
+        msgs.into_iter().map(|(_, m)| m).collect()
     }
 
     /// Get all plottable field names for a message by its ID.
-    pub fn get_plottable_fields_by_id(&self, message_id: u32) -> anyhow::Result<Vec<&str>> {
-        Ok(self
-            .mavlink_profile
-            .messages
-            .iter()
-            .find(|(_, m)| m.id == message_id)
-            .map(|(_, m)| &m.fields)
-            .ok_or(anyhow!("Message ID {} not found in profile", message_id))?
+    pub fn get_plottable_fields(
+        &'static self,
+        message_id: impl MessageLike,
+    ) -> Option<Vec<IndexedField>> {
+        let msg = message_id.to_mav_message(self).ok()?;
+        msg.fields
             .iter()
             .filter(|f| {
                 matches!(
@@ -93,21 +95,309 @@ impl ReflectionContext {
                         | MavType::Double
                 )
             })
-            .map(|f| f.name.as_str())
-            .collect())
+            .map(|f| f.to_mav_field(msg.id, self).ok())
+            .collect()
     }
+}
 
-    /// Get all field names for a message by its name.
-    pub fn get_fields_by_name(&self, message_name: &str) -> anyhow::Result<Vec<&str>> {
-        Ok(self
-            .mavlink_profile
+#[derive(Debug, Clone)]
+pub struct IndexedField {
+    id: usize,
+    msg: &'static MavMessage,
+    field: &'static MavField,
+}
+
+macro_rules! extract_as_type {
+    ($as_type: ty, $func: ident, $($mav_ty: ident, $rust_ty: ty),+) => {
+        pub fn $func(&self, message: &impl Message) -> Result<$as_type, String> {
+            macro_rules! downcast {
+                ($value: expr, $type: ty) => {
+                    Ok(*$value
+                        .downcast::<$type>()
+                        .map_err(|_| "Type mismatch".to_string())? as $as_type)
+                };
+            }
+
+            let value = message
+                .get_field(self.id)
+                .ok_or("Field not found".to_string())?;
+            match self.field.mavtype {
+                $(MavType::$mav_ty => downcast!(value, $rust_ty),)+
+                _ => Err("Field type not supported".to_string()),
+            }
+        }
+    };
+}
+
+impl IndexedField {
+    pub fn msg(&self) -> &MavMessage {
+        self.msg
+    }
+
+    pub fn msg_id(&self) -> u32 {
+        self.msg.id
+    }
+
+    pub fn id(&self) -> usize {
+        self.id
+    }
+
+    pub fn field(&self) -> &MavField {
+        self.field
+    }
+
+    pub fn name(&self) -> &str {
+        &self.field.name
+    }
+}
+
+/// ### Extractors
+/// These methods allow to extract the value of a field from a message, casting
+/// it to the desired type.
+impl IndexedField {
+    #[rustfmt::skip]
+    extract_as_type!(f32, extract_as_f32,
+        UInt8, u8,
+        UInt16, u16,
+        UInt32, u32,
+        UInt64, u64,
+        Int8, i8,
+        Int16, i16,
+        Int32, i32,
+        Int64, i64,
+        Float, f32,
+        Double, f64
+    );
+
+    #[rustfmt::skip]
+    extract_as_type!(f64, extract_as_f64,
+        UInt8, u8,
+        UInt16, u16,
+        UInt32, u32,
+        UInt64, u64,
+        Int8, i8,
+        Int16, i16,
+        Int32, i32,
+        Int64, i64,
+        Float, f32,
+        Double, f64
+    );
+
+    #[rustfmt::skip]
+    extract_as_type!(u8, extract_as_u8,
+        UInt8, u8,
+        Char, char
+    );
+
+    #[rustfmt::skip]
+    extract_as_type!(u16, extract_as_u16,
+        UInt8, u8,
+        Int8, i8,
+        UInt16, u16
+    );
+
+    #[rustfmt::skip]
+    extract_as_type!(u32, extract_as_u32,
+        UInt8, u8,
+        Int8, i8,
+        UInt16, u16,
+        Int16, i16,
+        UInt32, u32
+    );
+
+    #[rustfmt::skip]
+    extract_as_type!(u64, extract_as_u64,
+        UInt8, u8,
+        Int8, i8,
+        UInt16, u16,
+        Int16, i16,
+        UInt32, u32,
+        Int32, i32,
+        UInt64, u64
+    );
+
+    #[rustfmt::skip]
+    extract_as_type!(i8, extract_as_i8,
+        Int8, i8
+    );
+
+    #[rustfmt::skip]
+    extract_as_type!(i16, extract_as_i16,
+        UInt8, u8,
+        Int8, i8,
+        Int16, i16
+    );
+
+    #[rustfmt::skip]
+    extract_as_type!(i32, extract_as_i32,
+        UInt8, u8,
+        Int8, i8,
+        UInt16, u16,
+        Int16, i16,
+        Int32, i32
+    );
+
+    #[rustfmt::skip]
+    extract_as_type!(i64, extract_as_i64,
+        UInt8, u8,
+        Int8, i8,
+        UInt16, u16,
+        Int16, i16,
+        UInt32, u32,
+        Int32, i32,
+        Int64, i64
+    );
+
+    #[rustfmt::skip]
+    extract_as_type!(char, extract_as_char,
+        UInt8, u8,
+        Char, char
+    );
+}
+
+impl std::hash::Hash for IndexedField {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.id.hash(state);
+        self.msg.id.hash(state);
+    }
+}
+
+impl PartialEq for IndexedField {
+    fn eq(&self, other: &Self) -> bool {
+        self.id == other.id && self.msg.id == other.msg.id
+    }
+}
+
+impl serde::Serialize for IndexedField {
+    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let mut state = serializer.serialize_struct("IndexedField", 3)?;
+        state.serialize_field("id", &self.id)?;
+        state.serialize_field("msg_id", &self.msg.id)?;
+        state.end()
+    }
+}
+
+impl<'de> serde::Deserialize<'de> for IndexedField {
+    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+        #[derive(serde::Deserialize)]
+        struct IndexedFieldDe {
+            id: usize,
+            msg_id: u32,
+        }
+
+        let de = IndexedFieldDe::deserialize(deserializer)?;
+        let field = de
+            .id
+            .to_mav_field(de.msg_id, &MAVLINK_PROFILE)
+            .map_err(|u| serde::de::Error::custom(format!("Invalid field: {}", u)))?;
+        Ok(field)
+    }
+}
+
+pub trait MessageLike {
+    fn to_mav_message(
+        &self,
+        ctx: &'static ReflectionContext,
+    ) -> Result<&'static MavMessage, String>;
+}
+
+pub trait FieldLike {
+    fn to_mav_field(
+        &self,
+        msg_id: u32,
+        ctx: &'static ReflectionContext,
+    ) -> Result<IndexedField, String>;
+}
+
+impl MessageLike for u32 {
+    fn to_mav_message<'b>(&self, ctx: &'b ReflectionContext) -> Result<&'b MavMessage, String> {
+        ctx.id_msg_map
+            .get(self)
+            .ok_or_else(|| format!("Message {} not found", self))
+    }
+}
+
+impl MessageLike for &str {
+    fn to_mav_message<'b>(&self, ctx: &'b ReflectionContext) -> Result<&'b MavMessage, String> {
+        ctx.mavlink_profile
             .messages
             .iter()
-            .find(|(_, m)| m.name == message_name)
-            .map(|(_, m)| &m.fields)
-            .ok_or(anyhow!("Message {} not found in profile", message_name))?
-            .iter()
-            .map(|f| f.name.as_str())
-            .collect())
+            .find(|(_, m)| m.name == *self)
+            .map(|(_, m)| m)
+            .ok_or_else(|| format!("Message {} not found", self))
+    }
+}
+
+impl FieldLike for &MavField {
+    fn to_mav_field(
+        &self,
+        msg_id: u32,
+        ctx: &'static ReflectionContext,
+    ) -> Result<IndexedField, String> {
+        ctx.id_msg_map
+            .get(&msg_id)
+            .and_then(|msg| {
+                msg.fields
+                    .iter()
+                    .enumerate()
+                    .find(|(_, f)| f == self)
+                    .map(|(i, f)| IndexedField {
+                        id: i,
+                        msg,
+                        field: f,
+                    })
+            })
+            .ok_or_else(|| format!("Field {} not found in message {}", self.name, msg_id))
+    }
+}
+
+impl FieldLike for IndexedField {
+    fn to_mav_field(&self, _msg_id: u32, _ctx: &ReflectionContext) -> Result<IndexedField, String> {
+        Ok(IndexedField {
+            id: self.id,
+            msg: self.msg,
+            field: self.field,
+        })
+    }
+}
+
+impl FieldLike for usize {
+    fn to_mav_field(
+        &self,
+        msg_id: u32,
+        ctx: &'static ReflectionContext,
+    ) -> Result<IndexedField, String> {
+        ctx.id_msg_map
+            .get(&msg_id)
+            .and_then(|msg| {
+                msg.fields.get(*self).map(|f| IndexedField {
+                    id: *self,
+                    msg,
+                    field: f,
+                })
+            })
+            .ok_or_else(|| format!("Field {} not found in message {}", self, msg_id))
+    }
+}
+
+impl FieldLike for &str {
+    fn to_mav_field(
+        &self,
+        msg_id: u32,
+        ctx: &'static ReflectionContext,
+    ) -> Result<IndexedField, String> {
+        ctx.id_msg_map
+            .get(&msg_id)
+            .and_then(|msg| {
+                msg.fields
+                    .iter()
+                    .find(|f| f.name == *self)
+                    .map(|f| IndexedField {
+                        id: msg.fields.iter().position(|f2| f2 == f).log_unwrap(),
+                        msg,
+                        field: f,
+                    })
+            })
+            .ok_or_else(|| format!("Field {} not found in message {}", self, msg_id))
     }
 }
diff --git a/src/ui.rs b/src/ui.rs
index 383fabd62708460d45dd65f105faf3feb5f75975..077abdba17c67d09579d71aa56bf547fbe52910f 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -1,5 +1,5 @@
 mod app;
-mod cache;
+pub mod cache;
 mod panes;
 mod persistency;
 mod shortcuts;
diff --git a/src/ui/cache.rs b/src/ui/cache.rs
index 46b29cc2f5199005f2eb42e4ee4a5c827d457801..031a361506c55edec402f8fc099aae7810d1e494 100644
--- a/src/ui/cache.rs
+++ b/src/ui/cache.rs
@@ -1,14 +1,19 @@
 //! Module for caching expensive UI calls using egui's temporary memory storage.
 //! It provides utilities for caching the results of functions to avoid frequent recalculations.
 
-use std::time::{Duration, Instant};
+use std::{
+    collections::hash_map::DefaultHasher,
+    hash::{Hash, Hasher},
+    time::{Duration, Instant},
+};
 
-use egui::Context;
-use serialport::SerialPortInfo;
+use egui::{Context, Id, Ui};
 
-use crate::{communication, error::ErrInstrument};
+use crate::error::ErrInstrument;
 
 const SERIAL_PORT_REFRESH_INTERVAL: Duration = Duration::from_millis(500);
+const SHORT_REFRESH_INTERVAL: Duration = Duration::from_millis(500);
+const INDEF_REFRESH_INTERVAL: Duration = Duration::MAX;
 
 /// Internal helper function that caches the result of a given function call for a specified duration.
 ///
@@ -17,7 +22,7 @@ const SERIAL_PORT_REFRESH_INTERVAL: Duration = Duration::from_millis(500);
 /// * `id` - The unique identifier for the cached item.
 /// * `fun` - The function whose return value is to be cached.
 /// * `expiration_duration` - The duration after which the cache should be refreshed.
-fn call<T, F>(ctx: &egui::Context, id: egui::Id, fun: F, expiration_duration: Duration) -> T
+fn call<T, F>(ctx: &Context, id: Id, fun: F, expiration_duration: Duration) -> T
 where
     F: Fn() -> T,
     T: Clone + Send + Sync + 'static,
@@ -37,22 +42,44 @@ where
 }
 
 /// A trait to extend egui's Context with a caching function.
-pub trait CacheCall {
-    /// Calls the provided function and caches its result.
+pub trait RecentCallCache {
+    /// Calls the provided function and caches its result. Every time this
+    /// function is called, it will return the cached value if it is still
+    /// valid.
     ///
     /// # Arguments
     /// * `id` - A unique identifier for the cached value.
     /// * `fun` - The function to be cached.
     /// * `expiration_duration` - The cache expiration duration.
-    fn call_cached<F, T>(&self, id: egui::Id, fun: F, expiration_duration: Duration) -> T
+    fn cached_function_call_for<F, T>(&self, id: Id, fun: F, expiration_duration: Duration) -> T
     where
         F: Fn() -> T,
         T: Clone + Send + Sync + 'static;
+
+    fn call_cached_short<F, T, H>(&self, hashable: &H, fun: F) -> T
+    where
+        F: Fn() -> T,
+        T: Clone + Send + Sync + 'static,
+        H: Hash,
+    {
+        let id = Id::new(hashable);
+        self.cached_function_call_for(id, fun, SHORT_REFRESH_INTERVAL)
+    }
+
+    fn call_cached_indef<F, T, H>(&self, hashable: &H, fun: F) -> T
+    where
+        F: Fn() -> T,
+        T: Clone + Send + Sync + 'static,
+        H: Hash,
+    {
+        let id = Id::new(hashable);
+        self.cached_function_call_for(id, fun, INDEF_REFRESH_INTERVAL)
+    }
 }
 
-impl CacheCall for egui::Context {
+impl RecentCallCache for Context {
     /// Implements the caching call using the internal `call` function.
-    fn call_cached<F, T>(&self, id: egui::Id, fun: F, expiration_duration: Duration) -> T
+    fn cached_function_call_for<F, T>(&self, id: Id, fun: F, expiration_duration: Duration) -> T
     where
         F: Fn() -> T,
         T: Clone + Send + Sync + 'static,
@@ -61,32 +88,108 @@ impl CacheCall for egui::Context {
     }
 }
 
-/// Returns a cached list of all available USB ports.
-///
-/// # Arguments
-/// * `ctx` - The egui context used for caching.
-///
-/// # Returns
-/// * A Result containing a vector of `SerialPortInfo` or a `serialport::Error`.
-pub fn cached_list_all_usb_ports(ctx: &Context) -> Result<Vec<SerialPortInfo>, serialport::Error> {
-    ctx.call_cached(
-        egui::Id::new("list_usb_ports"),
-        communication::serial::list_all_usb_ports,
-        SERIAL_PORT_REFRESH_INTERVAL,
-    )
+impl RecentCallCache for &Ui {
+    /// Implements the caching call using the context of the UI.
+    fn cached_function_call_for<F, T>(&self, id: Id, fun: F, expiration_duration: Duration) -> T
+    where
+        F: Fn() -> T,
+        T: Clone + Send + Sync + 'static,
+    {
+        call(self.ctx(), id, fun, expiration_duration)
+    }
 }
 
-/// Returns the first cached STM32 port found, if any.
-///
-/// # Arguments
-/// * `ctx` - The egui context used for caching.
+pub trait CacheWithCondition {
+    fn cache_result_if<F, T, H>(&self, hashable: H, condition: bool, fun: F) -> T
+    where
+        F: Fn() -> T,
+        T: Clone + Send + Sync + 'static,
+        H: Hash;
+}
+
+impl CacheWithCondition for Ui {
+    fn cache_result_if<F, T, H>(&self, hashable: H, condition: bool, fun: F) -> T
+    where
+        F: Fn() -> T,
+        T: Clone + Send + Sync + 'static,
+        H: Hash,
+    {
+        let id = self.next_auto_id().with(hashable);
+        self.memory_mut(|m| {
+            let value = m.data.get_temp::<T>(id);
+            if !condition || value.is_none() {
+                let value = fun();
+                m.data.insert_temp(id, value.clone());
+                value
+            } else {
+                value.log_unwrap()
+            }
+        })
+    }
+}
+
+/// ChangeTracker manages the tracking of state changes using an integrity digest.
 ///
-/// # Returns
-/// * A Result containing an Option of `SerialPortInfo` or a `serialport::Error`.
-pub fn cached_first_stm32_port(ctx: &Context) -> Result<Option<SerialPortInfo>, serialport::Error> {
-    ctx.call_cached(
-        egui::Id::new("list_usb_ports"),
-        communication::serial::find_first_stm32_port,
-        SERIAL_PORT_REFRESH_INTERVAL,
-    )
+/// The `integrity_digest` field holds a 64-bit unsigned integer that represents
+/// a summary (or hash) of the current state. This can be used to verify that the
+/// cached UI state remains consistent, and to quickly detect any modifications.
+pub struct ChangeTracker {
+    integrity_digest: u64,
+}
+
+impl ChangeTracker {
+    /// Records the initial state of a hashable value by computing its hash digest.
+    ///
+    /// This method takes a reference to any value that implements the `Hash` trait,
+    /// computes its hash using the default hasher, and stores the resulting digest in a
+    /// newly created `ChangeTracker` instance. This digest serves as a reference point
+    /// for future state comparisons.
+    ///
+    /// # Parameters
+    ///
+    /// - `state`: A reference to the value whose state is to be recorded.
+    ///
+    /// # Returns
+    ///
+    /// A `ChangeTracker` initialized with the computed hash digest.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// let initial_tracker = ChangeTracker::record_initial_state(&state);
+    /// ```
+    pub fn record_initial_state<T: Hash>(state: &T) -> Self {
+        let mut hasher = DefaultHasher::new();
+        state.hash(&mut hasher);
+        let integrity_digest = hasher.finish();
+        Self { integrity_digest }
+    }
+
+    /// Checks whether the hash of the current state differs from the initially recorded state.
+    ///
+    /// This method computes the hash digest of the current state (which must implement the
+    /// `Hash` trait) and compares it with the digest stored in the `ChangeTracker`. If the digests
+    /// differ, it indicates that the state has changed since the initial recording.
+    ///
+    /// # Parameters
+    ///
+    /// - `state`: A reference to the current state to be checked for changes.
+    ///
+    /// # Returns
+    ///
+    /// `true` if the current state's hash digest does not match the initially recorded digest,
+    /// indicating a change; `false` otherwise.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// if tracker.has_changed(&state) {
+    ///     println!("The state has changed.");
+    /// }
+    /// ```
+    pub fn has_changed<T: Hash>(&self, state: &T) -> bool {
+        let mut hasher = DefaultHasher::new();
+        state.hash(&mut hasher);
+        self.integrity_digest != hasher.finish()
+    }
 }
diff --git a/src/ui/panes/plot.rs b/src/ui/panes/plot.rs
index e83bd2faa2a011abd45ccaba4bdbc23e6733be51..4a98372c1fd099015a2774a335a285fef93b3e2b 100644
--- a/src/ui/panes/plot.rs
+++ b/src/ui/panes/plot.rs
@@ -2,38 +2,43 @@ mod source_window;
 
 use super::PaneBehavior;
 use crate::{
+    MAVLINK_PROFILE,
     error::ErrInstrument,
-    mavlink::{MessageData, ROCKET_FLIGHT_TM_DATA, TimedMessage, extract_from_message},
+    mavlink::{
+        MessageData, ROCKET_FLIGHT_TM_DATA, TimedMessage,
+        reflection::{FieldLike, IndexedField},
+    },
     ui::app::PaneResponse,
+    utils::units::UnitOfMeasure,
 };
-use egui::{Color32, Vec2b};
-use egui_plot::{Legend, Line, PlotPoints};
+use egui::{Color32, Vec2, Vec2b};
+use egui_plot::{AxisHints, HPlacement, Legend, Line, PlotPoint, log_grid_spacer};
 use egui_tiles::TileId;
-use serde::{Deserialize, Serialize};
-use source_window::{SourceSettings, sources_window};
-use std::iter::zip;
+use serde::{self, Deserialize, Serialize};
+use source_window::sources_window;
+use std::{
+    hash::{DefaultHasher, Hash, Hasher},
+    iter::zip,
+    time::{Duration, Instant},
+};
 
 #[derive(Clone, Default, Debug, Serialize, Deserialize)]
 pub struct Plot2DPane {
+    settings: PlotSettings,
     // UI settings
     #[serde(skip)]
-    pub contains_pointer: bool,
+    line_data: Vec<TimeAwarePlotPoints>,
     #[serde(skip)]
-    settings_visible: bool,
-
-    line_settings: Vec<LineSettings>,
+    state_valid: bool,
     #[serde(skip)]
-    line_data: Vec<Vec<[f64; 2]>>,
-
-    settings: MsgSources,
-
+    settings_visible: bool,
     #[serde(skip)]
-    state_valid: bool,
+    pub contains_pointer: bool,
 }
 
 impl PartialEq for Plot2DPane {
     fn eq(&self, other: &Self) -> bool {
-        self.settings == other.settings && self.line_settings == other.line_settings
+        self.settings == other.settings
     }
 }
 
@@ -41,43 +46,135 @@ impl PaneBehavior for Plot2DPane {
     #[profiling::function]
     fn ui(&mut self, ui: &mut egui::Ui, _: TileId) -> PaneResponse {
         let mut response = PaneResponse::default();
+        let data_settings_digest = self.settings.data_digest();
 
         let ctrl_pressed = ui.input(|i| i.modifiers.ctrl);
 
-        egui_plot::Plot::new("plot")
+        let x_unit = UnitOfMeasure::from(
+            &self
+                .settings
+                .x_field
+                .field()
+                .unit
+                .clone()
+                .unwrap_or_default(),
+        );
+        let y_units = self
+            .settings
+            .y_fields
+            .iter()
+            .map(|(field, _)| field.field().unit.as_ref().map(UnitOfMeasure::from))
+            .collect::<Vec<_>>();
+        // define y_unit as the common unit of the y_fields if they are all the same
+        let y_unit = y_units
+            .iter()
+            .fold(y_units.first().log_unwrap(), |acc, unit| {
+                match (acc, unit) {
+                    (Some(uom), Some(unit)) if uom == unit => acc,
+                    _ => &None,
+                }
+            });
+        let x_name = self.settings.x_field.field().name.clone();
+
+        let x_axis = match x_unit {
+            UnitOfMeasure::Time(ref time_unit) => {
+                AxisHints::new_x().label(&x_name).formatter(move |m, r| {
+                    let scaling_factor_to_nanos = time_unit.scale() * 1e9;
+                    let r_span_in_nanos = (r.end() - r.start()).abs() * scaling_factor_to_nanos;
+                    let m_in_nanos = m.value * scaling_factor_to_nanos;
+                    // all the following numbers are arbitrary
+                    // they are chosen based on common sense
+                    if r_span_in_nanos < 4e3 {
+                        format!("{:.0}ns", m_in_nanos)
+                    } else if r_span_in_nanos < 4e6 {
+                        format!("{:.0}µs", m_in_nanos / 1e3)
+                    } else if r_span_in_nanos < 4e9 {
+                        format!("{:.0}ms", m_in_nanos / 1e6)
+                    } else if r_span_in_nanos < 24e10 {
+                        format!("{:.0}s", m_in_nanos / 1e9)
+                    } else if r_span_in_nanos < 144e11 {
+                        format!("{:.0}m{:.0}s", m_in_nanos / 60e9, (m_in_nanos % 60e9) / 1e9)
+                    } else if r_span_in_nanos < 3456e11 {
+                        format!(
+                            "{:.0}h{:.0}m",
+                            m_in_nanos / 3600e9,
+                            (m_in_nanos % 3600e9) / 60e9
+                        )
+                    } else {
+                        format!(
+                            "{:.0}d{:.0}h",
+                            m_in_nanos / 86400e9,
+                            (m_in_nanos % 86400e9) / 3600e9
+                        )
+                    }
+                })
+            }
+            _ => AxisHints::new_x().label(&x_name),
+        };
+        let y_axis = AxisHints::new_y().placement(HPlacement::Right);
+
+        let cursor_formatter = |name: &str, value: &PlotPoint| {
+            let x_unit = format!(" [{}]", x_unit);
+            let y_unit = y_unit
+                .as_ref()
+                .map(|unit| format!(" [{}]", unit))
+                .unwrap_or_default();
+            if name.is_empty() {
+                format!(
+                    "{}: {:.2}{}\ny: {:.2}{}",
+                    x_name, value.x, x_unit, value.y, y_unit
+                )
+            } else {
+                format!(
+                    "{}: {:.2}{}\n{}: {:.2}{}",
+                    x_name, value.x, x_unit, name, value.y, y_unit
+                )
+            }
+        };
+
+        let mut plot = egui_plot::Plot::new("plot")
+            .x_grid_spacer(log_grid_spacer(4)) // 4 was an arbitrary choice
             .auto_bounds(Vec2b::TRUE)
+            .set_margin_fraction(Vec2::splat(0.))
             .legend(Legend::default())
-            .label_formatter(|name, value| format!("{} - x:{:.2} y:{:.2}", name, value.x, value.y))
-            .show(ui, |plot_ui| {
-                self.contains_pointer = plot_ui.response().contains_pointer();
-                if plot_ui.response().dragged() && ctrl_pressed {
-                    response.set_drag_started();
-                }
+            .label_formatter(cursor_formatter);
 
-                for (settings, points) in zip(&self.line_settings, &mut self.line_data) {
-                    plot_ui.line(
-                        // TODO: remove clone when PlotPoints supports borrowing
-                        Line::new(PlotPoints::from(points.clone()))
-                            .color(settings.color)
-                            .width(settings.width)
-                            .name(&settings.field),
-                    );
-                }
-                plot_ui
-                    .response()
-                    .context_menu(|ui| show_menu(ui, &mut self.settings_visible));
-            });
+        if self.settings.axes_visible {
+            plot = plot.custom_x_axes(vec![x_axis]).custom_y_axes(vec![y_axis]);
+        } else {
+            plot = plot.show_axes(Vec2b::FALSE);
+        }
+
+        plot.show(ui, |plot_ui| {
+            self.contains_pointer = plot_ui.response().contains_pointer();
+            if plot_ui.response().dragged() && ctrl_pressed {
+                response.set_drag_started();
+            }
+
+            for ((field, settings), TimeAwarePlotPoints { points, .. }) in
+                zip(&self.settings.y_fields, &self.line_data)
+            {
+                plot_ui.line(
+                    Line::new(&points[..])
+                        .color(settings.color)
+                        .width(settings.width)
+                        .name(&field.field().name),
+                );
+            }
+            plot_ui
+                .response()
+                .context_menu(|ui| show_menu(ui, &mut self.settings_visible, &mut self.settings));
+        });
 
-        let mut settings = SourceSettings::new(&mut self.settings, &mut self.line_settings);
         egui::Window::new("Plot Settings")
-            .id(ui.auto_id_with("plot_settings")) // TODO: fix this issue with ids
+            .id(ui.auto_id_with("plot_settings"))
             .auto_sized()
             .collapsible(true)
             .movable(true)
             .open(&mut self.settings_visible)
-            .show(ui.ctx(), |ui| sources_window(ui, &mut settings));
+            .show(ui.ctx(), |ui| sources_window(ui, &mut self.settings));
 
-        if settings.are_sources_changed() {
+        if data_settings_digest != self.settings.data_digest() {
             self.state_valid = false;
         }
 
@@ -94,34 +191,43 @@ impl PaneBehavior for Plot2DPane {
             self.line_data.clear();
         }
 
-        let MsgSources {
-            x_field, y_fields, ..
+        let PlotSettings {
+            x_field,
+            y_fields,
+            points_lifespan,
+            ..
         } = &self.settings;
 
-        for msg in messages {
-            let x: f64 = extract_from_message(&msg.message, [x_field]).log_unwrap()[0];
-            let ys: Vec<f64> = extract_from_message(&msg.message, y_fields).log_unwrap();
+        // iter on filtered messages based on lifespan set
+        for msg in messages
+            .iter()
+            .filter(|msg| points_lifespan > &msg.time.elapsed())
+        {
+            let x: f64 = x_field.extract_as_f64(&msg.message).log_unwrap();
+            let ys: Vec<f64> = y_fields
+                .iter()
+                .map(|(field, _)| field.extract_as_f64(&msg.message).log_unwrap())
+                .collect();
 
             if self.line_data.len() < ys.len() {
-                self.line_data.resize(ys.len(), Vec::new());
+                self.line_data.resize(ys.len(), TimeAwarePlotPoints::new());
             }
 
-            for (line, y) in zip(&mut self.line_data, ys) {
-                let point = if x_field == "timestamp" {
-                    [x / 1e6, y]
-                } else {
-                    [x, y]
-                };
-
-                line.push(point);
+            for (points, y) in zip(&mut self.line_data, ys) {
+                points.push(msg.time, PlotPoint::new(x, y));
             }
         }
 
+        // clear points older than lifespan set
+        for line in &mut self.line_data {
+            line.clear_older_than(*points_lifespan);
+        }
+
         self.state_valid = true;
     }
 
     fn get_message_subscription(&self) -> Option<u32> {
-        Some(self.settings.msg_id)
+        Some(self.settings.plot_message_id)
     }
 
     fn should_send_message_history(&self) -> bool {
@@ -129,34 +235,78 @@ impl PaneBehavior for Plot2DPane {
     }
 }
 
-#[derive(Clone, Debug, Serialize, Deserialize)]
-struct MsgSources {
-    msg_id: u32,
-    x_field: String,
-    y_fields: Vec<String>,
+fn show_menu(ui: &mut egui::Ui, settings_visible: &mut bool, settings: &mut PlotSettings) {
+    ui.set_max_width(200.0); // To make sure we wrap long text
+
+    if ui.button("Source Data Settings…").clicked() {
+        *settings_visible = true;
+        ui.close_menu();
+    }
+
+    ui.checkbox(&mut settings.axes_visible, "Show Axes");
 }
 
-impl Default for MsgSources {
-    fn default() -> Self {
-        Self {
-            msg_id: ROCKET_FLIGHT_TM_DATA::ID,
-            x_field: "timestamp".to_owned(),
-            y_fields: Vec::new(),
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+struct PlotSettings {
+    /// The message id to plot
+    pub(super) plot_message_id: u32,
+    /// The field to plot on the x-axis
+    pub(super) x_field: IndexedField,
+    /// The fields to plot, with their respective line settings
+    pub(super) y_fields: Vec<(IndexedField, LineSettings)>,
+    /// Whether to show the axes of the plot
+    pub(super) axes_visible: bool,
+    /// Points will be shown for this duration before being removed
+    pub(super) points_lifespan: Duration,
+}
+
+impl PlotSettings {
+    fn add_field(&mut self, field: IndexedField) {
+        let line_settings = LineSettings::default();
+        self.y_fields.push((field, line_settings));
+    }
+
+    fn clear_fields(&mut self) {
+        self.x_field = 0
+            .to_mav_field(self.plot_message_id, &MAVLINK_PROFILE)
+            .log_unwrap();
+        self.y_fields.clear();
+    }
+
+    /// Returns a digest of the data settings, used to check if the settings
+    /// have changed IMPORTANT: To trigger a redraw, hash the settings that need
+    /// to redraw the plot here
+    fn data_digest(&self) -> u64 {
+        let mut hasher = DefaultHasher::new();
+        self.x_field.hash(&mut hasher);
+        for (field, _) in &self.y_fields {
+            field.hash(&mut hasher);
         }
+        self.points_lifespan.as_secs().hash(&mut hasher);
+        hasher.finish()
     }
 }
 
-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
+impl Default for PlotSettings {
+    fn default() -> Self {
+        let msg_id = ROCKET_FLIGHT_TM_DATA::ID;
+        let x_field = 0.to_mav_field(msg_id, &MAVLINK_PROFILE).log_unwrap();
+        let y_fields = vec![(
+            1.to_mav_field(msg_id, &MAVLINK_PROFILE).log_unwrap(),
+            LineSettings::default(),
+        )];
+        Self {
+            plot_message_id: msg_id,
+            x_field,
+            y_fields,
+            axes_visible: true,
+            points_lifespan: Duration::from_secs(600),
+        }
     }
 }
 
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
 struct LineSettings {
-    field: String,
     width: f32,
     color: Color32,
 }
@@ -164,27 +314,46 @@ struct LineSettings {
 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 {
+impl Hash for LineSettings {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.width.to_bits().hash(state);
+        self.color.hash(state);
+    }
+}
+
+#[derive(Clone, Debug)]
+struct TimeAwarePlotPoints {
+    times: Vec<Instant>,
+    points: Vec<PlotPoint>,
+}
+
+impl TimeAwarePlotPoints {
+    fn new() -> Self {
         Self {
-            field: field_y,
-            ..Default::default()
+            times: Vec::new(),
+            points: Vec::new(),
         }
     }
-}
 
-fn show_menu(ui: &mut egui::Ui, settings_visible: &mut bool) {
-    ui.set_max_width(200.0); // To make sure we wrap long text
+    fn push(&mut self, time: Instant, point: PlotPoint) {
+        self.times.push(time);
+        self.points.push(point);
+    }
 
-    if ui.button("Settings…").clicked() {
-        *settings_visible = true;
-        ui.close_menu();
+    fn clear_older_than(&mut self, lifespan: Duration) {
+        while let Some(time) = self.times.first().copied() {
+            if time.elapsed() > lifespan {
+                self.times.remove(0);
+                self.points.remove(0);
+            } else {
+                break;
+            }
+        }
     }
 }
diff --git a/src/ui/panes/plot/source_window.rs b/src/ui/panes/plot/source_window.rs
index f6def542663b70e9620103083c72a0b304d87db8..a62d1f9e41bcb3154c7bd4197327fc62c37a7306 100644
--- a/src/ui/panes/plot/source_window.rs
+++ b/src/ui/panes/plot/source_window.rs
@@ -1,47 +1,61 @@
-use crate::{
-    MAVLINK_PROFILE,
-    mavlink::{MavMessage, Message},
-};
+use std::time::Duration;
 
-use crate::error::ErrInstrument;
+use crate::{MAVLINK_PROFILE, error::ErrInstrument};
 
-use super::{LineSettings, MsgSources};
+use super::{LineSettings, PlotSettings};
 
 #[profiling::function]
-pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut SourceSettings) {
+pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut PlotSettings) {
+    // select how many points are shown on the plot
+    let mut points_lifespan_sec = plot_settings.points_lifespan.as_secs();
+    ui.horizontal(|ui| {
+        let res1 = ui.add(egui::Label::new("Points Lifespan: "));
+        let res2 = ui.add(
+            egui::DragValue::new(&mut points_lifespan_sec)
+                .range(5..=1800)
+                .speed(1)
+                .suffix(" seconds"),
+        );
+        res1.union(res2)
+    })
+    .inner
+    .on_hover_text("How long the data is shown on the plot");
+    plot_settings.points_lifespan = Duration::from_secs(points_lifespan_sec);
+
+    ui.add_sized([250., 10.], egui::Separator::default());
+
+    let data_settings_digest = plot_settings.data_digest();
     // 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())
+        .get_msg(plot_settings.plot_message_id)
+        .map(|m| m.name.clone())
         .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).log_expect("Invalid message name"),
-                    msg,
-                );
+            for msg in MAVLINK_PROFILE.get_sorted_msgs() {
+                ui.selectable_value(&mut plot_settings.plot_message_id, msg.id, &msg.name);
             }
         });
 
     // reset fields if the message is changed
-    if plot_settings.is_msg_id_changed() {
+    if data_settings_digest != plot_settings.data_digest() {
         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_plottable_fields(plot_settings.plot_message_id)
         .log_expect("Invalid message 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 x_field = &plot_settings.x_field;
     let new_field_x = fields
-        .contains(&x_field)
+        .iter()
+        .any(|f| f == x_field)
         .then(|| x_field.to_owned())
-        .or(fields.first().map(|s| s.to_string()));
+        .or(fields.first().map(|s| s.to_owned()));
 
     // if there are no fields, reset the field_x and plot_lines
     let Some(new_field_x) = new_field_x else {
@@ -49,55 +63,54 @@ pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut SourceSettings) {
         return;
     };
     // update the field_x
-    plot_settings.set_x_field(new_field_x);
-    let x_field = plot_settings.get_mut_x_field();
+    plot_settings.x_field = new_field_x;
 
     // if fields are valid, show the combo boxes for the x_axis
+    let x_field = &mut plot_settings.x_field;
     egui::ComboBox::from_label("X Axis")
-        .selected_text(x_field.as_str())
+        .selected_text(&x_field.field().name)
         .show_ui(ui, |ui| {
             for msg in fields.iter() {
-                ui.selectable_value(x_field, (*msg).to_owned(), *msg);
+                ui.selectable_value(x_field, msg.to_owned(), &msg.field().name);
             }
         });
 
     // 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());
+    if plot_settings.y_fields.is_empty() && fields.len() > 1 {
+        plot_settings.add_field(fields[1].to_owned());
     }
 
     // check how many fields are left and how many are selected
-    let plot_lines_len = plot_settings.fields_len();
+    let plot_lines_len = plot_settings.y_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;
+            for (i, (field, line_settings)) in plot_settings.y_fields[..].iter_mut().enumerate() {
+                let LineSettings { 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())
+                    .selected_text(&field.field().name)
                     .show_ui(ui, |ui| {
                         for msg in fields.iter() {
-                            ui.selectable_value(field, (*msg).to_owned(), *msg);
+                            ui.selectable_value(field, msg.to_owned(), &msg.field().name);
                         }
                     });
                 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.add(
+                    egui::DragValue::new(width)
+                        .range(0.0..=10.0)
+                        .speed(0.02)
+                        .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
@@ -106,85 +119,11 @@ pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut SourceSettings) {
             .on_hover_text("Add another Y axis")
             .clicked()
     {
+        // get the first field that is not in the plot_lines
         let next_field = fields
             .iter()
-            .find(|f| !plot_settings.contains_field(f))
+            .find(|field| !plot_settings.y_fields.iter().any(|(f, _)| f == *field))
             .log_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();
+        plot_settings.add_field(next_field.to_owned());
     }
 }
diff --git a/src/ui/windows/connections.rs b/src/ui/windows/connections.rs
index ebc34c32fcfd24d57cba0a17ff7f126d79b48e0b..30ffe5eae5606c493f4a37aae420f1b3d3254b2f 100644
--- a/src/ui/windows/connections.rs
+++ b/src/ui/windows/connections.rs
@@ -4,12 +4,15 @@ use tracing::{error, warn};
 
 use crate::{
     communication::{
-        ConnectionError, EthernetConfiguration, SerialConfiguration, serial::DEFAULT_BAUD_RATE,
+        ConnectionError, EthernetConfiguration, SerialConfiguration,
+        serial::{
+            DEFAULT_BAUD_RATE,
+            cached::{cached_first_stm32_port, cached_list_all_usb_ports},
+        },
     },
     error::ErrInstrument,
     mavlink::DEFAULT_ETHERNET_PORT,
     message_broker::MessageBroker,
-    ui::cache::{cached_first_stm32_port, cached_list_all_usb_ports},
 };
 
 #[derive(Default)]
diff --git a/src/utils.rs b/src/utils.rs
index 7e22ff58e02d0b7d8c76696d474d7624e4ac3f77..503a473f5b5c5a5bf2302a8ccad4bc54e89619b0 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -1,2 +1,2 @@
 mod ring_buffer;
-
+pub mod units;
diff --git a/src/utils/ring_buffer.rs b/src/utils/ring_buffer.rs
index c25df112697f5d3ff6737fe0b219b25c72d27010..5f6ee5732da733f864f492c3e6b8a86a8b4a0943 100644
--- a/src/utils/ring_buffer.rs
+++ b/src/utils/ring_buffer.rs
@@ -1,5 +1,3 @@
-
-
 #[derive(Debug)]
 pub struct RingBuffer<const G: usize> {
     buffer: Box<[u8; G]>,
diff --git a/src/utils/units.rs b/src/utils/units.rs
new file mode 100644
index 0000000000000000000000000000000000000000..98267d98cf45a12f542c22d5d7d2d8366c315867
--- /dev/null
+++ b/src/utils/units.rs
@@ -0,0 +1,70 @@
+use std::{fmt::Display, str::FromStr};
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum UnitOfMeasure {
+    Time(TimeUnits),
+    Other(String),
+}
+
+impl<T: AsRef<str>> From<T> for UnitOfMeasure {
+    fn from(s: T) -> Self {
+        if let Ok(unit) = TimeUnits::from_str(s.as_ref()) {
+            UnitOfMeasure::Time(unit)
+        } else {
+            UnitOfMeasure::Other(s.as_ref().to_string())
+        }
+    }
+}
+
+impl Display for UnitOfMeasure {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            UnitOfMeasure::Time(unit) => write!(f, "{}", unit),
+            UnitOfMeasure::Other(unit) => write!(f, "{}", unit),
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum TimeUnits {
+    Second,      // s
+    Millisecond, // ms
+    Microsecond, // us
+    Nanosecond,  // ns
+}
+
+impl TimeUnits {
+    pub fn scale(&self) -> f64 {
+        match self {
+            TimeUnits::Second => 1.0,
+            TimeUnits::Millisecond => 1e-3,
+            TimeUnits::Microsecond => 1e-6,
+            TimeUnits::Nanosecond => 1e-9,
+        }
+    }
+}
+
+impl FromStr for TimeUnits {
+    type Err = ();
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s {
+            "s" => Ok(TimeUnits::Second),
+            "ms" => Ok(TimeUnits::Millisecond),
+            "us" => Ok(TimeUnits::Microsecond),
+            "ns" => Ok(TimeUnits::Nanosecond),
+            _ => Err(()),
+        }
+    }
+}
+
+impl Display for TimeUnits {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            TimeUnits::Second => write!(f, "s"),
+            TimeUnits::Millisecond => write!(f, "ms"),
+            TimeUnits::Microsecond => write!(f, "µs"),
+            TimeUnits::Nanosecond => write!(f, "ns"),
+        }
+    }
+}