Merge pull request 'add tmtc-utils crate' (#273) from add-tmtc-utils-crate into main

Reviewed-on: #273
This commit was merged in pull request #273.
This commit is contained in:
2026-08-25 17:54:14 +02:00
27 changed files with 795 additions and 248 deletions
-19
View File
@@ -1,19 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Check" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="check" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="false" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
-19
View File
@@ -1,19 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Clippy" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="clippy" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="true" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
-18
View File
@@ -1,18 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Clippy Fix" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="clippy --fix" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
-19
View File
@@ -1,19 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Docs" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="doc --all-features" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
-19
View File
@@ -1,19 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Doctest" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="test --doc" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
-18
View File
@@ -1,18 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Examples" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="run --example test" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
-19
View File
@@ -1,19 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Format" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="fmt" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
-18
View File
@@ -1,18 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="run" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
-19
View File
@@ -1,19 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run obsw example" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="run -p satrs-example --bin satrs-example" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
-19
View File
@@ -1,19 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run obsw simple client" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="run --package fsrc-example --bin client" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
-19
View File
@@ -1,19 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Test" type="CargoCommandRunConfiguration" factoryName="Cargo Command" nameIsGenerated="true">
<option name="command" value="test" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="true" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
-18
View File
@@ -1,18 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Test All" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="test -- --include-ignored" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
-19
View File
@@ -1,19 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Test satrs-core" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="test -p satrs-core --all-features" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="true" />
<option name="emulateTerminal" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
+1
View File
@@ -8,6 +8,7 @@ members = [
"satrs-example/client",
"satrs-example/minisim",
"satrs-shared",
"tmtc-utils",
"embedded-examples/embedded-client",
"embedded-examples/types",
]
@@ -1,5 +1,4 @@
[package]
authors = ["Robin Mueller <robin.mueller.m@gmail.com>"]
name = "satrs-stm32h7-nucleo-rtic"
edition = "2021"
version = "0.1.0"
-1
View File
@@ -2,7 +2,6 @@
name = "satrs-example"
version = "0.1.1"
edition = "2024"
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
default-run = "satrs-example"
homepage = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
repository = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
-1
View File
@@ -3,7 +3,6 @@ name = "satrs-mib"
version = "0.1.3"
edition = "2021"
rust-version = "1.61"
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
description = """
Helper crate of the sat-rs framework to build a mission information base (MIB) from the
On-Board Software (OBSW) code directly."""
-1
View File
@@ -3,7 +3,6 @@ name = "satrs-shared"
description = "Components shared by multiple sat-rs crates"
version = "0.2.4"
edition = "2021"
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
homepage = "https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/"
repository = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
license = "Apache-2.0"
-1
View File
@@ -3,7 +3,6 @@ name = "satrs"
version = "0.3.0-alpha.3"
edition = "2024"
rust-version = "1.85.0"
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
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"
+2
View File
@@ -0,0 +1,2 @@
/target
/Cargo.lock
+16
View File
@@ -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"
+6
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
//! # TMTC Utilities
//!
//! This crate contains commonly required utilities when writing TMTC clients with Rust.
#![deny(missing_docs)]
pub mod transport;
+51
View File
@@ -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<F: FnMut(&[u8])>(&mut self, f: F) -> Result<usize, ReceiveError>;
/// Close the connection, used for graceful shutdowns.
fn close(&mut self);
}
+320
View File
@@ -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<dyn serialport::SerialPort>,
/// 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<Self, std::io::Error> {
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<dyn serialport::SerialPort>, 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<F: FnMut(&[u8])>(&mut self, mut f: F) -> Result<usize, super::ReceiveError> {
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<F: FnMut(&[u8])>(&mut self, f: F) -> Result<usize, super::ReceiveError> {
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<Vec<u8>>,
pub rx_data: std::sync::mpsc::Receiver<Vec<u8>>,
}
impl std::io::Write for SerialPortMock {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
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<usize> {
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<String> {
Some("mock".into())
}
fn baud_rate(&self) -> serialport::Result<u32> {
Ok(115200)
}
fn data_bits(&self) -> serialport::Result<serialport::DataBits> {
Ok(serialport::DataBits::Eight)
}
fn flow_control(&self) -> serialport::Result<serialport::FlowControl> {
Ok(serialport::FlowControl::None)
}
fn parity(&self) -> serialport::Result<serialport::Parity> {
Ok(serialport::Parity::None)
}
fn stop_bits(&self) -> serialport::Result<serialport::StopBits> {
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<bool> {
Ok(true)
}
fn read_data_set_ready(&mut self) -> serialport::Result<bool> {
Ok(true)
}
fn read_ring_indicator(&mut self) -> serialport::Result<bool> {
Ok(true)
}
fn read_carrier_detect(&mut self) -> serialport::Result<bool> {
Ok(true)
}
fn bytes_to_read(&self) -> serialport::Result<u32> {
Err(serialport::Error::new(
serialport::ErrorKind::NoDevice,
"Mock does not support bytes_to_read",
))
}
fn bytes_to_write(&self) -> serialport::Result<u32> {
Ok(0)
}
fn clear(&self, _buffer_to_clear: serialport::ClearBuffer) -> serialport::Result<()> {
Ok(())
}
fn try_clone(&self) -> serialport::Result<Box<dyn SerialPort>> {
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
);
}
}
+158
View File
@@ -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<Self> {
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<usize, super::ReceiveError> {
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<F: FnMut(&[u8])>(&mut self, f: F) -> Result<usize, super::ReceiveError> {
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();
}
}
+236
View File
@@ -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<Self, std::io::Error> {
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<A: ToSocketAddrs>(
&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<F: FnMut(&[u8])>(&mut self, mut f: F) -> Result<usize, super::ReceiveError> {
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<F: FnMut(&[u8])>(&mut self, f: F) -> Result<usize, super::ReceiveError> {
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<u8>> = 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<u8>> = 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<u8>> = 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());
}
}