diff --git a/Cargo.lock b/Cargo.lock
index 0ea4525d7a3f394555fca07fecf5dfef5146d156..95df11c4ccfc5f95e55e257648e49b9c23c2f546 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -38,7 +38,7 @@ dependencies = [
  "accesskit_consumer",
  "atspi-common",
  "serde",
- "thiserror",
+ "thiserror 1.0.69",
  "zvariant",
 ]
 
@@ -150,12 +150,6 @@ dependencies = [
  "memchr",
 ]
 
-[[package]]
-name = "allocator-api2"
-version = "0.2.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9"
-
 [[package]]
 name = "android-activity"
 version = "0.6.0"
@@ -174,7 +168,7 @@ dependencies = [
  "ndk-context",
  "ndk-sys 0.6.0+11769913",
  "num_enum",
- "thiserror",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
@@ -194,9 +188,9 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.93"
+version = "1.0.95"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
+checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
 
 [[package]]
 name = "arboard"
@@ -242,9 +236,9 @@ dependencies = [
 
 [[package]]
 name = "async-broadcast"
-version = "0.7.1"
+version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e"
+checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
 dependencies = [
  "event-listener",
  "event-listener-strategy",
@@ -345,7 +339,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -374,13 +368,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
 
 [[package]]
 name = "async-trait"
-version = "0.1.83"
+version = "0.1.85"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
+checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -542,22 +536,22 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
 
 [[package]]
 name = "bytemuck"
-version = "1.19.0"
+version = "1.21.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d"
+checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
 dependencies = [
  "bytemuck_derive",
 ]
 
 [[package]]
 name = "bytemuck_derive"
-version = "1.8.0"
+version = "1.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec"
+checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -574,9 +568,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
 
 [[package]]
 name = "bytes"
-version = "1.8.0"
+version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
+checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
 
 [[package]]
 name = "calloop"
@@ -589,7 +583,7 @@ dependencies = [
  "polling",
  "rustix",
  "slab",
- "thiserror",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
@@ -606,9 +600,9 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.2.1"
+version = "1.2.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47"
+checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7"
 dependencies = [
  "jobserver",
  "libc",
@@ -769,9 +763,9 @@ dependencies = [
 
 [[package]]
 name = "cpufeatures"
-version = "0.2.15"
+version = "0.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6"
+checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
 dependencies = [
  "libc",
 ]
@@ -793,27 +787,27 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-channel"
-version = "0.5.13"
+version = "0.5.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
+checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
 dependencies = [
  "crossbeam-utils",
 ]
 
 [[package]]
 name = "crossbeam-queue"
-version = "0.3.11"
+version = "0.3.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35"
+checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
 dependencies = [
  "crossbeam-utils",
 ]
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.8.20"
+version = "0.8.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
 
 [[package]]
 name = "crypto-common"
@@ -866,7 +860,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -976,7 +970,7 @@ dependencies = [
  "egui",
  "epaint",
  "log",
- "thiserror",
+ "thiserror 1.0.69",
  "type-map",
  "web-time",
  "wgpu",
@@ -1105,7 +1099,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -1117,7 +1111,7 @@ dependencies = [
  "once_cell",
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -1138,7 +1132,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -1149,7 +1143,7 @@ checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -1184,12 +1178,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
 
 [[package]]
 name = "errno"
-version = "0.3.9"
+version = "0.3.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
+checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
 dependencies = [
  "libc",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -1211,9 +1205,9 @@ dependencies = [
 
 [[package]]
 name = "event-listener-strategy"
-version = "0.5.2"
+version = "0.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1"
+checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2"
 dependencies = [
  "event-listener",
  "pin-project-lite",
@@ -1221,15 +1215,15 @@ dependencies = [
 
 [[package]]
 name = "fastrand"
-version = "2.2.0"
+version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
 
 [[package]]
 name = "fdeflate"
-version = "0.3.6"
+version = "0.3.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07c6f4c64c1d33a3111c4466f7365ebdcc37c5bd1ea0d62aae2e3d722aacbedb"
+checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
 dependencies = [
  "simd-adler32",
 ]
@@ -1244,6 +1238,12 @@ dependencies = [
  "miniz_oxide",
 ]
 
+[[package]]
+name = "foldhash"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
+
 [[package]]
 name = "foreign-types"
 version = "0.5.0"
@@ -1262,7 +1262,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -1349,7 +1349,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -1547,20 +1547,20 @@ checksum = "fdd4240fc91d3433d5e5b0fc5b67672d771850dc19bbee03c1381e19322803d7"
 dependencies = [
  "log",
  "presser",
- "thiserror",
+ "thiserror 1.0.69",
  "winapi",
  "windows 0.52.0",
 ]
 
 [[package]]
 name = "gpu-descriptor"
-version = "0.3.0"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c08c1f623a8d0b722b8b99f821eb0ba672a1618f0d3b16ddbee1cedd2dd8557"
+checksum = "dcf29e94d6d243368b7a56caa16bc213e4f9f8ed38c4d9557069527b5d5281ca"
 dependencies = [
  "bitflags 2.6.0",
  "gpu-descriptor-types",
- "hashbrown 0.14.5",
+ "hashbrown",
 ]
 
 [[package]]
@@ -1574,20 +1574,13 @@ dependencies = [
 
 [[package]]
 name = "hashbrown"
-version = "0.14.5"
+version = "0.15.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
 dependencies = [
- "ahash",
- "allocator-api2",
+ "foldhash",
 ]
 
-[[package]]
-name = "hashbrown"
-version = "0.15.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
-
 [[package]]
 name = "hassle-rs"
 version = "0.11.0"
@@ -1598,17 +1591,11 @@ dependencies = [
  "com",
  "libc",
  "libloading",
- "thiserror",
+ "thiserror 1.0.69",
  "widestring",
  "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"
@@ -1629,11 +1616,11 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
 
 [[package]]
 name = "home"
-version = "0.5.9"
+version = "0.5.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
+checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
 dependencies = [
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -1751,7 +1738,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -1798,12 +1785,12 @@ dependencies = [
 
 [[package]]
 name = "indexmap"
-version = "2.6.0"
+version = "2.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
+checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
 dependencies = [
  "equivalent",
- "hashbrown 0.15.1",
+ "hashbrown",
 ]
 
 [[package]]
@@ -1826,9 +1813,9 @@ dependencies = [
 
 [[package]]
 name = "itoa"
-version = "1.0.11"
+version = "1.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
 
 [[package]]
 name = "jni"
@@ -1841,7 +1828,7 @@ dependencies = [
  "combine",
  "jni-sys",
  "log",
- "thiserror",
+ "thiserror 1.0.69",
  "walkdir",
  "windows-sys 0.45.0",
 ]
@@ -1863,10 +1850,11 @@ dependencies = [
 
 [[package]]
 name = "js-sys"
-version = "0.3.72"
+version = "0.3.76"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9"
+checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
 dependencies = [
+ "once_cell",
  "wasm-bindgen",
 ]
 
@@ -1895,15 +1883,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
 
 [[package]]
 name = "libc"
-version = "0.2.162"
+version = "0.2.169"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
+checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
 
 [[package]]
 name = "libloading"
-version = "0.8.5"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
+checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
 dependencies = [
  "cfg-if",
  "windows-targets 0.52.6",
@@ -1917,7 +1905,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
 dependencies = [
  "bitflags 2.6.0",
  "libc",
- "redox_syscall 0.5.7",
+ "redox_syscall 0.5.8",
 ]
 
 [[package]]
@@ -1928,9 +1916,9 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
 
 [[package]]
 name = "litemap"
-version = "0.7.3"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
+checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
 
 [[package]]
 name = "litrs"
@@ -1984,7 +1972,7 @@ dependencies = [
  "quick-xml 0.26.0",
  "quote",
  "serde",
- "thiserror",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
@@ -2057,9 +2045,9 @@ dependencies = [
 
 [[package]]
 name = "miniz_oxide"
-version = "0.8.0"
+version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
+checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
 dependencies = [
  "adler2",
  "simd-adler32",
@@ -2067,11 +2055,10 @@ dependencies = [
 
 [[package]]
 name = "mio"
-version = "1.0.2"
+version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
+checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
 dependencies = [
- "hermit-abi 0.3.9",
  "libc",
  "wasi",
  "windows-sys 0.52.0",
@@ -2094,7 +2081,7 @@ dependencies = [
  "rustc-hash",
  "spirv",
  "termcolor",
- "thiserror",
+ "thiserror 1.0.69",
  "unicode-xid",
 ]
 
@@ -2110,7 +2097,7 @@ dependencies = [
  "ndk-sys 0.6.0+11769913",
  "num_enum",
  "raw-window-handle",
- "thiserror",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
@@ -2174,7 +2161,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -2204,7 +2191,7 @@ dependencies = [
  "proc-macro-crate",
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -2421,9 +2408,9 @@ dependencies = [
 
 [[package]]
 name = "object"
-version = "0.36.5"
+version = "0.36.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
 dependencies = [
  "memchr",
 ]
@@ -2492,7 +2479,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
 dependencies = [
  "cfg-if",
  "libc",
- "redox_syscall 0.5.7",
+ "redox_syscall 0.5.8",
  "smallvec",
  "windows-targets 0.52.6",
 ]
@@ -2511,29 +2498,29 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
 
 [[package]]
 name = "pin-project"
-version = "1.1.7"
+version = "1.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95"
+checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916"
 dependencies = [
  "pin-project-internal",
 ]
 
 [[package]]
 name = "pin-project-internal"
-version = "1.1.7"
+version = "1.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c"
+checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
 ]
 
 [[package]]
 name = "pin-project-lite"
-version = "0.2.15"
+version = "0.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
 
 [[package]]
 name = "pin-utils"
@@ -2560,9 +2547,9 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
 
 [[package]]
 name = "png"
-version = "0.17.14"
+version = "0.17.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0"
+checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
 dependencies = [
  "bitflags 1.3.2",
  "crc32fast",
@@ -2579,7 +2566,7 @@ checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
 dependencies = [
  "cfg-if",
  "concurrent-queue",
- "hermit-abi 0.4.0",
+ "hermit-abi",
  "pin-project-lite",
  "rustix",
  "tracing",
@@ -2612,9 +2599,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.89"
+version = "1.0.92"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
+checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
 dependencies = [
  "unicode-ident",
 ]
@@ -2655,9 +2642,9 @@ dependencies = [
 
 [[package]]
 name = "quote"
-version = "1.0.37"
+version = "1.0.38"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
+checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
 dependencies = [
  "proc-macro2",
 ]
@@ -2709,9 +2696,9 @@ dependencies = [
 
 [[package]]
 name = "redox_syscall"
-version = "0.5.7"
+version = "0.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
+checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
 dependencies = [
  "bitflags 2.6.0",
 ]
@@ -2806,15 +2793,15 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
 
 [[package]]
 name = "rustix"
-version = "0.38.40"
+version = "0.38.42"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0"
+checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
 dependencies = [
  "bitflags 2.6.0",
  "errno",
  "libc",
  "linux-raw-sys",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -2877,6 +2864,7 @@ dependencies = [
  "serde_json",
  "skyward_mavlink",
  "strum",
+ "thiserror 2.0.9",
  "tokio",
  "tracing",
  "tracing-subscriber",
@@ -2884,9 +2872,9 @@ dependencies = [
 
 [[package]]
 name = "serde"
-version = "1.0.215"
+version = "1.0.217"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
+checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
 dependencies = [
  "serde_derive",
 ]
@@ -2902,20 +2890,20 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.215"
+version = "1.0.217"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
+checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.133"
+version = "1.0.135"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
+checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
 dependencies = [
  "itoa",
  "memchr",
@@ -2931,7 +2919,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -3071,7 +3059,7 @@ dependencies = [
  "log",
  "memmap2",
  "rustix",
- "thiserror",
+ "thiserror 1.0.69",
  "wayland-backend",
  "wayland-client",
  "wayland-csd-frame",
@@ -3104,9 +3092,9 @@ dependencies = [
 
 [[package]]
 name = "socket2"
-version = "0.5.7"
+version = "0.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
+checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
 dependencies = [
  "libc",
  "windows-sys 0.52.0",
@@ -3164,9 +3152,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "2.0.87"
+version = "2.0.95"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
+checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -3181,17 +3169,18 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
 ]
 
 [[package]]
 name = "tempfile"
-version = "3.14.0"
+version = "3.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
+checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704"
 dependencies = [
  "cfg-if",
  "fastrand",
+ "getrandom",
  "once_cell",
  "rustix",
  "windows-sys 0.59.0",
@@ -3221,7 +3210,16 @@ version = "1.0.69"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
 dependencies = [
- "thiserror-impl",
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc"
+dependencies = [
+ "thiserror-impl 2.0.9",
 ]
 
 [[package]]
@@ -3232,7 +3230,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -3314,9 +3323,9 @@ dependencies = [
 
 [[package]]
 name = "tracing"
-version = "0.1.40"
+version = "0.1.41"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
 dependencies = [
  "pin-project-lite",
  "tracing-attributes",
@@ -3325,20 +3334,20 @@ dependencies = [
 
 [[package]]
 name = "tracing-attributes"
-version = "0.1.27"
+version = "0.1.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
+checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
 ]
 
 [[package]]
 name = "tracing-core"
-version = "0.1.32"
+version = "0.1.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
 dependencies = [
  "once_cell",
  "valuable",
@@ -3357,9 +3366,9 @@ dependencies = [
 
 [[package]]
 name = "tracing-subscriber"
-version = "0.3.18"
+version = "0.3.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
+checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
 dependencies = [
  "matchers",
  "nu-ansi-term",
@@ -3375,9 +3384,9 @@ dependencies = [
 
 [[package]]
 name = "ttf-parser"
-version = "0.25.0"
+version = "0.25.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5902c5d130972a0000f60860bfbf46f7ca3db5391eddfedd1b8728bd9dc96c0e"
+checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
 
 [[package]]
 name = "type-map"
@@ -3407,15 +3416,15 @@ dependencies = [
 
 [[package]]
 name = "unicase"
-version = "2.8.0"
+version = "2.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df"
+checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
 
 [[package]]
 name = "unicode-ident"
-version = "1.0.13"
+version = "1.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
+checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
 
 [[package]]
 name = "unicode-segmentation"
@@ -3437,9 +3446,9 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
 
 [[package]]
 name = "url"
-version = "2.5.3"
+version = "2.5.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada"
+checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
 dependencies = [
  "form_urlencoded",
  "idna",
@@ -3488,9 +3497,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.95"
+version = "0.2.99"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e"
+checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
 dependencies = [
  "cfg-if",
  "once_cell",
@@ -3499,36 +3508,36 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.95"
+version = "0.2.99"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358"
+checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
 dependencies = [
  "bumpalo",
  "log",
- "once_cell",
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
  "wasm-bindgen-shared",
 ]
 
 [[package]]
 name = "wasm-bindgen-futures"
-version = "0.4.45"
+version = "0.4.49"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b"
+checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2"
 dependencies = [
  "cfg-if",
  "js-sys",
+ "once_cell",
  "wasm-bindgen",
  "web-sys",
 ]
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.95"
+version = "0.2.99"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56"
+checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -3536,22 +3545,22 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.95"
+version = "0.2.99"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
+checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.95"
+version = "0.2.99"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
+checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
 
 [[package]]
 name = "wayland-backend"
@@ -3664,9 +3673,9 @@ dependencies = [
 
 [[package]]
 name = "web-sys"
-version = "0.3.72"
+version = "0.3.76"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112"
+checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -3684,9 +3693,9 @@ dependencies = [
 
 [[package]]
 name = "webbrowser"
-version = "1.0.2"
+version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e5f07fb9bc8de2ddfe6b24a71a75430673fd679e568c48b52716cef1cfae923"
+checksum = "ea9fe1ebb156110ff855242c1101df158b822487e4957b0556d9ffce9db0f535"
 dependencies = [
  "block2",
  "core-foundation 0.10.0",
@@ -3744,7 +3753,7 @@ dependencies = [
  "raw-window-handle",
  "rustc-hash",
  "smallvec",
- "thiserror",
+ "thiserror 1.0.69",
  "wgpu-hal",
  "wgpu-types",
 ]
@@ -3783,7 +3792,7 @@ dependencies = [
  "renderdoc-sys",
  "rustc-hash",
  "smallvec",
- "thiserror",
+ "thiserror 1.0.69",
  "wasm-bindgen",
  "web-sys",
  "wgpu-types",
@@ -3888,7 +3897,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -3899,7 +3908,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -4128,9 +4137,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
 [[package]]
 name = "winit"
-version = "0.30.5"
+version = "0.30.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0be9e76a1f1077e04a411f0b989cbd3c93339e1771cb41e71ac4aee95bfd2c67"
+checksum = "f5d74280aabb958072864bff6cfbcf9025cf8bfacdde5e32b5e12920ef703b0f"
 dependencies = [
  "ahash",
  "android-activity",
@@ -4180,9 +4189,9 @@ dependencies = [
 
 [[package]]
 name = "winnow"
-version = "0.6.20"
+version = "0.6.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
+checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980"
 dependencies = [
  "memchr",
 ]
@@ -4268,15 +4277,15 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
 
 [[package]]
 name = "xml-rs"
-version = "0.8.23"
+version = "0.8.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af310deaae937e48a26602b730250b4949e125f468f11e6990be3e5304ddd96f"
+checksum = "ea8b391c9a790b496184c29f7f93b9ed5b16abb306c05415b68bcc16e4d06432"
 
 [[package]]
 name = "yoke"
-version = "0.7.4"
+version = "0.7.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5"
+checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
 dependencies = [
  "serde",
  "stable_deref_trait",
@@ -4286,13 +4295,13 @@ dependencies = [
 
 [[package]]
 name = "yoke-derive"
-version = "0.7.4"
+version = "0.7.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
+checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
  "synstructure",
 ]
 
@@ -4352,7 +4361,7 @@ checksum = "709ab20fc57cb22af85be7b360239563209258430bccf38d8b979c5a2ae3ecce"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
  "zbus-lockstep",
  "zbus_xml",
  "zvariant",
@@ -4367,7 +4376,7 @@ dependencies = [
  "proc-macro-crate",
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
  "zvariant_utils",
 ]
 
@@ -4413,27 +4422,27 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
 ]
 
 [[package]]
 name = "zerofrom"
-version = "0.1.4"
+version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55"
+checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e"
 dependencies = [
  "zerofrom-derive",
 ]
 
 [[package]]
 name = "zerofrom-derive"
-version = "0.1.4"
+version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
+checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
  "synstructure",
 ]
 
@@ -4456,7 +4465,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -4481,7 +4490,7 @@ dependencies = [
  "proc-macro-crate",
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
  "zvariant_utils",
 ]
 
@@ -4493,5 +4502,5 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn 2.0.95",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 975dee222aa720c1a75d662474563cb73f656c5a..9b369adc9d499ce5afb1e682140090e9da747bdd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -46,3 +46,4 @@ egui_extras = "0.29.1"
 strum = "0.26"
 anyhow = "1.0"
 ring-channel = "0.12.0"
+thiserror = "2.0.7"
diff --git a/src/error.rs b/src/error.rs
index f00a0f523aabfcafa08a8a003be6c1fb988fff7d..725a6d35593169ed03c8ee5f2a08e78b982292df 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -5,6 +5,7 @@ pub trait ErrInstrument {
     type Inner;
 
     fn log_expect(self, msg: &str) -> Self::Inner;
+    fn log_unwrap(self) -> Self::Inner;
 }
 
 impl<T, E> ErrInstrument for Result<T, E>
@@ -22,6 +23,16 @@ where
             }
         }
     }
+
+    fn log_unwrap(self) -> Self::Inner {
+        match self {
+            Ok(t) => t,
+            Err(e) => {
+                error!("Called unwrap on an Err value: {:?}", e);
+                panic!("Called unwrap on an Err value: {:?}", e);
+            }
+        }
+    }
 }
 
 impl<T> ErrInstrument for Option<T> {
@@ -36,4 +47,14 @@ impl<T> ErrInstrument for Option<T> {
             }
         }
     }
+
+    fn log_unwrap(self) -> Self::Inner {
+        match self {
+            Some(t) => t,
+            None => {
+                error!("Called unwrap on a None value");
+                panic!("Called unwrap on a None value");
+            }
+        }
+    }
 }
diff --git a/src/main.rs b/src/main.rs
index 4c268010ecbb74a1660cc1b9584ef925a700ca20..d00f72ae3aa830528f61a4544248a12afd495ea0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,4 +1,6 @@
 #![warn(clippy::expect_used)]
+#![warn(clippy::unwrap_used)]
+#![warn(clippy::panic)]
 
 mod error;
 mod mavlink;
@@ -53,7 +55,7 @@ fn main() -> Result<(), eframe::Error> {
             MSG_MANAGER
                 .set(Mutex::new(MessageBroker::new(
                     // FIXME: Choose where to put the channel size of the MessageBroker
-                    NonZeroUsize::new(50).unwrap(),
+                    NonZeroUsize::new(50).log_unwrap(),
                     ctx.egui_ctx.clone(),
                 )))
                 .log_expect("Unable to set MessageManager");
diff --git a/src/mavlink.rs b/src/mavlink.rs
index 6e0c003f4f8b22ba24392ac27216606084442085..ab38cb5b543273413110c9c704bdea49313022eb 100644
--- a/src/mavlink.rs
+++ b/src/mavlink.rs
@@ -4,11 +4,13 @@
 //! rapid switching between different mavlink versions and profiles (_dialects_).
 
 mod base;
+mod error;
 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 error::{MavlinkError, Result as MavlinkResult};
 pub use message_broker::{MessageBroker, MessageView};
 pub use reflection::ReflectionContext;
 
diff --git a/src/mavlink/base.rs b/src/mavlink/base.rs
index 8b7a4113627a4960345cddb4afa64c8468d08caf..a1aa3c53a4ef2bab982ecaaef808c7af0e66ffbe 100644
--- a/src/mavlink/base.rs
+++ b/src/mavlink/base.rs
@@ -14,6 +14,10 @@ pub use skyward_mavlink::{
     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 {
@@ -37,20 +41,23 @@ impl TimedMessage {
 pub fn extract_from_message<K, T>(
     message: &MavMessage,
     fields: impl IntoIterator<Item = K>,
-) -> Vec<T>
+) -> Result<Vec<T>>
 where
     K: AsRef<str>,
     T: serde::de::DeserializeOwned + Default,
 {
-    let value: serde_json::Value = serde_json::to_value(message).unwrap();
-    fields
+    let value: serde_json::Value =
+        serde_json::to_value(message).log_expect("MavMessage should be serializable");
+    Ok(fields
         .into_iter()
-        .map(|field| {
+        .flat_map(|field| {
             let field = field.as_ref();
-            let value = value.get(field).unwrap();
-            serde_json::from_value::<T>(value.clone()).unwrap_or_default()
+            let value = value
+                .get(field)
+                .ok_or(MavlinkError::UnknownField(field.to_string()))?;
+            serde_json::from_value::<T>(value.clone()).map_err(MavlinkError::from)
         })
-        .collect()
+        .collect())
 }
 
 /// Read a stream of bytes and return an iterator of MavLink messages
diff --git a/src/mavlink/error.rs b/src/mavlink/error.rs
new file mode 100644
index 0000000000000000000000000000000000000000..23975eb59b7384774098dacb2e31f5c5b7f1c878
--- /dev/null
+++ b/src/mavlink/error.rs
@@ -0,0 +1,11 @@
+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/message_broker.rs b/src/mavlink/message_broker.rs
index a04d075ad3f5d8420e64f0a450d997b4e285e838..56f6060ccbc6bcbddf595fb06c3e9278b5b53497 100644
--- a/src/mavlink/message_broker.rs
+++ b/src/mavlink/message_broker.rs
@@ -18,11 +18,11 @@ 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 tracing::{debug, trace};
 
 use crate::mavlink::byte_parser;
 
-use super::{Message, TimedMessage};
+use super::{MavlinkResult, Message, TimedMessage};
 
 /// Maximum size of the UDP buffer
 const UDP_BUFFER_SIZE: usize = 65527;
@@ -42,10 +42,10 @@ pub trait MessageView {
     /// Populates the view with the initial messages. This method is called when
     /// the cache is invalid and the view needs to be populated from the stored
     /// map of messages
-    fn populate_view(&mut self, msg_slice: &[TimedMessage]);
+    fn populate_view(&mut self, msg_slice: &[TimedMessage]) -> MavlinkResult<()>;
     /// Updates the view with new messages. This method is called when the cache
     /// is valid, hence the view only needs to be updated with the new messages
-    fn update_view(&mut self, msg_slice: &[TimedMessage]);
+    fn update_view(&mut self, msg_slice: &[TimedMessage]) -> MavlinkResult<()>;
 }
 
 /// Responsible for storing & dispatching the Mavlink message received.
@@ -90,13 +90,14 @@ impl MessageBroker {
 
     /// Refreshes the view given as argument. It handles automatically the cache
     /// validity based on `is_valid` method of the view.
-    pub fn refresh_view<V: MessageView>(&mut self, view: &mut V) {
+    pub fn refresh_view<V: MessageView>(&mut self, view: &mut V) -> MavlinkResult<()> {
         self.process_incoming_msgs();
         if !view.is_valid() || !self.update_queues.contains_key(view.widget_id()) {
-            self.init_view(view);
+            self.init_view(view)?;
         } else {
-            self.update_view(view);
+            self.update_view(view)?;
         }
+        Ok(())
     }
 
     /// Stop the listener task from listening to incoming messages, if it is
@@ -123,6 +124,7 @@ impl MessageBroker {
         let mut buf = Box::new([0; UDP_BUFFER_SIZE]);
         let running_flag = self.running_flag.clone();
 
+        debug!("Spawning listener task at {}", bind_address);
         let handle = tokio::spawn(async move {
             let socket = UdpSocket::bind(bind_address)
                 .await
@@ -135,6 +137,7 @@ impl MessageBroker {
                     .await
                     .context("Failed to receive message")?;
                 for (_, mav_message) in byte_parser(&buf[..len]) {
+                    debug!("Received message: {:?}", mav_message);
                     tx.send(TimedMessage::just_received(mav_message))
                         .context("Failed to send message")?;
                     ctx.request_repaint();
@@ -153,27 +156,35 @@ impl MessageBroker {
     }
 
     /// Init a view in case of cache invalidation or first time initialization.
-    fn init_view<V: MessageView>(&mut self, view: &mut V) {
+    fn init_view<V: MessageView>(&mut self, view: &mut V) -> MavlinkResult<()> {
+        trace!("initializing view: {:?}", view.widget_id());
         if let Some(messages) = self.messages.get(&view.id_of_interest()) {
-            view.populate_view(messages);
+            view.populate_view(messages)?;
         }
         self.update_queues
             .insert(*view.widget_id(), (view.id_of_interest(), VecDeque::new()));
+        Ok(())
     }
 
     /// Update a view with new messages, used when the cache is valid.
-    fn update_view<V: MessageView>(&mut self, view: &mut V) {
+    fn update_view<V: MessageView>(&mut self, view: &mut V) -> MavlinkResult<()> {
+        trace!("updating view: {:?}", view.widget_id());
         if let Some((_, queue)) = self.update_queues.get_mut(view.widget_id()) {
             while let Some(msg) = queue.pop_front() {
-                view.update_view(&[msg]);
+                view.update_view(&[msg])?;
             }
         }
+        Ok(())
     }
 
     /// Process the incoming messages from the Mavlink listener, storing them in
     /// the messages map and updating the update queues.
     fn process_incoming_msgs(&mut self) {
         while let Ok(message) = self.rx.try_recv() {
+            debug!(
+                "processing received message: {:?}",
+                message.message.message_name()
+            );
             // first update the update queues
             for (_, (id, queue)) in self.update_queues.iter_mut() {
                 if *id == message.message.message_id() {
diff --git a/src/mavlink/reflection.rs b/src/mavlink/reflection.rs
index 7fa531ee79242ff8337a80e5afb3004b2592fc38..7f225a8ffd7c10d962fbad6d94b9c4279dc54b9e 100644
--- a/src/mavlink/reflection.rs
+++ b/src/mavlink/reflection.rs
@@ -8,6 +8,8 @@ use std::collections::HashMap;
 
 use mavlink_bindgen::parser::{MavProfile, MavType};
 
+use crate::error::ErrInstrument;
+
 use super::MAVLINK_PROFILE_SERIALIZED;
 
 /// Reflection context for MAVLink messages.
@@ -22,7 +24,7 @@ impl ReflectionContext {
     /// Create a new reflection context.
     pub fn new() -> Self {
         let profile: MavProfile = serde_json::from_str(MAVLINK_PROFILE_SERIALIZED)
-            .expect("Failed to deserialize MavProfile");
+            .log_expect("Failed to deserialize MavProfile");
         let id_name_map = profile
             .messages
             .iter()
diff --git a/src/ui.rs b/src/ui.rs
index 46926d541a2c15600212afe5fcb2dcb86bda982a..f276231768c6a750ac31d90e1a79a1eb5f42b1b0 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -1,6 +1,6 @@
 mod composable_view;
-mod layout_manager;
 mod panes;
+mod persistency;
 mod shortcuts;
 mod utils;
 
diff --git a/src/ui/composable_view.rs b/src/ui/composable_view.rs
index 7b71f6b6d06d2409b82501aa2892b9b8bcca8a34..174b62de2fb010362c2b9226864649d2e7361955 100644
--- a/src/ui/composable_view.rs
+++ b/src/ui/composable_view.rs
@@ -1,8 +1,8 @@
-use crate::{mavlink, MSG_MANAGER};
+use crate::{error::ErrInstrument, mavlink, ui::panes::PaneKind, MSG_MANAGER};
 
 use super::{
-    layout_manager::LayoutManager,
-    panes::{Pane, PaneBehavior, PaneKind},
+    panes::{Pane, PaneBehavior},
+    persistency::{LayoutManager, LayoutManagerWindow},
     shortcuts,
     utils::maximized_pane_ui,
 };
@@ -14,16 +14,19 @@ use std::{
 use egui::{Key, Modifiers};
 use egui_tiles::{Behavior, Container, Linear, LinearDir, Tile, TileId, Tiles, Tree};
 use serde::{Deserialize, Serialize};
+use tracing::{debug, error, trace, warn};
 
 #[derive(Default)]
 pub struct ComposableView {
     /// Persistent state of the app
-    pub state: ComposableViewState,
-
-    pub layout_manager: LayoutManager,
+    state: ComposableViewState,
+    layout_manager: LayoutManager,
     behavior: ComposableBehavior,
     maximized_pane: Option<TileId>,
+
+    // == Windows ==
     sources_window: SourceWindow,
+    layout_manager_window: LayoutManagerWindow,
 }
 
 // An app must implement the `App` trait to define how the ui is built
@@ -38,10 +41,12 @@ impl eframe::App for ComposableView {
             .iter()
             .find(|(_, tile)| matches!(tile, Tile::Pane(pane) if pane.contains_pointer()))
             .map(|(id, _)| *id);
+        trace!("Hovered pane: {:?}", hovered_pane);
 
         // Capture any pane action generated by pane children
         let pane_action = self.behavior.action.take();
         let mut pane_action = pane_action.zip(hovered_pane);
+        trace!("Pane action: {:?}", pane_action);
 
         // Capture any pane action generated by keyboard shortcuts
         if let Some(hovered_pane) = hovered_pane {
@@ -61,7 +66,11 @@ impl eframe::App for ComposableView {
             match action {
                 PaneAction::SplitH => {
                     if self.maximized_pane.is_none() {
-                        let hovered_tile_pane = panes_tree.tiles.remove(hovered_tile).unwrap();
+                        debug!("Called SplitH on tile {:?}", hovered_tile);
+                        let hovered_tile_pane = panes_tree
+                            .tiles
+                            .remove(hovered_tile)
+                            .log_expect("Hovered tile not found");
                         let left_pane = panes_tree.tiles.insert_new(hovered_tile_pane);
                         let right_pane = panes_tree.tiles.insert_pane(Pane::default());
                         panes_tree.tiles.insert(
@@ -76,7 +85,11 @@ impl eframe::App for ComposableView {
                 }
                 PaneAction::SplitV => {
                     if self.maximized_pane.is_none() {
-                        let hovered_tile_pane = panes_tree.tiles.remove(hovered_tile).unwrap();
+                        debug!("Called SplitV on tile {:?}", hovered_tile);
+                        let hovered_tile_pane = panes_tree
+                            .tiles
+                            .remove(hovered_tile)
+                            .log_expect("Hovered tile not found");
                         let replaced = panes_tree.tiles.insert_new(hovered_tile_pane);
                         let lower_pane = panes_tree.tiles.insert_pane(Pane::default());
                         panes_tree.tiles.insert(
@@ -90,12 +103,17 @@ impl eframe::App for ComposableView {
                     }
                 }
                 PaneAction::Close => {
+                    debug!("Called Close on tile {:?}", hovered_tile);
                     // Ignore if the root pane is the only one
                     if panes_tree.tiles.len() != 1 && self.maximized_pane.is_none() {
                         panes_tree.remove_recursively(hovered_tile);
                     }
                 }
                 PaneAction::Replace(new_pane) => {
+                    debug!(
+                        "Called Replace on tile {:?} with pane {:?}",
+                        hovered_tile, new_pane
+                    );
                     panes_tree.tiles.insert(hovered_tile, Tile::Pane(*new_pane));
                 }
                 PaneAction::Maximize => {
@@ -140,7 +158,8 @@ impl eframe::App for ComposableView {
                     self.sources_window.visible = !self.sources_window.visible;
                 }
                 if ui.button("Layout Manager").clicked() {
-                    self.layout_manager.toggle_open_state();
+                    self.layout_manager_window
+                        .toggle_open_state(&self.layout_manager);
                 }
 
                 // If a pane is maximized show a visual clue
@@ -163,23 +182,31 @@ impl eframe::App for ComposableView {
             }
         });
 
-        LayoutManager::show(self, ctx);
+        self.layout_manager_window
+            .show(ctx, &mut self.layout_manager, &mut self.state);
     }
 
     fn save(&mut self, storage: &mut dyn eframe::Storage) {
-        self.layout_manager.save_displayed(storage);
+        self.layout_manager.save_current_layout(storage);
     }
 }
 
 impl ComposableView {
     pub fn new(app_name: &str, storage: &dyn eframe::Storage) -> Self {
         let layout_manager = LayoutManager::new(app_name, storage);
-        let mut composable_view = Self {
+        let mut s = Self {
             layout_manager,
             ..Self::default()
         };
-        LayoutManager::try_display_selected_layout(&mut composable_view);
-        composable_view
+        // Load the selected layout if valid and existing
+        if let Some(layout) = s.layout_manager.current_layout().cloned() {
+            s.layout_manager
+                .load_layout(layout, &mut s.state)
+                .unwrap_or_else(|e| {
+                    error!("Error loading layout: {}", e);
+                });
+        }
+        s
     }
 }
 
@@ -199,47 +226,29 @@ impl Default for ComposableViewState {
 }
 
 impl ComposableViewState {
-    pub fn from_file(path: &PathBuf) -> Option<Self> {
-        match fs::read_to_string(path) {
-            Ok(json) => match serde_json::from_str::<ComposableViewState>(&json) {
-                Ok(layout) => Some(layout),
-                Err(e) => {
-                    eprintln!("Error deserializing layout: {}", e);
-                    None
-                }
-            },
-            Err(e) => {
-                eprintln!("Error reading file: {}", e);
-                None
-            }
-        }
+    pub fn from_file(path: &PathBuf) -> anyhow::Result<Self> {
+        fs::read_to_string(path)
+            .and_then(|json| serde_json::from_str::<ComposableViewState>(&json).map_err(Into::into))
+            .map_err(|e| anyhow::anyhow!("Error deserializing layout: {}", e))
     }
 
-    pub fn to_file(&self, path: &Path) {
+    pub fn to_file(&self, path: &Path) -> anyhow::Result<()> {
         // Check if the parent path exists, if not create it
         if let Some(parent) = path.parent() {
             if !parent.exists() {
-                match fs::create_dir_all(parent) {
-                    Ok(_) => {
-                        println!("Created directory {:?}", parent);
-                    }
-                    Err(e) => {
-                        eprintln!("Error creating directory: {}", e);
-                        return;
-                    }
-                }
+                fs::create_dir_all(parent)
+                    .map_err(|e| anyhow::anyhow!("Error creating directory: {}", e))?;
+                debug!("Created directory {:?}", parent);
             }
         }
 
-        match serde_json::to_string_pretty(self) {
-            Ok(serialized_layout) => {
-                println!("Saving layout into {:?}", path);
-                fs::write(path, serialized_layout).unwrap();
-            }
-            Err(e) => {
-                eprintln!("Error serializing layout: {}", e);
-            }
-        }
+        let serialized_layout = serde_json::to_string_pretty(self)
+            .map_err(|e| anyhow::anyhow!("Error serializing layout: {}", e))?;
+        debug!("Serialized layout: {}", serialized_layout);
+        fs::write(path, serialized_layout)
+            .map_err(|e| anyhow::anyhow!("Error writing layout: {}", e))?;
+
+        Ok(())
     }
 }
 
diff --git a/src/ui/layout_manager.rs b/src/ui/layout_manager.rs
deleted file mode 100644
index 526505f8b600e7607b3b20d4627365746d4727b0..0000000000000000000000000000000000000000
--- a/src/ui/layout_manager.rs
+++ /dev/null
@@ -1,307 +0,0 @@
-use std::{collections::BTreeMap, fs, path::PathBuf, str::FromStr};
-
-use egui::{
-    Button, Color32, Context, InnerResponse, RichText, Separator, Stroke, TextEdit, Ui, Vec2,
-    Widget,
-};
-use egui_extras::{Column, Size, StripBuilder, TableBuilder};
-use egui_file::FileDialog;
-
-use super::{composable_view::ComposableViewState, ComposableView};
-
-static LAYOUTS_DIR: &str = "layouts";
-static SELECTED_LAYOUT_KEY: &str = "selected_layout";
-
-#[derive(Default)]
-pub struct LayoutManager {
-    open: bool,
-
-    /// Currently dislayed layout in the ui
-    displayed: Option<PathBuf>,
-
-    /// Currently selected layout in the list, gets reset to the displayed layout when the dialog is opened
-    selection: Option<PathBuf>,
-
-    text_input: String,
-    file_dialog: Option<FileDialog>,
-    layouts: BTreeMap<PathBuf, ComposableViewState>,
-    layouts_path: PathBuf,
-}
-
-impl LayoutManager {
-    /// Chooses the layouts path and gets the previously selected layout from storage
-    pub fn new(app_name: &str, storage: &dyn eframe::Storage) -> Self {
-        let mut layout_manager = Self {
-            layouts_path: eframe::storage_dir(app_name).unwrap().join(LAYOUTS_DIR),
-            selection: storage
-                .get_string(SELECTED_LAYOUT_KEY)
-                .map(|path| PathBuf::from_str(path.as_str()).unwrap()),
-            ..Self::default()
-        };
-        layout_manager.reload_layouts();
-        layout_manager
-    }
-
-    /// Saves in permanent storage the file name of the currently displayed layout
-    pub fn save_displayed(&self, storage: &mut dyn eframe::Storage) {
-        if let Some(displayed) = self.displayed.as_ref().map(|s| s.to_str()).flatten() {
-            storage.set_string(SELECTED_LAYOUT_KEY, displayed.to_string());
-            println!("Layout \"{}\" will be displayed next time", displayed)
-        }
-    }
-
-    /// Scans the layout directory and reloads the layouts
-    pub fn reload_layouts(&mut self) {
-        if let Ok(files) = self.layouts_path.read_dir() {
-            self.layouts = files
-                .flat_map(|x| x)
-                .map(|path| path.path())
-                .map(|path| {
-                    if let Some(layout) = ComposableViewState::from_file(&path) {
-                        let path: PathBuf = path.file_stem().unwrap().into();
-                        Some((path, layout))
-                    } else {
-                        None
-                    }
-                })
-                .flatten()
-                .collect();
-        }
-    }
-
-    pub fn get_selected(&self) -> Option<&ComposableViewState> {
-        self.selection
-            .as_ref()
-            .map(|selection| self.layouts.get(selection))
-            .flatten()
-    }
-
-    pub fn delete(&mut self, key: &PathBuf) {
-        if self.layouts.contains_key(key) {
-            let _ = fs::remove_file(self.layouts_path.join(key).with_extension("json"));
-        }
-    }
-
-    pub fn try_display_selected_layout(cv: &mut ComposableView) {
-        if let Some(selection) = cv.layout_manager.selection.as_ref() {
-            if let Some(selected_layout) = cv.layout_manager.layouts.get(selection) {
-                cv.state = selected_layout.clone();
-                cv.layout_manager.displayed = Some(selection.clone());
-            }
-        }
-    }
-
-    pub fn save_current_layout(cv: &mut ComposableView, name: &String) {
-        let layouts_path = &cv.layout_manager.layouts_path;
-        let path = layouts_path.join(name).with_extension("json");
-        cv.state.to_file(&path);
-        cv.layout_manager.selection.replace(name.into());
-        cv.layout_manager.displayed.replace(name.into());
-    }
-
-    pub fn toggle_open_state(&mut self) {
-        self.open = !self.open;
-
-        if self.open {
-            // When opening, we set the selection to the current layout
-            self.selection = self.displayed.clone();
-        } else {
-            // When closing, we delete also the file dialog
-            self.file_dialog.take();
-        }
-    }
-
-    pub fn show(cv: &mut ComposableView, ctx: &Context) {
-        let mut window_visible = cv.layout_manager.open;
-        egui::Window::new("Layouts Manager")
-            .collapsible(false)
-            .open(&mut window_visible)
-            .show(ctx, |ui| {
-                // Make sure to reload the layots, this ways the user sees always
-                // the current content of the layouts folder
-                cv.layout_manager.reload_layouts();
-
-                let changed = match cv.layout_manager.get_selected() {
-                    Some(selected_layout) => *selected_layout != cv.state,
-                    None => true,
-                };
-
-                // Layouts table
-                StripBuilder::new(ui)
-                    .size(Size::remainder().at_least(100.0))
-                    .size(Size::exact(7.0))
-                    .size(Size::exact(40.0))
-                    .vertical(|mut strip| {
-                        strip.cell(|ui| LayoutManager::show_layouts_table(ui, cv, changed));
-                        strip.cell(|ui| {
-                            ui.add(Separator::default().spacing(7.0));
-                        });
-                        strip.strip(|builder| {
-                            LayoutManager::show_action_buttons(builder, cv, changed)
-                        });
-                    });
-            });
-        cv.layout_manager.open = window_visible;
-    }
-
-    fn show_layouts_table(ui: &mut Ui, cv: &mut ComposableView, changed: bool) {
-        let available_height = ui.available_height();
-        TableBuilder::new(ui)
-            .column(Column::remainder())
-            .column(Column::auto())
-            .column(Column::auto())
-            .min_scrolled_height(0.0)
-            .max_scroll_height(available_height)
-            .body(|mut body| {
-                let mut to_select: Option<PathBuf> = None;
-                let mut to_open: Option<PathBuf> = None;
-                let mut to_delete: Option<PathBuf> = None;
-
-                for key in cv.layout_manager.layouts.keys() {
-                    let name = key.to_str().unwrap();
-                    let is_selected = cv
-                        .layout_manager
-                        .selection
-                        .as_ref()
-                        .map_or_else(|| false, |selected_key| selected_key == key);
-
-                    let name_button = if is_selected && changed {
-                        Button::new(RichText::new(name).color(Color32::BLACK))
-                            .stroke(Stroke::new(1.0, Color32::BROWN))
-                            .fill(Color32::YELLOW)
-                    } else if is_selected && !changed {
-                        Button::new(RichText::new(name).color(Color32::BLACK))
-                            .stroke(Stroke::new(1.0, Color32::GREEN))
-                            .fill(Color32::LIGHT_GREEN)
-                    } else {
-                        Button::new(name).fill(Color32::TRANSPARENT)
-                    };
-                    let open_button = Button::new("↗");
-                    let delete_button = Button::new("🗑");
-
-                    body.row(20.0, |mut row| {
-                        row.col(|ui| {
-                            let name_button_resp = name_button.ui(ui);
-                            if name_button_resp.clicked() {
-                                to_select = Some(key.clone());
-                            }
-                            if name_button_resp.double_clicked() {
-                                to_open = Some(key.clone());
-                            }
-                        });
-                        row.col(|ui| {
-                            if open_button.ui(ui).clicked() {
-                                to_open = Some(key.clone());
-                            }
-                        });
-                        row.col(|ui| {
-                            if delete_button.ui(ui).clicked() {
-                                to_delete = Some(key.clone());
-                            }
-                        });
-                    });
-                }
-
-                if let Some(to_select) = to_select {
-                    cv.layout_manager.selection.replace(to_select);
-                }
-                if let Some(to_open) = to_open {
-                    cv.layout_manager.selection.replace(to_open);
-                    LayoutManager::try_display_selected_layout(cv);
-                }
-                if let Some(to_delete) = to_delete {
-                    cv.layout_manager.delete(&to_delete);
-                }
-            });
-    }
-
-    fn show_action_buttons(builder: StripBuilder, cv: &mut ComposableView, changed: bool) {
-        builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
-            // Load empty and import buttons
-            strip.cell(|ui| {
-                // Load empty button
-                let open_empty_resp = ui.add_sized(
-                    Vec2::new(ui.available_width(), 0.0),
-                    Button::new("Load empty"),
-                );
-                if open_empty_resp.clicked() {
-                    cv.state = ComposableViewState::default();
-                    cv.layout_manager.selection.take();
-                }
-
-                // Import button
-                let import_layout_resp =
-                    ui.add_sized(Vec2::new(ui.available_width(), 0.0), Button::new("Import"));
-                if import_layout_resp.clicked() {
-                    let mut file_dialog = FileDialog::open_file(None);
-                    file_dialog.open();
-                    cv.layout_manager.file_dialog = Some(file_dialog);
-                }
-                if let Some(file_dialog) = &mut cv.layout_manager.file_dialog {
-                    if file_dialog.show(ui.ctx()).selected() {
-                        if let Some(file) = file_dialog.path() {
-                            println!("Selected layout to import: {:?}", file);
-
-                            let file_name = file.file_name().unwrap();
-                            let destination = cv.layout_manager.layouts_path.join(file_name);
-
-                            // First check if the layouts folder exists
-                            if !cv.layout_manager.layouts_path.exists() {
-                                match fs::create_dir_all(&cv.layout_manager.layouts_path) {
-                                    Ok(_) => println!("Created layouts folder"),
-                                    Err(e) => {
-                                        println!("Error creating layouts folder: {:?}", e)
-                                    }
-                                }
-                            }
-
-                            match fs::copy(file, destination.clone()) {
-                                Ok(_) => {
-                                    println!(
-                                        "Layout imported in {}",
-                                        destination.to_str().unwrap()
-                                    );
-                                    cv.layout_manager.selection.replace(file_name.into());
-                                    cv.layout_manager.reload_layouts();
-                                    LayoutManager::try_display_selected_layout(cv);
-                                }
-                                Err(e) => println!("Error importing layout: {:?}", e),
-                            }
-                        }
-                    }
-                }
-            });
-            // Layout save ui
-            strip.cell(|ui| {
-                let InnerResponse { inner: to_save, .. } = ui.add_enabled_ui(changed, |ui| {
-                    // Text edit
-                    let text_edit_resp = ui.add_sized(
-                        Vec2::new(ui.available_width(), 0.0),
-                        TextEdit::singleline(&mut cv.layout_manager.text_input),
-                    );
-
-                    // Save button
-                    let InnerResponse {
-                        inner: save_button_resp,
-                        ..
-                    } = ui.add_enabled_ui(!cv.layout_manager.text_input.is_empty(), |ui| {
-                        ui.add_sized(
-                            Vec2::new(ui.available_width(), 0.0),
-                            Button::new("Save layout"),
-                        )
-                    });
-
-                    let to_save = text_edit_resp.lost_focus()
-                        && ui.input(|i| i.key_pressed(egui::Key::Enter));
-                    let to_save = to_save || save_button_resp.clicked();
-                    to_save
-                });
-
-                if to_save {
-                    let name = cv.layout_manager.text_input.clone();
-                    LayoutManager::save_current_layout(cv, &name);
-                }
-            });
-        });
-    }
-}
diff --git a/src/ui/panes/plot.rs b/src/ui/panes/plot.rs
index 16b90332e33fae3b1ca51f70c133d680bd660f7e..d17d257455a52e70db4765e013126f500f8cba10 100644
--- a/src/ui/panes/plot.rs
+++ b/src/ui/panes/plot.rs
@@ -6,8 +6,10 @@ use serde::{Deserialize, Serialize};
 use source_window::{sources_window, SourceSettings};
 
 use crate::{
+    error::ErrInstrument,
     mavlink::{
-        extract_from_message, MessageData, MessageView, TimedMessage, ROCKET_FLIGHT_TM_DATA,
+        extract_from_message, MavlinkResult, MessageData, MessageView, TimedMessage,
+        ROCKET_FLIGHT_TM_DATA,
     },
     ui::composable_view::PaneResponse,
     MSG_MANAGER,
@@ -76,7 +78,12 @@ impl PaneBehavior for Plot2DPane {
 
         let mut plot_lines = Vec::new();
         if self.plot_active {
-            MSG_MANAGER.get().unwrap().lock().refresh_view(view);
+            MSG_MANAGER
+                .get()
+                .unwrap()
+                .lock()
+                .refresh_view(view)
+                .log_expect("MessageView may be invalid");
             let acc_points = &view.points;
 
             let field_x = &view.settings.x_field;
@@ -163,27 +170,29 @@ impl MessageView for PlotMessageView {
         self.cache_valid
     }
 
-    fn populate_view(&mut self, msg_slice: &[TimedMessage]) {
+    fn populate_view(&mut self, msg_slice: &[TimedMessage]) -> MavlinkResult<()> {
         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);
+            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));
         }
+        Ok(())
     }
 
-    fn update_view(&mut self, msg_slice: &[TimedMessage]) {
+    fn update_view(&mut self, msg_slice: &[TimedMessage]) -> MavlinkResult<()> {
         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);
+            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));
         }
+        Ok(())
     }
 }
 
diff --git a/src/ui/persistency.rs b/src/ui/persistency.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7998384cd60b8800f37bfd19f3d862e9257f16e4
--- /dev/null
+++ b/src/ui/persistency.rs
@@ -0,0 +1,5 @@
+mod layout_manager;
+mod layout_manager_window;
+
+pub use layout_manager::LayoutManager;
+pub use layout_manager_window::LayoutManagerWindow;
diff --git a/src/ui/persistency/layout_manager.rs b/src/ui/persistency/layout_manager.rs
new file mode 100644
index 0000000000000000000000000000000000000000..173fea11360bf4f0049957d210992ec38417c5b2
--- /dev/null
+++ b/src/ui/persistency/layout_manager.rs
@@ -0,0 +1,133 @@
+use std::{
+    collections::BTreeMap,
+    fs,
+    path::{Path, PathBuf},
+    str::FromStr,
+};
+
+use tracing::{info, trace, warn};
+
+use crate::error::ErrInstrument;
+
+use super::super::composable_view::ComposableViewState;
+
+static LAYOUTS_DIR: &str = "layouts";
+static SELECTED_LAYOUT_KEY: &str = "selected_layout";
+
+#[derive(Default)]
+pub struct LayoutManager {
+    layouts: BTreeMap<PathBuf, ComposableViewState>,
+    layouts_path: PathBuf,
+    current_layout: Option<PathBuf>,
+}
+
+impl LayoutManager {
+    /// Chooses the layouts path and gets the previously selected layout from storage
+    pub fn new(app_name: &str, storage: &dyn eframe::Storage) -> Self {
+        let mut layout_manager = Self {
+            layouts_path: eframe::storage_dir(app_name)
+                .log_expect("Unable to get storage dir")
+                .join(LAYOUTS_DIR),
+            current_layout: storage
+                .get_string(SELECTED_LAYOUT_KEY)
+                .map(|path| PathBuf::from_str(&path).log_expect("Path is not valid")),
+            ..Self::default()
+        };
+        layout_manager.reload_layouts();
+        layout_manager
+    }
+
+    pub fn current_layout(&self) -> Option<&PathBuf> {
+        self.current_layout.as_ref()
+    }
+
+    pub fn layouts_path(&self) -> &PathBuf {
+        &self.layouts_path
+    }
+
+    pub fn layouts(&self) -> &BTreeMap<PathBuf, ComposableViewState> {
+        &self.layouts
+    }
+
+    /// Saves in permanent storage the file name of the currently displayed layout
+    pub fn save_current_layout(&self, storage: &mut dyn eframe::Storage) {
+        if let Some(current_layout) = self.current_layout.as_ref().and_then(|s| s.to_str()) {
+            storage.set_string(SELECTED_LAYOUT_KEY, current_layout.to_string());
+            trace!("Current layout {:?} saved in storage", current_layout);
+        }
+    }
+
+    /// Scans the layout directory and reloads the layouts
+    pub fn reload_layouts(&mut self) {
+        if let Ok(files) = self.layouts_path.read_dir() {
+            trace!("Reloading layouts from {:?}", self.layouts_path);
+            self.layouts = files
+                .flatten()
+                .map(|path| path.path())
+                .flat_map(|path| match ComposableViewState::from_file(&path) {
+                    Ok(layout) => {
+                        let path: PathBuf = path
+                            .file_stem()
+                            .log_expect("Unable to get file stem")
+                            .into();
+                        Some((path, layout))
+                    }
+                    Err(e) => {
+                        warn!("Error loading layout at {:?}: {:?}", path, e);
+                        None
+                    }
+                })
+                .collect();
+        }
+    }
+
+    pub fn get_layout(&self, name: impl Into<PathBuf>) -> Option<&ComposableViewState> {
+        self.layouts.get(&name.into())
+    }
+
+    pub fn save_layout(&mut self, name: &str, state: &ComposableViewState) -> anyhow::Result<()> {
+        let path = self.layouts_path.join(name).with_extension("json");
+        state.to_file(&path)?;
+        self.reload_layouts();
+        Ok(())
+    }
+
+    pub fn load_layout(
+        &mut self,
+        path: impl AsRef<Path>,
+        state: &mut ComposableViewState,
+    ) -> anyhow::Result<()> {
+        let layout = self
+            .layouts
+            .get(path.as_ref())
+            .ok_or(anyhow::anyhow!("Layout not found"))?;
+        *state = layout.clone();
+        self.current_layout = Some(path.as_ref().into());
+        Ok(())
+    }
+
+    pub fn delete(&mut self, key: &PathBuf) -> anyhow::Result<()> {
+        if self.layouts.contains_key(key) {
+            info!("Deleting layout {:?}", key);
+            fs::remove_file(self.layouts_path.join(key).with_extension("json"))?;
+        }
+        Ok(())
+    }
+
+    // pub fn display_selected_layout(&mut self, state: &mut ComposableViewState) {
+    //     if let Some(selection) = self.selection.as_ref() {
+    //         if let Some(selected_layout) = self.layouts.get(selection) {
+    //             *state = selected_layout.clone();
+    //             self.displayed = Some(selection.clone());
+    //         }
+    //     }
+    // }
+
+    // pub fn save_current_layout(&mut self, state: &ComposableViewState, name: &String) {
+    //     let layouts_path = &self.layouts_path;
+    //     let path = layouts_path.join(name).with_extension("json");
+    //     state.to_file(&path);
+    //     self.selection.replace(name.into());
+    //     self.displayed.replace(name.into());
+    // }
+}
diff --git a/src/ui/persistency/layout_manager_window.rs b/src/ui/persistency/layout_manager_window.rs
new file mode 100644
index 0000000000000000000000000000000000000000..6ecd567c8a34c89d561b7638eaaaddb70b1546ac
--- /dev/null
+++ b/src/ui/persistency/layout_manager_window.rs
@@ -0,0 +1,260 @@
+use std::{fs, path::PathBuf};
+
+use egui::{
+    Button, Color32, Context, InnerResponse, RichText, Separator, Stroke, TextEdit, Ui, Vec2,
+    Widget,
+};
+use egui_extras::{Column, Size, StripBuilder, TableBuilder};
+use egui_file::FileDialog;
+
+use crate::ui::composable_view::ComposableViewState;
+
+use super::LayoutManager;
+
+#[derive(Default)]
+pub struct LayoutManagerWindow {
+    visible: bool,
+    file_dialog: Option<FileDialog>,
+    text_input: String,
+    /// Currently selected layout in the list, gets reset to the displayed layout when the dialog is opened
+    pub selection: Option<PathBuf>,
+}
+
+impl LayoutManagerWindow {
+    pub fn toggle_open_state(&mut self, layout_manager: &LayoutManager) {
+        self.visible = !self.visible;
+
+        if self.visible {
+            // When opening, we set the selection to the current layout
+            self.selection = layout_manager.current_layout().cloned();
+        } else {
+            // When closing, we delete also the file dialog
+            self.file_dialog.take();
+        }
+    }
+
+    pub fn show(
+        &mut self,
+        ctx: &Context,
+        layout_manager: &mut LayoutManager,
+        state: &mut ComposableViewState,
+    ) {
+        let LayoutManagerWindow {
+            visible: window_visible,
+            file_dialog,
+            text_input,
+            selection,
+        } = self;
+        egui::Window::new("Layouts Manager")
+            .collapsible(false)
+            .open(window_visible)
+            .show(ctx, |ui| {
+                // Make sure to reload the layots, this ways the user sees always
+                // the current content of the layouts folder
+                layout_manager.reload_layouts();
+
+                let changed = selection
+                    .as_ref()
+                    .and_then(|path| layout_manager.get_layout(path))
+                    .map(|layout| layout != state)
+                    .unwrap_or(true);
+
+                // Layouts table
+                StripBuilder::new(ui)
+                    .size(Size::remainder().at_least(100.0))
+                    .size(Size::exact(7.0))
+                    .size(Size::exact(40.0))
+                    .vertical(|mut strip| {
+                        strip.cell(|ui| {
+                            show_layouts_table(ui, layout_manager, state, selection, changed)
+                        });
+                        strip.cell(|ui| {
+                            ui.add(Separator::default().spacing(7.0));
+                        });
+                        strip.strip(|builder| {
+                            show_action_buttons(
+                                builder,
+                                layout_manager,
+                                state,
+                                file_dialog,
+                                text_input,
+                                selection,
+                                changed,
+                            )
+                        });
+                    });
+            });
+    }
+}
+
+fn show_layouts_table(
+    ui: &mut Ui,
+    layout_manager: &mut LayoutManager,
+    state: &mut ComposableViewState,
+    selection: &mut Option<PathBuf>,
+    changed: bool,
+) {
+    let available_height = ui.available_height();
+    TableBuilder::new(ui)
+        .column(Column::remainder())
+        .column(Column::auto())
+        .column(Column::auto())
+        .min_scrolled_height(0.0)
+        .max_scroll_height(available_height)
+        .body(|mut body| {
+            let mut to_select: Option<PathBuf> = None;
+            let mut to_open: Option<PathBuf> = None;
+            let mut to_delete: Option<PathBuf> = None;
+
+            for key in layout_manager.layouts().keys() {
+                let name = key.to_str().unwrap();
+                let is_selected = selection
+                    .as_ref()
+                    .map_or_else(|| false, |selected_key| selected_key == key);
+
+                let name_button = if is_selected && changed {
+                    Button::new(RichText::new(name).color(Color32::BLACK))
+                        .stroke(Stroke::new(1.0, Color32::BROWN))
+                        .fill(Color32::YELLOW)
+                } else if is_selected && !changed {
+                    Button::new(RichText::new(name).color(Color32::BLACK))
+                        .stroke(Stroke::new(1.0, Color32::GREEN))
+                        .fill(Color32::LIGHT_GREEN)
+                } else {
+                    Button::new(name).fill(Color32::TRANSPARENT)
+                };
+                let open_button = Button::new("↗");
+                let delete_button = Button::new("🗑");
+
+                body.row(20.0, |mut row| {
+                    row.col(|ui| {
+                        let name_button_resp = name_button.ui(ui);
+                        if name_button_resp.clicked() {
+                            to_select = Some(key.clone());
+                        }
+                        if name_button_resp.double_clicked() {
+                            to_open = Some(key.clone());
+                        }
+                    });
+                    row.col(|ui| {
+                        if open_button.ui(ui).clicked() {
+                            to_open = Some(key.clone());
+                        }
+                    });
+                    row.col(|ui| {
+                        if delete_button.ui(ui).clicked() {
+                            to_delete = Some(key.clone());
+                        }
+                    });
+                });
+            }
+
+            if let Some(to_select) = to_select {
+                selection.replace(to_select);
+            }
+            if let Some(to_open) = to_open {
+                layout_manager.load_layout(&to_open, state);
+                selection.replace(to_open.clone());
+            }
+            if let Some(to_delete) = to_delete {
+                layout_manager.delete(&to_delete);
+            }
+        });
+}
+
+fn show_action_buttons(
+    builder: StripBuilder,
+    layout_manager: &mut LayoutManager,
+    state: &mut ComposableViewState,
+    file_dialog: &mut Option<FileDialog>,
+    text_input: &mut String,
+    selection: &mut Option<PathBuf>,
+    changed: bool,
+) {
+    builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
+        // Load empty and import buttons
+        strip.cell(|ui| {
+            // Load empty button
+            let open_empty_resp = ui.add_sized(
+                Vec2::new(ui.available_width(), 0.0),
+                Button::new("Load empty"),
+            );
+            if open_empty_resp.clicked() {
+                *state = ComposableViewState::default();
+                selection.take();
+            }
+
+            // Import button
+            let import_layout_resp =
+                ui.add_sized(Vec2::new(ui.available_width(), 0.0), Button::new("Import"));
+            if import_layout_resp.clicked() {
+                let mut file_dialog_inner = FileDialog::open_file(None);
+                file_dialog_inner.open();
+                *file_dialog = Some(file_dialog_inner);
+            }
+            if let Some(file_dialog) = file_dialog {
+                if file_dialog.show(ui.ctx()).selected() {
+                    if let Some(file) = file_dialog.path() {
+                        println!("Selected layout to import: {:?}", file);
+
+                        let file_name: &std::ffi::OsStr = file.file_name().unwrap();
+                        let layout_path = layout_manager.layouts_path();
+                        let destination = layout_path.join(file_name);
+
+                        // First check if the layouts folder exists
+                        if !layout_path.exists() {
+                            match fs::create_dir_all(&layout_manager.layouts_path()) {
+                                Ok(_) => println!("Created layouts folder"),
+                                Err(e) => {
+                                    println!("Error creating layouts folder: {:?}", e)
+                                }
+                            }
+                        }
+
+                        match fs::copy(file, destination.clone()) {
+                            Ok(_) => {
+                                println!("Layout imported in {}", destination.to_str().unwrap());
+                                selection.replace(file_name.into());
+                                layout_manager.reload_layouts();
+                                layout_manager.load_layout(&file_name, state);
+                            }
+                            Err(e) => println!("Error importing layout: {:?}", e),
+                        }
+                    }
+                }
+            }
+        });
+        // Layout save ui
+        strip.cell(|ui| {
+            let InnerResponse { inner: to_save, .. } = ui.add_enabled_ui(changed, |ui| {
+                // Text edit
+                let text_edit_resp = ui.add_sized(
+                    Vec2::new(ui.available_width(), 0.0),
+                    TextEdit::singleline(text_input),
+                );
+
+                // Save button
+                let InnerResponse {
+                    inner: save_button_resp,
+                    ..
+                } = ui.add_enabled_ui(!text_input.is_empty(), |ui| {
+                    ui.add_sized(
+                        Vec2::new(ui.available_width(), 0.0),
+                        Button::new("Save layout"),
+                    )
+                });
+
+                let to_save =
+                    text_edit_resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
+                let to_save = to_save || save_button_resp.clicked();
+                to_save
+            });
+
+            if to_save {
+                let name = text_input.clone();
+                layout_manager.save_layout(&name, &state);
+                *selection = Some(name.clone().into());
+            }
+        });
+    });
+}