diff --git a/Cargo.lock b/Cargo.lock index 79aa3aeef6726adfbcf333daadc020a6598d9489..fbc96f04cf891edfccba6e0babd32cce2d6a18fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1799,6 +1799,16 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "io-kit-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" +dependencies = [ + "core-foundation-sys", + "mach2", +] + [[package]] name = "ioctl-rs" version = "0.1.6" @@ -1914,6 +1924,26 @@ dependencies = [ "redox_syscall 0.5.8", ] +[[package]] +name = "libudev" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0" +dependencies = [ + "libc", + "libudev-sys", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1948,6 +1978,15 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -2130,6 +2169,17 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + [[package]] name = "nix" version = "0.29.0" @@ -2874,6 +2924,7 @@ dependencies = [ "ring-channel", "serde", "serde_json", + "serialport", "skyward_mavlink", "strum", "strum_macros", @@ -2978,6 +3029,25 @@ dependencies = [ "serial-core", ] +[[package]] +name = "serialport" +version = "4.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ecfc4858c2266c7695d8b8460bbd612fa81bd2e250f5f0dd16195e4b4f8b3d8" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "core-foundation 0.10.0", + "core-foundation-sys", + "io-kit-sys", + "libudev", + "mach2", + "nix 0.26.4", + "scopeguard", + "unescaper", + "winapi", +] + [[package]] name = "sha1" version = "0.10.6" @@ -3441,6 +3511,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "unescaper" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c878a167baa8afd137494101a688ef8c67125089ff2249284bd2b5f9bfedb815" +dependencies = [ + "thiserror 1.0.69", +] + [[package]] name = "unicase" version = "2.8.1" @@ -4364,7 +4443,7 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix", + "nix 0.29.0", "ordered-stream", "rand", "serde", diff --git a/Cargo.toml b/Cargo.toml index 74d48c05a7f7710372ada9a6f21f9ed8979fc846..be31c45f165358550e065d57e2ae05e09efa6070 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ skyward_mavlink = { git = "https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyw "serde", ] } mavlink-bindgen = { version = "0.13.1", features = ["serde"] } +serialport = "4.7.0" # ========= Persistency ========= serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/src/main.rs b/src/main.rs index df0dcc6b3874cde18eeee4307973f6d20715102a..baa1fb9f8d6916358b8d9e19bfce4f7a1fe94799 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,9 @@ mod error; mod mavlink; +mod serial; mod ui; +mod utils; use std::{ num::NonZeroUsize, diff --git a/src/mavlink/base.rs b/src/mavlink/base.rs index a1aa3c53a4ef2bab982ecaaef808c7af0e66ffbe..063871967cae1df0f5dc50a98d2366a4878caa95 100644 --- a/src/mavlink/base.rs +++ b/src/mavlink/base.rs @@ -61,7 +61,9 @@ where } /// 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); +pub fn byte_parser<'a>( + reader: impl std::io::Read + 'a, +) -> impl Iterator<Item = (MavHeader, MavMessage)> + 'a { + let mut reader = PeekReader::new(reader); 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 index 2f611c806d0691163b1d3a4ed724685408ff7dd1..cb7e90c64e1ac7379c38fd5ea5429a6ddb90c6cd 100644 --- a/src/mavlink/message_broker.rs +++ b/src/mavlink/message_broker.rs @@ -7,6 +7,7 @@ use std::{ collections::{HashMap, VecDeque}, + io::Write, num::NonZeroUsize, sync::{ atomic::{AtomicBool, Ordering}, @@ -21,7 +22,7 @@ use tokio::{net::UdpSocket, task::JoinHandle}; use tracing::{debug, trace}; use uuid::Uuid; -use crate::mavlink::byte_parser; +use crate::{error::ErrInstrument, mavlink::byte_parser, utils::RingBuffer}; use super::{MavlinkResult, Message, TimedMessage}; @@ -111,8 +112,7 @@ impl MessageBroker { } /// Start a listener task that listens to incoming messages from the given - /// Ethernet port, and accumulates them in a ring buffer, read only when - /// views request a refresh. + /// Ethernet port and stores them in a ring buffer. pub fn listen_from_ethernet_port(&mut self, port: u16) { // Stop the current listener if it exists self.stop_listening(); @@ -150,6 +150,51 @@ impl MessageBroker { self.task = Some(handle); } + /// Start a listener task that listens to incoming messages from the given + /// serial port and stores them in a ring buffer. + pub fn listen_from_serial_port(&mut self, port: String, baud_rate: u32) { + // 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 running_flag = self.running_flag.clone(); + + debug!("Spawning listener task at {}", port); + let handle = tokio::task::spawn_blocking(move || { + let mut serial_port = serialport::new(port, baud_rate) + .timeout(std::time::Duration::from_millis(100)) + .open() + .context("Failed to open serial port")?; + debug!("Listening on serial port"); + + let mut ring_buf = RingBuffer::<1024>::new(); + let mut temp_buf = [0; 512]; + // need to do a better error handling for this (need toast errors) + while running_flag.load(Ordering::Relaxed) { + let result = serial_port + .read(&mut temp_buf) + .log_expect("Failed to read from serial port"); + debug!("Read {} bytes from serial port", result); + trace!("data read from serial: {:?}", &temp_buf[..result]); + ring_buf + .write(&temp_buf[..result]) + .log_expect("Failed to write to ring buffer, check buffer size"); + for (_, mav_message) in byte_parser(&mut ring_buf) { + debug!("Received message: {:?}", mav_message); + tx.send(TimedMessage::just_received(mav_message)) + .context("Failed to send message")?; + ctx.request_repaint(); + } + } + + Ok::<(), anyhow::Error>(()) + }); + self.task = Some(handle); + } + pub fn unsubscribe_all_views(&mut self) { self.update_queues.clear(); } diff --git a/src/serial.rs b/src/serial.rs new file mode 100644 index 0000000000000000000000000000000000000000..29ffb2d17feb44a2167696ec9c9e5226a151641d --- /dev/null +++ b/src/serial.rs @@ -0,0 +1,28 @@ +//! Serial port utilities +//! +//! This module provides utilities for working with serial ports, such as listing all available serial ports and finding the first serial port that contains "STM32" or "ST-LINK" in its product name. + +use anyhow::Context; + +use crate::error::ErrInstrument; + +/// Get the first serial port that contains "STM32" or "ST-LINK" in its product name +pub fn get_first_stm32_serial_port() -> Option<String> { + let ports = serialport::available_ports().log_expect("Serial ports cannot be listed!"); + for port in ports { + if let serialport::SerialPortType::UsbPort(info) = port.port_type { + if let Some(p) = info.product { + if p.contains("STM32") || p.contains("ST-LINK") { + return Some(port.port_name); + } + } + } + } + None +} + +/// Get a list of all serial ports available on the system +pub fn list_all_serial_ports() -> anyhow::Result<Vec<String>> { + let ports = serialport::available_ports().context("No serial ports found!")?; + Ok(ports.iter().map(|p| p.port_name.clone()).collect()) +} diff --git a/src/ui/composable_view.rs b/src/ui/composable_view.rs index 2d5ecd5e0d887f9e5441dabf00fa947cdd067236..743f366a8301290a22b5ebcf1054a562b5c921f9 100644 --- a/src/ui/composable_view.rs +++ b/src/ui/composable_view.rs @@ -1,4 +1,9 @@ -use crate::{error::ErrInstrument, mavlink, msg_broker, ui::panes::PaneKind}; +use crate::{ + error::ErrInstrument, + mavlink, msg_broker, + serial::{get_first_stm32_serial_port, list_all_serial_ports}, + ui::panes::PaneKind, +}; use super::{ panes::{Pane, PaneBehavior}, @@ -12,7 +17,8 @@ use std::{ path::{Path, PathBuf}, }; -use egui::{Key, Modifiers}; +use egui::{Align2, Button, ComboBox, Key, Modifiers, Vec2}; +use egui_extras::{Size, StripBuilder}; use egui_tiles::{Behavior, Container, Linear, LinearDir, Tile, TileId, Tiles, Tree}; use serde::{Deserialize, Serialize}; use tracing::{debug, error, trace}; @@ -272,29 +278,45 @@ impl ComposableViewState { } } -struct SourceWindow { - port: u16, - visible: bool, +#[derive(Debug, PartialEq, Eq, Default)] +enum ConnectionKind { + #[default] + Ethernet, + Serial, } -impl Default for SourceWindow { +#[derive(Debug)] +enum ConnectionDetails { + Ethernet { port: u16 }, + Serial { port: String, baud_rate: u32 }, +} + +impl Default for ConnectionDetails { fn default() -> Self { - Self { + ConnectionDetails::Ethernet { port: mavlink::DEFAULT_ETHERNET_PORT, - visible: false, } } } +#[derive(Debug, Default)] +struct SourceWindow { + visible: bool, + connected: bool, + connection_kind: ConnectionKind, + connection_details: ConnectionDetails, +} + 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() + .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) + .max_width(200.0) .collapsible(false) - .movable(true) + .resizable(false) .open(&mut window_is_open) .show(ui.ctx(), |ui| { self.ui(ui, &mut can_be_closed); @@ -303,22 +325,123 @@ impl SourceWindow { } 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_broker!().listen_from_ethernet_port(self.port); - *can_be_closed = true; - } + let SourceWindow { + connected, + connection_kind, + connection_details, + .. + } = self; + ui.label("Select Source:"); + ui.horizontal_top(|ui| { + ui.radio_value(connection_kind, ConnectionKind::Ethernet, "Ethernet"); + ui.radio_value(connection_kind, ConnectionKind::Serial, "Serial"); + }); + + ui.separator(); + + match *connection_kind { + ConnectionKind::Ethernet => { + if !matches!(connection_details, ConnectionDetails::Ethernet { .. }) { + *connection_details = ConnectionDetails::Ethernet { + port: mavlink::DEFAULT_ETHERNET_PORT, + }; + } + let ConnectionDetails::Ethernet { port } = connection_details else { + error!("UNREACHABLE: Connection kind is not Ethernet"); + unreachable!("Connection kind is not Ethernet"); + }; + + egui::Grid::new("grid") + .num_columns(2) + .spacing([10.0, 5.0]) + .show(ui, |ui| { + ui.label("Ethernet Port:"); + ui.add(egui::DragValue::new(port).range(0..=65535).speed(10)); + ui.end_row(); + }); + } + ConnectionKind::Serial => { + if !matches!(connection_details, ConnectionDetails::Serial { .. }) { + *connection_details = ConnectionDetails::Serial { + // Default to the first STM32 serial port if available, otherwise + // default to the first serial port available + port: get_first_stm32_serial_port().unwrap_or( + list_all_serial_ports() + .ok() + .and_then(|ports| ports.first().cloned()) + .unwrap_or_default(), + ), + baud_rate: 115200, + }; + } + let ConnectionDetails::Serial { port, baud_rate } = connection_details else { + error!("UNREACHABLE: Connection kind is not Serial"); + unreachable!("Connection kind is not Serial"); + }; + + egui::Grid::new("grid") + .num_columns(2) + .spacing([10.0, 5.0]) + .show(ui, |ui| { + ui.label("Serial Port:"); + ComboBox::from_id_salt("serial_port") + .selected_text(port.clone()) + .show_ui(ui, |ui| { + for available_port in list_all_serial_ports().unwrap_or_default() { + ui.selectable_value( + port, + available_port.clone(), + available_port, + ); + } + }); + ui.end_row(); + ui.label("Baud Rate:"); + ui.add( + egui::DragValue::new(baud_rate) + .range(110..=256000) + .speed(100), + ); + ui.end_row(); + }); + } + }; + + ui.separator(); + + ui.allocate_ui(Vec2::new(ui.available_width(), 20.0), |ui| { + StripBuilder::new(ui) + .sizes(Size::remainder(), 2) // top cell + .horizontal(|mut strip| { + strip.cell(|ui| { + let btn1 = Button::new("Connect"); + ui.add_enabled_ui(!*connected, |ui| { + if ui.add_sized(ui.available_size(), btn1).clicked() { + match connection_details { + ConnectionDetails::Ethernet { port } => { + msg_broker!().listen_from_ethernet_port(*port); + } + ConnectionDetails::Serial { port, baud_rate } => { + msg_broker!() + .listen_from_serial_port(port.clone(), *baud_rate); + } + } + *can_be_closed = true; + *connected = true; + } + }); + }); + strip.cell(|ui| { + let btn2 = Button::new("Disconnect"); + ui.add_enabled_ui(*connected, |ui| { + if ui.add_sized(ui.available_size(), btn2).clicked() { + msg_broker!().stop_listening(); + *connected = false; + } + }); + }); + }); + }); } } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000000000000000000000000000000000000..d3699bbc4d745f0db992d5721d08abbd1ec550ba --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,58 @@ +#[derive(Debug)] +pub struct RingBuffer<const G: usize> { + buffer: Box<[u8; G]>, + write_cursor: usize, + read_cursor: usize, +} + +impl<const G: usize> std::io::Write for RingBuffer<G> { + fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { + let mut written = 0; + for byte in buf { + if self.is_full() { + Err(std::io::Error::new( + std::io::ErrorKind::WriteZero, + "Buffer full", + ))?; + } + self.buffer[self.write_cursor] = *byte; + self.write_cursor = (self.write_cursor + 1) % G; + written += 1; + } + Ok(written) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +impl<const G: usize> std::io::Read for RingBuffer<G> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { + let mut read = 0; + while read < buf.len() { + if self.read_cursor == self.write_cursor { + break; + } + buf[read] = self.buffer[self.read_cursor]; + self.read_cursor = (self.read_cursor + 1) % G; + read += 1; + } + Ok(read) + } +} + +impl<const G: usize> RingBuffer<G> { + pub fn new() -> Self { + Self { + buffer: Box::new([0; G]), + write_cursor: 0, + read_cursor: 0, + } + } + + #[inline] + pub fn is_full(&self) -> bool { + (self.write_cursor + 1) % G == self.read_cursor + } +}