From 1e44b2d59f883eacf256c54f7d8b9ada3032beb5 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 25 Aug 2026 17:43:51 +0200 Subject: [PATCH] add tmtc-utils crate --- .idea/runConfigurations/Check.xml | 19 -- .idea/runConfigurations/Clippy.xml | 19 -- .idea/runConfigurations/Clippy_Fix.xml | 18 - .idea/runConfigurations/Docs.xml | 19 -- .idea/runConfigurations/Doctest.xml | 19 -- .idea/runConfigurations/Examples.xml | 18 - .idea/runConfigurations/Format.xml | 19 -- .idea/runConfigurations/Run.xml | 18 - .idea/runConfigurations/Run_obsw_example.xml | 19 -- .../Run_obsw_simple_client.xml | 19 -- .idea/runConfigurations/Test.xml | 19 -- .idea/runConfigurations/Test_All.xml | 18 - .idea/runConfigurations/Test_satrs_core.xml | 19 -- Cargo.toml | 1 + .../stm32h7-nucleo-rtic/Cargo.toml | 1 - satrs-example/Cargo.toml | 1 - satrs-mib/Cargo.toml | 1 - satrs-shared/Cargo.toml | 1 - satrs/Cargo.toml | 1 - tmtc-utils/.gitignore | 2 + tmtc-utils/Cargo.toml | 16 + tmtc-utils/README.md | 6 + tmtc-utils/src/lib.rs | 5 + tmtc-utils/src/transport/mod.rs | 51 +++ tmtc-utils/src/transport/serial.rs | 320 ++++++++++++++++++ tmtc-utils/src/transport/tcp.rs | 158 +++++++++ tmtc-utils/src/transport/udp.rs | 236 +++++++++++++ 27 files changed, 795 insertions(+), 248 deletions(-) delete mode 100644 .idea/runConfigurations/Check.xml delete mode 100644 .idea/runConfigurations/Clippy.xml delete mode 100644 .idea/runConfigurations/Clippy_Fix.xml delete mode 100644 .idea/runConfigurations/Docs.xml delete mode 100644 .idea/runConfigurations/Doctest.xml delete mode 100644 .idea/runConfigurations/Examples.xml delete mode 100644 .idea/runConfigurations/Format.xml delete mode 100644 .idea/runConfigurations/Run.xml delete mode 100644 .idea/runConfigurations/Run_obsw_example.xml delete mode 100644 .idea/runConfigurations/Run_obsw_simple_client.xml delete mode 100644 .idea/runConfigurations/Test.xml delete mode 100644 .idea/runConfigurations/Test_All.xml delete mode 100644 .idea/runConfigurations/Test_satrs_core.xml create mode 100644 tmtc-utils/.gitignore create mode 100644 tmtc-utils/Cargo.toml create mode 100644 tmtc-utils/README.md create mode 100644 tmtc-utils/src/lib.rs create mode 100644 tmtc-utils/src/transport/mod.rs create mode 100644 tmtc-utils/src/transport/serial.rs create mode 100644 tmtc-utils/src/transport/tcp.rs create mode 100644 tmtc-utils/src/transport/udp.rs diff --git a/.idea/runConfigurations/Check.xml b/.idea/runConfigurations/Check.xml deleted file mode 100644 index 44c4c7f..0000000 --- a/.idea/runConfigurations/Check.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Clippy.xml b/.idea/runConfigurations/Clippy.xml deleted file mode 100644 index 8634d06..0000000 --- a/.idea/runConfigurations/Clippy.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Clippy_Fix.xml b/.idea/runConfigurations/Clippy_Fix.xml deleted file mode 100644 index 9722c38..0000000 --- a/.idea/runConfigurations/Clippy_Fix.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Docs.xml b/.idea/runConfigurations/Docs.xml deleted file mode 100644 index 43ef045..0000000 --- a/.idea/runConfigurations/Docs.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Doctest.xml b/.idea/runConfigurations/Doctest.xml deleted file mode 100644 index 73a8b79..0000000 --- a/.idea/runConfigurations/Doctest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Examples.xml b/.idea/runConfigurations/Examples.xml deleted file mode 100644 index e63f661..0000000 --- a/.idea/runConfigurations/Examples.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Format.xml b/.idea/runConfigurations/Format.xml deleted file mode 100644 index e6f696d..0000000 --- a/.idea/runConfigurations/Format.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Run.xml b/.idea/runConfigurations/Run.xml deleted file mode 100644 index 9f99c6e..0000000 --- a/.idea/runConfigurations/Run.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Run_obsw_example.xml b/.idea/runConfigurations/Run_obsw_example.xml deleted file mode 100644 index df610ba..0000000 --- a/.idea/runConfigurations/Run_obsw_example.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Run_obsw_simple_client.xml b/.idea/runConfigurations/Run_obsw_simple_client.xml deleted file mode 100644 index 4679680..0000000 --- a/.idea/runConfigurations/Run_obsw_simple_client.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Test.xml b/.idea/runConfigurations/Test.xml deleted file mode 100644 index 81a2166..0000000 --- a/.idea/runConfigurations/Test.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Test_All.xml b/.idea/runConfigurations/Test_All.xml deleted file mode 100644 index 6f884df..0000000 --- a/.idea/runConfigurations/Test_All.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Test_satrs_core.xml b/.idea/runConfigurations/Test_satrs_core.xml deleted file mode 100644 index fe5e4da..0000000 --- a/.idea/runConfigurations/Test_satrs_core.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index f8a4267..56dbd33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "satrs-example/client", "satrs-example/minisim", "satrs-shared", + "tmtc-utils", "embedded-examples/embedded-client", "embedded-examples/types", ] diff --git a/embedded-examples/stm32h7-nucleo-rtic/Cargo.toml b/embedded-examples/stm32h7-nucleo-rtic/Cargo.toml index 126406e..5271a72 100644 --- a/embedded-examples/stm32h7-nucleo-rtic/Cargo.toml +++ b/embedded-examples/stm32h7-nucleo-rtic/Cargo.toml @@ -1,5 +1,4 @@ [package] -authors = ["Robin Mueller "] name = "satrs-stm32h7-nucleo-rtic" edition = "2021" version = "0.1.0" diff --git a/satrs-example/Cargo.toml b/satrs-example/Cargo.toml index e7a8477..a953281 100644 --- a/satrs-example/Cargo.toml +++ b/satrs-example/Cargo.toml @@ -2,7 +2,6 @@ name = "satrs-example" version = "0.1.1" edition = "2024" -authors = ["Robin Mueller "] default-run = "satrs-example" homepage = "https://egit.irs.uni-stuttgart.de/rust/sat-rs" repository = "https://egit.irs.uni-stuttgart.de/rust/sat-rs" diff --git a/satrs-mib/Cargo.toml b/satrs-mib/Cargo.toml index 6c0c351..79d4d74 100644 --- a/satrs-mib/Cargo.toml +++ b/satrs-mib/Cargo.toml @@ -3,7 +3,6 @@ name = "satrs-mib" version = "0.1.3" edition = "2021" rust-version = "1.61" -authors = ["Robin Mueller "] description = """ Helper crate of the sat-rs framework to build a mission information base (MIB) from the On-Board Software (OBSW) code directly.""" diff --git a/satrs-shared/Cargo.toml b/satrs-shared/Cargo.toml index 4b172cd..64c5827 100644 --- a/satrs-shared/Cargo.toml +++ b/satrs-shared/Cargo.toml @@ -3,7 +3,6 @@ name = "satrs-shared" description = "Components shared by multiple sat-rs crates" version = "0.2.4" edition = "2021" -authors = ["Robin Mueller "] homepage = "https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/" repository = "https://egit.irs.uni-stuttgart.de/rust/sat-rs" license = "Apache-2.0" diff --git a/satrs/Cargo.toml b/satrs/Cargo.toml index a104eea..8140fee 100644 --- a/satrs/Cargo.toml +++ b/satrs/Cargo.toml @@ -3,7 +3,6 @@ name = "satrs" version = "0.3.0-alpha.3" edition = "2024" rust-version = "1.85.0" -authors = ["Robin Mueller "] description = "A library collection to build software for remote systems" homepage = "https://github.com/us-irs/sat-rs" repository = "https://egit.irs.uni-stuttgart.de/rust/sat-rs" diff --git a/tmtc-utils/.gitignore b/tmtc-utils/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/tmtc-utils/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/tmtc-utils/Cargo.toml b/tmtc-utils/Cargo.toml new file mode 100644 index 0000000..57f7c91 --- /dev/null +++ b/tmtc-utils/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "tmtc-utils" +description = "Commonly required utilities for writing TMTC clients with Rust" +version = "0.1.0" +edition = "2024" +homepage = "https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/" +repository = "https://egit.irs.uni-stuttgart.de/rust/sat-rs" +license = "Apache-2.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +thiserror = "2" +serialport = { version = "4" } +cobs = "0.5" +log = "0.4" diff --git a/tmtc-utils/README.md b/tmtc-utils/README.md new file mode 100644 index 0000000..00322a7 --- /dev/null +++ b/tmtc-utils/README.md @@ -0,0 +1,6 @@ +TMTC Utilities +========= + +This crate contains commonly required utilities when writing TMTC clients with Rust. +This includes a transport module which introduces a packet based communication abstraction and +some concrete implementations for common communication interfaces. diff --git a/tmtc-utils/src/lib.rs b/tmtc-utils/src/lib.rs new file mode 100644 index 0000000..b662403 --- /dev/null +++ b/tmtc-utils/src/lib.rs @@ -0,0 +1,5 @@ +//! # TMTC Utilities +//! +//! This crate contains commonly required utilities when writing TMTC clients with Rust. +#![deny(missing_docs)] +pub mod transport; diff --git a/tmtc-utils/src/transport/mod.rs b/tmtc-utils/src/transport/mod.rs new file mode 100644 index 0000000..c4db470 --- /dev/null +++ b/tmtc-utils/src/transport/mod.rs @@ -0,0 +1,51 @@ +//! # Packet Transport Module. +//! +//! Introduces a communication abstraction for packet based communication and some concrete +//! implementations for common communication interfaces. This can be useful for exchanging +//! something like CCSDS space packets over different transport mechanisms. +pub mod serial; +pub mod tcp; +pub mod udp; + +/// Generic send error. +#[derive(Debug, thiserror::Error)] +pub enum SendError { + /// Queue is full. + #[error("queue is full")] + QueueFull, + /// IO error. + #[error("io error: {0}")] + Io(#[from] std::io::Error), + /// Other error. + #[error("other error")] + Other, +} + +/// Generic reception error. +#[derive(Debug, thiserror::Error)] +pub enum ReceiveError { + /// IO error. + #[error("io error: {0}")] + Io(#[from] std::io::Error), + /// Other error. + #[error("other error")] + Other, +} + +/// Generic packet transport trait. +/// +/// This abstraction allows different transport mechanism for packetized data like CCSDS space +/// packets. +pub trait PacketTransport { + /// Send a packet. + fn send(&mut self, packet: &[u8]) -> Result<(), SendError>; + + /// Receivd packets. + /// + /// For each received packet, the closure will be called with the packet as an argument. + /// The function will return the number of received packets. + fn receive(&mut self, f: F) -> Result; + + /// Close the connection, used for graceful shutdowns. + fn close(&mut self); +} diff --git a/tmtc-utils/src/transport/serial.rs b/tmtc-utils/src/transport/serial.rs new file mode 100644 index 0000000..49c93e3 --- /dev/null +++ b/tmtc-utils/src/transport/serial.rs @@ -0,0 +1,320 @@ +//! # Serial packet transport with COBS encoding. +use std::time::Duration; + +use cobs::CobsDecoderOwned; + +use crate::transport::PacketTransport; + +/// Packet transport via a serial interface with COBS encoding. +pub struct PacketTransportSerialCobs { + /// Underlying serial port. + pub serial: Box, + /// Enables/disables logging of decoding errors. + pub log_decoding_errors: bool, + reception_buffer: [u8; 1024], + decoder: cobs::CobsDecoderOwned, +} + +impl PacketTransportSerialCobs { + /// Constructor which constructs the [serialport::SerialPort] and [cobs::CobsDecoderOwned] from + /// the passed parameters. + /// + /// The `max_rx_packet_size` parameter defines the expected maximum size of a received packet. + /// On non-linux platforms, the serial timeout parameter has to be specified as well. + pub fn new_from_params( + port_name: &str, + baud_rate: u32, + max_rx_packet_size: usize, + ) -> Result { + let serial = serialport::new(port_name, baud_rate).open_native()?; + // Not merged yet in upstream.. + //#[cfg(target_os = "linux")] + //serial.set_read_mode(ReadMode::Immediate)?; + Ok(Self::new( + Box::new(serial), + CobsDecoderOwned::new(max_rx_packet_size), + )) + } + + /// Generic constructor. + pub fn new(serial: Box, decoder: cobs::CobsDecoderOwned) -> Self { + Self { + serial, + decoder, + reception_buffer: [0u8; 1024], + log_decoding_errors: true, + } + } + + /// Set the serial port timeout. + pub fn set_serial_timeout(&mut self, timeout: Duration) -> Result<(), serialport::Error> { + self.serial.set_timeout(timeout) + } + + /// Send a packet. + /// + /// It encodes the packet using COBS encoding before sending it over the serial port. + pub fn send(&mut self, packet: &[u8]) -> Result<(), super::SendError> { + let encoded = cobs::encode_vec_including_sentinels(packet); + log::debug!("sending COBS encoded packet: {:?}", encoded); + self.serial.write_all(&encoded)?; + // This is required to avoid timeout errors on bursty data. + self.serial.flush()?; + Ok(()) + } + + /// Received packets. + /// + /// This function pulls bytes from the serial port and feeds them into the COBS decoder. + /// For each received packet, the closure will be called with the decoded packet as an argument. + /// The function will return the number of received packets. + pub fn receive(&mut self, mut f: F) -> Result { + let mut decoded_packets = 0; + loop { + match self.serial.read(&mut self.reception_buffer) { + Ok(read_bytes) => { + if read_bytes == 0 { + break; + } + for byte in self.reception_buffer[..read_bytes].iter() { + match self.decoder.feed(*byte) { + Ok(Some(packet_len)) => { + f(&self.decoder.dest()[0..packet_len]); + decoded_packets += 1; + } + Ok(None) => (), + Err(e) => self.error_handler(e), + } + } + } + Err(e) => { + if e.kind() == std::io::ErrorKind::TimedOut + || e.kind() == std::io::ErrorKind::WouldBlock + { + break; + } + return Err(super::ReceiveError::Io(e)); + } + } + } + Ok(decoded_packets) + } + + fn error_handler(&self, error: cobs::DecodeError) { + if self.log_decoding_errors { + log::warn!("COBS decoding error: {:?}", error); + } + } +} + +impl PacketTransport for PacketTransportSerialCobs { + fn send(&mut self, packet: &[u8]) -> Result<(), super::SendError> { + self.send(packet) + } + + fn receive(&mut self, f: F) -> Result { + self.receive(f) + } + + fn close(&mut self) {} +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::Duration; + + use serialport::SerialPort; + + #[derive(Debug)] + pub struct SerialPortMock { + baud: u32, + pub tx_data: std::sync::mpsc::Sender>, + pub rx_data: std::sync::mpsc::Receiver>, + } + + impl std::io::Write for SerialPortMock { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.tx_data.send(buf.to_vec()).unwrap(); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } + } + + impl std::io::Read for SerialPortMock { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + match self.rx_data.try_recv() { + Ok(packet) => { + buf[0..packet.len()].copy_from_slice(&packet); + Ok(packet.len()) + } + Err(e) => match e { + std::sync::mpsc::TryRecvError::Empty => Ok(0), + std::sync::mpsc::TryRecvError::Disconnected => panic!("sender disconnected"), + }, + } + } + } + + impl SerialPort for SerialPortMock { + fn name(&self) -> Option { + Some("mock".into()) + } + + fn baud_rate(&self) -> serialport::Result { + Ok(115200) + } + + fn data_bits(&self) -> serialport::Result { + Ok(serialport::DataBits::Eight) + } + + fn flow_control(&self) -> serialport::Result { + Ok(serialport::FlowControl::None) + } + + fn parity(&self) -> serialport::Result { + Ok(serialport::Parity::None) + } + + fn stop_bits(&self) -> serialport::Result { + Ok(serialport::StopBits::One) + } + + fn timeout(&self) -> std::time::Duration { + Duration::from_millis(0) + } + + fn set_baud_rate(&mut self, baud_rate: u32) -> serialport::Result<()> { + self.baud = baud_rate; + Ok(()) + } + + fn set_data_bits(&mut self, _data_bits: serialport::DataBits) -> serialport::Result<()> { + Ok(()) + } + + fn set_flow_control( + &mut self, + _flow_control: serialport::FlowControl, + ) -> serialport::Result<()> { + Ok(()) + } + + fn set_parity(&mut self, _parity: serialport::Parity) -> serialport::Result<()> { + Ok(()) + } + + fn set_stop_bits(&mut self, _stop_bits: serialport::StopBits) -> serialport::Result<()> { + Ok(()) + } + + fn set_timeout(&mut self, _timeout: std::time::Duration) -> serialport::Result<()> { + Ok(()) + } + + fn write_request_to_send(&mut self, _level: bool) -> serialport::Result<()> { + Ok(()) + } + + fn write_data_terminal_ready(&mut self, _level: bool) -> serialport::Result<()> { + Ok(()) + } + + fn read_clear_to_send(&mut self) -> serialport::Result { + Ok(true) + } + + fn read_data_set_ready(&mut self) -> serialport::Result { + Ok(true) + } + + fn read_ring_indicator(&mut self) -> serialport::Result { + Ok(true) + } + + fn read_carrier_detect(&mut self) -> serialport::Result { + Ok(true) + } + + fn bytes_to_read(&self) -> serialport::Result { + Err(serialport::Error::new( + serialport::ErrorKind::NoDevice, + "Mock does not support bytes_to_read", + )) + } + + fn bytes_to_write(&self) -> serialport::Result { + Ok(0) + } + + fn clear(&self, _buffer_to_clear: serialport::ClearBuffer) -> serialport::Result<()> { + Ok(()) + } + + fn try_clone(&self) -> serialport::Result> { + serialport::Result::Err(serialport::Error::new( + serialport::ErrorKind::NoDevice, + "Mock clone not supported", + )) + } + + fn set_break(&self) -> serialport::Result<()> { + Ok(()) + } + + fn clear_break(&self) -> serialport::Result<()> { + Ok(()) + } + } + + #[test] + fn basic_tx_test() { + let (tx_tx, tx_rx) = std::sync::mpsc::channel(); + let (_rx_tx, rx_rx) = std::sync::mpsc::channel(); + let mut transport = PacketTransportSerialCobs::new( + Box::new(SerialPortMock { + baud: 115200, + tx_data: tx_tx, + rx_data: rx_rx, + }), + CobsDecoderOwned::new(128), + ); + let sent_data = [1, 2, 3, 4]; + transport.send(&sent_data).unwrap(); + let encoded_data = tx_rx.recv().unwrap(); + assert_eq!( + encoded_data, + cobs::encode_vec_including_sentinels(&sent_data) + ); + } + + #[test] + fn basic_rx_test() { + let (tx_tx, _tx_rx) = std::sync::mpsc::channel(); + let (rx_tx, rx_rx) = std::sync::mpsc::channel(); + let mut transport = PacketTransportSerialCobs::new( + Box::new(SerialPortMock { + baud: 115200, + tx_data: tx_tx, + rx_data: rx_rx, + }), + CobsDecoderOwned::new(128), + ); + let rx_data = [1, 2, 3, 4]; + rx_tx + .send(cobs::encode_vec_including_sentinels(&rx_data)) + .unwrap(); + assert_eq!( + transport + .receive(|packet| { + assert_eq!(packet, &rx_data); + }) + .unwrap(), + 1 + ); + } +} diff --git a/tmtc-utils/src/transport/tcp.rs b/tmtc-utils/src/transport/tcp.rs new file mode 100644 index 0000000..7af22e2 --- /dev/null +++ b/tmtc-utils/src/transport/tcp.rs @@ -0,0 +1,158 @@ +//! # Packet transport via TCP with COBS encoding. +use std::{ + io::{Read as _, Write as _}, + time::Duration, +}; + +use crate::transport::PacketTransport; + +/// Packet transport via TCP with COBS encoding. +/// +/// Currently only allows a maxium packet size of 4096. +pub struct PacketTransportTcpWithCobs { + /// Underlying TCP stream. + pub tcp_stream: std::net::TcpStream, + /// Can be used to disable logging of decoding errors. + pub log_decoding_errors: bool, + /// Decoder object. + decoder: cobs::CobsDecoderOwned, + reception_buffer: [u8; 4096], +} + +impl PacketTransportTcpWithCobs { + /// Generic constructor. + /// + /// The `tcp_stream` parameter is the underlying TCP stream which should already be connected. + pub fn new( + tcp_stream: std::net::TcpStream, + decoder: cobs::CobsDecoderOwned, + ) -> std::io::Result { + tcp_stream.set_nonblocking(true)?; + tcp_stream.set_read_timeout(Some(Duration::from_millis(100)))?; + Ok(Self { + tcp_stream, + decoder, + reception_buffer: [0u8; 4096], + log_decoding_errors: true, + }) + } + + /// Send a packet. + /// + /// It encodes the packet using COBS encoding before sending it over the TCP stream. + pub fn send(&mut self, packet: &[u8]) -> Result<(), super::SendError> { + let cobs_encoded_packet = cobs::encode_vec_including_sentinels(packet); + self.tcp_stream.write_all(&cobs_encoded_packet)?; + Ok(()) + } + + /// Received packets. + /// + /// This function pulls bytes from the TCP stream and feeds them into the COBS decoder. + /// For each received packet, the closure will be called with the decoded packet as an argument. + /// The function will return the number of received packets. + /// + /// Please note that this function may block on the TCP stream read call, but it will not + /// block indifinitely due to the read timeout set on the TCP stream. + pub fn receive(&mut self, mut f: impl FnMut(&[u8])) -> Result { + let mut decoded_packets = 0; + loop { + let read_size = self + .tcp_stream + .read(&mut self.reception_buffer) + .unwrap_or(0); + if read_size == 0 { + break; + } + for byte in &self.reception_buffer[0..read_size] { + match self.decoder.feed(*byte) { + Ok(Some(packet_len)) => { + f(&self.decoder.dest()[0..packet_len]); + decoded_packets += 1; + } + Ok(None) => (), + Err(e) => self.error_handler(e), + } + } + } + Ok(decoded_packets) + } + + fn error_handler(&self, error: cobs::DecodeError) { + if self.log_decoding_errors { + log::warn!("COBS decoding error: {:?}", error); + } + } + + /// Close the connection by shutting down the TCP stream. + pub fn close(&mut self) -> std::io::Result<()> { + self.tcp_stream.shutdown(std::net::Shutdown::Both) + } +} + +impl PacketTransport for PacketTransportTcpWithCobs { + fn send(&mut self, packet: &[u8]) -> Result<(), super::SendError> { + self.send(packet) + } + + fn receive(&mut self, f: F) -> Result { + self.receive(f) + } + + fn close(&mut self) { + let _ = self.close(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn basic_send_test() { + let tcp_server = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + tcp_server + .set_nonblocking(true) + .expect("failed to set blocking mode"); + let addr = tcp_server.local_addr().unwrap(); + let tcp_client = std::net::TcpStream::connect(addr).unwrap(); + let mut transport = + PacketTransportTcpWithCobs::new(tcp_client, cobs::CobsDecoderOwned::new(1024)).unwrap(); + let packet = [1, 2, 3, 4]; + transport.send(&packet).unwrap(); + tcp_server + .accept() + .map(|(mut stream, _)| { + let mut buffer = [0u8; 1024]; + let read_size = stream.read(&mut buffer).unwrap(); + let decoded_packet = cobs::decode_vec(&buffer[0..read_size]).unwrap(); + assert_eq!(decoded_packet, packet); + }) + .unwrap(); + } + + #[test] + fn basic_receive_test() { + let tcp_server = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + tcp_server + .set_nonblocking(true) + .expect("failed to set blocking mode"); + let addr = tcp_server.local_addr().unwrap(); + let tcp_client = std::net::TcpStream::connect(addr).unwrap(); + let mut transport = + PacketTransportTcpWithCobs::new(tcp_client, cobs::CobsDecoderOwned::new(1024)).unwrap(); + let rx_data = [1, 2, 3, 4]; + let encoded_data = cobs::encode_vec_including_sentinels(&rx_data); + tcp_server + .accept() + .map(|(mut stream, _)| { + stream.write_all(&encoded_data).unwrap(); + }) + .unwrap(); + transport + .receive(|packet| { + assert_eq!(packet, &rx_data); + }) + .unwrap(); + } +} diff --git a/tmtc-utils/src/transport/udp.rs b/tmtc-utils/src/transport/udp.rs new file mode 100644 index 0000000..21cdcd3 --- /dev/null +++ b/tmtc-utils/src/transport/udp.rs @@ -0,0 +1,236 @@ +//! # Packet transport via UDP + +use std::io::ErrorKind; +use std::net::ToSocketAddrs; + +use crate::transport::PacketTransport; + +/// Generic packet transport via UDP. +/// +/// Currently only allows a maxium packet size of 4096. +pub struct PacketTransportUdp { + /// Underlying UDP socket. + pub socket: std::net::UdpSocket, + target: std::net::SocketAddr, + reception_buffer: [u8; 4096], +} + +impl PacketTransportUdp { + /// Generic constructor. + /// + /// The `socket` parameter is the underlying UDP stream which should already be connected. + /// It will be set non-blocking by the construtor. + pub fn new( + socket: std::net::UdpSocket, + target: std::net::SocketAddr, + ) -> Result { + socket.set_nonblocking(true)?; + Ok(Self { + socket, + target, + reception_buffer: [0u8; 4096], + }) + } + + /// Update default target. + pub fn set_default_target(&mut self, target: std::net::SocketAddr) { + self.target = target; + } + + /// Send a packet to the target address specified in the constructor. + pub fn send(&mut self, packet: &[u8]) -> Result<(), super::SendError> { + self.socket + .send_to(packet, self.target) + .map_err(super::SendError::Io)?; + Ok(()) + } + + /// Send packet to a specific address. + pub fn send_to( + &mut self, + packet: &[u8], + addr: A, + ) -> Result<(), super::SendError> { + self.socket + .send_to(packet, addr) + .map_err(super::SendError::Io)?; + Ok(()) + } + + /// Receive packets and call the provided callback for each received packet. + pub fn receive(&mut self, mut f: F) -> Result { + let mut packets_received = 0; + loop { + match self.socket.recv_from(&mut self.reception_buffer) { + Ok((bytes, _)) => { + packets_received += 1; + f(&self.reception_buffer[..bytes]); + } + Err(e) => { + if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut { + break; + } + log::error!("UDP reception error: {e}"); + } + } + } + Ok(packets_received) + } +} + +impl PacketTransport for PacketTransportUdp { + fn send(&mut self, packet: &[u8]) -> Result<(), super::SendError> { + self.send(packet) + } + + fn receive(&mut self, f: F) -> Result { + self.receive(f) + } + + fn close(&mut self) {} +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn basic_send_test() { + let receiver = std::net::UdpSocket::bind("127.0.0.1:0").unwrap(); + let sender = std::net::UdpSocket::bind("127.0.0.1:0").unwrap(); + + let receiver_addr = receiver.local_addr().unwrap(); + sender.connect(receiver_addr).unwrap(); + + let mut transport = PacketTransportUdp::new(sender, receiver_addr).unwrap(); + let payload = [1u8, 2, 3, 4]; + + transport.send(&payload).unwrap(); + + let mut buf = [0u8; 16]; + let (len, from) = receiver.recv_from(&mut buf).unwrap(); + + assert_eq!(&buf[..len], &payload); + assert_eq!(from, transport.socket.local_addr().unwrap()); + } + + #[test] + fn receive_is_non_blocking_when_no_data_is_available() { + let receiver = std::net::UdpSocket::bind("127.0.0.1:0").unwrap(); + let sender = std::net::UdpSocket::bind("127.0.0.1:0").unwrap(); + + let receiver_addr = receiver.local_addr().unwrap(); + let mut transport = PacketTransportUdp::new(sender, receiver_addr).unwrap(); + + let start = std::time::Instant::now(); + let mut callback_called = false; + + let packets_received = transport + .receive(|_| { + callback_called = true; + }) + .unwrap(); + + let elapsed = start.elapsed(); + + assert_eq!(packets_received, 0); + assert!(!callback_called); + assert!( + elapsed < std::time::Duration::from_millis(50), + "receive() took too long for a non-blocking socket: {:?}", + elapsed + ); + } + + #[test] + fn basic_receive_test_single() { + let plain_sender = std::net::UdpSocket::bind("127.0.0.1:0").unwrap(); + let transport_socket = std::net::UdpSocket::bind("127.0.0.1:0").unwrap(); + + let transport_addr = transport_socket.local_addr().unwrap(); + + let mut transport = PacketTransportUdp::new(transport_socket, transport_addr).unwrap(); + + plain_sender + .send_to(&[1u8, 2, 3, 4], transport_addr) + .unwrap(); + + let mut received_packets: Vec> = Vec::new(); + let packets_received = transport + .receive(|packet| received_packets.push(packet.to_vec())) + .unwrap(); + + assert_eq!(packets_received, 1); + assert_eq!(received_packets.len(), 1); + assert_eq!(received_packets[0], vec![1u8, 2, 3, 4]); + } + + #[test] + fn multi_packet_receive_test() { + let plain_sender = std::net::UdpSocket::bind("127.0.0.1:0").unwrap(); + let transport_socket = std::net::UdpSocket::bind("127.0.0.1:0").unwrap(); + + let receiver_addr = transport_socket.local_addr().unwrap(); + let mut transport = PacketTransportUdp::new(transport_socket, receiver_addr).unwrap(); + + plain_sender + .send_to(&[1u8, 2, 3, 4], receiver_addr) + .unwrap(); + plain_sender + .send_to(&[5u8, 6, 7, 8], receiver_addr) + .unwrap(); + plain_sender + .send_to(&[9u8, 10, 11, 12], receiver_addr) + .unwrap(); + + let mut received_packets: Vec> = Vec::new(); + let packets_received = transport + .receive(|packet| received_packets.push(packet.to_vec())) + .unwrap(); + + assert_eq!(packets_received, 3); + assert_eq!(received_packets.len(), 3); + assert_eq!(received_packets[0], vec![1u8, 2, 3, 4]); + assert_eq!(received_packets[1], vec![5u8, 6, 7, 8]); + assert_eq!(received_packets[2], vec![9u8, 10, 11, 12]); + } + + #[test] + fn send_and_receive_test() { + // Bind both sockets + let plain_receiver = std::net::UdpSocket::bind("127.0.0.1:0").unwrap(); + let sender_socket = std::net::UdpSocket::bind("127.0.0.1:0").unwrap(); + + let receiver_addr = plain_receiver.local_addr().unwrap(); + + // Build the transport around the sender socket + let mut send_transport = PacketTransportUdp::new(sender_socket, receiver_addr).unwrap(); + + // Send a packet + let payload = [1u8, 2, 3, 4]; + send_transport.send(&payload).unwrap(); + + // Plain receiver reads what was sent + let mut buf = [0u8; 4096]; + let (n, src) = plain_receiver.recv_from(&mut buf).unwrap(); + assert_eq!(n, payload.len()); + assert_eq!(&buf[..n], &payload); + + // Plain receiver sends back some test data + let reply = [5u8, 6, 7, 8]; + plain_receiver.send_to(&reply, src).unwrap(); + + // Read the reply via the transport (non-blocking socket, reply should already be in flight) + let mut received_packets: Vec> = Vec::new(); + // Small yield to ensure the reply has arrived on the loopback interface + std::thread::sleep(std::time::Duration::from_millis(10)); + + let packets_received = send_transport + .receive(|packet| received_packets.push(packet.to_vec())) + .unwrap(); + + assert_eq!(packets_received, 1); + assert_eq!(received_packets.len(), 1); + assert_eq!(received_packets[0], reply.to_vec()); + } +}