diff --git a/Cargo.lock b/Cargo.lock index a6a4c77effac769fbbc29724e165f0eca851eeb6..0ea4525d7a3f394555fca07fecf5dfef5146d156 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ab_glyph" @@ -112,6 +112,15 @@ dependencies = [ "winit", ] +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.0" @@ -143,9 +152,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" [[package]] name = "android-activity" @@ -184,53 +193,10 @@ dependencies = [ ] [[package]] -name = "anstream" -version = "0.6.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" - -[[package]] -name = "anstyle-parse" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.6" +name = "anyhow" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" -dependencies = [ - "anstyle", - "windows-sys 0.59.0", -] +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" [[package]] name = "arboard" @@ -324,9 +290,9 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.4" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" dependencies = [ "async-lock", "cfg-if", @@ -480,6 +446,21 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + [[package]] name = "base64" version = "0.21.7" @@ -625,9 +606,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.36" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baee610e9452a8f6f0a1b6194ec09ff9e2d85dea54432acdae41aa0761c95d70" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" dependencies = [ "jobserver", "libc", @@ -686,12 +667,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "colorchoice" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" - [[package]] name = "com" version = "0.6.0" @@ -794,13 +769,19 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6" dependencies = [ "libc", ] +[[package]] +name = "crc-any" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62ec9ff5f7965e4d7280bd5482acd20aadb50d632cf6c1d74493856b011fa73" + [[package]] name = "crc32fast" version = "1.4.2" @@ -810,6 +791,24 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.20" @@ -832,6 +831,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "digest" version = "0.10.7" @@ -1142,29 +1152,6 @@ dependencies = [ "syn 2.0.87", ] -[[package]] -name = "env_filter" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "humantime", - "log", -] - [[package]] name = "epaint" version = "0.29.1" @@ -1234,9 +1221,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" [[package]] name = "fdeflate" @@ -1249,9 +1236,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.34" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "miniz_oxide", @@ -1293,12 +1280,48 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.31" @@ -1307,9 +1330,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1fa2f9765705486b33fd2acf1577f8ec449c2ba1f318ae5447697b7c08d210" +checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" dependencies = [ "fastrand", "futures-core", @@ -1347,6 +1370,7 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1389,6 +1413,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "gl_generator" version = "0.14.0" @@ -1573,6 +1603,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "hermit-abi" version = "0.4.0" @@ -1600,12 +1636,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "icu_collections" version = "1.5.0" @@ -1777,10 +1807,13 @@ dependencies = [ ] [[package]] -name = "is_terminal_polyfill" -version = "1.70.1" +name = "ioctl-rs" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "f7970510895cee30b3e9128319f2cefd4bde883a39f38baa279567ba3a7eb97d" +dependencies = [ + "libc", +] [[package]] name = "itertools" @@ -1854,11 +1887,17 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" -version = "0.2.161" +version = "0.2.162" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" [[package]] name = "libloading" @@ -1924,6 +1963,43 @@ dependencies = [ "libc", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "mavlink-bindgen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83b15a4ad504e29cabfb03fdc97250a22d2354a5404d80fc48dbf02e06acf5f" +dependencies = [ + "crc-any", + "lazy_static", + "proc-macro2", + "quick-xml 0.26.0", + "quote", + "serde", + "thiserror", +] + +[[package]] +name = "mavlink-core" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e64d975ca3cf0ad8a7c278553f91d77de15fcde9b79bf6bc542e209dd0c7dee" +dependencies = [ + "byteorder", + "crc-any", + "serde", + "serde_arrays", + "serial", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1989,6 +2065,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "wasi", + "windows-sys 0.52.0", +] + [[package]] name = "naga" version = "22.1.0" @@ -2068,6 +2156,27 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2310,6 +2419,15 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.20.2" @@ -2335,6 +2453,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "owned_ttf_parser" version = "0.25.0" @@ -2449,13 +2573,13 @@ dependencies = [ [[package]] name = "polling" -version = "3.7.3" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi", + "hermit-abi 0.4.0", "pin-project-lite", "rustix", "tracing", @@ -2501,6 +2625,15 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +[[package]] +name = "quick-xml" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.30.0" @@ -2591,21 +2724,36 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.5", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.5" @@ -2618,6 +2766,20 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "ring-channel" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10c5f5a2d656b9018cc447cae1e23c028bff5faae491fe61fd0777c57cfe0706" +dependencies = [ + "crossbeam-queue", + "crossbeam-utils", + "derivative", + "futures", + "slotmap", + "spin", +] + [[package]] name = "ron" version = "0.8.1" @@ -2630,6 +2792,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -2638,9 +2806,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.39" +version = "0.38.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" +checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" dependencies = [ "bitflags 2.6.0", "errno", @@ -2693,6 +2861,8 @@ dependencies = [ name = "segs" version = "0.1.0" dependencies = [ + "anyhow", + "crossbeam-channel", "eframe", "egui", "egui_extras", @@ -2700,26 +2870,41 @@ dependencies = [ "egui_plot", "egui_tiles", "enum_dispatch", - "env_logger", - "log", + "mavlink-bindgen", + "parking_lot", + "ring-channel", "serde", "serde_json", + "skyward_mavlink", + "strum", + "tokio", + "tracing", + "tracing-subscriber", ] [[package]] name = "serde" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] +[[package]] +name = "serde_arrays" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38636132857f68ec3d5f3eb121166d2af33cb55174c4d5ff645db6165cbef0fd" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", @@ -2728,9 +2913,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", @@ -2749,6 +2934,48 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "serial" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1237a96570fc377c13baa1b88c7589ab66edced652e43ffb17088f003db3e86" +dependencies = [ + "serial-core", + "serial-unix", + "serial-windows", +] + +[[package]] +name = "serial-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f46209b345401737ae2125fe5b19a77acce90cd53e1658cda928e4fe9a64581" +dependencies = [ + "libc", +] + +[[package]] +name = "serial-unix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f03fbca4c9d866e24a459cbca71283f545a37f8e3e002ad8c70593871453cab7" +dependencies = [ + "ioctl-rs", + "libc", + "serial-core", + "termios", +] + +[[package]] +name = "serial-windows" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c6d3b776267a75d31bbdfd5d36c0ca051251caafc285827052bc53bcdc8162" +dependencies = [ + "libc", + "serial-core", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2760,6 +2987,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -2781,6 +3017,22 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "skyward_mavlink" +version = "0.1.0" +source = "git+https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git?branch=rust-strum#0a67d0afc508c38faecc611a58819479686bee27" +dependencies = [ + "bitflags 2.6.0", + "mavlink-bindgen", + "mavlink-core", + "num-derive", + "num-traits", + "paste", + "serde", + "serde_arrays", + "serde_json", +] + [[package]] name = "slab" version = "0.4.9" @@ -2850,6 +3102,22 @@ dependencies = [ "serde", ] +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" @@ -2877,6 +3145,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + [[package]] name = "syn" version = "1.0.109" @@ -2912,9 +3186,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", "fastrand", @@ -2932,26 +3206,45 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termios" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a" +dependencies = [ + "libc", +] + [[package]] name = "thiserror" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", "syn 2.0.87", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "tiny-skia" version = "0.11.4" @@ -2987,6 +3280,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tokio" +version = "1.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +dependencies = [ + "backtrace", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "socket2", + "windows-sys 0.52.0", +] + [[package]] name = "toml_datetime" version = "0.6.8" @@ -3033,6 +3341,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -3121,10 +3459,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] -name = "utf8parse" -version = "0.2.2" +name = "valuable" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "version_check" diff --git a/Cargo.toml b/Cargo.toml index 9a6f56ef05241e31014ffc9b165089a8561ef6a6..975dee222aa720c1a75d662474563cb73f656c5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,13 +14,35 @@ eframe = { version = "0.29", features = ["persistence"] } egui = { version = "0.29", features = ["log"] } egui_plot = "0.29" egui_file = "0.19" +# =========== Asynchronous =========== +tokio = { version = "1.41", features = [ + "rt-multi-thread", + "net", + "parking_lot", + "sync", +] } +# =========== Mavlink =========== +skyward_mavlink = { git = "https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git", branch = "rust-strum", features = [ + "reflection", + "orion", + "serde", +] } +mavlink-bindgen = { version = "0.13.1", features = ["serde"] } # ========= Persistency ========= serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" # =========== Logging =========== -env_logger = "0.11" -log = "0.4" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +# =========== Performance =========== +# for fast mutexes +parking_lot = "0.12" +# for fast channels +crossbeam-channel = "0.5" # =========== Utility =========== # for dynamic dispatch enum_dispatch = "0.3" egui_extras = "0.29.1" +strum = "0.26" +anyhow = "1.0" +ring-channel = "0.12.0" diff --git a/justfile b/justfile new file mode 100644 index 0000000000000000000000000000000000000000..613e908a144ea7e61f0d4e3997c57386b9fbb19e --- /dev/null +++ b/justfile @@ -0,0 +1,11 @@ +alias r := run +alias d := doc + +default: + just run + +run LEVEL="debug": + RUST_LOG=segs={{LEVEL}} cargo r + +doc: + cargo doc --no-deps --open diff --git a/src/main.rs b/src/main.rs index c988cd73ecab35c754722869bba9d6b72dc8df98..b4b269e9b7b5275e1e928d20691a9cc313ccfe95 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,31 @@ +mod mavlink; +mod ui; + +use std::{ + num::NonZeroUsize, + sync::{LazyLock, OnceLock}, +}; + +use mavlink::{MessageBroker, ReflectionContext}; +use parking_lot::Mutex; +use tokio::runtime::Runtime; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer}; use ui::ComposableView; -mod ui; +static MSG_MANAGER: OnceLock<Mutex<MessageBroker>> = OnceLock::new(); +static MAVLINK_PROFILE: LazyLock<ReflectionContext> = LazyLock::new(ReflectionContext::new); static APP_NAME: &str = "segs"; fn main() -> Result<(), eframe::Error> { // set up logging (USE RUST_LOG=debug to see logs) - env_logger::init(); + let env_filter = EnvFilter::builder().from_env_lossy(); + tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer().with_filter(env_filter)) + .init(); + + let rt = Runtime::new().expect("Unable to create Runtime"); + let _enter = rt.enter(); let native_options = eframe::NativeOptions { // By modifying the viewport, we can change things like the windows size @@ -25,6 +44,12 @@ fn main() -> Result<(), eframe::Error> { APP_NAME, // This is the app id, used for example by Wayland native_options, Box::new(|ctx| { + MSG_MANAGER + .set(Mutex::new(MessageBroker::new( + NonZeroUsize::new(50).unwrap(), + ctx.egui_ctx.clone(), + ))) + .expect("Unable to set MessageManager"); let app = ctx .storage .map(|storage| ComposableView::new(APP_NAME, storage)) diff --git a/src/mavlink.rs b/src/mavlink.rs new file mode 100644 index 0000000000000000000000000000000000000000..116f3059f1423ee208bdcde881ca80182f94acae --- /dev/null +++ b/src/mavlink.rs @@ -0,0 +1,10 @@ +mod base; +mod message_broker; +mod reflection; + +// Export all the types from the base module as if they were defined in this module +pub use base::*; +pub use message_broker::{MessageBroker, MessageView}; +pub use reflection::ReflectionContext; + +pub const DEFAULT_ETHERNET_PORT: u16 = 42069; diff --git a/src/mavlink/base.rs b/src/mavlink/base.rs new file mode 100644 index 0000000000000000000000000000000000000000..f80c2a1eeef30fc14e36ab2f74f84baf226551a1 --- /dev/null +++ b/src/mavlink/base.rs @@ -0,0 +1,57 @@ +//! This module is a wrapper around the `skyward_mavlink` crate, facilitates +//! rapid switching between different mavlink versions and profiles. +//! +//! In addition, it provides few utility functions to work with mavlink messages. + +use std::time::Instant; + +use skyward_mavlink::mavlink::peek_reader::PeekReader; + +// Re-export from the mavlink crate +pub use skyward_mavlink::{ + mavlink::*, orion::*, + reflection::ORION_MAVLINK_PROFILE_SERIALIZED as MAVLINK_PROFILE_SERIALIZED, +}; + +/// A wrapper around the `MavMessage` struct, adding a received time field. +#[derive(Debug, Clone)] +pub struct TimedMessage { + /// The underlying mavlink message + pub message: MavMessage, + /// The time instant at which the message was received + pub time: Instant, +} + +impl TimedMessage { + pub fn just_received(message: MavMessage) -> Self { + Self { + message, + time: Instant::now(), + } + } +} + +pub fn extract_from_message<K, T>( + message: &MavMessage, + fields: impl IntoIterator<Item = K>, +) -> Vec<T> +where + K: AsRef<str>, + T: serde::de::DeserializeOwned + Default, +{ + let value: serde_json::Value = serde_json::to_value(message).unwrap(); + fields + .into_iter() + .map(|field| { + let field = field.as_ref(); + let value = value.get(field).unwrap(); + serde_json::from_value::<T>(value.clone()).unwrap_or_default() + }) + .collect() +} + +/// Helper function to read a stream of bytes and return an iterator of MavLink messages +pub fn byte_parser(buf: &[u8]) -> impl Iterator<Item = (MavHeader, MavMessage)> + '_ { + let mut reader = PeekReader::new(buf); + std::iter::from_fn(move || read_v1_msg(&mut reader).ok()) +} diff --git a/src/mavlink/message_broker.rs b/src/mavlink/message_broker.rs new file mode 100644 index 0000000000000000000000000000000000000000..0a70820950aebeb1e1477b1ac3d6b3db5a7bfaa2 --- /dev/null +++ b/src/mavlink/message_broker.rs @@ -0,0 +1,153 @@ +use std::{ + collections::{HashMap, VecDeque}, + num::NonZeroUsize, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; + +use anyhow::{Context, Result}; +use egui::{ahash::HashMapExt, IdMap}; +use ring_channel::{ring_channel, RingReceiver, RingSender}; +use tokio::{net::UdpSocket, task::JoinHandle}; +use tracing::debug; + +use crate::mavlink::byte_parser; + +use super::{Message, TimedMessage}; + +const UDP_BUFFER_SIZE: usize = 65527; + +pub trait MessageView { + fn widget_id(&self) -> &egui::Id; + fn id_of_interest(&self) -> u32; + fn is_valid(&self) -> bool; + fn populate_view(&mut self, msg_slice: &[TimedMessage]); + fn update_view(&mut self, msg_slice: &[TimedMessage]); +} + +#[derive(Debug)] +pub struct MessageBroker { + // == Messages == + /// map(message ID -> vector of messages received so far) + messages: HashMap<u32, Vec<TimedMessage>>, + /// map(widget ID -> queue of messages left for update) + update_queues: IdMap<(u32, VecDeque<TimedMessage>)>, + // == Internal == + /// Flag to stop the listener + running_flag: Arc<AtomicBool>, + /// Listener message sender + tx: RingSender<TimedMessage>, + /// Broker message receiver + rx: RingReceiver<TimedMessage>, + /// Task handle for the listener + task: Option<JoinHandle<Result<()>>>, + /// Egui context + ctx: egui::Context, +} + +impl MessageBroker { + pub fn new(channel_size: NonZeroUsize, ctx: egui::Context) -> Self { + let (tx, rx) = ring_channel(channel_size); + Self { + messages: HashMap::new(), + update_queues: IdMap::new(), + tx, + rx, + ctx, + running_flag: Arc::new(AtomicBool::new(false)), + task: None, + } + } + + pub fn refresh_view<V: MessageView>(&mut self, view: &mut V) { + self.process_incoming_msgs(); + if !view.is_valid() || !self.update_queues.contains_key(view.widget_id()) { + self.init_view(view); + } else { + self.update_view(view); + } + } + + pub fn stop_listening(&mut self) { + self.running_flag.store(false, Ordering::Relaxed); + if let Some(t) = self.task.take() { + t.abort() + } + } + + pub fn listen_from_ethernet_port(&mut self, port: u16) { + // Stop the current listener if it exists + self.stop_listening(); + self.running_flag.store(true, Ordering::Relaxed); + + let tx = self.tx.clone(); + let ctx = self.ctx.clone(); + + let bind_address = format!("0.0.0.0:{}", port); + let mut buf = Box::new([0; UDP_BUFFER_SIZE]); + let running_flag = self.running_flag.clone(); + + let handle = tokio::spawn(async move { + let socket = UdpSocket::bind(bind_address) + .await + .context("Failed to bind socket")?; + debug!("Listening on UDP"); + + while running_flag.load(Ordering::Relaxed) { + let (len, _) = socket + .recv_from(buf.as_mut_slice()) + .await + .context("Failed to receive message")?; + for (_, mav_message) in byte_parser(&buf[..len]) { + tx.send(TimedMessage::just_received(mav_message)) + .context("Failed to send message")?; + ctx.request_repaint(); + } + } + + Ok::<(), anyhow::Error>(()) + }); + self.task = Some(handle); + } + + pub fn clear(&mut self) { + self.messages.clear(); + } + + fn init_view<V: MessageView>(&mut self, view: &mut V) { + if let Some(messages) = self.messages.get(&view.id_of_interest()) { + view.populate_view(messages); + } + self.update_queues + .insert(*view.widget_id(), (view.id_of_interest(), VecDeque::new())); + } + + fn update_view<V: MessageView>(&mut self, view: &mut V) { + if let Some((_, queue)) = self.update_queues.get_mut(view.widget_id()) { + while let Some(msg) = queue.pop_front() { + view.update_view(&[msg]); + } + } + } + + fn process_incoming_msgs(&mut self) { + while let Ok(message) = self.rx.try_recv() { + // first update the update queues + for (_, (id, queue)) in self.update_queues.iter_mut() { + if *id == message.message.message_id() { + queue.push_back(message.clone()); + } + } + // then store the message in the messages map + self.messages + .entry(message.message.message_id()) + .or_default() + .push(message); + } + } + + // TODO: Implement a scheduler removal of old messages (configurable, must not hurt performance) + // TODO: Add a Dashmap if performance is a problem (Personally don't think it will be) +} diff --git a/src/mavlink/reflection.rs b/src/mavlink/reflection.rs new file mode 100644 index 0000000000000000000000000000000000000000..ae2394adc63e7ee27df2800293fe35f619ba5c63 --- /dev/null +++ b/src/mavlink/reflection.rs @@ -0,0 +1,98 @@ +use std::collections::HashMap; + +use mavlink_bindgen::parser::{MavProfile, MavType}; + +use super::MAVLINK_PROFILE_SERIALIZED; + +pub struct ReflectionContext { + mavlink_profile: MavProfile, + id_name_map: HashMap<u32, String>, +} + +impl ReflectionContext { + pub fn new() -> Self { + let profile: MavProfile = serde_json::from_str(MAVLINK_PROFILE_SERIALIZED) + .expect("Failed to deserialize MavProfile"); + let id_name_map = profile + .messages + .iter() + .map(|(name, m)| (m.id, name.clone())) + .collect(); + Self { + mavlink_profile: profile, + id_name_map, + } + } + + pub fn get_name_from_id(&self, message_id: u32) -> Option<&str> { + self.id_name_map.get(&message_id).map(|s| s.as_str()) + } + + pub fn sorted_messages(&self) -> Vec<&str> { + let mut msgs: Vec<&str> = self + .mavlink_profile + .messages + .keys() + .map(|s| s.as_str()) + .collect(); + msgs.sort(); + msgs + } + + pub fn get_fields_by_id(&self, message_id: u32) -> Vec<&str> { + self.mavlink_profile + .messages + .iter() + .find(|(_, m)| m.id == message_id) + .map(|(_, m)| &m.fields) + .unwrap_or_else(|| { + panic!("Message ID {} not found in profile", message_id); + }) + .iter() + .map(|f| f.name.as_str()) + .collect() + } + + pub fn get_plottable_fields_by_id(&self, message_id: u32) -> Vec<&str> { + self.mavlink_profile + .messages + .iter() + .find(|(_, m)| m.id == message_id) + .map(|(_, m)| &m.fields) + .unwrap_or_else(|| { + panic!("Message ID {} not found in profile", message_id); + }) + .iter() + .filter(|f| { + matches!( + f.mavtype, + MavType::UInt8 + | MavType::UInt16 + | MavType::UInt32 + | MavType::UInt64 + | MavType::Int8 + | MavType::Int16 + | MavType::Int32 + | MavType::Int64 + | MavType::Float + | MavType::Double + ) + }) + .map(|f| f.name.as_str()) + .collect() + } + + pub fn get_fields_by_name(&self, message_name: &str) -> Vec<&str> { + self.mavlink_profile + .messages + .iter() + .find(|(_, m)| m.name == message_name) + .map(|(_, m)| &m.fields) + .unwrap_or_else(|| { + panic!("Message {} not found in profile", message_name); + }) + .iter() + .map(|f| f.name.as_str()) + .collect() + } +} diff --git a/src/ui/composable_view.rs b/src/ui/composable_view.rs index 7351601b00ded36cf8511ebbc8b90b6fff031910..7b71f6b6d06d2409b82501aa2892b9b8bcca8a34 100644 --- a/src/ui/composable_view.rs +++ b/src/ui/composable_view.rs @@ -1,3 +1,5 @@ +use crate::{mavlink, MSG_MANAGER}; + use super::{ layout_manager::LayoutManager, panes::{Pane, PaneBehavior, PaneKind}, @@ -21,6 +23,7 @@ pub struct ComposableView { pub layout_manager: LayoutManager, behavior: ComposableBehavior, maximized_pane: Option<TileId>, + sources_window: SourceWindow, } // An app must implement the `App` trait to define how the ui is built @@ -126,9 +129,16 @@ impl eframe::App for ComposableView { // Show a panel at the bottom of the screen with few global controls egui::TopBottomPanel::bottom("bottom_control").show(ctx, |ui| { + // Horizontal belt of controls ui.horizontal(|ui| { egui::global_theme_preference_switch(ui); + // Window for the sources + self.sources_window.show_window(ui); + + if ui.button("Sources").clicked() { + self.sources_window.visible = !self.sources_window.visible; + } if ui.button("Layout Manager").clicked() { self.layout_manager.toggle_open_state(); } @@ -233,6 +243,60 @@ impl ComposableViewState { } } +struct SourceWindow { + port: u16, + visible: bool, +} + +impl Default for SourceWindow { + fn default() -> Self { + Self { + port: mavlink::DEFAULT_ETHERNET_PORT, + visible: false, + } + } +} + +impl SourceWindow { + fn show_window(&mut self, ui: &mut egui::Ui) { + let mut window_is_open = self.visible; + let mut can_be_closed = false; + egui::Window::new("Sources") + .id(ui.id()) + .auto_sized() + .collapsible(false) + .movable(true) + .open(&mut window_is_open) + .show(ui.ctx(), |ui| { + self.ui(ui, &mut can_be_closed); + }); + self.visible = window_is_open && !can_be_closed; + } + + fn ui(&mut self, ui: &mut egui::Ui, can_be_closed: &mut bool) { + egui::Grid::new(ui.id()) + .num_columns(2) + .spacing([10.0, 5.0]) + .show(ui, |ui| { + ui.label("Ethernet Port:"); + ui.add( + egui::DragValue::new(&mut self.port) + .range(0..=65535) + .speed(10), + ); + ui.end_row(); + }); + if ui.button("Connect").clicked() { + MSG_MANAGER + .get() + .unwrap() + .lock() + .listen_from_ethernet_port(self.port); + *can_be_closed = true; + } + } +} + /// Behavior for the tree of panes in the composable view #[derive(Default)] pub struct ComposableBehavior { diff --git a/src/ui/panes.rs b/src/ui/panes.rs index 50c4f9007cbb314cceb2d684fc068feaf6714e52..c0ae378ce1336e592e85f93ed9d5177c2c3ac0ce 100644 --- a/src/ui/panes.rs +++ b/src/ui/panes.rs @@ -1,25 +1,17 @@ mod default; mod messages_viewer; -mod plot_2d; +mod plot; use enum_dispatch::enum_dispatch; use serde::{Deserialize, Serialize}; use super::composable_view::PaneResponse; -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] pub struct Pane { pub pane: PaneKind, } -impl Default for Pane { - fn default() -> Self { - Self { - pane: PaneKind::default(), - } - } -} - impl Pane { pub fn boxed(pane: PaneKind) -> Box<Self> { Box::new(Self { pane }) @@ -48,7 +40,7 @@ impl PaneBehavior for Pane { pub enum PaneKind { Default(default::DefaultPane), MessagesViewer(messages_viewer::MessagesViewerPane), - Plot2D(plot_2d::Plot2DPane), + Plot2D(plot::Plot2DPane), } impl Default for PaneKind { diff --git a/src/ui/panes/default.rs b/src/ui/panes/default.rs index 2d1b51aeed68fff6033806ca74eed0e3106705e5..3cfd31c2a6e04b159fed5b6bb16a46680c59c0d2 100644 --- a/src/ui/panes/default.rs +++ b/src/ui/panes/default.rs @@ -1,13 +1,16 @@ -use super::{plot_2d::Plot2DPane, Pane, PaneBehavior, PaneKind}; +use super::{plot::Plot2DPane, Pane, PaneBehavior, PaneKind}; use serde::{Deserialize, Serialize}; +use tracing::debug; -use crate::ui::composable_view::{PaneAction, PaneResponse}; +use crate::ui::{ + composable_view::{PaneAction, PaneResponse}, + utils::{vertically_centered, SizingMemo}, +}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct DefaultPane { - occupied: f32, - fixed: bool, - + #[serde(skip)] + centering_memo: SizingMemo, #[serde(skip)] contains_pointer: bool, } @@ -15,8 +18,7 @@ pub struct DefaultPane { impl Default for DefaultPane { fn default() -> Self { DefaultPane { - occupied: 0.0, - fixed: false, + centering_memo: SizingMemo::default(), contains_pointer: false, } } @@ -24,55 +26,36 @@ impl Default for DefaultPane { impl PartialEq for DefaultPane { fn eq(&self, other: &Self) -> bool { - self.occupied == other.occupied && self.fixed == other.fixed + true } } impl PaneBehavior for DefaultPane { fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse { let mut response = PaneResponse::default(); - let pane_rect = ui.max_rect(); - let parent = ui.vertical_centered(|ui| { - let hpad = (pane_rect.height() - self.occupied) / 2.0; - if self.fixed { - ui.add_space(hpad); - } - let mut height_occupied = 0.0; - let btn = ui.button("Vertical Split"); - if btn.clicked() { - response.set_action(PaneAction::SplitV); - log::debug!("Vertical Split button clicked"); - } - height_occupied += btn.rect.height(); - let btn = ui.button("Horizontal Split"); - if btn.clicked() { - response.set_action(PaneAction::SplitH); - log::debug!("Horizontal Split button clicked"); - } - height_occupied += btn.rect.height(); - let btn = ui.button("Plot"); - if btn.clicked() { - response.set_action(PaneAction::Replace(Pane::boxed(PaneKind::Plot2D( - Plot2DPane::default(), - )))); - } - height_occupied += btn.rect.height(); - if !self.fixed { - self.occupied = height_occupied; - ui.ctx().request_discard("test"); - self.fixed = true; - } - if self.fixed { - ui.add_space(hpad); - } - ui.set_min_height(pane_rect.height()); + let parent = vertically_centered(ui, &mut self.centering_memo, |ui| { + ui.vertical_centered(|ui| { + if ui.button("Vertical Split").clicked() { + response.set_action(PaneAction::SplitV); + debug!("Vertical Split button clicked"); + } + if ui.button("Horizontal Split").clicked() { + response.set_action(PaneAction::SplitH); + debug!("Horizontal Split button clicked"); + } + if ui.button("Plot").clicked() { + response.set_action(PaneAction::Replace(Pane::boxed(PaneKind::Plot2D( + Plot2DPane::new(ui.auto_id_with("plot_2d")), + )))); + } + }) + .response }); - self.contains_pointer = parent.response.contains_pointer(); + self.contains_pointer = parent.contains_pointer(); if parent - .response .interact(egui::Sense::click_and_drag()) .on_hover_cursor(egui::CursorIcon::Grab) .dragged() diff --git a/src/ui/panes/plot.rs b/src/ui/panes/plot.rs new file mode 100644 index 0000000000000000000000000000000000000000..16b90332e33fae3b1ca51f70c133d680bd660f7e --- /dev/null +++ b/src/ui/panes/plot.rs @@ -0,0 +1,248 @@ +mod source_window; + +use egui::{Color32, Vec2b}; +use egui_plot::{Legend, Line, PlotPoints}; +use serde::{Deserialize, Serialize}; +use source_window::{sources_window, SourceSettings}; + +use crate::{ + mavlink::{ + extract_from_message, MessageData, MessageView, TimedMessage, ROCKET_FLIGHT_TM_DATA, + }, + ui::composable_view::PaneResponse, + MSG_MANAGER, +}; + +use super::PaneBehavior; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Plot2DPane { + // UI settings + #[serde(skip)] + pub contains_pointer: bool, + #[serde(skip)] + settings_visible: bool, + line_settings: Vec<LineSettings>, + plot_active: bool, + view: PlotMessageView, +} + +impl Plot2DPane { + pub fn new(id: egui::Id) -> Self { + Self { + contains_pointer: false, + settings_visible: false, + line_settings: vec![], + plot_active: false, + view: PlotMessageView::new(id), + } + } +} + +impl PartialEq for Plot2DPane { + fn eq(&self, other: &Self) -> bool { + self.view.settings == other.view.settings + && self.line_settings == other.line_settings + && self.plot_active == other.plot_active + } +} + +impl PaneBehavior for Plot2DPane { + fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse { + let mut response = PaneResponse::default(); + + let Self { + line_settings: plot_lines, + settings_visible, + plot_active, + view, + .. + } = self; + + let mut settings = SourceSettings::new(&mut view.settings, plot_lines); + egui::Window::new("Plot Settings") + .id(ui.auto_id_with("plot_settings")) // TODO: fix this issue with ids + .auto_sized() + .collapsible(true) + .movable(true) + .open(settings_visible) + .show(ui.ctx(), |ui| sources_window(ui, &mut settings)); + // if settings are changed, invalidate the cache + view.cache_valid = !settings.are_sources_changed(); + // if there are no fields, do not plot + *plot_active = !settings.fields_empty(); + + let ctrl_pressed = ui.input(|i| i.modifiers.ctrl); + + let mut plot_lines = Vec::new(); + if self.plot_active { + MSG_MANAGER.get().unwrap().lock().refresh_view(view); + let acc_points = &view.points; + + let field_x = &view.settings.x_field; + if !acc_points.is_empty() { + for (i, plot_line) in self.line_settings.iter().enumerate() { + let points: Vec<[f64; 2]> = { + let iter = acc_points.iter(); + if field_x == "timestamp" { + iter.map(|(x, ys)| [x / 1e6, ys[i]]).collect() + } else { + iter.map(|(x, ys)| [*x, ys[i]]).collect() + } + }; + plot_lines.push((plot_line.clone(), points)); + } + } + } + + let plot = egui_plot::Plot::new("plot") + .auto_bounds(Vec2b::TRUE) + .legend(Legend::default()) + .label_formatter(|name, value| format!("{} - x:{:.2} y:{:.2}", name, value.x, value.y)); + plot.show(ui, |plot_ui| { + self.contains_pointer = plot_ui.response().contains_pointer(); + if plot_ui.response().dragged() && ctrl_pressed { + println!("ctrl + drag"); + response.set_drag_started(); + } + for (plot_settings, data_points) in plot_lines { + plot_ui.line( + Line::new(PlotPoints::from(data_points)) + .color(plot_settings.color) + .width(plot_settings.width) + .name(&plot_settings.field), + ); + } + plot_ui + .response() + .context_menu(|ui| show_menu(ui, settings_visible)); + }); + + response + } + + fn contains_pointer(&self) -> bool { + self.contains_pointer + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct PlotMessageView { + // == Settings from the UI == + settings: MsgSources, + // == Data == + #[serde(skip)] + points: Vec<(f64, Vec<f64>)>, + // == Internal == + id: egui::Id, + #[serde(skip)] + cache_valid: bool, +} + +impl PlotMessageView { + fn new(id: egui::Id) -> Self { + Self { + settings: Default::default(), + points: Vec::new(), + id, + cache_valid: false, + } + } +} + +impl MessageView for PlotMessageView { + fn widget_id(&self) -> &egui::Id { + &self.id + } + + fn id_of_interest(&self) -> u32 { + self.settings.msg_id + } + + fn is_valid(&self) -> bool { + self.cache_valid + } + + fn populate_view(&mut self, msg_slice: &[TimedMessage]) { + self.points.clear(); + let MsgSources { + x_field, y_fields, .. + } = &self.settings; + for msg in msg_slice { + let x: f64 = extract_from_message(&msg.message, [x_field])[0]; + let ys: Vec<f64> = extract_from_message(&msg.message, y_fields); + self.points.push((x, ys)); + } + } + + fn update_view(&mut self, msg_slice: &[TimedMessage]) { + let MsgSources { + x_field, y_fields, .. + } = &self.settings; + for msg in msg_slice { + let x: f64 = extract_from_message(&msg.message, [x_field])[0]; + let ys: Vec<f64> = extract_from_message(&msg.message, y_fields); + self.points.push((x, ys)); + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct MsgSources { + msg_id: u32, + x_field: String, + y_fields: Vec<String>, +} + +impl Default for MsgSources { + fn default() -> Self { + Self { + msg_id: ROCKET_FLIGHT_TM_DATA::ID, + x_field: "timestamp".to_owned(), + y_fields: Vec::new(), + } + } +} + +impl PartialEq for MsgSources { + fn eq(&self, other: &Self) -> bool { + self.msg_id == other.msg_id + && self.x_field == other.x_field + && self.y_fields == other.y_fields + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +struct LineSettings { + field: String, + width: f32, + color: Color32, +} + +impl Default for LineSettings { + fn default() -> Self { + Self { + field: "".to_owned(), + width: 1.0, + color: Color32::BLUE, + } + } +} + +impl LineSettings { + fn new(field_y: String) -> Self { + Self { + field: field_y, + ..Default::default() + } + } +} + +fn show_menu(ui: &mut egui::Ui, settings_visible: &mut bool) { + ui.set_max_width(200.0); // To make sure we wrap long text + + if ui.button("Settings…").clicked() { + *settings_visible = true; + ui.close_menu(); + } +} diff --git a/src/ui/panes/plot/source_window.rs b/src/ui/panes/plot/source_window.rs new file mode 100644 index 0000000000000000000000000000000000000000..ecb4d610275296b0fc236b86d0c15c5418accf07 --- /dev/null +++ b/src/ui/panes/plot/source_window.rs @@ -0,0 +1,185 @@ +use crate::{ + mavlink::{MavMessage, Message}, + MAVLINK_PROFILE, +}; + +use super::{LineSettings, MsgSources}; + +pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut SourceSettings) { + // extract the msg name from the id to show it in the combo box + let msg_name = MAVLINK_PROFILE + .get_name_from_id(*plot_settings.get_msg_id()) + .unwrap_or_default(); + + // show the first combo box with the message name selection + egui::ComboBox::from_label("Message Kind") + .selected_text(msg_name) + .show_ui(ui, |ui| { + for msg in MAVLINK_PROFILE.sorted_messages() { + ui.selectable_value( + plot_settings.get_mut_msg_id(), + MavMessage::message_id_from_name(msg).unwrap(), + msg, + ); + } + }); + + // reset fields if the message is changed + if plot_settings.is_msg_id_changed() { + plot_settings.clear_fields(); + } + + // check fields and assign a default field_x and field_y once the msg is changed + let fields = MAVLINK_PROFILE.get_plottable_fields_by_id(*plot_settings.get_msg_id()); + // get the first field that is in the list of fields or the previous if valid + let x_field = plot_settings.get_x_field(); + let new_field_x = fields + .contains(&x_field) + .then(|| x_field.to_owned()) + .or(fields.first().map(|s| s.to_string())); + + // if there are no fields, reset the field_x and plot_lines + let Some(new_field_x) = new_field_x else { + plot_settings.clear_fields(); + return; + }; + // update the field_x + plot_settings.set_x_field(new_field_x); + let x_field = plot_settings.get_mut_x_field(); + + // if fields are valid, show the combo boxes for the x_axis + egui::ComboBox::from_label("X Axis") + .selected_text(x_field.as_str()) + .show_ui(ui, |ui| { + for msg in fields.iter() { + ui.selectable_value(x_field, (*msg).to_owned(), *msg); + } + }); + + // populate the plot_lines with the first field if it is empty and there are more than 1 fields + if plot_settings.fields_empty() && fields.len() > 1 { + plot_settings.add_field(fields[1].to_string()); + } + + // check how many fields are left and how many are selected + let plot_lines_len = plot_settings.fields_len(); + egui::Grid::new(ui.auto_id_with("y_axis")) + .num_columns(3) + .spacing([10.0, 2.5]) + .show(ui, |ui| { + for (i, line_settings) in plot_settings.line_settings.iter_mut().enumerate() { + let LineSettings { + field, + width, + color, + } = line_settings; + let widget_label = if plot_lines_len > 1 { + format!("Y Axis {}", i + 1) + } else { + "Y Axis".to_owned() + }; + egui::ComboBox::from_label(widget_label) + .selected_text(field.as_str()) + .show_ui(ui, |ui| { + for msg in fields.iter() { + ui.selectable_value(field, (*msg).to_owned(), *msg); + } + }); + ui.color_edit_button_srgba(color); + ui.add(egui::DragValue::new(width).speed(0.1).suffix(" pt")) + .on_hover_text("Width of the line in points"); + ui.end_row(); + } + }); + // Sync changes applied to line_settings with msg_sources + plot_settings.sync_fields_with_lines(); + + // if we have fields left, show the add button + if fields.len().saturating_sub(plot_lines_len + 1) > 0 + && ui + .button("Add Y Axis") + .on_hover_text("Add another Y axis") + .clicked() + { + let next_field = fields + .iter() + .find(|f| !plot_settings.contains_field(f)) + .unwrap(); + plot_settings.add_field(next_field.to_string()); + } +} + +pub struct SourceSettings<'a> { + msg_sources: &'a mut MsgSources, + old_msg_sources: MsgSources, + line_settings: &'a mut Vec<LineSettings>, +} + +impl<'a> SourceSettings<'a> { + pub fn new(msg_sources: &'a mut MsgSources, line_settings: &'a mut Vec<LineSettings>) -> Self { + Self { + old_msg_sources: msg_sources.clone(), + msg_sources, + line_settings, + } + } + + pub fn are_sources_changed(&self) -> bool { + self.msg_sources != &self.old_msg_sources + } + + pub fn fields_empty(&self) -> bool { + self.msg_sources.y_fields.is_empty() + } + + fn get_msg_id(&self) -> &u32 { + &self.msg_sources.msg_id + } + + fn get_x_field(&self) -> &str { + &self.msg_sources.x_field + } + + fn get_mut_msg_id(&mut self) -> &mut u32 { + &mut self.msg_sources.msg_id + } + + fn get_mut_x_field(&mut self) -> &mut String { + &mut self.msg_sources.x_field + } + + fn set_x_field(&mut self, field: String) { + self.msg_sources.x_field = field; + } + + fn fields_len(&self) -> usize { + self.msg_sources.y_fields.len() + } + + fn is_msg_id_changed(&self) -> bool { + self.msg_sources.msg_id != self.old_msg_sources.msg_id + } + + fn contains_field(&self, field: &str) -> bool { + self.msg_sources.y_fields.contains(&field.to_owned()) + } + + fn sync_fields_with_lines(&mut self) { + self.msg_sources.y_fields = self + .line_settings + .iter() + .map(|ls| ls.field.clone()) + .collect(); + } + + fn add_field(&mut self, field: String) { + self.line_settings.push(LineSettings::new(field.clone())); + self.msg_sources.y_fields.push(field); + } + + fn clear_fields(&mut self) { + self.msg_sources.y_fields.clear(); + self.line_settings.clear(); + self.msg_sources.x_field = "".to_owned(); + } +} diff --git a/src/ui/panes/plot_2d.rs b/src/ui/panes/plot_2d.rs deleted file mode 100644 index 6ac065822b6fdf0711e8cac9f229f2f8ea87d196..0000000000000000000000000000000000000000 --- a/src/ui/panes/plot_2d.rs +++ /dev/null @@ -1,119 +0,0 @@ -use crate::ui::composable_view::PaneResponse; - -use super::PaneBehavior; - -use egui_plot::{Line, PlotPoints}; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Plot2DPane { - n_points: u32, - frequency: f64, - width: f32, - color: egui::Color32, - - #[serde(skip)] - pub contains_pointer: bool, - - #[serde(skip)] - settings_visible: bool, -} - -impl Default for Plot2DPane { - fn default() -> Self { - Self { - contains_pointer: false, - settings_visible: false, - n_points: 2, - frequency: 1.0, - width: 1.0, - color: egui::Color32::from_rgb(0, 120, 240), - } - } -} - -impl PartialEq for Plot2DPane { - fn eq(&self, other: &Self) -> bool { - self.n_points == other.n_points - && self.frequency == other.frequency - && self.width == other.width - && self.color == other.color - } -} - -impl PaneBehavior for Plot2DPane { - fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse { - let mut response = PaneResponse::default(); - - let mut window_visible = self.settings_visible; - egui::Window::new("Plot Settings") - .id(ui.id()) - .auto_sized() - .collapsible(true) - .movable(true) - .open(&mut window_visible) - .show(ui.ctx(), |ui| self.settings_window(ui)); - self.settings_visible = window_visible; - - let ctrl_pressed = ui.input(|i| i.modifiers.ctrl); - - let plot = egui_plot::Plot::new("plot"); - plot.show(ui, |plot_ui| { - self.contains_pointer = plot_ui.response().contains_pointer(); - if plot_ui.response().dragged() && ctrl_pressed { - println!("ctrl + drag"); - response.set_drag_started(); - } - let points: Vec<[f64; 2]> = (0..self.n_points) - .map(|i| i as f64 * 100.0 / (self.n_points - 1) as f64) - .map(|i| [i, (i * std::f64::consts::PI * 2.0 * self.frequency).sin()]) - .collect(); - plot_ui.line( - Line::new(PlotPoints::from(points)) - .color(self.color) - .width(self.width), - ); - plot_ui.response().context_menu(|ui| self.menu(ui)); - }); - - response - } - - fn contains_pointer(&self) -> bool { - self.contains_pointer - } -} - -impl Plot2DPane { - fn menu(&mut self, ui: &mut egui::Ui) { - ui.set_max_width(200.0); // To make sure we wrap long text - - if ui.button("Settings…").clicked() { - self.settings_visible = true; - ui.close_menu(); - } - } - - fn settings_window(&mut self, ui: &mut egui::Ui) { - egui::Grid::new(ui.id()) - .num_columns(2) - .spacing([10.0, 5.0]) - .show(ui, |ui| { - ui.label("Size:"); - ui.add(egui::Slider::new(&mut self.n_points, 2..=1000).text("Points")); - ui.end_row(); - - ui.label("Frequency:"); - ui.add(egui::Slider::new(&mut self.frequency, 0.1..=10.0).text("Hz")); - ui.end_row(); - - ui.label("Color:"); - ui.color_edit_button_srgba(&mut self.color); - ui.end_row(); - - ui.label("Width:"); - ui.add(egui::Slider::new(&mut self.width, 0.1..=10.0).text("pt")); - ui.end_row(); - }); - } -} diff --git a/src/ui/utils.rs b/src/ui/utils.rs index b9f510bda78fab7513f25defa33d1377ffdd6cd7..3172463e01bd62f94570e37218be9dd3447f730b 100644 --- a/src/ui/utils.rs +++ b/src/ui/utils.rs @@ -1,5 +1,5 @@ use egui::containers::Frame; -use egui::{Shadow, Stroke, Style, Ui}; +use egui::{Response, Shadow, Stroke, Style, Ui}; use super::panes::{Pane, PaneBehavior}; @@ -12,3 +12,32 @@ pub fn maximized_pane_ui(ui: &mut Ui, pane: &mut Pane) { .stroke(Stroke::NONE) .show(ui, |ui| pane.ui(ui)); } + +#[derive(Debug, Default, Clone)] +pub struct SizingMemo { + occupied_height: f32, + sizing_pass_done: bool, +} + +pub fn vertically_centered( + ui: &mut Ui, + memo: &mut SizingMemo, + add_contents: impl FnOnce(&mut Ui) -> Response, +) -> egui::Response { + if !memo.sizing_pass_done { + let r = add_contents(ui); + memo.occupied_height = r.rect.height(); + memo.sizing_pass_done = true; + ui.ctx() + .request_discard("horizontally_centered requires a sizing pass"); + r + } else { + let spacing = (ui.available_height() - memo.occupied_height) / 2.0; + ui.vertical_centered(|ui| { + ui.add_space(spacing); + add_contents(ui); + ui.add_space(spacing); + }) + .response + } +}