486 lines
17 KiB
Rust
486 lines
17 KiB
Rust
use alloc::sync::Arc;
|
|
use core::{sync::atomic::AtomicBool, time::Duration};
|
|
use delegate::delegate;
|
|
use mio::net::{TcpListener, TcpStream};
|
|
use std::{io::Write, net::SocketAddr};
|
|
|
|
use crate::{
|
|
ComponentId,
|
|
encoding::{ccsds::SpacePacketValidator, parse_buffer_for_ccsds_space_packets},
|
|
queue::GenericSendError,
|
|
tmtc::{PacketHandler, PacketSource},
|
|
};
|
|
|
|
use super::tcp_server::{
|
|
ConnectionResult, HandledConnectionHandler, HandledConnectionInfo, ServerConfig, TcpTcParser,
|
|
TcpTmSender, TcpTmtcGenericServer,
|
|
};
|
|
|
|
pub struct CcsdsPacketParser<
|
|
PacketValidator: SpacePacketValidator,
|
|
PacketHandlerInstance: PacketHandler,
|
|
> {
|
|
sender_id: ComponentId,
|
|
parsing_buffer: alloc::vec::Vec<u8>,
|
|
validator: PacketValidator,
|
|
packet_handler: PacketHandlerInstance,
|
|
current_write_index: usize,
|
|
}
|
|
|
|
impl<PacketValidator: SpacePacketValidator, PacketHandlerInstance: PacketHandler>
|
|
CcsdsPacketParser<PacketValidator, PacketHandlerInstance>
|
|
{
|
|
pub fn new(
|
|
sender_id: ComponentId,
|
|
parsing_buf_size: usize,
|
|
packet_handler: PacketHandlerInstance,
|
|
validator: PacketValidator,
|
|
) -> Self {
|
|
Self {
|
|
sender_id,
|
|
parsing_buffer: alloc::vec![0; parsing_buf_size],
|
|
validator,
|
|
packet_handler,
|
|
current_write_index: 0,
|
|
}
|
|
}
|
|
|
|
fn write_to_buffer(&mut self, data: &[u8]) -> usize {
|
|
let available = self.parsing_buffer.len() - self.current_write_index;
|
|
let to_write = core::cmp::min(data.len(), available);
|
|
|
|
self.parsing_buffer[self.current_write_index..self.current_write_index + to_write]
|
|
.copy_from_slice(&data[..to_write]);
|
|
self.current_write_index += to_write;
|
|
|
|
to_write
|
|
}
|
|
|
|
fn parse_and_handle_packets(&mut self) -> u32 {
|
|
match parse_buffer_for_ccsds_space_packets(
|
|
&self.parsing_buffer[..self.current_write_index],
|
|
&self.validator,
|
|
self.sender_id,
|
|
&self.packet_handler,
|
|
) {
|
|
Ok(parse_result) => {
|
|
self.parsing_buffer
|
|
.copy_within(parse_result.parsed_bytes..self.current_write_index, 0);
|
|
self.current_write_index -= parse_result.parsed_bytes;
|
|
parse_result.packets_found
|
|
}
|
|
Err(_) => 0,
|
|
}
|
|
}
|
|
|
|
fn drop_first_half_of_buffer(&mut self) {
|
|
let mid = self.parsing_buffer.len() / 2;
|
|
self.parsing_buffer.copy_within(mid.., 0);
|
|
self.current_write_index -= mid;
|
|
}
|
|
}
|
|
impl<PacketValidator: SpacePacketValidator, PacketHandlerInstance: PacketHandler> TcpTcParser
|
|
for CcsdsPacketParser<PacketValidator, PacketHandlerInstance>
|
|
{
|
|
fn reset(&mut self) {
|
|
self.current_write_index = 0;
|
|
}
|
|
|
|
fn push(&mut self, mut tc_buffer: &[u8], conn_result: &mut HandledConnectionInfo) {
|
|
while !tc_buffer.is_empty() {
|
|
// Write as much as possible to buffer
|
|
let written = self.write_to_buffer(tc_buffer);
|
|
tc_buffer = &tc_buffer[written..];
|
|
|
|
// Parse for complete packets
|
|
let packets_found = self.parse_and_handle_packets();
|
|
conn_result.num_received_tcs += packets_found;
|
|
|
|
if tc_buffer.is_empty() {
|
|
break;
|
|
}
|
|
|
|
// Handle buffer overflow
|
|
if self.current_write_index == self.parsing_buffer.len() {
|
|
self.drop_first_half_of_buffer();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Concrete [TcpTmSender] implementation for the [TcpSpacepacketsServer].
|
|
#[derive(Default)]
|
|
pub struct SpacepacketsTmSender {}
|
|
|
|
impl TcpTmSender for SpacepacketsTmSender {
|
|
fn handle_tm_sending(
|
|
&mut self,
|
|
tm_buffer: &mut [u8],
|
|
tm_source: &mut (impl PacketSource<Error = ()> + ?Sized),
|
|
conn_result: &mut HandledConnectionInfo,
|
|
stream: &mut TcpStream,
|
|
) -> Result<bool, std::io::Error> {
|
|
let mut tm_was_sent = false;
|
|
while let Ok(read_tm_len) = tm_source.retrieve_packet(tm_buffer) {
|
|
// Write TM until TM source is exhausted. For now, there is no limit for the amount
|
|
// of TM written this way.
|
|
|
|
if read_tm_len == 0 {
|
|
return Ok(tm_was_sent);
|
|
}
|
|
tm_was_sent = true;
|
|
conn_result.num_sent_tms += 1;
|
|
|
|
stream.write_all(&tm_buffer[..read_tm_len])?;
|
|
}
|
|
Ok(tm_was_sent)
|
|
}
|
|
}
|
|
|
|
/// TCP TMTC server implementation for exchange of tightly stuffed
|
|
/// [CCSDS space packets](https://public.ccsds.org/Pubs/133x0b2e1.pdf).
|
|
///
|
|
/// This serves only works if
|
|
/// [CCSDS 133.0-B-2 space packets](https://public.ccsds.org/Pubs/133x0b2e1.pdf) are the only
|
|
/// packet type being exchanged. It uses the CCSDS space packet header [spacepackets::SpHeader] and
|
|
/// a user specified [SpacePacketValidator] to determine the space packets relevant for further
|
|
/// processing.
|
|
///
|
|
/// ## Example
|
|
///
|
|
/// The [TCP server integration tests](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs/tests/tcp_servers.rs)
|
|
/// also serves as the example application for this module.
|
|
pub struct TcpSpacepacketsServer<
|
|
TmSource: PacketSource<Error = ()>,
|
|
TcSender: PacketHandler<Error = GenericSendError>,
|
|
Validator: SpacePacketValidator,
|
|
HandledConnection: HandledConnectionHandler,
|
|
> {
|
|
pub generic_server: TcpTmtcGenericServer<
|
|
TmSource,
|
|
SpacepacketsTmSender,
|
|
CcsdsPacketParser<Validator, TcSender>,
|
|
HandledConnection,
|
|
>,
|
|
}
|
|
|
|
impl<
|
|
TmSource: PacketSource<Error = ()>,
|
|
TcSender: PacketHandler<Error = GenericSendError>,
|
|
Validator: SpacePacketValidator,
|
|
HandledConnection: HandledConnectionHandler,
|
|
> TcpSpacepacketsServer<TmSource, TcSender, Validator, HandledConnection>
|
|
{
|
|
///
|
|
/// ## Parameter
|
|
///
|
|
/// * `cfg` - Configuration of the server.
|
|
/// * `tm_source` - Generic TM source used by the server to pull telemetry packets which are
|
|
/// then sent back to the client.
|
|
/// * `tc_sender` - Any received telecommands which were decoded successfully will be
|
|
/// forwarded using this [PacketHandler].
|
|
/// * `validator` - Used to determine the space packets relevant for further processing and
|
|
/// to detect broken space packets.
|
|
/// * `handled_connection_hook` - Called to notify the user about a succesfully handled
|
|
/// connection.
|
|
/// * `stop_signal` - Can be used to shut down the TCP server even for longer running
|
|
/// connections.
|
|
pub fn new(
|
|
cfg: ServerConfig,
|
|
tm_source: TmSource,
|
|
tc_parser: CcsdsPacketParser<Validator, TcSender>,
|
|
handled_connection_hook: HandledConnection,
|
|
stop_signal: Option<Arc<AtomicBool>>,
|
|
) -> Result<Self, std::io::Error> {
|
|
Ok(Self {
|
|
generic_server: TcpTmtcGenericServer::new(
|
|
cfg,
|
|
tc_parser,
|
|
SpacepacketsTmSender::default(),
|
|
tm_source,
|
|
handled_connection_hook,
|
|
stop_signal,
|
|
)?,
|
|
})
|
|
}
|
|
|
|
delegate! {
|
|
to self.generic_server {
|
|
pub fn listener(&mut self) -> &mut TcpListener;
|
|
|
|
/// Can be used to retrieve the local assigned address of the TCP server. This is especially
|
|
/// useful if using the port number 0 for OS auto-assignment.
|
|
pub fn local_addr(&self) -> std::io::Result<SocketAddr>;
|
|
|
|
/// Delegation to the [TcpTmtcGenericServer::handle_all_connections] call.
|
|
pub fn handle_all_connections(
|
|
&mut self,
|
|
poll_timeout: Option<Duration>
|
|
) -> Result<ConnectionResult, std::io::Error>;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use core::{
|
|
sync::atomic::{AtomicBool, Ordering},
|
|
time::Duration,
|
|
};
|
|
#[allow(unused_imports)]
|
|
use std::println;
|
|
use std::{
|
|
io::{Read, Write},
|
|
net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream},
|
|
sync::mpsc,
|
|
thread,
|
|
};
|
|
|
|
use alloc::sync::Arc;
|
|
use arbitrary_int::u11;
|
|
use hashbrown::HashSet;
|
|
use spacepackets::{
|
|
CcsdsPacket, PacketId, SpHeader,
|
|
ecss::{CreatorConfig, MessageTypeId, WritablePusPacket, tc::PusTcCreator},
|
|
};
|
|
|
|
use crate::{
|
|
ComponentId,
|
|
encoding::ccsds::{SpValidity, SpacePacketValidator},
|
|
hal::std::tcp_server::{
|
|
ConnectionResult, ServerConfig,
|
|
tests::{ConnectionFinishedHandler, SyncTmSource},
|
|
},
|
|
tmtc::PacketAsVec,
|
|
};
|
|
|
|
use super::TcpSpacepacketsServer;
|
|
|
|
const TCP_SERVER_ID: ComponentId = 0x05;
|
|
const TEST_APID_0: u11 = u11::new(0x02);
|
|
const TEST_PACKET_ID_0: PacketId = PacketId::new_for_tc(true, TEST_APID_0);
|
|
const TEST_APID_1: u11 = u11::new(0x10);
|
|
const TEST_PACKET_ID_1: PacketId = PacketId::new_for_tc(true, TEST_APID_1);
|
|
|
|
#[derive(Default)]
|
|
pub struct SimpleValidator(pub HashSet<PacketId>);
|
|
|
|
impl SpacePacketValidator for SimpleValidator {
|
|
fn validate(&self, sp_header: &SpHeader, _raw_buf: &[u8]) -> SpValidity {
|
|
if self.0.contains(&sp_header.packet_id()) {
|
|
return SpValidity::Valid;
|
|
}
|
|
// Simple case: Assume that the interface always contains valid space packets.
|
|
SpValidity::Skip
|
|
}
|
|
}
|
|
|
|
fn generic_tmtc_server(
|
|
addr: &SocketAddr,
|
|
tc_parser: super::CcsdsPacketParser<SimpleValidator, mpsc::Sender<PacketAsVec>>,
|
|
tm_source: SyncTmSource,
|
|
stop_signal: Option<Arc<AtomicBool>>,
|
|
) -> TcpSpacepacketsServer<
|
|
SyncTmSource,
|
|
mpsc::Sender<PacketAsVec>,
|
|
SimpleValidator,
|
|
ConnectionFinishedHandler,
|
|
> {
|
|
TcpSpacepacketsServer::new(
|
|
ServerConfig::new(TCP_SERVER_ID, *addr, Duration::from_millis(2), 1024, 1024),
|
|
tm_source,
|
|
tc_parser,
|
|
ConnectionFinishedHandler::default(),
|
|
stop_signal,
|
|
)
|
|
.expect("TCP server generation failed")
|
|
}
|
|
|
|
#[test]
|
|
fn test_basic_tc_only() {
|
|
let auto_port_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0);
|
|
let (tc_sender, tc_receiver) = mpsc::channel();
|
|
let tm_source = SyncTmSource::default();
|
|
let mut validator = SimpleValidator::default();
|
|
validator.0.insert(TEST_PACKET_ID_0);
|
|
let mut tcp_server = generic_tmtc_server(
|
|
&auto_port_addr,
|
|
super::CcsdsPacketParser::new(TCP_SERVER_ID, 1024, tc_sender.clone(), validator),
|
|
tm_source,
|
|
None,
|
|
);
|
|
let dest_addr = tcp_server
|
|
.local_addr()
|
|
.expect("retrieving dest addr failed");
|
|
let conn_handled: Arc<AtomicBool> = Default::default();
|
|
let set_if_done = conn_handled.clone();
|
|
// Call the connection handler in separate thread, does block.
|
|
thread::spawn(move || {
|
|
let result = tcp_server.handle_all_connections(Some(Duration::from_millis(100)));
|
|
if result.is_err() {
|
|
panic!("handling connection failed: {:?}", result.unwrap_err());
|
|
}
|
|
let conn_result = result.unwrap();
|
|
matches!(conn_result, ConnectionResult::HandledConnections(1));
|
|
tcp_server
|
|
.generic_server
|
|
.finished_handler
|
|
.check_last_connection(0, 1);
|
|
tcp_server
|
|
.generic_server
|
|
.finished_handler
|
|
.check_no_connections_left();
|
|
set_if_done.store(true, Ordering::Relaxed);
|
|
});
|
|
let ping_tc = PusTcCreator::new_simple(
|
|
SpHeader::new_from_apid(TEST_APID_0),
|
|
MessageTypeId::new(17, 1),
|
|
&[],
|
|
CreatorConfig::default(),
|
|
);
|
|
let tc_0 = ping_tc.to_vec().expect("packet generation failed");
|
|
let mut stream = TcpStream::connect(dest_addr).expect("connecting to TCP server failed");
|
|
stream
|
|
.write_all(&tc_0)
|
|
.expect("writing to TCP server failed");
|
|
drop(stream);
|
|
|
|
// A certain amount of time is allowed for the transaction to complete.
|
|
for _ in 0..3 {
|
|
if !conn_handled.load(Ordering::Relaxed) {
|
|
thread::sleep(Duration::from_millis(5));
|
|
}
|
|
}
|
|
if !conn_handled.load(Ordering::Relaxed) {
|
|
panic!("connection was not handled properly");
|
|
}
|
|
let packet = tc_receiver.try_recv().expect("receiving TC failed");
|
|
assert_eq!(packet.packet, tc_0);
|
|
matches!(tc_receiver.try_recv(), Err(mpsc::TryRecvError::Empty));
|
|
}
|
|
|
|
#[test]
|
|
fn test_multi_tc_multi_tm() {
|
|
let auto_port_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0);
|
|
let (tc_sender, tc_receiver) = mpsc::channel();
|
|
let mut tm_source = SyncTmSource::default();
|
|
|
|
// Add telemetry
|
|
let mut total_tm_len = 0;
|
|
let verif_tm = PusTcCreator::new_simple(
|
|
SpHeader::new_from_apid(TEST_APID_0),
|
|
MessageTypeId::new(1, 1),
|
|
&[],
|
|
CreatorConfig::default(),
|
|
);
|
|
let tm_0 = verif_tm.to_vec().expect("writing packet failed");
|
|
total_tm_len += tm_0.len();
|
|
tm_source.add_tm(&tm_0);
|
|
let verif_tm = PusTcCreator::new_simple(
|
|
SpHeader::new_from_apid(TEST_APID_1),
|
|
MessageTypeId::new(1, 3),
|
|
&[],
|
|
CreatorConfig::default(),
|
|
);
|
|
let tm_1 = verif_tm.to_vec().expect("writing packet failed");
|
|
total_tm_len += tm_1.len();
|
|
tm_source.add_tm(&tm_1);
|
|
|
|
// Set up server
|
|
let mut validator = SimpleValidator::default();
|
|
validator.0.insert(TEST_PACKET_ID_0);
|
|
validator.0.insert(TEST_PACKET_ID_1);
|
|
let mut tcp_server = generic_tmtc_server(
|
|
&auto_port_addr,
|
|
super::CcsdsPacketParser::new(TCP_SERVER_ID, 1024, tc_sender.clone(), validator),
|
|
tm_source,
|
|
None,
|
|
);
|
|
let dest_addr = tcp_server
|
|
.local_addr()
|
|
.expect("retrieving dest addr failed");
|
|
let conn_handled: Arc<AtomicBool> = Default::default();
|
|
let set_if_done = conn_handled.clone();
|
|
|
|
// Call the connection handler in separate thread, does block.
|
|
thread::spawn(move || {
|
|
let result = tcp_server.handle_all_connections(Some(Duration::from_millis(100)));
|
|
if result.is_err() {
|
|
panic!("handling connection failed: {:?}", result.unwrap_err());
|
|
}
|
|
let conn_result = result.unwrap();
|
|
matches!(conn_result, ConnectionResult::HandledConnections(1));
|
|
tcp_server
|
|
.generic_server
|
|
.finished_handler
|
|
.check_last_connection(2, 2);
|
|
tcp_server
|
|
.generic_server
|
|
.finished_handler
|
|
.check_no_connections_left();
|
|
set_if_done.store(true, Ordering::Relaxed);
|
|
});
|
|
let mut stream = TcpStream::connect(dest_addr).expect("connecting to TCP server failed");
|
|
stream
|
|
.set_read_timeout(Some(Duration::from_millis(10)))
|
|
.expect("setting reas timeout failed");
|
|
|
|
// Send telecommands
|
|
let ping_tc = PusTcCreator::new_simple(
|
|
SpHeader::new_from_apid(TEST_APID_0),
|
|
MessageTypeId::new(17, 1),
|
|
&[],
|
|
CreatorConfig::default(),
|
|
);
|
|
let tc_0 = ping_tc.to_vec().expect("ping tc creation failed");
|
|
stream
|
|
.write_all(&tc_0)
|
|
.expect("writing to TCP server failed");
|
|
let action_tc = PusTcCreator::new_simple(
|
|
SpHeader::new_from_apid(TEST_APID_1),
|
|
MessageTypeId::new(8, 0),
|
|
&[],
|
|
CreatorConfig::default(),
|
|
);
|
|
let tc_1 = action_tc.to_vec().expect("action tc creation failed");
|
|
stream
|
|
.write_all(&tc_1)
|
|
.expect("writing to TCP server failed");
|
|
|
|
// Done with writing.
|
|
stream
|
|
.shutdown(std::net::Shutdown::Write)
|
|
.expect("shutting down write failed");
|
|
let mut read_buf: [u8; 32] = [0; 32];
|
|
let mut current_idx = 0;
|
|
let mut read_len_total = 0;
|
|
// Timeout ensures this does not block forever.
|
|
while read_len_total < total_tm_len {
|
|
let read_len = stream
|
|
.read(&mut read_buf[current_idx..])
|
|
.expect("read failed");
|
|
current_idx += read_len;
|
|
read_len_total += read_len;
|
|
}
|
|
drop(stream);
|
|
assert_eq!(read_buf[..tm_0.len()], tm_0);
|
|
assert_eq!(read_buf[tm_0.len()..tm_0.len() + tm_1.len()], tm_1);
|
|
|
|
// A certain amount of time is allowed for the transaction to complete.
|
|
for _ in 0..3 {
|
|
if !conn_handled.load(Ordering::Relaxed) {
|
|
thread::sleep(Duration::from_millis(5));
|
|
}
|
|
}
|
|
if !conn_handled.load(Ordering::Relaxed) {
|
|
panic!("connection was not handled properly");
|
|
}
|
|
// Check that TC has arrived.
|
|
let packet_0 = tc_receiver.try_recv().expect("receiving TC failed");
|
|
assert_eq!(packet_0.packet, tc_0);
|
|
let packet_1 = tc_receiver.try_recv().expect("receiving TC failed");
|
|
assert_eq!(packet_1.packet, tc_1);
|
|
matches!(tc_receiver.try_recv(), Err(mpsc::TryRecvError::Empty));
|
|
}
|
|
}
|