diff --git a/Cargo.lock b/Cargo.lock index 4051aa103d01bc76ab27f69f43b56d1d47132a3d..704911bcb39c166185cde98f2fd3e5c30b20cbb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,6 +192,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" + [[package]] name = "arboard" version = "3.4.1" @@ -761,6 +767,12 @@ dependencies = [ "libc", ] +[[package]] +name = "crc-any" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62ec9ff5f7965e4d7280bd5482acd20aadb50d632cf6c1d74493856b011fa73" + [[package]] name = "crc32fast" version = "1.4.2" @@ -770,6 +782,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.20" @@ -1468,6 +1489,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "hermit-abi" version = "0.4.0" @@ -1665,6 +1698,15 @@ dependencies = [ "hashbrown 0.15.1", ] +[[package]] +name = "ioctl-rs" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7970510895cee30b3e9128319f2cefd4bde883a39f38baa279567ba3a7eb97d" +dependencies = [ + "libc", +] + [[package]] name = "itertools" version = "0.13.0" @@ -1822,6 +1864,31 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "mavlink-bindgen" +version = "0.13.2" +source = "git+https://github.com/federico123579/rust-mavlink.git?branch=strum-integration#02a3eebe16ac1aacb93c2c61696dcd731f6f2ef4" +dependencies = [ + "crc-any", + "lazy_static", + "proc-macro2", + "quick-xml 0.36.2", + "quote", + "thiserror", +] + +[[package]] +name = "mavlink-core" +version = "0.13.2" +source = "git+https://github.com/federico123579/rust-mavlink.git?branch=strum-integration#02a3eebe16ac1aacb93c2c61696dcd731f6f2ef4" +dependencies = [ + "byteorder", + "crc-any", + "serde", + "serde_arrays", + "serial", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1871,6 +1938,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" @@ -1960,6 +2039,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2362,7 +2452,7 @@ checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi", + "hermit-abi 0.4.0", "pin-project-lite", "rustix", "tracing", @@ -2565,6 +2655,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + [[package]] name = "ryu" version = "1.0.18" @@ -2609,13 +2705,18 @@ dependencies = [ name = "segs" version = "0.1.0" dependencies = [ + "anyhow", + "crossbeam-channel", "eframe", "egui", "egui_plot", "egui_tiles", "enum_dispatch", + "parking_lot", "serde", "serde_json", + "skyward_mavlink", + "strum", "tokio", "tracing", "tracing-subscriber", @@ -2630,6 +2731,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_arrays" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38636132857f68ec3d5f3eb121166d2af33cb55174c4d5ff645db6165cbef0fd" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.215" @@ -2664,6 +2774,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" @@ -2705,6 +2857,22 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "skyward_mavlink" +version = "0.1.0" +source = "git+https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git?branch=rust-strum#5b515ef056e5f783dc1cbc1c73eadd2f08a5652d" +dependencies = [ + "bitflags 2.6.0", + "mavlink-bindgen", + "mavlink-core", + "num-derive", + "num-traits", + "serde", + "serde_arrays", + "strum", + "strum_macros", +] + [[package]] name = "slab" version = "0.4.9" @@ -2774,6 +2942,16 @@ dependencies = [ "serde", ] +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" @@ -2801,6 +2979,25 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.87", +] + [[package]] name = "syn" version = "1.0.109" @@ -2856,6 +3053,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termios" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a" +dependencies = [ + "libc", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -2928,7 +3134,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" dependencies = [ "backtrace", + "libc", + "mio", + "parking_lot", "pin-project-lite", + "socket2", + "windows-sys 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0461670afe27960259726f3c03af5ba5665bda90..116dfdfbe150e9d1ae4507ea64daa435b3ba59f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,14 +13,30 @@ egui_tiles = "0.10" eframe = "0.29" egui = { version = "0.29" } egui_plot = "0.29" -# =========== Asynchronous =========== -tokio = { version = "1.41", features = ["rt-multi-thread"] } # ========= Persistency ========= serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" # =========== Logging =========== tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +# =========== Performance =========== +# for fast mutexes +parking_lot = "0.12" +# for fast channels +crossbeam-channel = "0.5" # =========== Utility =========== # for dynamic dispatch enum_dispatch = "0.3" +strum = "0.26" +anyhow = "1.0" + +# =========== Asynchronous =========== +[dependencies.tokio] +version = "1.41" +features = ["rt-multi-thread", "net", "parking_lot", "sync"] + +# =========== Mavlink =========== +[dependencies.skyward_mavlink] +git = "https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git" +branch = "rust-strum" +features = ["lyra", "serde", "strum"] diff --git a/src/main.rs b/src/main.rs index 70126df50bbf4f92eb8862ad6ea60d7d4d6ba87c..495e4e984723d1c4dc83e600f793baf195a5705c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,15 @@ +mod mavlink; +mod ui; + +use std::sync::OnceLock; + +use mavlink::MessageManager; +use parking_lot::Mutex; use tokio::runtime::Runtime; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer}; use ui::ComposableView; -mod ui; +static MSG_MANAGER: OnceLock<Mutex<MessageManager>> = OnceLock::new(); fn main() -> Result<(), eframe::Error> { // set up logging (USE RUST_LOG=debug to see logs) @@ -30,6 +37,11 @@ fn main() -> Result<(), eframe::Error> { eframe::run_native( "segs", // This is the app id, used for example by Wayland native_options, - Box::new(|_| Ok(Box::<ComposableView>::default())), + Box::new(|cc| { + MSG_MANAGER + .set(Mutex::new(MessageManager::new(50, cc.egui_ctx.clone()))) + .expect("Unable to set MessageManager"); + Ok(Box::<ComposableView>::default()) + }), ) } diff --git a/src/mavlink.rs b/src/mavlink.rs new file mode 100644 index 0000000000000000000000000000000000000000..6f042703f7317cc35ff5ea6910eea803ab886938 --- /dev/null +++ b/src/mavlink.rs @@ -0,0 +1,128 @@ +use std::{ + collections::HashMap, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + time::Instant, +}; + +use anyhow::{Context, Result}; +use crossbeam_channel::{Receiver, Sender}; +use skyward_mavlink::{ + lyra::MavMessage, + mavlink::{peek_reader::PeekReader, read_v1_msg, MavHeader, Message}, +}; +use strum::VariantNames; +use tokio::{net::UdpSocket, task::JoinHandle}; +use tracing::{debug, info}; + +pub const DEFAULT_ETHERNET_PORT: u16 = 42069; +const UDP_BUFFER_SIZE: usize = 65527; + +#[derive(Debug)] +pub struct MessageManager { + messages: HashMap<u32, Vec<TimedMessage>>, + tx: Sender<MavMessage>, + rx: Receiver<MavMessage>, + ctx: egui::Context, + running_flag: Arc<AtomicBool>, + task: Option<JoinHandle<Result<()>>>, +} + +impl MessageManager { + pub fn new(channel_size: usize, ctx: egui::Context) -> Self { + let (tx, rx) = crossbeam_channel::bounded(channel_size); + Self { + messages: HashMap::new(), + tx, + rx, + ctx, + running_flag: Arc::new(AtomicBool::new(false)), + task: None, + } + } + + pub fn get_message(&mut self, message_id: u32) -> Option<&[TimedMessage]> { + while let Ok(message) = self.rx.try_recv() { + info!("Received message: {:?}", message); + self.add_message(message); + } + self.messages.get(&message_id).map(|v| v.as_slice()) + } + + pub fn stop_listening(&mut self) { + self.running_flag.store(false, Ordering::Relaxed); + self.task.take().map(|t| t.abort()); + } + + pub fn listen_from_ethernet_port(&mut self, port: u16) { + // Stop the current listener if it exists + self.stop_listening(); + self.running_flag.store(true, Ordering::Relaxed); + + let tx = self.tx.clone(); + let ctx = self.ctx.clone(); + + let bind_address = format!("0.0.0.0:{}", port); + let mut buf = Box::new([0; UDP_BUFFER_SIZE]); + let running_flag = self.running_flag.clone(); + + let handle = tokio::spawn(async move { + let socket = UdpSocket::bind(bind_address) + .await + .context("Failed to bind socket")?; + debug!("Listening on UDP"); + + while running_flag.load(Ordering::Relaxed) { + let (len, _) = socket + .recv_from(buf.as_mut_slice()) + .await + .context("Failed to receive message")?; + for (_, mav_message) in iter_messages(&buf[..len]) { + tx.send(mav_message).context("Failed to send message")?; + ctx.request_repaint(); + } + // buf.iter_mut().for_each(|b| *b = 0); + } + + Ok::<(), anyhow::Error>(()) + }); + self.task = Some(handle); + } + + pub fn clear(&mut self) { + self.messages.clear(); + } + + fn add_message(&mut self, message: MavMessage) { + self.messages + .entry(message.message_id()) + .or_default() + .push(TimedMessage::just_received(message)); + } + + // TODO: Implement a scheduler removal of old messages (configurable, must not hurt performance) + // TODO: Add a Dashmap if performance is a problem (Personally don't think it will be) +} + +#[derive(Debug, Clone)] +pub struct TimedMessage { + message: MavMessage, + time: Instant, +} + +impl TimedMessage { + fn just_received(message: MavMessage) -> Self { + Self { + message, + time: Instant::now(), + } + } +} + +/// Helper function to read a stream of bytes and return an iterator of MavLink messages +fn iter_messages(buf: &[u8]) -> impl Iterator<Item = (MavHeader, MavMessage)> + '_ { + let mut reader = PeekReader::new(buf); + std::iter::from_fn(move || read_v1_msg(&mut reader).ok()) +} diff --git a/src/ui/composable_view.rs b/src/ui/composable_view.rs index db58813273f2e34aa1368fe12487da795d7a0a3c..5ba5b3e60f109eaa767fa4388ed4414365eaab78 100644 --- a/src/ui/composable_view.rs +++ b/src/ui/composable_view.rs @@ -1,3 +1,5 @@ +use crate::{mavlink, MSG_MANAGER}; + use super::{ panes::{Pane, PaneBehavior}, shortcuts, @@ -9,6 +11,7 @@ use egui_tiles::{Behavior, Container, Linear, LinearDir, Tile, TileId, Tiles, Tr pub struct ComposableView { panes_tree: Tree<Pane>, behavior: ComposableBehavior, + sources_window: SourceWindow, } // Implementing the default trait allows us to define a default configuration for our app @@ -21,6 +24,7 @@ impl Default for ComposableView { Self { panes_tree, behavior: Default::default(), + sources_window: Default::default(), } } } @@ -97,7 +101,17 @@ 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| { - egui::global_theme_preference_switch(ui); + // Window for the sources + self.sources_window.show_window(ui); + + // Horizontal belt of controls + ui.horizontal(|ui| { + egui::global_theme_preference_switch(ui); + + if ui.button("Sources").clicked() { + self.sources_window.visible = !self.sources_window.visible; + } + }) }); // A central panel covers the remainder of the screen, i.e. whatever area is left after adding other panels. @@ -107,6 +121,56 @@ impl eframe::App for ComposableView { } } +struct SourceWindow { + port: u16, + visible: bool, +} + +impl Default for SourceWindow { + fn default() -> Self { + Self { + port: mavlink::DEFAULT_ETHERNET_PORT, + visible: false, + } + } +} + +impl SourceWindow { + fn show_window(&mut self, ui: &mut egui::Ui) { + let mut window_is_open = self.visible; + let mut can_be_closed = false; + egui::Window::new("Sources") + .id(ui.id()) + .auto_sized() + .collapsible(true) + .movable(true) + .open(&mut window_is_open) + .show(ui.ctx(), |ui| { + self.ui(ui, &mut can_be_closed); + }); + self.visible = window_is_open && !can_be_closed; + } + + fn ui(&mut self, ui: &mut egui::Ui, can_be_closed: &mut bool) { + egui::Grid::new(ui.id()) + .num_columns(2) + .spacing([10.0, 5.0]) + .show(ui, |ui| { + ui.label("Ethernet Port:"); + ui.add(egui::Slider::new(&mut self.port, 1024..=65535).text("Port")); + ui.end_row(); + }); + if ui.button("Connect").clicked() { + MSG_MANAGER + .get() + .unwrap() + .lock() + .listen_from_ethernet_port(self.port); + *can_be_closed = true; + } + } +} + /// Behavior for the tree of panes in the composable view #[derive(Default)] pub struct ComposableBehavior {