tmtc-utils init commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
/Cargo.lock
|
||||
+10
@@ -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"
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
//! # TMTC Utilities
|
||||
//!
|
||||
//! This crate contains commonly required utilities when writing TMTC clients with Rust.
|
||||
#![deny(missing_docs)]
|
||||
pub mod transport;
|
||||
@@ -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<F: FnMut(&[u8])>(&mut self, f: F) -> Result<usize, ReceiveError>;
|
||||
}
|
||||
@@ -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<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.
|
||||
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()?;
|
||||
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<dyn serialport::SerialPort>, 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<F: FnMut(&[u8])>(&mut self, mut f: F) -> Result<usize, super::ReceiveError> {
|
||||
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<F: FnMut(&[u8])>(&mut self, f: F) -> Result<usize, super::ReceiveError> {
|
||||
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<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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#[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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user