//! # PUS support modules //! //! This module contains structures to make working with the PUS C standard easier. //! The satrs-example application contains various usage examples of these components. use crate::pool::{PoolAddr, PoolError}; use crate::pus::verification::{TcStateAccepted, TcStateToken, VerificationToken}; use crate::queue::{GenericReceiveError, GenericSendError}; use crate::request::{GenericMessage, MessageMetadata, RequestId}; #[cfg(feature = "alloc")] use crate::tmtc::PacketAsVec; use crate::tmtc::PacketInPool; use crate::ComponentId; use core::fmt::{Display, Formatter}; use core::time::Duration; #[cfg(feature = "alloc")] use downcast_rs::{impl_downcast, Downcast}; #[cfg(feature = "alloc")] use dyn_clone::DynClone; #[cfg(feature = "std")] use std::error::Error; use spacepackets::ecss::tc::{PusTcCreator, PusTcReader}; use spacepackets::ecss::tm::PusTmCreator; use spacepackets::ecss::PusError; use spacepackets::{ByteConversionError, SpHeader}; pub mod action; pub mod event; pub mod event_man; #[cfg(feature = "std")] pub mod event_srv; pub mod mode; pub mod scheduler; #[cfg(feature = "std")] pub mod scheduler_srv; #[cfg(feature = "std")] pub mod test; pub mod verification; #[cfg(feature = "alloc")] pub use alloc_mod::*; #[cfg(feature = "std")] pub use std_mod::*; use self::verification::VerificationReportingProvider; #[derive(Debug, PartialEq, Eq, Clone)] pub enum PusTmVariant<'time, 'src_data> { InStore(PoolAddr), Direct(PusTmCreator<'time, 'src_data>), } impl From for PusTmVariant<'_, '_> { fn from(value: PoolAddr) -> Self { Self::InStore(value) } } impl<'time, 'src_data> From> for PusTmVariant<'time, 'src_data> { fn from(value: PusTmCreator<'time, 'src_data>) -> Self { Self::Direct(value) } } #[derive(Debug, Clone, PartialEq, Eq)] pub enum EcssTmtcError { Store(PoolError), ByteConversion(ByteConversionError), Pus(PusError), CantSendAddr(PoolAddr), CantSendDirectTm, Send(GenericSendError), Receive(GenericReceiveError), } impl Display for EcssTmtcError { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { match self { EcssTmtcError::Store(store) => { write!(f, "ecss tmtc error: {store}") } EcssTmtcError::ByteConversion(e) => { write!(f, "ecss tmtc error: {e}") } EcssTmtcError::Pus(e) => { write!(f, "ecss tmtc error: {e}") } EcssTmtcError::CantSendAddr(addr) => { write!(f, "can not send address {addr}") } EcssTmtcError::CantSendDirectTm => { write!(f, "can not send TM directly") } EcssTmtcError::Send(e) => { write!(f, "ecss tmtc error: {e}") } EcssTmtcError::Receive(e) => { write!(f, "ecss tmtc error {e}") } } } } impl From for EcssTmtcError { fn from(value: PoolError) -> Self { Self::Store(value) } } impl From for EcssTmtcError { fn from(value: PusError) -> Self { Self::Pus(value) } } impl From for EcssTmtcError { fn from(value: GenericSendError) -> Self { Self::Send(value) } } impl From for EcssTmtcError { fn from(value: ByteConversionError) -> Self { Self::ByteConversion(value) } } impl From for EcssTmtcError { fn from(value: GenericReceiveError) -> Self { Self::Receive(value) } } #[cfg(feature = "std")] impl Error for EcssTmtcError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { EcssTmtcError::Store(e) => Some(e), EcssTmtcError::ByteConversion(e) => Some(e), EcssTmtcError::Pus(e) => Some(e), EcssTmtcError::Send(e) => Some(e), EcssTmtcError::Receive(e) => Some(e), _ => None, } } } pub trait ChannelWithId: Send { /// Each sender can have an ID associated with it fn id(&self) -> ComponentId; fn name(&self) -> &'static str { "unset" } } /// Generic trait for a user supplied sender object. /// /// This sender object is responsible for sending PUS telemetry to a TM sink. pub trait EcssTmSender: Send { fn send_tm(&self, sender_id: ComponentId, tm: PusTmVariant) -> Result<(), EcssTmtcError>; } /// Generic trait for a user supplied sender object. /// /// This sender object is responsible for sending PUS telecommands to a TC recipient. Each /// telecommand can optionally have a token which contains its verification state. pub trait EcssTcSender { fn send_tc(&self, tc: PusTcCreator, token: Option) -> Result<(), EcssTmtcError>; } /// Dummy object which can be useful for tests. #[derive(Default)] pub struct EcssTmDummySender {} impl EcssTmSender for EcssTmDummySender { fn send_tm(&self, _source_id: ComponentId, _tm: PusTmVariant) -> Result<(), EcssTmtcError> { Ok(()) } } /// A PUS telecommand packet can be stored in memory and sent using different methods. Right now, /// storage inside a pool structure like [crate::pool::StaticMemoryPool], and storage inside a /// `Vec` are supported. #[non_exhaustive] #[derive(Debug, Clone, PartialEq, Eq)] pub enum TcInMemory { Pool(PacketInPool), #[cfg(feature = "alloc")] Vec(PacketAsVec), } impl From for TcInMemory { fn from(value: PacketInPool) -> Self { Self::Pool(value) } } #[cfg(feature = "alloc")] impl From for TcInMemory { fn from(value: PacketAsVec) -> Self { Self::Vec(value) } } /// Generic structure for an ECSS PUS Telecommand and its correspoding verification token. #[derive(Debug, Clone, PartialEq, Eq)] pub struct EcssTcAndToken { pub tc_in_memory: TcInMemory, pub token: Option, } impl EcssTcAndToken { pub fn new(tc_in_memory: impl Into, token: impl Into) -> Self { Self { tc_in_memory: tc_in_memory.into(), token: Some(token.into()), } } } /// Generic abstraction for a telecommand being sent around after is has been accepted. pub struct AcceptedEcssTcAndToken { pub tc_in_memory: TcInMemory, pub token: VerificationToken, } impl From for EcssTcAndToken { fn from(value: AcceptedEcssTcAndToken) -> Self { EcssTcAndToken { tc_in_memory: value.tc_in_memory, token: Some(value.token.into()), } } } impl TryFrom for AcceptedEcssTcAndToken { type Error = (); fn try_from(value: EcssTcAndToken) -> Result { if let Some(TcStateToken::Accepted(token)) = value.token { return Ok(AcceptedEcssTcAndToken { tc_in_memory: value.tc_in_memory, token, }); } Err(()) } } #[derive(Debug, Clone)] pub enum TryRecvTmtcError { Tmtc(EcssTmtcError), Empty, } impl From for TryRecvTmtcError { fn from(value: EcssTmtcError) -> Self { Self::Tmtc(value) } } impl From for TryRecvTmtcError { fn from(value: PusError) -> Self { Self::Tmtc(value.into()) } } impl From for TryRecvTmtcError { fn from(value: PoolError) -> Self { Self::Tmtc(value.into()) } } /// Generic trait for a user supplied receiver object. pub trait EcssTcReceiver { fn recv_tc(&self) -> Result; } /// Generic trait for objects which can send ECSS PUS telecommands. pub trait PacketSenderPusTc: Send { type Error; fn send_pus_tc( &self, sender_id: ComponentId, header: &SpHeader, pus_tc: &PusTcReader, ) -> Result<(), Self::Error>; } pub trait ActiveRequestMapProvider: Sized { fn insert(&mut self, request_id: &RequestId, request_info: V); fn get(&self, request_id: RequestId) -> Option<&V>; fn get_mut(&mut self, request_id: RequestId) -> Option<&mut V>; fn remove(&mut self, request_id: RequestId) -> bool; /// Call a user-supplied closure for each active request. fn for_each(&self, f: F); /// Call a user-supplied closure for each active request. Mutable variant. fn for_each_mut(&mut self, f: F); } pub trait ActiveRequestProvider { fn target_id(&self) -> ComponentId; fn token(&self) -> TcStateToken; fn set_token(&mut self, token: TcStateToken); fn has_timed_out(&self) -> bool; fn timeout(&self) -> Duration; } /// This trait is an abstraction for the routing of PUS request to a dedicated /// recipient using the generic [ComponentId]. pub trait PusRequestRouter { type Error; fn route( &self, requestor_info: MessageMetadata, target_id: ComponentId, request: Request, ) -> Result<(), Self::Error>; } pub trait PusReplyHandler { type Error; /// This function handles a reply for a given PUS request and returns whether that request /// is finished. A finished PUS request will be removed from the active request map. fn handle_reply( &mut self, reply: &GenericMessage, active_request: &ActiveRequestInfo, tm_sender: &impl EcssTmSender, verification_handler: &impl VerificationReportingProvider, time_stamp: &[u8], ) -> Result; fn handle_unrequested_reply( &mut self, reply: &GenericMessage, tm_sender: &impl EcssTmSender, ) -> Result<(), Self::Error>; /// Handle the timeout of an active request. fn handle_request_timeout( &mut self, active_request: &ActiveRequestInfo, tm_sender: &impl EcssTmSender, verification_handler: &impl VerificationReportingProvider, time_stamp: &[u8], ) -> Result<(), Self::Error>; } #[cfg(feature = "alloc")] pub mod alloc_mod { use hashbrown::HashMap; use super::*; /// Extension trait for [EcssTmSender]. /// /// It provides additional functionality, for example by implementing the [Downcast] trait /// and the [DynClone] trait. /// /// [Downcast] is implemented to allow passing the sender as a boxed trait object and still /// retrieve the concrete type at a later point. /// /// [DynClone] allows cloning the trait object as long as the boxed object implements /// [Clone]. #[cfg(feature = "alloc")] #[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] pub trait EcssTmSenderExt: EcssTmSender + Downcast + DynClone { // Remove this once trait upcasting coercion has been implemented. // Tracking issue: https://github.com/rust-lang/rust/issues/65991 fn upcast(&self) -> &dyn EcssTmSender; // Remove this once trait upcasting coercion has been implemented. // Tracking issue: https://github.com/rust-lang/rust/issues/65991 fn upcast_mut(&mut self) -> &mut dyn EcssTmSender; } /// Blanket implementation for all types which implement [EcssTmSender] and are clonable. impl EcssTmSenderExt for T where T: EcssTmSender + Clone + 'static, { // Remove this once trait upcasting coercion has been implemented. // Tracking issue: https://github.com/rust-lang/rust/issues/65991 fn upcast(&self) -> &dyn EcssTmSender { self } // Remove this once trait upcasting coercion has been implemented. // Tracking issue: https://github.com/rust-lang/rust/issues/65991 fn upcast_mut(&mut self) -> &mut dyn EcssTmSender { self } } dyn_clone::clone_trait_object!(EcssTmSenderExt); impl_downcast!(EcssTmSenderExt); /// Extension trait for [EcssTcSender]. /// /// It provides additional functionality, for example by implementing the [Downcast] trait /// and the [DynClone] trait. /// /// [Downcast] is implemented to allow passing the sender as a boxed trait object and still /// retrieve the concrete type at a later point. /// /// [DynClone] allows cloning the trait object as long as the boxed object implements /// [Clone]. #[cfg(feature = "alloc")] #[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] pub trait EcssTcSenderExt: EcssTcSender + Downcast + DynClone {} /// Blanket implementation for all types which implement [EcssTcSender] and are clonable. impl EcssTcSenderExt for T where T: EcssTcSender + Clone + 'static {} dyn_clone::clone_trait_object!(EcssTcSenderExt); impl_downcast!(EcssTcSenderExt); /// Extension trait for [EcssTcReceiver]. /// /// It provides additional functionality, for example by implementing the [Downcast] trait /// and the [DynClone] trait. /// /// [Downcast] is implemented to allow passing the sender as a boxed trait object and still /// retrieve the concrete type at a later point. /// /// [DynClone] allows cloning the trait object as long as the boxed object implements /// [Clone]. #[cfg(feature = "alloc")] #[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] pub trait EcssTcReceiverExt: EcssTcReceiver + Downcast {} /// Blanket implementation for all types which implement [EcssTcReceiver] and are clonable. impl EcssTcReceiverExt for T where T: EcssTcReceiver + 'static {} impl_downcast!(EcssTcReceiverExt); /// This trait is an abstraction for the conversion of a PUS telecommand into a generic request /// type. /// /// Having a dedicated trait for this allows maximum flexiblity and tailoring of the standard. /// The only requirement is that a valid active request information instance and a request /// are returned by the core conversion function. The active request type needs to fulfill /// the [ActiveRequestProvider] trait bound. /// /// The user should take care of performing the error handling as well. Some of the following /// aspects might be relevant: /// /// - Checking the validity of the APID, service ID, subservice ID. /// - Checking the validity of the user data. /// /// A [VerificationReportingProvider] instance is passed to the user to also allow handling /// of the verification process as part of the PUS standard requirements. pub trait PusTcToRequestConverter { type Error; fn convert( &mut self, token: VerificationToken, tc: &PusTcReader, tm_sender: &(impl EcssTmSender + ?Sized), verif_reporter: &impl VerificationReportingProvider, time_stamp: &[u8], ) -> Result<(ActiveRequestInfo, Request), Self::Error>; } #[derive(Clone, Debug)] pub struct DefaultActiveRequestMap(pub HashMap); impl Default for DefaultActiveRequestMap { fn default() -> Self { Self(HashMap::new()) } } impl ActiveRequestMapProvider for DefaultActiveRequestMap { fn insert(&mut self, request_id: &RequestId, request: V) { self.0.insert(*request_id, request); } fn get(&self, request_id: RequestId) -> Option<&V> { self.0.get(&request_id) } fn get_mut(&mut self, request_id: RequestId) -> Option<&mut V> { self.0.get_mut(&request_id) } fn remove(&mut self, request_id: RequestId) -> bool { self.0.remove(&request_id).is_some() } fn for_each(&self, mut f: F) { for (req_id, active_req) in &self.0 { f(req_id, active_req); } } fn for_each_mut(&mut self, mut f: F) { for (req_id, active_req) in &mut self.0 { f(req_id, active_req); } } } /* /// Generic reply handler structure which can be used to handle replies for a specific PUS /// service. /// /// This is done by keeping track of active requests using an internal map structure. An API /// to register new active requests is exposed as well. /// The reply handler performs boilerplate tasks like performing the verification handling and /// timeout handling. /// /// This object is not useful by itself but serves as a common building block for high-level /// PUS reply handlers. Concrete PUS handlers should constrain the [ActiveRequestProvider] and /// the `ReplyType` generics to specific types tailored towards PUS services in addition to /// providing an API which can process received replies and convert them into verification /// completions or other operation like user hook calls. The framework also provides some /// concrete PUS handlers for common PUS services like the mode, action and housekeeping /// service. /// /// This object does not automatically update its internal time information used to check for /// timeouts. The user should call the [Self::update_time] and [Self::update_time_from_now] /// methods to do this. pub struct PusServiceReplyHandler< ActiveRequestMap: ActiveRequestMapProvider, ReplyHook: ReplyHandlerHook, ActiveRequestType: ActiveRequestProvider, ReplyType, > { pub active_request_map: ActiveRequestMap, pub tm_buf: alloc::vec::Vec, pub current_time: UnixTimestamp, pub user_hook: ReplyHook, phantom: PhantomData<(ActiveRequestType, ReplyType)>, } impl< ActiveRequestMap: ActiveRequestMapProvider, ReplyHook: ReplyHandlerHook, ActiveRequestType: ActiveRequestProvider, ReplyType, > PusServiceReplyHandler< ActiveRequestMap, ReplyHook, ActiveRequestType, ReplyType, > { #[cfg(feature = "std")] #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] pub fn new_from_now( active_request_map: ActiveRequestMap, fail_data_buf_size: usize, user_hook: ReplyHook, ) -> Result { let current_time = UnixTimestamp::from_now()?; Ok(Self::new( active_request_map, fail_data_buf_size, user_hook, tm_sender, current_time, )) } pub fn new( active_request_map: ActiveRequestMap, fail_data_buf_size: usize, user_hook: ReplyHook, tm_sender: TmSender, init_time: UnixTimestamp, ) -> Self { Self { active_request_map, tm_buf: alloc::vec![0; fail_data_buf_size], current_time: init_time, user_hook, tm_sender, phantom: PhantomData, } } pub fn add_routed_request( &mut self, request_id: verification::RequestId, active_request_type: ActiveRequestType, ) { self.active_request_map .insert(&request_id.into(), active_request_type); } pub fn request_active(&self, request_id: RequestId) -> bool { self.active_request_map.get(request_id).is_some() } /// Check for timeouts across all active requests. /// /// It will call [Self::handle_timeout] for all active requests which have timed out. pub fn check_for_timeouts(&mut self, time_stamp: &[u8]) -> Result<(), EcssTmtcError> { let mut timed_out_commands = alloc::vec::Vec::new(); self.active_request_map.for_each(|request_id, active_req| { let diff = self.current_time - active_req.start_time(); if diff.duration_absolute > active_req.timeout() { self.handle_timeout(active_req, time_stamp); } timed_out_commands.push(*request_id); }); for timed_out_command in timed_out_commands { self.active_request_map.remove(timed_out_command); } Ok(()) } /// Handle the timeout for a given active request. /// /// This implementation will report a verification completion failure with a user-provided /// error code. It supplies the configured request timeout in milliseconds as a [u64] /// serialized in big-endian format as the failure data. pub fn handle_timeout(&self, active_request: &ActiveRequestType, time_stamp: &[u8]) { let timeout = active_request.timeout().as_millis() as u64; let timeout_raw = timeout.to_be_bytes(); self.verification_reporter .completion_failure( active_request.token(), FailParams::new( time_stamp, &self.user_hook.timeout_error_code(), &timeout_raw, ), ) .unwrap(); self.user_hook.timeout_callback(active_request); } /// Update the current time used for timeout checks based on the current OS time. #[cfg(feature = "std")] #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] pub fn update_time_from_now(&mut self) -> Result<(), std::time::SystemTimeError> { self.current_time = UnixTimestamp::from_now()?; Ok(()) } /// Update the current time used for timeout checks. pub fn update_time(&mut self, time: UnixTimestamp) { self.current_time = time; } } */ } #[cfg(feature = "std")] #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] pub mod std_mod { use crate::pool::{ PoolAddr, PoolError, PoolProvider, PoolProviderWithGuards, SharedStaticMemoryPool, }; use crate::pus::verification::{TcStateAccepted, VerificationToken}; use crate::pus::{ EcssTcAndToken, EcssTcReceiver, EcssTmSender, EcssTmtcError, GenericReceiveError, GenericSendError, PusTmVariant, TryRecvTmtcError, }; use crate::tmtc::{PacketAsVec, PacketSenderWithSharedPool}; use crate::ComponentId; use alloc::vec::Vec; use core::time::Duration; use spacepackets::ecss::tc::PusTcReader; use spacepackets::ecss::WritablePusPacket; use spacepackets::time::StdTimestampError; use spacepackets::ByteConversionError; use std::string::String; use std::sync::mpsc; use std::sync::mpsc::TryRecvError; use thiserror::Error; #[cfg(feature = "crossbeam")] pub use cb_mod::*; use super::verification::{TcStateToken, VerificationReportingProvider}; use super::{AcceptedEcssTcAndToken, ActiveRequestProvider, TcInMemory}; use crate::tmtc::PacketInPool; impl From> for EcssTmtcError { fn from(_: mpsc::SendError) -> Self { Self::Send(GenericSendError::RxDisconnected) } } impl EcssTmSender for mpsc::Sender { fn send_tm(&self, source_id: ComponentId, tm: PusTmVariant) -> Result<(), EcssTmtcError> { match tm { PusTmVariant::InStore(store_addr) => self .send(PacketInPool { sender_id: source_id, store_addr, }) .map_err(|_| GenericSendError::RxDisconnected)?, PusTmVariant::Direct(_) => return Err(EcssTmtcError::CantSendDirectTm), }; Ok(()) } } impl EcssTmSender for mpsc::SyncSender { fn send_tm(&self, source_id: ComponentId, tm: PusTmVariant) -> Result<(), EcssTmtcError> { match tm { PusTmVariant::InStore(store_addr) => self .try_send(PacketInPool { sender_id: source_id, store_addr, }) .map_err(|e| EcssTmtcError::Send(e.into()))?, PusTmVariant::Direct(_) => return Err(EcssTmtcError::CantSendDirectTm), }; Ok(()) } } pub type MpscTmAsVecSender = mpsc::Sender; impl EcssTmSender for MpscTmAsVecSender { fn send_tm(&self, source_id: ComponentId, tm: PusTmVariant) -> Result<(), EcssTmtcError> { match tm { PusTmVariant::InStore(addr) => return Err(EcssTmtcError::CantSendAddr(addr)), PusTmVariant::Direct(tm) => self .send(PacketAsVec { sender_id: source_id, packet: tm.to_vec()?, }) .map_err(|e| EcssTmtcError::Send(e.into()))?, }; Ok(()) } } pub type MpscTmAsVecSenderBounded = mpsc::SyncSender; impl EcssTmSender for MpscTmAsVecSenderBounded { fn send_tm(&self, source_id: ComponentId, tm: PusTmVariant) -> Result<(), EcssTmtcError> { match tm { PusTmVariant::InStore(addr) => return Err(EcssTmtcError::CantSendAddr(addr)), PusTmVariant::Direct(tm) => self .send(PacketAsVec { sender_id: source_id, packet: tm.to_vec()?, }) .map_err(|e| EcssTmtcError::Send(e.into()))?, }; Ok(()) } } pub type MpscTcReceiver = mpsc::Receiver; impl EcssTcReceiver for MpscTcReceiver { fn recv_tc(&self) -> Result { self.try_recv().map_err(|e| match e { TryRecvError::Empty => TryRecvTmtcError::Empty, TryRecvError::Disconnected => TryRecvTmtcError::Tmtc(EcssTmtcError::from( GenericReceiveError::TxDisconnected(None), )), }) } } #[cfg(feature = "crossbeam")] pub mod cb_mod { use super::*; use crossbeam_channel as cb; impl From> for EcssTmtcError { fn from(_: cb::SendError) -> Self { Self::Send(GenericSendError::RxDisconnected) } } impl From> for EcssTmtcError { fn from(value: cb::TrySendError) -> Self { match value { cb::TrySendError::Full(_) => Self::Send(GenericSendError::QueueFull(None)), cb::TrySendError::Disconnected(_) => { Self::Send(GenericSendError::RxDisconnected) } } } } impl EcssTmSender for cb::Sender { fn send_tm( &self, sender_id: ComponentId, tm: PusTmVariant, ) -> Result<(), EcssTmtcError> { match tm { PusTmVariant::InStore(addr) => self .try_send(PacketInPool::new(sender_id, addr)) .map_err(|e| EcssTmtcError::Send(e.into()))?, PusTmVariant::Direct(_) => return Err(EcssTmtcError::CantSendDirectTm), }; Ok(()) } } impl EcssTmSender for cb::Sender { fn send_tm( &self, sender_id: ComponentId, tm: PusTmVariant, ) -> Result<(), EcssTmtcError> { match tm { PusTmVariant::InStore(addr) => return Err(EcssTmtcError::CantSendAddr(addr)), PusTmVariant::Direct(tm) => self .send(PacketAsVec::new(sender_id, tm.to_vec()?)) .map_err(|e| EcssTmtcError::Send(e.into()))?, }; Ok(()) } } pub type CrossbeamTcReceiver = cb::Receiver; } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ActivePusRequestStd { target_id: ComponentId, token: TcStateToken, start_time: std::time::Instant, timeout: Duration, } impl ActivePusRequestStd { pub fn new( target_id: ComponentId, token: impl Into, timeout: Duration, ) -> Self { Self { target_id, token: token.into(), start_time: std::time::Instant::now(), timeout, } } } impl ActiveRequestProvider for ActivePusRequestStd { fn target_id(&self) -> ComponentId { self.target_id } fn token(&self) -> TcStateToken { self.token } fn timeout(&self) -> Duration { self.timeout } fn set_token(&mut self, token: TcStateToken) { self.token = token; } fn has_timed_out(&self) -> bool { std::time::Instant::now() - self.start_time > self.timeout } } // TODO: All these types could probably be no_std if we implemented error handling ourselves.. // but thiserror is really nice, so keep it like this for simplicity for now. Maybe thiserror // will be no_std soon, see https://github.com/rust-lang/rust/issues/103765 . #[derive(Debug, Clone, Error)] pub enum PusTcFromMemError { #[error("generic PUS error: {0}")] EcssTmtc(#[from] EcssTmtcError), #[error("invalid format of TC in memory: {0:?}")] InvalidFormat(TcInMemory), } #[derive(Debug, Clone, Error)] pub enum GenericRoutingError { // #[error("not enough application data, expected at least {expected}, found {found}")] // NotEnoughAppData { expected: usize, found: usize }, #[error("Unknown target ID {0}")] UnknownTargetId(ComponentId), #[error("Sending action request failed: {0}")] Send(GenericSendError), } /// This error can be used for generic conversions from PUS Telecommands to request types. /// /// Please note that this error can also be used if no request is generated and the PUS /// service, subservice and application data is used directly to perform some request. #[derive(Debug, Clone, Error)] pub enum GenericConversionError { #[error("wrong service number {0} for packet handler")] WrongService(u8), #[error("invalid subservice {0}")] InvalidSubservice(u8), #[error("not enough application data, expected at least {expected}, found {found}")] NotEnoughAppData { expected: usize, found: usize }, #[error("invalid application data")] InvalidAppData(String), } /// Wrapper type which tries to encapsulate all possible errors when handling PUS packets. #[derive(Debug, Clone, Error)] pub enum PusPacketHandlingError { #[error("error polling PUS TC packet: {0}")] TcPolling(#[from] EcssTmtcError), #[error("error generating PUS reader from memory: {0}")] TcFromMem(#[from] PusTcFromMemError), #[error("generic request conversion error: {0}")] RequestConversion(#[from] GenericConversionError), #[error("request routing error: {0}")] RequestRouting(#[from] GenericRoutingError), #[error("invalid verification token")] InvalidVerificationToken, #[error("other error {0}")] Other(String), } #[derive(Debug, Clone, Error)] pub enum PartialPusHandlingError { #[error("generic timestamp generation error")] Time(#[from] StdTimestampError), #[error("error sending telemetry: {0}")] TmSend(#[from] EcssTmtcError), #[error("error sending verification message")] Verification, #[error("invalid verification token")] NoVerificationToken, } /// Generic result type for handlers which can process PUS packets. #[derive(Debug, Clone)] pub enum PusPacketHandlerResult { RequestHandled, RequestHandledPartialSuccess(PartialPusHandlingError), SubserviceNotImplemented(u8, VerificationToken), CustomSubservice(u8, VerificationToken), Empty, } impl From for PusPacketHandlerResult { fn from(value: PartialPusHandlingError) -> Self { Self::RequestHandledPartialSuccess(value) } } pub trait EcssTcInMemConverter { fn cache(&mut self, possible_packet: &TcInMemory) -> Result<(), PusTcFromMemError>; fn tc_slice_raw(&self) -> &[u8]; fn sender_id(&self) -> Option; fn cache_and_convert( &mut self, possible_packet: &TcInMemory, ) -> Result, PusTcFromMemError> { self.cache(possible_packet)?; Ok(PusTcReader::new(self.tc_slice_raw()) .map_err(EcssTmtcError::Pus)? .0) } fn convert(&self) -> Result, PusTcFromMemError> { Ok(PusTcReader::new(self.tc_slice_raw()) .map_err(EcssTmtcError::Pus)? .0) } } /// Converter structure for PUS telecommands which are stored inside a `Vec` structure. /// Please note that this structure is not able to convert TCs which are stored inside a /// [SharedStaticMemoryPool]. #[derive(Default, Clone)] pub struct EcssTcInVecConverter { sender_id: Option, pub pus_tc_raw: Option>, } impl EcssTcInMemConverter for EcssTcInVecConverter { fn cache(&mut self, tc_in_memory: &TcInMemory) -> Result<(), PusTcFromMemError> { self.pus_tc_raw = None; match tc_in_memory { super::TcInMemory::Pool(_packet_in_pool) => { return Err(PusTcFromMemError::InvalidFormat(tc_in_memory.clone())); } super::TcInMemory::Vec(packet_with_sender) => { self.pus_tc_raw = Some(packet_with_sender.packet.clone()); self.sender_id = Some(packet_with_sender.sender_id); } }; Ok(()) } fn sender_id(&self) -> Option { self.sender_id } fn tc_slice_raw(&self) -> &[u8] { if self.pus_tc_raw.is_none() { return &[]; } self.pus_tc_raw.as_ref().unwrap() } } /// Converter structure for PUS telecommands which are stored inside /// [SharedStaticMemoryPool] structure. This is useful if run-time allocation for these /// packets should be avoided. Please note that this structure is not able to convert TCs which /// are stored as a `Vec`. pub struct EcssTcInSharedStoreConverter { sender_id: Option, shared_tc_store: SharedStaticMemoryPool, pus_buf: Vec, } impl EcssTcInSharedStoreConverter { pub fn new(shared_tc_store: SharedStaticMemoryPool, max_expected_tc_size: usize) -> Self { Self { sender_id: None, shared_tc_store, pus_buf: alloc::vec![0; max_expected_tc_size], } } pub fn copy_tc_to_buf(&mut self, addr: PoolAddr) -> Result<(), PusTcFromMemError> { // Keep locked section as short as possible. let mut tc_pool = self.shared_tc_store.write().map_err(|_| { PusTcFromMemError::EcssTmtc(EcssTmtcError::Store(PoolError::LockError)) })?; let tc_size = tc_pool.len_of_data(&addr).map_err(EcssTmtcError::Store)?; if tc_size > self.pus_buf.len() { return Err( EcssTmtcError::ByteConversion(ByteConversionError::ToSliceTooSmall { found: self.pus_buf.len(), expected: tc_size, }) .into(), ); } let tc_guard = tc_pool.read_with_guard(addr); // TODO: Proper error handling. tc_guard.read(&mut self.pus_buf[0..tc_size]).unwrap(); Ok(()) } } impl EcssTcInMemConverter for EcssTcInSharedStoreConverter { fn cache(&mut self, tc_in_memory: &TcInMemory) -> Result<(), PusTcFromMemError> { match tc_in_memory { super::TcInMemory::Pool(packet_in_pool) => { self.copy_tc_to_buf(packet_in_pool.store_addr)?; self.sender_id = Some(packet_in_pool.sender_id); } super::TcInMemory::Vec(_) => { return Err(PusTcFromMemError::InvalidFormat(tc_in_memory.clone())); } }; Ok(()) } fn tc_slice_raw(&self) -> &[u8] { self.pus_buf.as_ref() } fn sender_id(&self) -> Option { self.sender_id } } pub struct PusServiceBase< TcReceiver: EcssTcReceiver, TmSender: EcssTmSender, VerificationReporter: VerificationReportingProvider, > { pub id: ComponentId, pub tc_receiver: TcReceiver, pub tm_sender: TmSender, pub verif_reporter: VerificationReporter, } /// This is a high-level PUS packet handler helper. /// /// It performs some of the boilerplate acitivities involved when handling PUS telecommands and /// it can be used to implement the handling of PUS telecommands for certain PUS telecommands /// groups (for example individual services). /// /// This base class can handle PUS telecommands backed by different memory storage machanisms /// by using the [EcssTcInMemConverter] abstraction. This object provides some convenience /// methods to make the generic parts of TC handling easier. pub struct PusServiceHelper< TcReceiver: EcssTcReceiver, TmSender: EcssTmSender, TcInMemConverter: EcssTcInMemConverter, VerificationReporter: VerificationReportingProvider, > { pub common: PusServiceBase, pub tc_in_mem_converter: TcInMemConverter, } impl< TcReceiver: EcssTcReceiver, TmSender: EcssTmSender, TcInMemConverter: EcssTcInMemConverter, VerificationReporter: VerificationReportingProvider, > PusServiceHelper { pub fn new( id: ComponentId, tc_receiver: TcReceiver, tm_sender: TmSender, verification_handler: VerificationReporter, tc_in_mem_converter: TcInMemConverter, ) -> Self { Self { common: PusServiceBase { id, tc_receiver, tm_sender, verif_reporter: verification_handler, }, tc_in_mem_converter, } } pub fn id(&self) -> ComponentId { self.common.id } pub fn tm_sender(&self) -> &TmSender { &self.common.tm_sender } /// This function can be used to poll the internal [EcssTcReceiver] object for the next /// telecommand packet. It will return `Ok(None)` if there are not packets available. /// In any other case, it will perform the acceptance of the ECSS TC packet using the /// internal [VerificationReportingProvider] object. It will then return the telecommand /// and the according accepted token. pub fn retrieve_and_accept_next_packet( &mut self, ) -> Result, PusPacketHandlingError> { match self.common.tc_receiver.recv_tc() { Ok(EcssTcAndToken { tc_in_memory, token, }) => { if token.is_none() { return Err(PusPacketHandlingError::InvalidVerificationToken); } let token = token.unwrap(); let accepted_token = VerificationToken::::try_from(token) .map_err(|_| PusPacketHandlingError::InvalidVerificationToken)?; Ok(Some(AcceptedEcssTcAndToken { tc_in_memory, token: accepted_token, })) } Err(e) => match e { TryRecvTmtcError::Tmtc(e) => Err(PusPacketHandlingError::TcPolling(e)), TryRecvTmtcError::Empty => Ok(None), }, } } pub fn verif_reporter(&self) -> &VerificationReporter { &self.common.verif_reporter } pub fn verif_reporter_mut(&mut self) -> &mut VerificationReporter { &mut self.common.verif_reporter } pub fn tc_in_mem_converter(&self) -> &TcInMemConverter { &self.tc_in_mem_converter } pub fn tc_in_mem_converter_mut(&mut self) -> &mut TcInMemConverter { &mut self.tc_in_mem_converter } } pub type PusServiceHelperDynWithMpsc = PusServiceHelper; pub type PusServiceHelperDynWithBoundedMpsc = PusServiceHelper< MpscTcReceiver, MpscTmAsVecSenderBounded, TcInMemConverter, VerificationReporter, >; pub type PusServiceHelperStaticWithMpsc = PusServiceHelper< MpscTcReceiver, PacketSenderWithSharedPool, TcInMemConverter, VerificationReporter, >; pub type PusServiceHelperStaticWithBoundedMpsc = PusServiceHelper< MpscTcReceiver, PacketSenderWithSharedPool, TcInMemConverter, VerificationReporter, >; } pub(crate) fn source_buffer_large_enough( cap: usize, len: usize, ) -> Result<(), ByteConversionError> { if len > cap { return Err(ByteConversionError::ToSliceTooSmall { found: cap, expected: len, }); } Ok(()) } #[cfg(any(feature = "test_util", test))] pub mod test_util { use crate::request::UniqueApidTargetId; use spacepackets::ecss::{tc::PusTcCreator, tm::PusTmReader}; use super::{ verification::{self, TcStateAccepted, VerificationToken}, PusPacketHandlerResult, PusPacketHandlingError, }; pub const TEST_APID: u16 = 0x101; pub const TEST_UNIQUE_ID_0: u32 = 0x05; pub const TEST_UNIQUE_ID_1: u32 = 0x06; pub const TEST_COMPONENT_ID_0: UniqueApidTargetId = UniqueApidTargetId::new(TEST_APID, TEST_UNIQUE_ID_0); pub const TEST_COMPONENT_ID_1: UniqueApidTargetId = UniqueApidTargetId::new(TEST_APID, TEST_UNIQUE_ID_1); pub trait PusTestHarness { fn init_verification(&mut self, tc: &PusTcCreator) -> VerificationToken; fn send_tc(&self, token: &VerificationToken, tc: &PusTcCreator); fn read_next_tm(&mut self) -> PusTmReader<'_>; fn check_no_tm_available(&self) -> bool; fn check_next_verification_tm( &self, subservice: u8, expected_request_id: verification::RequestId, ); } pub trait SimplePusPacketHandler { fn handle_one_tc(&mut self) -> Result; } } #[cfg(test)] pub mod tests { use core::cell::RefCell; use std::sync::mpsc::TryRecvError; use std::sync::{mpsc, RwLock}; use alloc::collections::VecDeque; use alloc::vec::Vec; use satrs_shared::res_code::ResultU16; use spacepackets::ecss::tc::{PusTcCreator, PusTcReader}; use spacepackets::ecss::tm::{GenericPusTmSecondaryHeader, PusTmCreator, PusTmReader}; use spacepackets::ecss::{PusPacket, WritablePusPacket}; use spacepackets::CcsdsPacket; use crate::pool::{PoolProvider, SharedStaticMemoryPool, StaticMemoryPool, StaticPoolConfig}; use crate::pus::verification::{RequestId, VerificationReporter}; use crate::tmtc::{PacketAsVec, PacketInPool, PacketSenderWithSharedPool, SharedPacketPool}; use crate::ComponentId; use super::test_util::{TEST_APID, TEST_COMPONENT_ID_0}; use super::verification::test_util::TestVerificationReporter; use super::verification::{ TcStateAccepted, VerificationReporterCfg, VerificationReportingProvider, VerificationToken, }; use super::*; #[derive(Debug, Eq, PartialEq, Clone)] pub(crate) struct CommonTmInfo { pub subservice: u8, pub apid: u16, pub seq_count: u16, pub msg_counter: u16, pub dest_id: u16, pub time_stamp: [u8; 7], } impl CommonTmInfo { pub fn new_zero_seq_count( subservice: u8, apid: u16, dest_id: u16, time_stamp: [u8; 7], ) -> Self { Self { subservice, apid, seq_count: 0, msg_counter: 0, dest_id, time_stamp, } } pub fn new_from_tm(tm: &PusTmCreator) -> Self { let mut time_stamp = [0; 7]; time_stamp.clone_from_slice(&tm.timestamp()[0..7]); Self { subservice: PusPacket::subservice(tm), apid: tm.apid(), seq_count: tm.seq_count(), msg_counter: tm.msg_counter(), dest_id: tm.dest_id(), time_stamp, } } } /// Common fields for a PUS service test harness. pub struct PusServiceHandlerWithSharedStoreCommon { pus_buf: RefCell<[u8; 2048]>, tm_buf: [u8; 2048], tc_pool: SharedStaticMemoryPool, tm_pool: SharedPacketPool, tc_sender: mpsc::SyncSender, tm_receiver: mpsc::Receiver, } pub type PusServiceHelperStatic = PusServiceHelper< MpscTcReceiver, PacketSenderWithSharedPool, EcssTcInSharedStoreConverter, VerificationReporter, >; impl PusServiceHandlerWithSharedStoreCommon { /// This function generates the structure in addition to the PUS service handler /// [PusServiceHandler] which might be required for a specific PUS service handler. /// /// The PUS service handler is instantiated with a [EcssTcInStoreConverter]. pub fn new(id: ComponentId) -> (Self, PusServiceHelperStatic) { let pool_cfg = StaticPoolConfig::new(alloc::vec![(16, 16), (8, 32), (4, 64)], false); let tc_pool = StaticMemoryPool::new(pool_cfg.clone()); let tm_pool = StaticMemoryPool::new(pool_cfg); let shared_tc_pool = SharedStaticMemoryPool::new(RwLock::new(tc_pool)); let shared_tm_pool = SharedStaticMemoryPool::new(RwLock::new(tm_pool)); let shared_tm_pool_wrapper = SharedPacketPool::new(&shared_tm_pool); let (test_srv_tc_tx, test_srv_tc_rx) = mpsc::sync_channel(10); let (tm_tx, tm_rx) = mpsc::sync_channel(10); let verif_cfg = VerificationReporterCfg::new(TEST_APID, 1, 2, 8).unwrap(); let verification_handler = VerificationReporter::new(TEST_COMPONENT_ID_0.id(), &verif_cfg); let test_srv_tm_sender = PacketSenderWithSharedPool::new(tm_tx, shared_tm_pool_wrapper.clone()); let in_store_converter = EcssTcInSharedStoreConverter::new(shared_tc_pool.clone(), 2048); ( Self { pus_buf: RefCell::new([0; 2048]), tm_buf: [0; 2048], tc_pool: shared_tc_pool, tm_pool: shared_tm_pool_wrapper, tc_sender: test_srv_tc_tx, tm_receiver: tm_rx, }, PusServiceHelper::new( id, test_srv_tc_rx, test_srv_tm_sender, verification_handler, in_store_converter, ), ) } pub fn send_tc( &self, sender_id: ComponentId, token: &VerificationToken, tc: &PusTcCreator, ) { let mut mut_buf = self.pus_buf.borrow_mut(); let tc_size = tc.write_to_bytes(mut_buf.as_mut_slice()).unwrap(); let mut tc_pool = self.tc_pool.write().unwrap(); let addr = tc_pool.add(&mut_buf[..tc_size]).unwrap(); drop(tc_pool); // Send accepted TC to test service handler. self.tc_sender .send(EcssTcAndToken::new( PacketInPool::new(sender_id, addr), *token, )) .expect("sending tc failed"); } pub fn read_next_tm(&mut self) -> PusTmReader<'_> { let next_msg = self.tm_receiver.try_recv(); assert!(next_msg.is_ok()); let tm_in_pool = next_msg.unwrap(); let tm_pool = self.tm_pool.0.read().unwrap(); let tm_raw = tm_pool.read_as_vec(&tm_in_pool.store_addr).unwrap(); self.tm_buf[0..tm_raw.len()].copy_from_slice(&tm_raw); PusTmReader::new(&self.tm_buf, 7).unwrap().0 } pub fn check_no_tm_available(&self) -> bool { let next_msg = self.tm_receiver.try_recv(); if let TryRecvError::Empty = next_msg.unwrap_err() { return true; } false } pub fn check_next_verification_tm(&self, subservice: u8, expected_request_id: RequestId) { let next_msg = self.tm_receiver.try_recv(); assert!(next_msg.is_ok()); let tm_in_pool = next_msg.unwrap(); let tm_pool = self.tm_pool.0.read().unwrap(); let tm_raw = tm_pool.read_as_vec(&tm_in_pool.store_addr).unwrap(); let tm = PusTmReader::new(&tm_raw, 7).unwrap().0; assert_eq!(PusPacket::service(&tm), 1); assert_eq!(PusPacket::subservice(&tm), subservice); assert_eq!(tm.apid(), TEST_APID); let req_id = RequestId::from_bytes(tm.user_data()).expect("generating request ID failed"); assert_eq!(req_id, expected_request_id); } } pub struct PusServiceHandlerWithVecCommon { current_tm: Option>, tc_sender: mpsc::Sender, tm_receiver: mpsc::Receiver, } pub type PusServiceHelperDynamic = PusServiceHelper< MpscTcReceiver, MpscTmAsVecSender, EcssTcInVecConverter, VerificationReporter, >; impl PusServiceHandlerWithVecCommon { pub fn new_with_standard_verif_reporter( id: ComponentId, ) -> (Self, PusServiceHelperDynamic) { let (test_srv_tc_tx, test_srv_tc_rx) = mpsc::channel(); let (tm_tx, tm_rx) = mpsc::channel(); let verif_cfg = VerificationReporterCfg::new(TEST_APID, 1, 2, 8).unwrap(); let verification_handler = VerificationReporter::new(TEST_COMPONENT_ID_0.id(), &verif_cfg); let in_store_converter = EcssTcInVecConverter::default(); ( Self { current_tm: None, tc_sender: test_srv_tc_tx, tm_receiver: tm_rx, }, PusServiceHelper::new( id, test_srv_tc_rx, tm_tx, verification_handler, in_store_converter, ), ) } } impl PusServiceHandlerWithVecCommon { pub fn new_with_test_verif_sender( id: ComponentId, ) -> ( Self, PusServiceHelper< MpscTcReceiver, MpscTmAsVecSender, EcssTcInVecConverter, TestVerificationReporter, >, ) { let (test_srv_tc_tx, test_srv_tc_rx) = mpsc::channel(); let (tm_tx, tm_rx) = mpsc::channel(); let in_store_converter = EcssTcInVecConverter::default(); let verification_handler = TestVerificationReporter::new(id); ( Self { current_tm: None, tc_sender: test_srv_tc_tx, tm_receiver: tm_rx, //verification_handler: verification_handler.clone(), }, PusServiceHelper::new( id, test_srv_tc_rx, tm_tx, verification_handler, in_store_converter, ), ) } } impl PusServiceHandlerWithVecCommon { pub fn send_tc( &self, sender_id: ComponentId, token: &VerificationToken, tc: &PusTcCreator, ) { // Send accepted TC to test service handler. self.tc_sender .send(EcssTcAndToken::new( TcInMemory::Vec(PacketAsVec::new( sender_id, tc.to_vec().expect("pus tc conversion to vec failed"), )), *token, )) .expect("sending tc failed"); } pub fn read_next_tm(&mut self) -> PusTmReader<'_> { let next_msg = self.tm_receiver.try_recv(); assert!(next_msg.is_ok()); self.current_tm = Some(next_msg.unwrap().packet); PusTmReader::new(self.current_tm.as_ref().unwrap(), 7) .unwrap() .0 } pub fn check_no_tm_available(&self) -> bool { let next_msg = self.tm_receiver.try_recv(); if let TryRecvError::Empty = next_msg.unwrap_err() { return true; } false } pub fn check_next_verification_tm(&self, subservice: u8, expected_request_id: RequestId) { let next_msg = self.tm_receiver.try_recv(); assert!(next_msg.is_ok()); let next_msg = next_msg.unwrap(); let tm = PusTmReader::new(next_msg.packet.as_slice(), 7).unwrap().0; assert_eq!(PusPacket::service(&tm), 1); assert_eq!(PusPacket::subservice(&tm), subservice); assert_eq!(tm.apid(), TEST_APID); let req_id = RequestId::from_bytes(tm.user_data()).expect("generating request ID failed"); assert_eq!(req_id, expected_request_id); } } pub const APP_DATA_TOO_SHORT: ResultU16 = ResultU16::new(1, 1); #[derive(Default)] pub struct TestConverter { pub conversion_request: VecDeque>, } impl TestConverter { pub fn check_service(&self, tc: &PusTcReader) -> Result<(), PusPacketHandlingError> { if tc.service() != SERVICE { return Err(PusPacketHandlingError::RequestConversion( GenericConversionError::WrongService(tc.service()), )); } Ok(()) } pub fn is_empty(&self) { self.conversion_request.is_empty(); } pub fn check_next_conversion(&mut self, tc: &PusTcCreator) { assert!(!self.conversion_request.is_empty()); assert_eq!( self.conversion_request.pop_front().unwrap(), tc.to_vec().unwrap() ); } } pub struct TestRouter { pub routing_requests: RefCell>, pub routing_errors: RefCell>, pub injected_routing_failure: RefCell>, } impl Default for TestRouter { fn default() -> Self { Self { routing_requests: Default::default(), routing_errors: Default::default(), injected_routing_failure: Default::default(), } } } impl TestRouter { pub fn check_for_injected_error(&self) -> Result<(), GenericRoutingError> { if self.injected_routing_failure.borrow().is_some() { return Err(self.injected_routing_failure.borrow_mut().take().unwrap()); } Ok(()) } pub fn handle_error( &self, target_id: ComponentId, _token: VerificationToken, _tc: &PusTcReader, error: GenericRoutingError, _time_stamp: &[u8], _verif_reporter: &impl VerificationReportingProvider, ) { self.routing_errors .borrow_mut() .push_back((target_id, error)); } pub fn no_routing_errors(&self) -> bool { self.routing_errors.borrow().is_empty() } pub fn retrieve_next_routing_error(&mut self) -> (ComponentId, GenericRoutingError) { if self.routing_errors.borrow().is_empty() { panic!("no routing request available"); } self.routing_errors.borrow_mut().pop_front().unwrap() } pub fn inject_routing_error(&mut self, error: GenericRoutingError) { *self.injected_routing_failure.borrow_mut() = Some(error); } pub fn is_empty(&self) -> bool { self.routing_requests.borrow().is_empty() } pub fn retrieve_next_request(&mut self) -> (ComponentId, REQUEST) { if self.routing_requests.borrow().is_empty() { panic!("no routing request available"); } self.routing_requests.borrow_mut().pop_front().unwrap() } } }