commit 45c1c05f8e36aac0a005da504e8c39378834ac93 Author: Robin Mueller Date: Thu Oct 30 11:39:31 2025 +0100 tmtc-utils init commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..041f16d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "tmtc-utils" +version = "0.1.0" +edition = "2024" + +[dependencies] +thiserror = "2" +serialport = "4" +cobs = "0.5" +log = "0.4" diff --git a/README.md b/README.md new file mode 100644 index 0000000..00322a7 --- /dev/null +++ b/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/justfile b/justfile new file mode 100644 index 0000000..a4f4cf8 --- /dev/null +++ b/justfile @@ -0,0 +1,29 @@ +all: check build test clippy fmt docs coverage + +clippy: + cargo clippy -- -D warnings + +fmt: + cargo fmt --all -- --check + +check: + cargo check --all-features + +test: + cargo nextest r --all-features + cargo test --doc + +build: + cargo build --all-features + +docs: + RUSTDOCFLAGS="--cfg docsrs -Z unstable-options --generate-link-to-definition" cargo +nightly doc + +docs-html: + RUSTDOCFLAGS="--cfg docsrs -Z unstable-options --generate-link-to-definition" cargo +nightly doc --open + +coverage: + cargo llvm-cov nextest + +coverage-html: + cargo llvm-cov nextest --html --open diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b662403 --- /dev/null +++ b/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/src/transport/mod.rs b/src/transport/mod.rs new file mode 100644 index 0000000..423da47 --- /dev/null +++ b/src/transport/mod.rs @@ -0,0 +1,47 @@ +//! # 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; + +/// 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; +} diff --git a/src/transport/serial.rs b/src/transport/serial.rs new file mode 100644 index 0000000..733e52f --- /dev/null +++ b/src/transport/serial.rs @@ -0,0 +1,296 @@ +//! # Serial packet transport with COBS encoding. +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. + 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()?; + Ok(PacketTransportSerialCobs { + serial, + decoder: CobsDecoderOwned::new(max_rx_packet_size), + reception_buffer: [0u8; 1024], + log_decoding_errors: true, + }) + } + + /// Generic constructor. + pub fn new(serial: Box, decoder: cobs::CobsDecoderOwned) -> Self { + PacketTransportSerialCobs { + serial, + decoder, + reception_buffer: [0u8; 1024], + log_decoding_errors: true, + } + } + + /// Send a packet. + /// + /// It encodes the packet using COBS encoding before sending it over the serial port. + 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)?; + 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. + fn receive(&mut self, mut f: F) -> Result { + let mut decoded_packets = 0; + loop { + let read_bytes = self.serial.read(&mut self.reception_buffer)?; + 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), + } + } + } + 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) + } +} + +#[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/src/transport/tcp.rs b/src/transport/tcp.rs new file mode 100644 index 0000000..1c65b00 --- /dev/null +++ b/src/transport/tcp.rs @@ -0,0 +1,137 @@ +//! # Packet transport via TCP with COBS encoding. +use std::io::{Read as _, Write as _}; + +use crate::transport::PacketTransport; + +/// Packet transport via TCP with COBS encoding. +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; 1024], +} + +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) -> Self { + tcp_stream.set_nonblocking(true).unwrap(); + Self { + tcp_stream, + decoder, + reception_buffer: [0u8; 1024], + 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. + 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); + } + } +} + +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) + } +} + +#[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)); + 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)); + 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(); + } +}