From 3d344c11cca23e8d536a5d6ab68935977b25743d Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 29 Jul 2025 20:26:37 +0200 Subject: [PATCH] add PUS A support --- CHANGELOG.md | 17 +- src/cfdp/lv.rs | 4 +- src/cfdp/pdu/file_data.rs | 6 +- src/cfdp/pdu/metadata.rs | 4 +- src/cfdp/pdu/nak.rs | 2 +- src/cfdp/tlv/mod.rs | 9 +- src/ecss/mod.rs | 22 +- src/ecss/tc.rs | 26 +- src/ecss/tc_pus_a.rs | 1353 +++++++++++++++++++++++++ src/ecss/tm.rs | 46 +- src/ecss/tm_pus_a.rs | 1952 +++++++++++++++++++++++++++++++++++++ 11 files changed, 3385 insertions(+), 56 deletions(-) create mode 100644 src/ecss/tc_pus_a.rs create mode 100644 src/ecss/tm_pus_a.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index e9b0320..72630f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,21 @@ and this project adheres to [Semantic Versioning](http://semver.org/). # [unreleased] +## Changed + +- `PusVersion::VersionNotSupported` contains raw version number instead of `PusVersion` enum now + to make it more flexible. +- `pus_version` API now returns a `Result` instead of a `PusVersion` to allow + modelling invalid version numbers properly. + +## Removed + +- `PusVersion::Invalid`, which will be modelled with `Result` now. + +## Added + +- Added PUS A legacy support for telecommands inside the `ecss.tc_pus_a` module + # [v0.15.0] 2025-07-18 ## Added @@ -35,7 +50,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). # [v0.13.1] 2025-03-21 -- Bugfix due to operator precendence for `PusTcSecondaryHeader::pus_version`, +- Bugfix due to operator precendence for `PusTcSecondaryHeader::pus_version`, `PusTcSecondaryHeaderWithoutTimestamp::pus_version`, `CdsTime::from_bytes_with_u16_days` and `CdsTime::from_bytes_with_u24_days` diff --git a/src/cfdp/lv.rs b/src/cfdp/lv.rs index fd07a5b..9c14cfc 100644 --- a/src/cfdp/lv.rs +++ b/src/cfdp/lv.rs @@ -64,7 +64,7 @@ pub(crate) fn generic_len_check_deserialization( impl<'data> Lv<'data> { #[inline] - pub fn new(data: &[u8]) -> Result { + pub fn new(data: &[u8]) -> Result, TlvLvDataTooLargeError> { if data.len() > u8::MAX as usize { return Err(TlvLvDataTooLargeError(data.len())); } @@ -86,7 +86,7 @@ impl<'data> Lv<'data> { /// Helper function to build a string LV. This is especially useful for the file or directory /// path LVs #[inline] - pub fn new_from_str(str_slice: &str) -> Result { + pub fn new_from_str(str_slice: &str) -> Result, TlvLvDataTooLargeError> { Self::new(str_slice.as_bytes()) } diff --git a/src/cfdp/pdu/file_data.rs b/src/cfdp/pdu/file_data.rs index 114e235..686a523 100644 --- a/src/cfdp/pdu/file_data.rs +++ b/src/cfdp/pdu/file_data.rs @@ -201,7 +201,7 @@ impl<'seg_meta, 'file_data> FileDataPdu<'seg_meta, 'file_data> { .calc_pdu_datafield_len(self.file_data.len() as u64) } - pub fn segment_metadata(&self) -> Option<&SegmentMetadata> { + pub fn segment_metadata(&self) -> Option<&SegmentMetadata<'_>> { self.common.segment_metadata.as_ref() } @@ -419,8 +419,8 @@ pub fn calculate_max_file_seg_len_for_max_packet_len_and_pdu_header( segment_metadata: Option<&SegmentMetadata>, ) -> usize { let mut subtract = pdu_header.header_len(); - if segment_metadata.is_some() { - subtract += 1 + segment_metadata.as_ref().unwrap().metadata().unwrap().len(); + if let Some(segment_metadata) = segment_metadata { + subtract += 1 + segment_metadata.metadata().unwrap().len(); } if pdu_header.common_pdu_conf().file_flag == LargeFileFlag::Large { subtract += 8; diff --git a/src/cfdp/pdu/metadata.rs b/src/cfdp/pdu/metadata.rs index a63e11c..f1ae30d 100644 --- a/src/cfdp/pdu/metadata.rs +++ b/src/cfdp/pdu/metadata.rs @@ -338,11 +338,11 @@ impl<'raw> MetadataPduReader<'raw> { &self.metadata_params } - pub fn src_file_name(&self) -> Lv { + pub fn src_file_name(&self) -> Lv<'_> { self.src_file_name } - pub fn dest_file_name(&self) -> Lv { + pub fn dest_file_name(&self) -> Lv<'_> { self.dest_file_name } } diff --git a/src/cfdp/pdu/nak.rs b/src/cfdp/pdu/nak.rs index 7f23a15..6be4fd1 100644 --- a/src/cfdp/pdu/nak.rs +++ b/src/cfdp/pdu/nak.rs @@ -131,7 +131,7 @@ impl<'seg_reqs> NakPduCreator<'seg_reqs> { self.end_of_scope } - pub fn segment_requests(&self) -> Option<&SegmentRequests> { + pub fn segment_requests(&self) -> Option<&SegmentRequests<'_>> { self.segment_requests.as_ref() } diff --git a/src/cfdp/tlv/mod.rs b/src/cfdp/tlv/mod.rs index 2b80cc6..d0fd38f 100644 --- a/src/cfdp/tlv/mod.rs +++ b/src/cfdp/tlv/mod.rs @@ -153,14 +153,17 @@ pub struct Tlv<'data> { } impl<'data> Tlv<'data> { - pub fn new(tlv_type: TlvType, data: &[u8]) -> Result { + pub fn new(tlv_type: TlvType, data: &[u8]) -> Result, TlvLvDataTooLargeError> { Ok(Tlv { tlv_type_field: TlvTypeField::Standard(tlv_type), lv: Lv::new(data)?, }) } - pub fn new_with_custom_type(tlv_type: u8, data: &[u8]) -> Result { + pub fn new_with_custom_type( + tlv_type: u8, + data: &[u8], + ) -> Result, TlvLvDataTooLargeError> { Ok(Tlv { tlv_type_field: TlvTypeField::Custom(tlv_type), lv: Lv::new(data)?, @@ -370,7 +373,7 @@ impl EntityIdTlv { } /// Convert to a generic [Tlv], which also erases the programmatic type information. - pub fn to_tlv(self, buf: &mut [u8]) -> Result { + pub fn to_tlv(self, buf: &mut [u8]) -> Result, ByteConversionError> { Self::len_check(buf)?; self.entity_id .write_to_be_bytes(&mut buf[2..2 + self.entity_id.size()])?; diff --git a/src/ecss/mod.rs b/src/ecss/mod.rs index 63229d5..8649933 100644 --- a/src/ecss/mod.rs +++ b/src/ecss/mod.rs @@ -19,7 +19,9 @@ pub mod event; pub mod hk; pub mod scheduling; pub mod tc; +pub mod tc_pus_a; pub mod tm; +pub mod tm_pus_a; pub mod verification; pub type CrcType = u16; @@ -80,18 +82,17 @@ pub enum PusVersion { EsaPus = 0, PusA = 1, PusC = 2, - Invalid = 0b1111, } impl TryFrom for PusVersion { - type Error = (); + type Error = u8; fn try_from(value: u8) -> Result { match value { x if x == PusVersion::EsaPus as u8 => Ok(PusVersion::EsaPus), x if x == PusVersion::PusA as u8 => Ok(PusVersion::PusA), x if x == PusVersion::PusC as u8 => Ok(PusVersion::PusC), - _ => Err(()), + _ => Err(value), } } } @@ -154,7 +155,7 @@ pub enum PfcReal { #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum PusError { #[error("PUS version {0:?} not supported")] - VersionNotSupported(PusVersion), + VersionNotSupported(u8), #[error("checksum verification for crc16 {0:#06x} failed")] ChecksumFailure(u16), /// CRC16 needs to be calculated first @@ -167,9 +168,7 @@ pub enum PusError { /// Generic trait to describe common attributes for both PUS Telecommands (TC) and PUS Telemetry /// (TM) packets. All PUS packets are also a special type of [CcsdsPacket]s. pub trait PusPacket: CcsdsPacket { - const PUS_VERSION: PusVersion = PusVersion::PusC; - - fn pus_version(&self) -> PusVersion; + fn pus_version(&self) -> Result; fn service(&self) -> u8; fn subservice(&self) -> u8; fn user_data(&self) -> &[u8]; @@ -369,6 +368,7 @@ generic_ecss_enum_typedefs_and_from_impls! { /// byte representation. This is especially useful for generic abstractions which depend only /// on the serialization of those packets. pub trait WritablePusPacket { + /// The length here also includes the CRC length. fn len_written(&self) -> usize; /// Writes the packet to the given slice without writing the CRC. @@ -536,9 +536,9 @@ mod tests { #[test] fn test_pus_error_display() { - let unsupport_version = PusError::VersionNotSupported(super::PusVersion::EsaPus); + let unsupport_version = PusError::VersionNotSupported(super::PusVersion::EsaPus as u8); let write_str = unsupport_version.to_string(); - assert_eq!(write_str, "PUS version EsaPus not supported") + assert_eq!(write_str, "PUS version 0 not supported") } #[test] @@ -572,8 +572,8 @@ mod tests { #[test] fn test_pus_error_eq_impl() { assert_eq!( - PusError::VersionNotSupported(PusVersion::EsaPus), - PusError::VersionNotSupported(PusVersion::EsaPus) + PusError::VersionNotSupported(PusVersion::EsaPus as u8), + PusError::VersionNotSupported(PusVersion::EsaPus as u8) ); } diff --git a/src/ecss/tc.rs b/src/ecss/tc.rs index 11d5b9b..5832666 100644 --- a/src/ecss/tc.rs +++ b/src/ecss/tc.rs @@ -79,7 +79,7 @@ pub const ACK_ALL: u8 = AckOpts::Acceptance as u8 | AckOpts::Completion as u8; pub trait GenericPusTcSecondaryHeader { - fn pus_version(&self) -> PusVersion; + fn pus_version(&self) -> Result; fn ack_flags(&self) -> u8; fn service(&self) -> u8; fn subservice(&self) -> u8; @@ -104,7 +104,7 @@ pub mod zc { type Error = PusError; fn try_from(value: crate::ecss::tc::PusTcSecondaryHeader) -> Result { if value.version != PusVersion::PusC { - return Err(PusError::VersionNotSupported(value.version)); + return Err(PusError::VersionNotSupported(value.version as u8)); } Ok(PusTcSecondaryHeader { version_ack: ((value.version as u8) << 4) | value.ack, @@ -117,8 +117,8 @@ pub mod zc { impl GenericPusTcSecondaryHeader for PusTcSecondaryHeader { #[inline] - fn pus_version(&self) -> PusVersion { - PusVersion::try_from((self.version_ack >> 4) & 0b1111).unwrap_or(PusVersion::Invalid) + fn pus_version(&self) -> Result { + PusVersion::try_from((self.version_ack >> 4) & 0b1111) } #[inline] @@ -156,8 +156,8 @@ pub struct PusTcSecondaryHeader { impl GenericPusTcSecondaryHeader for PusTcSecondaryHeader { #[inline] - fn pus_version(&self) -> PusVersion { - self.version + fn pus_version(&self) -> Result { + Ok(self.version) } #[inline] @@ -444,7 +444,7 @@ impl CcsdsPacket for PusTcCreator<'_> { impl PusPacket for PusTcCreator<'_> { delegate!(to self.sec_header { #[inline] - fn pus_version(&self) -> PusVersion; + fn pus_version(&self) -> Result; #[inline] fn service(&self) -> u8; #[inline] @@ -465,7 +465,7 @@ impl PusPacket for PusTcCreator<'_> { impl GenericPusTcSecondaryHeader for PusTcCreator<'_> { delegate!(to self.sec_header { #[inline] - fn pus_version(&self) -> PusVersion; + fn pus_version(&self) -> Result; #[inline] fn service(&self) -> u8; #[inline] @@ -730,7 +730,7 @@ impl CcsdsPacket for PusTcReader<'_> { impl PusPacket for PusTcReader<'_> { delegate!(to self.sec_header { #[inline] - fn pus_version(&self) -> PusVersion; + fn pus_version(&self) -> Result; #[inline] fn service(&self) -> u8; #[inline] @@ -751,7 +751,7 @@ impl PusPacket for PusTcReader<'_> { impl GenericPusTcSecondaryHeader for PusTcReader<'_> { delegate!(to self.sec_header { #[inline] - fn pus_version(&self) -> PusVersion; + fn pus_version(&self) -> Result; #[inline] fn service(&self) -> u8; #[inline] @@ -1157,14 +1157,14 @@ mod tests { assert_eq!(PusPacket::subservice(tc), 1); assert_eq!(GenericPusTcSecondaryHeader::subservice(tc), 1); assert!(tc.sec_header_flag()); - assert_eq!(PusPacket::pus_version(tc), PusC); + assert_eq!(PusPacket::pus_version(tc).unwrap(), PusC); assert_eq!(tc.seq_count(), 0x34); assert_eq!(tc.source_id(), 0); assert_eq!(tc.apid(), 0x02); assert_eq!(tc.ack_flags(), ACK_ALL); - assert_eq!(PusPacket::pus_version(tc), PusVersion::PusC); + assert_eq!(PusPacket::pus_version(tc).unwrap(), PusVersion::PusC); assert_eq!( - GenericPusTcSecondaryHeader::pus_version(tc), + GenericPusTcSecondaryHeader::pus_version(tc).unwrap(), PusVersion::PusC ); } diff --git a/src/ecss/tc_pus_a.rs b/src/ecss/tc_pus_a.rs new file mode 100644 index 0000000..6324090 --- /dev/null +++ b/src/ecss/tc_pus_a.rs @@ -0,0 +1,1353 @@ +//! This module contains all components required to create ECSS PUS A legacy telecommands. +//! +//! # Examples +//! +//! ```rust +//! use spacepackets::{CcsdsPacket, SpHeader}; +//! use spacepackets::ecss::{PusPacket, WritablePusPacket}; +//! use spacepackets::ecss::tc_pus_a::{PusTcCreator, PusTcReader, PusTcSecondaryHeader}; +//! +//! // Create a ping telecommand with no user application data +//! let pus_tc = PusTcCreator::new_no_app_data( +//! SpHeader::new_from_apid(0x02), +//! PusTcSecondaryHeader::new_simple(17, 1), +//! true +//! ); +//! println!("{:?}", pus_tc); +//! assert_eq!(pus_tc.service(), 17); +//! assert_eq!(pus_tc.subservice(), 1); +//! assert_eq!(pus_tc.apid(), 0x02); +//! +//! // Serialize TC into a raw buffer +//! let mut test_buf: [u8; 32] = [0; 32]; +//! let size = pus_tc +//! .write_to_bytes(test_buf.as_mut_slice()) +//! .expect("Error writing TC to buffer"); +//! assert_eq!(size, 11); +//! println!("{:?}", &test_buf[0..size]); +//! +//! // Deserialize from the raw byte representation. No source ID, 0 spare bytes. +//! let pus_tc_deserialized = PusTcReader::new(&test_buf, None, 0).expect("Deserialization failed"); +//! assert_eq!(pus_tc.service(), 17); +//! assert_eq!(pus_tc.subservice(), 1); +//! assert_eq!(pus_tc.apid(), 0x02); +//! ``` +use crate::crc::{CRC_CCITT_FALSE, CRC_CCITT_FALSE_NO_TABLE}; +use crate::ecss::{ + ccsds_impl, crc_from_raw_data, sp_header_impls, user_data_from_raw, + verify_crc16_ccitt_false_from_raw_to_pus_error, PusError, PusPacket, PusVersion, + WritablePusPacket, +}; +use crate::util::{UnsignedByteField, UnsignedEnum}; +use crate::SpHeader; +use crate::{ByteConversionError, CcsdsPacket, PacketType, SequenceFlags, CCSDS_HEADER_LEN}; +use core::mem::size_of; +use delegate::delegate; +use num_enum::{IntoPrimitive, TryFromPrimitive}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use zerocopy::IntoBytes; + +#[cfg(feature = "alloc")] +use alloc::vec::Vec; + +use super::verify_crc16_ccitt_false_from_raw_to_pus_error_no_table; + +const PUS_VERSION: PusVersion = PusVersion::PusA; + +/// Marker trait for PUS telecommand structures. +pub trait IsPusTelecommand {} + +#[derive(Debug, Eq, PartialEq, Copy, Clone, IntoPrimitive, TryFromPrimitive)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +enum AckOpts { + Acceptance = 0b1000, + Start = 0b0100, + Progress = 0b0010, + Completion = 0b0001, +} + +/// Assuming 8 bytes of source ID and 7 bytes of spare. +pub const MAX_SEC_HEADER_LEN: usize = 18; +pub const MAX_SPARE_BYTES: usize = 7; + +#[derive(Debug, Eq, PartialEq, Copy, Clone, thiserror::Error)] +#[error("invalid number of spare bytes, must be between 0 and 7")] +pub struct InvalidNumberOfSpareBytesError; + +#[derive(Debug, Eq, PartialEq, Copy, Clone, thiserror::Error)] +#[error("invalid version, expected PUS A (1), got {0}")] +pub struct VersionError(pub u8); + +pub const ACK_ALL: u8 = AckOpts::Acceptance as u8 + | AckOpts::Start as u8 + | AckOpts::Progress as u8 + | AckOpts::Completion as u8; + +pub trait GenericPusTcSecondaryHeader { + fn pus_version(&self) -> Result; + fn ack_flags(&self) -> u8; + fn service(&self) -> u8; + fn subservice(&self) -> u8; + fn source_id(&self) -> Option; + fn spare_bytes(&self) -> usize; +} + +#[derive(PartialEq, Eq, Copy, Clone, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PusTcSecondaryHeader { + pub service: u8, + pub subservice: u8, + pub source_id: Option, + pub ack: u8, + pub version: PusVersion, + spare_bytes: usize, +} + +impl GenericPusTcSecondaryHeader for PusTcSecondaryHeader { + #[inline] + fn pus_version(&self) -> Result { + Ok(self.version) + } + + #[inline] + fn ack_flags(&self) -> u8 { + self.ack + } + + #[inline] + fn service(&self) -> u8 { + self.service + } + + #[inline] + fn subservice(&self) -> u8 { + self.subservice + } + + #[inline] + fn source_id(&self) -> Option { + self.source_id + } + + #[inline] + fn spare_bytes(&self) -> usize { + self.spare_bytes + } +} + +impl PusTcSecondaryHeader { + #[inline] + pub fn new_simple(service: u8, subservice: u8) -> Self { + PusTcSecondaryHeader { + service, + subservice, + ack: ACK_ALL, + source_id: None, + version: PUS_VERSION, + spare_bytes: 0, + } + } + + #[inline] + pub fn new( + service: u8, + subservice: u8, + ack: u8, + source_id: Option, + spare_bytes: usize, + ) -> Self { + PusTcSecondaryHeader { + service, + subservice, + ack: ack & 0b1111, + source_id, + version: PUS_VERSION, + spare_bytes, + } + } + + /// Set number of spare bytes. Any value larger than 7 will be ignored. + pub fn set_spare_bytes(&mut self, spare_bytes: usize) { + if spare_bytes > 7 { + return; + } + self.spare_bytes = spare_bytes; + } + + pub fn written_len(&self) -> usize { + let mut len = 3 + self.spare_bytes; + if let Some(source_id) = self.source_id { + len += source_id.size(); + } + len + } + + #[cfg(feature = "alloc")] + pub fn to_vec(&self) -> Result, ByteConversionError> { + let mut buf = alloc::vec![0; self.written_len()]; + self.write_to_be_bytes(&mut buf)?; + Ok(buf) + } + + pub fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result { + if buf.len() < self.written_len() { + return Err(ByteConversionError::ToSliceTooSmall { + found: buf.len(), + expected: self.written_len(), + }); + } + let mut current_idx = 0; + buf[0] = ((PUS_VERSION as u8) << 4) | self.ack; + buf[1] = self.service; + buf[2] = self.subservice; + current_idx += 3; + if let Some(source_id) = self.source_id { + current_idx += source_id + .write_to_be_bytes(&mut buf[current_idx..current_idx + source_id.size()]) + .unwrap(); + } + if self.spare_bytes > 0 { + buf[current_idx..current_idx + self.spare_bytes].fill(0); + current_idx += self.spare_bytes; + } + + Ok(current_idx) + } + + pub fn from_bytes( + data: &[u8], + source_id_size: Option, + spare_bytes: usize, + ) -> Result { + let expected_len = 3 + source_id_size.unwrap_or(0) + spare_bytes; + if data.len() < expected_len { + return Err(PusError::ByteConversion( + ByteConversionError::FromSliceTooSmall { + found: data.len(), + expected: expected_len, + }, + )); + } + let version = (data[0] >> 4) & 0b111; + if version != PusVersion::PusA as u8 { + return Err(PusError::VersionNotSupported(version)); + } + let ack = data[0] & 0b1111; + let service = data[1]; + let subservice = data[2]; + let mut source_id = None; + if let Some(source_id_len) = source_id_size { + source_id = Some( + UnsignedByteField::new_from_be_bytes(source_id_len, &data[3..3 + source_id_len]) + .unwrap(), + ); + } + Ok(Self { + service, + subservice, + source_id, + ack, + version: PusVersion::PusA, + spare_bytes, + }) + } +} + +/// This class can be used to create PUS C telecommand packet. It is the primary data structure to +/// generate the raw byte representation of a PUS telecommand. +/// +/// This class also derives the [serde::Serialize] and [serde::Deserialize] trait if the +/// [serde] feature is used, which allows to send around TC packets in a raw byte format using a +/// serde provider like [postcard](https://docs.rs/postcard/latest/postcard/). +/// +/// There is no spare bytes support yet. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PusTcCreator<'app_data> { + sp_header: SpHeader, + pub sec_header: PusTcSecondaryHeader, + app_data: &'app_data [u8], +} + +impl<'app_data> PusTcCreator<'app_data> { + /// Generates a new struct instance. + /// + /// # Arguments + /// + /// * `sp_header` - Space packet header information. The correct packet type and the secondary + /// header flag are set correctly by the constructor. + /// * `sec_header` - Information contained in the data field header, including the service + /// and subservice type + /// * `app_data` - Custom application data + /// * `set_ccsds_len` - Can be used to automatically update the CCSDS space packet data length + /// field. If this is not set to true, [Self::update_ccsds_data_len] can be called to set + /// the correct value to this field manually + #[inline] + pub fn new( + mut sp_header: SpHeader, + sec_header: PusTcSecondaryHeader, + app_data: &'app_data [u8], + set_ccsds_len: bool, + ) -> Self { + sp_header.set_packet_type(PacketType::Tc); + sp_header.set_sec_header_flag(); + let mut pus_tc = Self { + sp_header, + app_data, + sec_header, + }; + if set_ccsds_len { + pus_tc.update_ccsds_data_len(); + } + pus_tc + } + + /// Simplified version of the [Self::new] function which allows to only specify service + /// and subservice instead of the full PUS TC secondary header. + #[inline] + pub fn new_simple( + sph: SpHeader, + service: u8, + subservice: u8, + app_data: &'app_data [u8], + set_ccsds_len: bool, + ) -> Self { + Self::new( + sph, + PusTcSecondaryHeader::new(service, subservice, ACK_ALL, None, 0), + app_data, + set_ccsds_len, + ) + } + + #[inline] + pub fn new_no_app_data( + sp_header: SpHeader, + sec_header: PusTcSecondaryHeader, + set_ccsds_len: bool, + ) -> Self { + Self::new(sp_header, sec_header, &[], set_ccsds_len) + } + + #[inline] + pub fn sp_header(&self) -> &SpHeader { + &self.sp_header + } + + #[inline] + pub fn sp_header_mut(&mut self) -> &mut SpHeader { + &mut self.sp_header + } + + #[inline] + pub fn set_ack_field(&mut self, ack: u8) -> bool { + if ack > 0b1111 { + return false; + } + self.sec_header.ack = ack & 0b1111; + true + } + + #[inline] + pub fn set_source_id(&mut self, source_id: Option) { + self.sec_header.source_id = source_id; + } + + #[inline] + pub fn app_data(&'app_data self) -> &'app_data [u8] { + self.user_data() + } + + sp_header_impls!(); + + /// Calculate the CCSDS space packet data length field and sets it + /// This is called automatically if the `set_ccsds_len` argument in the [Self::new] call was + /// used. + /// If this was not done or the application data is set or changed after construction, + /// this function needs to be called to ensure that the data length field of the CCSDS header + /// is set correctly. + #[inline] + pub fn update_ccsds_data_len(&mut self) { + self.sp_header.data_len = + self.len_written() as u16 - size_of::() as u16 - 1; + } + + /// This function calculates and returns the CRC16 for the current packet. + pub fn calc_own_crc16(&self) -> u16 { + let mut digest = CRC_CCITT_FALSE.digest(); + let sph_zc = crate::zc::SpHeader::from(self.sp_header); + digest.update(sph_zc.as_bytes()); + let mut buf: [u8; MAX_SEC_HEADER_LEN] = [0; MAX_SEC_HEADER_LEN]; + // Can not fail. + let sec_header_len = self.sec_header.write_to_be_bytes(&mut buf).unwrap(); + digest.update(&buf[0..sec_header_len]); + digest.update(self.app_data); + digest.finalize() + } + + /// This function calculates and returns the CRC16 for the current packet. + pub fn calc_own_crc16_no_table(&self) -> u16 { + let mut digest = CRC_CCITT_FALSE_NO_TABLE.digest(); + let sph_zc = crate::zc::SpHeader::from(self.sp_header); + digest.update(sph_zc.as_bytes()); + let mut buf: [u8; MAX_SEC_HEADER_LEN] = [0; MAX_SEC_HEADER_LEN]; + // Can not fail. + let sec_header_len = self.sec_header.write_to_be_bytes(&mut buf).unwrap(); + digest.update(&buf[0..sec_header_len]); + digest.update(self.app_data); + digest.finalize() + } + + #[cfg(feature = "alloc")] + pub fn append_to_vec(&self, vec: &mut Vec) -> usize { + let sph_zc = crate::zc::SpHeader::from(self.sp_header); + let start_idx = vec.len(); + vec.extend_from_slice(sph_zc.as_bytes()); + vec.extend_from_slice(self.sec_header.to_vec().unwrap().as_slice()); + vec.extend_from_slice(self.app_data); + let mut digest = CRC_CCITT_FALSE.digest(); + let mut appended_len = + CCSDS_HEADER_LEN + self.sec_header.written_len() + self.app_data.len(); + digest.update(&vec[start_idx..start_idx + appended_len]); + vec.extend_from_slice(&digest.finalize().to_be_bytes()); + appended_len += 2; + appended_len + } + + /// Write the raw PUS byte representation to a provided buffer. + pub fn write_to_bytes(&self, slice: &mut [u8]) -> Result { + let writer_unfinalized = self.common_write(slice)?; + Ok(writer_unfinalized.finalize()) + } + + /// Write the raw PUS byte representation to a provided buffer. + pub fn write_to_bytes_crc_no_table( + &self, + slice: &mut [u8], + ) -> Result { + let writer_unfinalized = self.common_write(slice)?; + Ok(writer_unfinalized.finalize_crc_no_table()) + } + + /// Write the raw PUS byte representation to a provided buffer. + pub fn write_to_bytes_no_crc(&self, slice: &mut [u8]) -> Result { + let writer_unfinalized = self.common_write(slice)?; + Ok(writer_unfinalized.finalize_no_crc()) + } + + fn common_write<'a>( + &self, + slice: &'a mut [u8], + ) -> Result, ByteConversionError> { + if self.len_written() > slice.len() { + return Err(ByteConversionError::ToSliceTooSmall { + found: slice.len(), + expected: self.len_written(), + }); + } + let mut writer_unfinalized = PusTcCreatorWithReservedAppData::write_to_bytes_partially( + slice, + self.sp_header, + self.sec_header, + self.app_data.len(), + )?; + writer_unfinalized + .app_data_mut() + .copy_from_slice(self.app_data); + Ok(writer_unfinalized) + } +} + +impl WritablePusPacket for PusTcCreator<'_> { + #[inline] + fn len_written(&self) -> usize { + CCSDS_HEADER_LEN + self.sec_header.written_len() + self.app_data.len() + 2 + } + + /// Write the raw PUS byte representation to a provided buffer. + fn write_to_bytes_no_crc(&self, slice: &mut [u8]) -> Result { + Ok(Self::write_to_bytes_no_crc(self, slice)?) + } + + fn write_to_bytes(&self, slice: &mut [u8]) -> Result { + Ok(Self::write_to_bytes(self, slice)?) + } + + fn write_to_bytes_crc_no_table(&self, slice: &mut [u8]) -> Result { + Ok(Self::write_to_bytes_crc_no_table(self, slice)?) + } +} + +impl CcsdsPacket for PusTcCreator<'_> { + ccsds_impl!(); +} + +impl PusPacket for PusTcCreator<'_> { + delegate!(to self.sec_header { + #[inline] + fn pus_version(&self) -> Result; + #[inline] + fn service(&self) -> u8; + #[inline] + fn subservice(&self) -> u8; + }); + + #[inline] + fn user_data(&self) -> &[u8] { + self.app_data + } + + #[inline] + fn opt_crc16(&self) -> Option { + Some(self.calc_own_crc16()) + } +} + +impl GenericPusTcSecondaryHeader for PusTcCreator<'_> { + delegate!(to self.sec_header { + #[inline] + fn pus_version(&self) -> Result; + #[inline] + fn service(&self) -> u8; + #[inline] + fn subservice(&self) -> u8; + #[inline] + fn source_id(&self) -> Option; + #[inline] + fn ack_flags(&self) -> u8; + #[inline] + fn spare_bytes(&self) -> usize; + }); +} + +impl IsPusTelecommand for PusTcCreator<'_> {} + +/// A specialized variant of [PusTcCreator] designed for efficiency when handling large source +/// data. +/// +/// Unlike [PusTcCreator], this type does not require the user to provide the application data +/// as a separate slice. Instead, it allows writing the application data directly into the provided +/// serialization buffer. This eliminates the need for an intermediate buffer and the associated +/// memory copy, improving performance, particularly when working with large payloads. +/// +/// **Important:** The total length of the source data must be known and specified in advance +/// to ensure correct serialization behavior. +/// +/// Note that this abstraction intentionally omits certain trait implementations that are available +/// on [PusTcCreator], as they are not applicable in this optimized usage pattern. +pub struct PusTcCreatorWithReservedAppData<'buf> { + buf: &'buf mut [u8], + app_data_offset: usize, + full_len: usize, +} + +impl<'buf> PusTcCreatorWithReservedAppData<'buf> { + /// Generates a new instance with reserved space for the user application data. + /// + /// # Arguments + /// + /// * `sp_header` - Space packet header information. The correct packet type and the secondary + /// header flag are set correctly by the constructor. + /// * `sec_header` - Information contained in the secondary header, including the service + /// and subservice type + /// * `app_data_len` - Custom application data length + #[inline] + pub fn new( + buf: &'buf mut [u8], + mut sp_header: SpHeader, + sec_header: PusTcSecondaryHeader, + app_data_len: usize, + ) -> Result { + sp_header.set_packet_type(PacketType::Tc); + sp_header.set_sec_header_flag(); + let len_written = CCSDS_HEADER_LEN + sec_header.written_len() + app_data_len + 2; + if len_written > buf.len() { + return Err(ByteConversionError::ToSliceTooSmall { + found: buf.len(), + expected: len_written, + }); + } + sp_header.data_len = len_written as u16 - size_of::() as u16 - 1; + Self::write_to_bytes_partially(buf, sp_header, sec_header, app_data_len) + } + + fn write_to_bytes_partially( + buf: &'buf mut [u8], + sp_header: SpHeader, + sec_header: PusTcSecondaryHeader, + app_data_len: usize, + ) -> Result { + let mut curr_idx = 0; + sp_header.write_to_be_bytes(&mut buf[0..CCSDS_HEADER_LEN])?; + curr_idx += CCSDS_HEADER_LEN; + curr_idx += sec_header + .write_to_be_bytes(&mut buf[curr_idx..curr_idx + sec_header.written_len()])?; + let app_data_offset = curr_idx; + curr_idx += app_data_len; + Ok(Self { + buf, + app_data_offset, + full_len: curr_idx + 2, + }) + } + + #[inline] + pub const fn len_written(&self) -> usize { + self.full_len + } + + /// Mutable access to the application data buffer. + #[inline] + pub fn app_data_mut(&mut self) -> &mut [u8] { + &mut self.buf[self.app_data_offset..self.full_len - 2] + } + + /// Access to the source data buffer. + #[inline] + pub fn app_data(&self) -> &[u8] { + &self.buf[self.app_data_offset..self.full_len - 2] + } + + #[inline] + pub fn app_data_len(&self) -> usize { + self.full_len - 2 - self.app_data_offset + } + + /// Finalize the TC packet by calculating and writing the CRC16. + /// + /// Returns the full packet length. + pub fn finalize(self) -> usize { + let mut digest = CRC_CCITT_FALSE.digest(); + digest.update(&self.buf[0..self.full_len - 2]); + self.buf[self.full_len - 2..self.full_len] + .copy_from_slice(&digest.finalize().to_be_bytes()); + self.full_len + } + + /// Finalize the TC packet by calculating and writing the CRC16 using a table-less + /// implementation. + /// + /// Returns the full packet length. + pub fn finalize_crc_no_table(self) -> usize { + let mut digest = CRC_CCITT_FALSE_NO_TABLE.digest(); + digest.update(&self.buf[0..self.full_len - 2]); + self.buf[self.full_len - 2..self.full_len] + .copy_from_slice(&digest.finalize().to_be_bytes()); + self.full_len + } + + /// Finalize the TC packet without writing the CRC16. + /// + /// Returns the length WITHOUT the CRC16. + #[inline] + pub fn finalize_no_crc(self) -> usize { + self.full_len - 2 + } +} + +/// This class can be used to read a PUS TC telecommand from raw memory. +/// +/// This class also derives the [serde::Serialize] and [serde::Deserialize] trait if the +/// [serde] feature is used, which allows to send around TC packets in a raw byte format using a +/// serde provider like [postcard](https://docs.rs/postcard/latest/postcard/). +/// +/// There is no spare bytes support yet. +/// +/// # Lifetimes +/// +/// * `'raw_data` - Lifetime of the provided raw slice. +#[derive(Eq, Copy, Clone, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PusTcReader<'raw_data> { + #[cfg_attr(feature = "serde", serde(skip))] + raw_data: &'raw_data [u8], + sp_header: SpHeader, + sec_header: PusTcSecondaryHeader, + app_data: &'raw_data [u8], + crc16: u16, +} + +impl<'raw_data> PusTcReader<'raw_data> { + /// Create a [PusTcReader] instance from a raw slice. On success, it returns a tuple containing + /// the instance and the found byte length of the packet. This function also performs a CRC + /// check and will return an appropriate [PusError] if the check fails. + pub fn new( + slice: &'raw_data [u8], + source_id_size: Option, + spare_bytes: usize, + ) -> Result { + let pus_tc = Self::new_no_crc_check(slice, source_id_size, spare_bytes)?; + verify_crc16_ccitt_false_from_raw_to_pus_error(pus_tc.raw_data(), pus_tc.crc16())?; + Ok(pus_tc) + } + + /// Similar to [PusTcReader::new], but uses a table-less CRC16 algorithm which can reduce + /// binary size and memory usage. + pub fn new_crc_no_table( + slice: &'raw_data [u8], + source_id_size: Option, + spare_bytes: usize, + ) -> Result { + let pus_tc = Self::new_no_crc_check(slice, source_id_size, spare_bytes)?; + verify_crc16_ccitt_false_from_raw_to_pus_error_no_table(pus_tc.raw_data(), pus_tc.crc16())?; + Ok(pus_tc) + } + + /// Creates a new instance without performing a CRC check. + pub fn new_no_crc_check( + slice: &'raw_data [u8], + source_id_size: Option, + spare_bytes: usize, + ) -> Result { + let raw_data_len = slice.len(); + if raw_data_len < CCSDS_HEADER_LEN { + return Err(ByteConversionError::FromSliceTooSmall { + found: raw_data_len, + expected: CCSDS_HEADER_LEN, + } + .into()); + } + let mut current_idx = 0; + let (sp_header, _) = SpHeader::from_be_bytes(&slice[0..CCSDS_HEADER_LEN])?; + current_idx += CCSDS_HEADER_LEN; + let total_len = sp_header.total_len(); + if raw_data_len < total_len { + return Err(ByteConversionError::FromSliceTooSmall { + found: raw_data_len, + expected: total_len, + } + .into()); + } + let sec_header = + PusTcSecondaryHeader::from_bytes(&slice[current_idx..], source_id_size, spare_bytes) + .map_err(|_| ByteConversionError::ZeroCopyFromError)?; + current_idx += sec_header.written_len(); + let raw_data = &slice[0..total_len]; + Ok(Self { + sp_header, + sec_header, + raw_data, + app_data: user_data_from_raw(current_idx, total_len, slice)?, + crc16: crc_from_raw_data(raw_data)?, + }) + } + + #[inline] + pub fn app_data(&self) -> &[u8] { + self.user_data() + } + + #[inline] + pub fn raw_data(&self) -> &[u8] { + self.raw_data + } + + #[inline] + pub fn len_packed(&self) -> usize { + self.sp_header.total_len() + } + + #[inline] + pub fn sp_header(&self) -> &SpHeader { + &self.sp_header + } + + #[inline] + pub fn crc16(&self) -> u16 { + self.crc16 + } +} + +impl PartialEq for PusTcReader<'_> { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.raw_data == other.raw_data + } +} + +impl CcsdsPacket for PusTcReader<'_> { + ccsds_impl!(); +} + +impl PusPacket for PusTcReader<'_> { + delegate!(to self.sec_header { + #[inline] + fn service(&self) -> u8; + #[inline] + fn subservice(&self) -> u8; + #[inline] + fn pus_version(&self) -> Result; + }); + + #[inline] + fn user_data(&self) -> &[u8] { + self.app_data + } + + #[inline] + fn opt_crc16(&self) -> Option { + Some(self.crc16) + } +} + +impl GenericPusTcSecondaryHeader for PusTcReader<'_> { + delegate!(to self.sec_header { + #[inline] + fn pus_version(&self) -> Result; + #[inline] + fn service(&self) -> u8; + #[inline] + fn subservice(&self) -> u8; + #[inline] + fn source_id(&self) -> Option; + #[inline] + fn ack_flags(&self) -> u8; + #[inline] + fn spare_bytes(&self) -> usize; + }); +} + +impl IsPusTelecommand for PusTcReader<'_> {} + +impl PartialEq> for PusTcReader<'_> { + fn eq(&self, other: &PusTcCreator) -> bool { + self.sp_header == other.sp_header + && self.sec_header == other.sec_header + && self.app_data == other.app_data + } +} + +impl PartialEq> for PusTcCreator<'_> { + fn eq(&self, other: &PusTcReader) -> bool { + self.sp_header == other.sp_header + && self.sec_header == other.sec_header + && self.app_data == other.app_data + } +} + +#[cfg(all(test, feature = "std"))] +mod tests { + use std::println; + + use super::*; + use crate::ecss::{PusError, PusPacket, WritablePusPacket}; + use crate::util::{UnsignedByteFieldU16, UnsignedByteFieldU8}; + use crate::{ByteConversionError, SpHeader}; + use crate::{CcsdsPacket, SequenceFlags}; + use alloc::string::ToString; + use alloc::vec::Vec; + #[cfg(feature = "serde")] + use postcard::{from_bytes, to_allocvec}; + + fn base_ping_tc_full_ctor() -> PusTcCreator<'static> { + let sph = SpHeader::new_for_unseg_tc_checked(0x02, 0x34, 0).unwrap(); + let tc_header = PusTcSecondaryHeader::new_simple(17, 1); + PusTcCreator::new_no_app_data(sph, tc_header, true) + } + + fn base_ping_tc_simple_ctor() -> PusTcCreator<'static> { + let sph = SpHeader::new_for_unseg_tc_checked(0x02, 0x34, 0).unwrap(); + PusTcCreator::new_simple(sph, 17, 1, &[], true) + } + + fn base_ping_tc_simple_ctor_with_app_data(app_data: &'static [u8]) -> PusTcCreator<'static> { + let sph = SpHeader::new_for_unseg_tc_checked(0x02, 0x34, 0).unwrap(); + PusTcCreator::new_simple(sph, 17, 1, app_data, true) + } + + #[test] + fn test_tc_fields() { + let pus_tc = base_ping_tc_full_ctor(); + verify_test_tc(&pus_tc, false, 11); + } + + #[test] + fn test_serialization() { + let pus_tc = base_ping_tc_simple_ctor(); + let mut test_buf: [u8; 32] = [0; 32]; + let size = pus_tc + .write_to_bytes(test_buf.as_mut_slice()) + .expect("Error writing TC to buffer"); + assert_eq!(size, 11); + assert_eq!( + pus_tc.opt_crc16().unwrap(), + u16::from_be_bytes(test_buf[size - 2..size].try_into().unwrap()) + ); + } + + #[test] + fn test_serialization_with_trait_1() { + let pus_tc = base_ping_tc_simple_ctor(); + let mut test_buf: [u8; 32] = [0; 32]; + let size = WritablePusPacket::write_to_bytes(&pus_tc, test_buf.as_mut_slice()) + .expect("Error writing TC to buffer"); + assert_eq!(size, 11); + assert_eq!( + pus_tc.opt_crc16().unwrap(), + u16::from_be_bytes(test_buf[size - 2..size].try_into().unwrap()) + ); + } + + #[test] + fn test_serialization_with_trait_2() { + let pus_tc = base_ping_tc_simple_ctor(); + let mut test_buf: [u8; 32] = [0; 32]; + let size = WritablePusPacket::write_to_bytes_crc_no_table(&pus_tc, test_buf.as_mut_slice()) + .expect("Error writing TC to buffer"); + assert_eq!(size, 11); + assert_eq!( + pus_tc.opt_crc16().unwrap(), + u16::from_be_bytes(test_buf[size - 2..size].try_into().unwrap()) + ); + } + + #[test] + fn test_serialization_crc_no_table() { + let pus_tc = base_ping_tc_simple_ctor(); + let mut test_buf: [u8; 32] = [0; 32]; + let size = pus_tc + .write_to_bytes_crc_no_table(test_buf.as_mut_slice()) + .expect("error writing tc to buffer"); + assert_eq!(size, 11); + assert_eq!( + pus_tc.opt_crc16().unwrap(), + u16::from_be_bytes(test_buf[size - 2..size].try_into().unwrap()) + ); + } + + #[test] + fn test_serialization_no_crc() { + let pus_tc = base_ping_tc_simple_ctor(); + let mut test_buf: [u8; 32] = [0; 32]; + let size = pus_tc + .write_to_bytes_no_crc(test_buf.as_mut_slice()) + .expect("error writing tc to buffer"); + assert_eq!(size, 9); + assert_eq!(test_buf[10], 0); + assert_eq!(test_buf[11], 0); + } + + #[test] + fn test_serialization_no_crc_with_trait() { + let pus_tc = base_ping_tc_simple_ctor(); + let mut test_buf: [u8; 32] = [0; 32]; + let size = WritablePusPacket::write_to_bytes_no_crc(&pus_tc, test_buf.as_mut_slice()) + .expect("error writing tc to buffer"); + assert_eq!(size, 9); + assert_eq!(test_buf[9], 0); + assert_eq!(test_buf[10], 0); + } + + #[test] + fn test_deserialization() { + let pus_tc = base_ping_tc_simple_ctor(); + let mut test_buf: [u8; 32] = [0; 32]; + let size = pus_tc + .write_to_bytes(test_buf.as_mut_slice()) + .expect("Error writing TC to buffer"); + assert_eq!(size, 11); + let tc_from_raw = PusTcReader::new(&test_buf, None, 0) + .expect("Creating PUS TC struct from raw buffer failed"); + assert_eq!(tc_from_raw.total_len(), 11); + verify_test_tc_with_reader(&tc_from_raw, false, 11); + assert!(tc_from_raw.user_data().is_empty()); + verify_test_tc_raw(&test_buf); + verify_crc_no_app_data(&test_buf); + } + + #[test] + fn test_deserialization_alt_ctor() { + let sph = SpHeader::new_for_unseg_tc_checked(0x02, 0x34, 0).unwrap(); + let tc_header = PusTcSecondaryHeader::new_simple(17, 1); + let mut test_buf: [u8; 32] = [0; 32]; + let mut pus_tc = + PusTcCreatorWithReservedAppData::new(&mut test_buf, sph, tc_header, 0).unwrap(); + assert_eq!(pus_tc.len_written(), 11); + assert_eq!(pus_tc.app_data_len(), 0); + assert_eq!(pus_tc.app_data(), &[]); + assert_eq!(pus_tc.app_data_mut(), &[]); + let size = pus_tc.finalize(); + assert_eq!(size, 11); + let tc_from_raw = PusTcReader::new(&test_buf, None, 0) + .expect("Creating PUS TC struct from raw buffer failed"); + assert_eq!(tc_from_raw.total_len(), 11); + verify_test_tc_with_reader(&tc_from_raw, false, 11); + assert!(tc_from_raw.user_data().is_empty()); + verify_test_tc_raw(&test_buf); + verify_crc_no_app_data(&test_buf); + } + + #[test] + fn test_deserialization_no_table() { + let pus_tc = base_ping_tc_simple_ctor(); + let mut test_buf: [u8; 32] = [0; 32]; + let size = pus_tc + .write_to_bytes(test_buf.as_mut_slice()) + .expect("Error writing TC to buffer"); + assert_eq!(size, 11); + let tc_from_raw = PusTcReader::new_crc_no_table(&test_buf, None, 0) + .expect("Creating PUS TC struct from raw buffer failed"); + assert_eq!(tc_from_raw.total_len(), 11); + verify_test_tc_with_reader(&tc_from_raw, false, 11); + assert!(tc_from_raw.user_data().is_empty()); + verify_test_tc_raw(&test_buf); + verify_crc_no_app_data(&test_buf); + } + + #[test] + fn test_writing_into_vec() { + let pus_tc = base_ping_tc_simple_ctor(); + let tc_vec = pus_tc.to_vec().expect("Error writing TC to buffer"); + assert_eq!(tc_vec.len(), 11); + let tc_from_raw = PusTcReader::new(tc_vec.as_slice(), None, 0) + .expect("Creating PUS TC struct from raw buffer failed"); + assert_eq!(tc_from_raw.total_len(), 11); + verify_test_tc_with_reader(&tc_from_raw, false, 11); + assert!(tc_from_raw.user_data().is_empty()); + verify_test_tc_raw(&tc_vec); + verify_crc_no_app_data(&tc_vec); + } + + #[test] + fn test_update_func() { + let sph = SpHeader::new_for_unseg_tc_checked(0x02, 0x34, 0).unwrap(); + let mut tc = PusTcCreator::new_simple(sph, 17, 1, &[], false); + assert_eq!(tc.data_len(), 0); + tc.update_ccsds_data_len(); + assert_eq!(tc.data_len(), 4); + } + + #[test] + fn test_deserialization_with_app_data() { + let pus_tc = base_ping_tc_simple_ctor_with_app_data(&[1, 2, 3]); + let mut test_buf: [u8; 32] = [0; 32]; + let size = pus_tc + .write_to_bytes(test_buf.as_mut_slice()) + .expect("Error writing TC to buffer"); + assert_eq!(size, 14); + let tc_from_raw = PusTcReader::new(&test_buf, None, 0) + .expect("Creating PUS TC struct from raw buffer failed"); + assert_eq!(tc_from_raw.total_len(), 14); + verify_test_tc_with_reader(&tc_from_raw, true, 14); + let user_data = tc_from_raw.user_data(); + assert_eq!(tc_from_raw.user_data(), tc_from_raw.app_data()); + assert_eq!(tc_from_raw.raw_data(), &test_buf[..size]); + assert_eq!( + tc_from_raw.opt_crc16().unwrap(), + u16::from_be_bytes(test_buf[size - 2..size].try_into().unwrap()) + ); + assert_eq!(user_data[0], 1); + assert_eq!(user_data[1], 2); + assert_eq!(user_data[2], 3); + } + + #[test] + fn test_reader_eq() { + let pus_tc = base_ping_tc_simple_ctor_with_app_data(&[1, 2, 3]); + let mut test_buf: [u8; 32] = [0; 32]; + pus_tc + .write_to_bytes(test_buf.as_mut_slice()) + .expect("Error writing TC to buffer"); + let tc_from_raw_0 = PusTcReader::new(&test_buf, None, 0) + .expect("Creating PUS TC struct from raw buffer failed"); + let tc_from_raw_1 = PusTcReader::new(&test_buf, None, 0) + .expect("Creating PUS TC struct from raw buffer failed"); + assert_eq!(tc_from_raw_0, tc_from_raw_1); + } + + #[test] + fn test_vec_ser_deser() { + let pus_tc = base_ping_tc_simple_ctor(); + let mut test_vec = Vec::new(); + let size = pus_tc.append_to_vec(&mut test_vec); + assert_eq!(size, 11); + println!("Test vector: {test_vec:x?}"); + verify_test_tc_raw(&test_vec.as_slice()); + verify_crc_no_app_data(&test_vec.as_slice()); + } + + #[test] + fn test_incorrect_crc() { + let pus_tc = base_ping_tc_simple_ctor(); + let mut test_buf: [u8; 32] = [0; 32]; + pus_tc + .write_to_bytes(test_buf.as_mut_slice()) + .expect("Error writing TC to buffer"); + test_buf[9] = 0; + test_buf[10] = 0; + let res = PusTcReader::new(&test_buf, None, 0); + assert!(res.is_err()); + let err = res.unwrap_err(); + if let PusError::ChecksumFailure(crc) = err { + assert_eq!(crc, 0); + assert_eq!( + err.to_string(), + "checksum verification for crc16 0x0000 failed" + ); + } else { + panic!("unexpected error {err}"); + } + } + + #[test] + fn test_manual_crc_calculation() { + let pus_tc = base_ping_tc_simple_ctor(); + let mut test_buf: [u8; 32] = [0; 32]; + pus_tc.calc_own_crc16(); + pus_tc + .write_to_bytes(test_buf.as_mut_slice()) + .expect("Error writing TC to buffer"); + verify_test_tc_raw(&test_buf); + verify_crc_no_app_data(&test_buf); + } + + #[test] + fn test_with_application_data_vec() { + let pus_tc = base_ping_tc_simple_ctor_with_app_data(&[1, 2, 3]); + verify_test_tc(&pus_tc, true, 14); + let mut test_vec = Vec::new(); + let size = pus_tc.append_to_vec(&mut test_vec); + assert_eq!(test_vec[9], 1); + assert_eq!(test_vec[10], 2); + assert_eq!(test_vec[11], 3); + assert_eq!(size, 14); + } + + #[test] + fn test_write_buf_too_small() { + let pus_tc = base_ping_tc_simple_ctor(); + let mut test_buf = [0; 10]; + let res = pus_tc.write_to_bytes(test_buf.as_mut_slice()); + assert!(res.is_err()); + let err = res.unwrap_err(); + assert_eq!( + err, + ByteConversionError::ToSliceTooSmall { + found: 10, + expected: 11 + } + ); + assert_eq!( + err.to_string(), + "target slice with size 10 is too small, expected size of at least 11" + ); + } + + #[test] + fn test_with_application_data_buf() { + let pus_tc = base_ping_tc_simple_ctor_with_app_data(&[1, 2, 3]); + verify_test_tc(&pus_tc, true, 14); + let mut test_buf: [u8; 32] = [0; 32]; + let size = pus_tc + .write_to_bytes(test_buf.as_mut_slice()) + .expect("Error writing TC to buffer"); + assert_eq!(test_buf[9], 1); + assert_eq!(test_buf[10], 2); + assert_eq!(test_buf[11], 3); + assert_eq!(size, 14); + } + + #[test] + fn test_custom_setters() { + let mut pus_tc = base_ping_tc_simple_ctor(); + let mut test_buf: [u8; 32] = [0; 32]; + pus_tc.set_apid(0x7ff); + pus_tc.set_seq_count(0x3fff); + pus_tc.set_ack_field(0b11); + let source_id = UnsignedByteFieldU16::new(0xffff).into(); + pus_tc.set_source_id(Some(source_id)); + pus_tc.set_seq_flags(SequenceFlags::Unsegmented); + assert_eq!(pus_tc.source_id(), Some(source_id)); + assert_eq!(pus_tc.seq_count(), 0x3fff); + assert_eq!(pus_tc.ack_flags(), 0b11); + assert_eq!(pus_tc.apid(), 0x7ff); + assert_eq!(pus_tc.sequence_flags(), SequenceFlags::Unsegmented); + pus_tc.calc_own_crc16(); + pus_tc + .write_to_bytes(test_buf.as_mut_slice()) + .expect("Error writing TC to buffer"); + assert_eq!(test_buf[0], 0x1f); + assert_eq!(test_buf[1], 0xff); + assert_eq!(test_buf[2], 0xff); + assert_eq!(test_buf[3], 0xff); + assert_eq!(test_buf[6], 0x13); + // Source ID 0 + assert_eq!(test_buf[9], 0xff); + assert_eq!(test_buf[10], 0xff); + } + + fn verify_test_tc(tc: &PusTcCreator, has_user_data: bool, exp_full_len: usize) { + verify_test_tc_generic(tc); + if !has_user_data { + assert!(tc.user_data().is_empty()); + } + let mut comp_header = + SpHeader::new_for_unseg_tc_checked(0x02, 0x34, exp_full_len as u16 - 7).unwrap(); + comp_header.set_sec_header_flag(); + assert_eq!(*tc.sp_header(), comp_header); + } + + fn verify_test_tc_with_reader(tc: &PusTcReader, has_user_data: bool, exp_full_len: usize) { + verify_test_tc_generic(tc); + if !has_user_data { + assert!(tc.user_data().is_empty()); + } + assert_eq!(tc.len_packed(), exp_full_len); + let mut comp_header = + SpHeader::new_for_unseg_tc_checked(0x02, 0x34, exp_full_len as u16 - 7).unwrap(); + comp_header.set_sec_header_flag(); + assert_eq!(*tc.sp_header(), comp_header); + } + + fn verify_test_tc_generic(tc: &(impl PusPacket + GenericPusTcSecondaryHeader)) { + assert_eq!(PusPacket::service(tc), 17); + assert_eq!(GenericPusTcSecondaryHeader::service(tc), 17); + assert_eq!(PusPacket::subservice(tc), 1); + assert_eq!(GenericPusTcSecondaryHeader::subservice(tc), 1); + assert!(tc.sec_header_flag()); + assert_eq!(PusPacket::pus_version(tc).unwrap(), PusVersion::PusA); + assert_eq!(tc.seq_count(), 0x34); + assert!(tc.source_id().is_none()); + assert_eq!(tc.apid(), 0x02); + assert_eq!(tc.ack_flags(), ACK_ALL); + assert_eq!(PusPacket::pus_version(tc).unwrap(), PusVersion::PusA); + assert_eq!( + GenericPusTcSecondaryHeader::pus_version(tc).unwrap(), + PusVersion::PusA + ); + } + fn verify_test_tc_raw(slice: &impl AsRef<[u8]>) { + // Reference comparison implementation: + // https://github.com/us-irs/py-spacepackets/blob/v0.13.0/tests/ecss/test_pus_tc.py + let slice = slice.as_ref(); + // 0x1801 is the generic + assert_eq!(slice[0], 0x18); + // APID is 0x01 + assert_eq!(slice[1], 0x02); + // Unsegmented packets + assert_eq!(slice[2], 0xc0); + // Sequence count 0x34 + assert_eq!(slice[3], 0x34); + assert_eq!(slice[4], 0x00); + // Space data length of 4 equals total packet length of 11 + assert_eq!(slice[5], 0x04); + // PUS Version A 0b0001 and ACK flags 0b1111 + assert_eq!(slice[6], 0x1f); + // Service 17 + assert_eq!(slice[7], 0x11); + // Subservice 1 + assert_eq!(slice[8], 0x01); + } + + fn verify_crc_no_app_data(slice: &impl AsRef<[u8]>) { + // Reference comparison implementation: + // https://github.com/us-irs/py-spacepackets/blob/v0.13.0/tests/ecss/test_pus_tc.py + let slice = slice.as_ref(); + assert_eq!(slice[9], 0x37); + assert_eq!(slice[10], 0x2d); + } + + #[test] + fn partial_eq_pus_tc() { + // new vs new simple + let pus_tc_1 = base_ping_tc_simple_ctor(); + let pus_tc_2 = base_ping_tc_full_ctor(); + assert_eq!(pus_tc_1, pus_tc_2); + } + + #[test] + fn partial_eq_serialized_vs_derialized() { + let pus_tc = base_ping_tc_simple_ctor(); + let mut buf = [0; 32]; + pus_tc.write_to_bytes(&mut buf).unwrap(); + assert_eq!(pus_tc, PusTcReader::new(&buf, None, 0).unwrap()); + assert_eq!(PusTcReader::new(&buf, None, 0).unwrap(), pus_tc); + } + + #[test] + fn test_ack_opts_from_raw() { + let ack_opts_raw = AckOpts::Start as u8; + let ack_opts = AckOpts::try_from(ack_opts_raw).unwrap(); + assert_eq!(ack_opts, AckOpts::Start); + } + + #[test] + fn test_reader_buf_too_small() { + let app_data = &[1, 2, 3, 4]; + let pus_tc = base_ping_tc_simple_ctor_with_app_data(app_data); + let mut buf = [0; 32]; + let written_len = pus_tc.write_to_bytes(&mut buf).unwrap(); + let error = PusTcReader::new(&buf[0..7], None, 0); + assert!(error.is_err()); + let error = error.unwrap_err(); + if let PusError::ByteConversion(ByteConversionError::FromSliceTooSmall { + found, + expected, + }) = error + { + assert_eq!(found, 7); + assert_eq!(expected, written_len); + } else { + panic!("unexpected error {error}") + } + } + + #[test] + fn test_reader_input_too_small() { + let buf: [u8; 5] = [0; 5]; + let error = PusTcReader::new(&buf, None, 0); + assert!(error.is_err()); + let error = error.unwrap_err(); + if let PusError::ByteConversion(ByteConversionError::FromSliceTooSmall { + found, + expected, + }) = error + { + assert_eq!(found, 5); + assert_eq!(expected, 6); + } else { + panic!("unexpected error {error}") + } + } + + #[test] + fn test_with_source_id_and_spare_bytes() { + let sph = SpHeader::new_for_unseg_tc_checked(0x02, 0x34, 0).unwrap(); + let source_id = UnsignedByteFieldU8::new(5).into(); + let tc_header = PusTcSecondaryHeader::new(17, 1, 0b1111, Some(source_id), 2); + let creator = PusTcCreator::new(sph, tc_header, &[1, 2, 3], true); + assert_eq!(creator.len_written(), 17); + let mut buf: [u8; 32] = [0; 32]; + assert_eq!(creator.write_to_bytes(&mut buf).unwrap(), 17); + // Source ID + assert_eq!(buf[9], 5); + // Two spare bytes which should be 0 + assert_eq!(buf[10], 0); + assert_eq!(buf[11], 0); + // App data. + assert_eq!(buf[12], 1); + assert_eq!(buf[13], 2); + assert_eq!(buf[14], 3); + let tc_reader = PusTcReader::new(&buf, Some(1), 2) + .expect("Creating PUS TC struct from raw buffer failed"); + assert_eq!(tc_reader.sp_header(), creator.sp_header()); + assert_eq!(tc_reader.app_data(), creator.app_data()); + assert_eq!(tc_reader.source_id(), Some(source_id)); + assert_eq!(creator.spare_bytes(), 2); + } + + #[test] + #[cfg(feature = "serde")] + fn test_serialization_tc_serde() { + let pus_tc = base_ping_tc_simple_ctor(); + let output = to_allocvec(&pus_tc).unwrap(); + let output_converted_back: PusTcCreator = from_bytes(&output).unwrap(); + assert_eq!(output_converted_back, pus_tc); + } +} diff --git a/src/ecss/tm.rs b/src/ecss/tm.rs index 116a9f1..335de32 100644 --- a/src/ecss/tm.rs +++ b/src/ecss/tm.rs @@ -75,7 +75,7 @@ pub const PUS_TM_MIN_LEN_WITHOUT_SOURCE_DATA: usize = CCSDS_HEADER_LEN + PUS_TM_MIN_SEC_HEADER_LEN + size_of::(); pub trait GenericPusTmSecondaryHeader { - fn pus_version(&self) -> PusVersion; + fn pus_version(&self) -> Result; fn sc_time_ref_status(&self) -> u8; fn service(&self) -> u8; fn subservice(&self) -> u8; @@ -107,7 +107,7 @@ pub mod zc { type Error = PusError; fn try_from(header: crate::ecss::tm::PusTmSecondaryHeader) -> Result { if header.pus_version != PusVersion::PusC { - return Err(PusError::VersionNotSupported(header.pus_version)); + return Err(PusError::VersionNotSupported(header.pus_version as u8)); } Ok(PusTmSecHeaderWithoutTimestamp { pus_version_and_sc_time_ref_status: ((header.pus_version as u8) << 4) @@ -122,9 +122,8 @@ pub mod zc { impl GenericPusTmSecondaryHeader for PusTmSecHeaderWithoutTimestamp { #[inline] - fn pus_version(&self) -> PusVersion { + fn pus_version(&self) -> Result { PusVersion::try_from((self.pus_version_and_sc_time_ref_status >> 4) & 0b1111) - .unwrap_or(PusVersion::Invalid) } #[inline] @@ -201,8 +200,8 @@ impl<'stamp> PusTmSecondaryHeader<'stamp> { impl GenericPusTmSecondaryHeader for PusTmSecondaryHeader<'_> { #[inline] - fn pus_version(&self) -> PusVersion { - self.pus_version + fn pus_version(&self) -> Result { + Ok(self.pus_version) } #[inline] @@ -232,12 +231,16 @@ impl GenericPusTmSecondaryHeader for PusTmSecondaryHeader<'_> { } impl<'slice> TryFrom> for PusTmSecondaryHeader<'slice> { - type Error = (); + type Error = PusError; #[inline] fn try_from(sec_header: zc::PusTmSecHeader<'slice>) -> Result { + let version = sec_header.zc_header.pus_version(); + if let Err(e) = version { + return Err(PusError::VersionNotSupported(e)); + } Ok(PusTmSecondaryHeader { - pus_version: sec_header.zc_header.pus_version(), + pus_version: version.unwrap(), sc_time_ref_status: sec_header.zc_header.sc_time_ref_status(), service: sec_header.zc_header.service(), subservice: sec_header.zc_header.subservice(), @@ -488,9 +491,12 @@ impl CcsdsPacket for PusTmCreator<'_, '_> { } impl PusPacket for PusTmCreator<'_, '_> { + #[inline] + fn pus_version(&self) -> Result { + Ok(self.sec_header.pus_version) + } + delegate!(to self.sec_header { - #[inline] - fn pus_version(&self) -> PusVersion; #[inline] fn service(&self) -> u8; #[inline] @@ -511,7 +517,7 @@ impl PusPacket for PusTmCreator<'_, '_> { impl GenericPusTmSecondaryHeader for PusTmCreator<'_, '_> { delegate!(to self.sec_header { #[inline] - fn pus_version(&self) -> PusVersion; + fn pus_version(&self) -> Result; #[inline] fn service(&self) -> u8; #[inline] @@ -797,7 +803,7 @@ impl CcsdsPacket for PusTmReader<'_> { impl PusPacket for PusTmReader<'_> { delegate!(to self.sec_header { #[inline] - fn pus_version(&self) -> PusVersion; + fn pus_version(&self) -> Result; #[inline] fn service(&self) -> u8; #[inline] @@ -818,7 +824,7 @@ impl PusPacket for PusTmReader<'_> { impl GenericPusTmSecondaryHeader for PusTmReader<'_> { delegate!(to self.sec_header { #[inline] - fn pus_version(&self) -> PusVersion; + fn pus_version(&self) -> Result; #[inline] fn service(&self) -> u8; #[inline] @@ -977,7 +983,7 @@ impl CcsdsPacket for PusTmZeroCopyWriter<'_> { impl PusPacket for PusTmZeroCopyWriter<'_> { #[inline] - fn pus_version(&self) -> PusVersion { + fn pus_version(&self) -> Result { self.sec_header_without_timestamp().pus_version() } @@ -1011,7 +1017,7 @@ impl GenericPusTmSecondaryHeader for PusTmZeroCopyWriter<'_> { delegate! { to self.sec_header_without_timestamp() { #[inline] - fn pus_version(&self) -> PusVersion; + fn pus_version(&self) -> Result; #[inline] fn sc_time_ref_status(&self) -> u8; #[inline] @@ -1047,12 +1053,12 @@ mod tests { const DUMMY_DATA: &[u8] = &[0, 1, 2]; - fn base_ping_reply_full_ctor(timestamp: &[u8]) -> PusTmCreator { + fn base_ping_reply_full_ctor<'a, 'b>(timestamp: &'a [u8]) -> PusTmCreator<'a, 'b> { let sph = SpHeader::new_for_unseg_tm_checked(0x123, 0x234, 0).unwrap(); let tm_header = PusTmSecondaryHeader::new_simple(17, 2, timestamp); PusTmCreator::new_no_source_data(sph, tm_header, true) } - fn ping_reply_with_data(timestamp: &[u8]) -> PusTmCreator { + fn ping_reply_with_data<'a, 'b>(timestamp: &'a [u8]) -> PusTmCreator<'a, 'b> { let sph = SpHeader::new_for_unseg_tm_checked(0x123, 0x234, 0).unwrap(); let tm_header = PusTmSecondaryHeader::new_simple(17, 2, timestamp); PusTmCreator::new(sph, tm_header, DUMMY_DATA, true) @@ -1442,12 +1448,12 @@ mod tests { if has_user_data { assert!(!tm.user_data().is_empty()); } - assert_eq!(PusPacket::pus_version(tm), PusC); + assert_eq!(PusPacket::pus_version(tm).unwrap(), PusC); assert_eq!(tm.apid(), 0x123); assert_eq!(tm.seq_count(), 0x234); - assert_eq!(PusPacket::pus_version(tm), PusVersion::PusC); + assert_eq!(PusPacket::pus_version(tm).unwrap(), PusVersion::PusC); assert_eq!( - GenericPusTmSecondaryHeader::pus_version(tm), + GenericPusTmSecondaryHeader::pus_version(tm).unwrap(), PusVersion::PusC ); assert_eq!(tm.data_len(), exp_full_len as u16 - 7); diff --git a/src/ecss/tm_pus_a.rs b/src/ecss/tm_pus_a.rs new file mode 100644 index 0000000..c7877b1 --- /dev/null +++ b/src/ecss/tm_pus_a.rs @@ -0,0 +1,1952 @@ +//! This module contains all components required to create ECSS PUS A legacy telemetry. +//! +//! # Examples +//! +//! ```rust +//! use spacepackets::time::TimeWriter; +//! use spacepackets::time::cds::CdsTime; +//! use spacepackets::{CcsdsPacket, SpHeader}; +//! use spacepackets::ecss::{PusPacket, WritablePusPacket}; +//! use spacepackets::ecss::tm_pus_a::{ +//! PusTmCreator, +//! PusTmReader, +//! PusTmSecondaryHeader, +//! SecondaryHeaderParameters +//! }; +//! +//! let mut time_buf: [u8; 7] = [0; 7]; +//! let time_now = CdsTime::now_with_u16_days().expect("creating CDS timestamp failed"); +//! // This can definitely hold the timestamp, so it is okay to unwrap. +//! time_now.write_to_bytes(&mut time_buf).unwrap(); +//! +//! // Create a ping telemetry with no user source data +//! let ping_tm = PusTmCreator::new_no_source_data( +//! SpHeader::new_from_apid(0x02), +//! PusTmSecondaryHeader::new_simple(17, 2, &time_buf), +//! true +//! ); +//! println!("{:?}", ping_tm); +//! assert_eq!(ping_tm.service(), 17); +//! assert_eq!(ping_tm.subservice(), 2); +//! assert_eq!(ping_tm.apid(), 0x02); +//! +//! // Serialize TM into a raw buffer +//! let mut test_buf: [u8; 32] = [0; 32]; +//! let written_size = ping_tm +//! .write_to_bytes(test_buf.as_mut_slice()) +//! .expect("Error writing TC to buffer"); +//! assert_eq!(written_size, 18); +//! println!("{:?}", &test_buf[0..written_size]); +//! +//! // Deserialize from the raw byte representation +//! let ping_tm_reader = PusTmReader::new(&test_buf, &SecondaryHeaderParameters::new_minimal(7)).expect("deserialization failed"); +//! assert_eq!(written_size, ping_tm_reader.total_len()); +//! assert_eq!(ping_tm_reader.service(), 17); +//! assert_eq!(ping_tm_reader.subservice(), 2); +//! assert_eq!(ping_tm_reader.apid(), 0x02); +//! assert_eq!(ping_tm_reader.timestamp(), &time_buf); +//! ``` +use crate::crc::{CRC_CCITT_FALSE, CRC_CCITT_FALSE_NO_TABLE}; +use crate::ecss::{ + calc_pus_crc16, ccsds_impl, crc_from_raw_data, sp_header_impls, user_data_from_raw, + verify_crc16_ccitt_false_from_raw_to_pus_error, CrcType, PusError, PusPacket, PusVersion, + WritablePusPacket, +}; +use crate::util::{UnsignedByteField, UnsignedEnum}; +use crate::{ + ByteConversionError, CcsdsPacket, PacketType, SequenceFlags, SpHeader, CCSDS_HEADER_LEN, + MAX_APID, MAX_SEQ_COUNT, +}; +use core::mem::size_of; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use zerocopy::{FromBytes, IntoBytes}; + +#[cfg(feature = "alloc")] +use alloc::vec::Vec; +use delegate::delegate; + +use crate::time::{TimeWriter, TimestampError}; + +use super::verify_crc16_ccitt_false_from_raw_to_pus_error_no_table; + +pub trait IsPusTelemetry {} + +/// Length without timestamp +pub const PUS_TM_MIN_SEC_HEADER_LEN: usize = 3; +pub const PUS_TM_MIN_LEN_WITHOUT_SOURCE_DATA: usize = + CCSDS_HEADER_LEN + PUS_TM_MIN_SEC_HEADER_LEN + size_of::(); + +pub trait GenericPusTmSecondaryHeader { + fn pus_version(&self) -> PusVersion; + fn service(&self) -> u8; + fn subservice(&self) -> u8; + fn msg_counter(&self) -> Option; + fn dest_id(&self) -> Option; + fn spare_bytes(&self) -> usize; +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct SecondaryHeaderParameters { + pub timestamp_len: usize, + pub has_msg_counter: bool, + pub dest_id_len: Option, + pub spare_bytes: usize, +} + +impl SecondaryHeaderParameters { + pub const fn new_minimal(timestamp_len: usize) -> Self { + Self { + timestamp_len, + has_msg_counter: false, + dest_id_len: None, + spare_bytes: 0, + } + } +} + +#[derive(PartialEq, Eq, Copy, Clone, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PusTmSecondaryHeader<'stamp> { + pus_version: PusVersion, + pub service: u8, + pub subservice: u8, + pub msg_counter: Option, + pub dest_id: Option, + pub timestamp: &'stamp [u8], + pub spare_bytes: usize, +} + +impl<'stamp> PusTmSecondaryHeader<'stamp> { + #[inline] + pub fn new_simple(service: u8, subservice: u8, timestamp: &'stamp [u8]) -> Self { + Self::new(service, subservice, None, None, timestamp, 0) + } + + /// Like [Self::new_simple] but without a timestamp. + #[inline] + pub fn new_simple_no_timestamp(service: u8, subservice: u8) -> Self { + Self::new(service, subservice, None, None, &[], 0) + } + + #[inline] + pub fn new( + service: u8, + subservice: u8, + msg_counter: Option, + dest_id: Option, + timestamp: &'stamp [u8], + spare_bytes: usize, + ) -> Self { + PusTmSecondaryHeader { + pus_version: PusVersion::PusC, + service, + subservice, + msg_counter, + dest_id, + timestamp, + spare_bytes, + } + } + + pub fn from_bytes( + buf: &'stamp [u8], + params: &SecondaryHeaderParameters, + ) -> Result, PusError> { + let sec_header_len = Self::len_for_params(params); + if buf.len() < sec_header_len { + return Err(ByteConversionError::FromSliceTooSmall { + found: buf.len(), + expected: sec_header_len, + } + .into()); + } + let pus_version = PusVersion::try_from((buf[0] >> 4) & 0x0F); + if let Err(version_raw) = pus_version { + return Err(PusError::VersionNotSupported(version_raw)); + } + let pus_version = pus_version.unwrap(); + if pus_version != PusVersion::PusC { + return Err(PusError::VersionNotSupported(pus_version as u8)); + } + let mut msg_counter = None; + let mut current_idx = 3; + if params.has_msg_counter { + msg_counter = Some(buf[current_idx]); + current_idx += 1; + } + let mut dest_id = None; + if let Some(dest_id_len) = params.dest_id_len { + dest_id = Some( + UnsignedByteField::new_from_be_bytes( + dest_id_len, + &buf[current_idx..current_idx + dest_id_len], + ) + .unwrap(), + ); + current_idx += dest_id_len; + } + Ok(Self { + pus_version, + service: buf[1], + subservice: buf[2], + msg_counter, + dest_id, + timestamp: &buf[current_idx..current_idx + params.timestamp_len], + spare_bytes: params.spare_bytes, + }) + } + + pub fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result { + let written_len = self.written_len(); + if buf.len() < written_len { + return Err(ByteConversionError::ToSliceTooSmall { + found: buf.len(), + expected: PUS_TM_MIN_SEC_HEADER_LEN, + }); + } + buf[0] = (self.pus_version as u8) << 4; + buf[1] = self.service; + buf[2] = self.subservice; + let mut current_idx = 3; + if let Some(msg_counter) = self.msg_counter { + buf[current_idx] = msg_counter; + current_idx += 1; + } + if let Some(dest_id) = self.dest_id { + dest_id.write_to_be_bytes(&mut buf[current_idx..current_idx + dest_id.size()])?; + current_idx += dest_id.size(); + } + buf[current_idx..current_idx + self.timestamp.len()].copy_from_slice(self.timestamp); + current_idx += self.timestamp.len(); + if self.spare_bytes > 0 { + buf[current_idx..current_idx + self.spare_bytes].fill(0); + } + Ok(written_len) + } + + #[cfg(feature = "alloc")] + pub fn to_vec(&self) -> Vec { + let mut vec = alloc::vec![0; self.written_len()]; + self.write_to_be_bytes(&mut vec).unwrap(); + vec + } + + pub fn written_len(&self) -> usize { + let mut len = PUS_TM_MIN_SEC_HEADER_LEN + self.timestamp.len() + self.spare_bytes; + if let Some(dest_id) = self.dest_id { + len += dest_id.size(); + } + if self.msg_counter.is_some() { + len += 1; + } + len + } + + pub fn len_for_params(params: &SecondaryHeaderParameters) -> usize { + let mut len = PUS_TM_MIN_SEC_HEADER_LEN + params.timestamp_len + params.spare_bytes; + if let Some(dest_id) = params.dest_id_len { + len += dest_id; + } + if params.has_msg_counter { + len += 1; + } + len + } +} + +impl GenericPusTmSecondaryHeader for PusTmSecondaryHeader<'_> { + #[inline] + fn pus_version(&self) -> PusVersion { + self.pus_version + } + + #[inline] + fn service(&self) -> u8 { + self.service + } + + #[inline] + fn subservice(&self) -> u8 { + self.subservice + } + + #[inline] + fn msg_counter(&self) -> Option { + self.msg_counter + } + + #[inline] + fn dest_id(&self) -> Option { + self.dest_id + } + + #[inline] + fn spare_bytes(&self) -> usize { + self.spare_bytes + } +} + +/// This class models the PUS C telemetry packet. It is the primary data structure to generate the +/// raw byte representation of PUS telemetry. +/// +/// This class also derives the [serde::Serialize] and [serde::Deserialize] trait if the [serde] +/// feature is used which allows to send around TM packets in a raw byte format using a serde +/// provider like [postcard](https://docs.rs/postcard/latest/postcard/). +/// +/// There is no spare bytes support yet. +/// +/// # Lifetimes +/// +/// * `'time` - This is the lifetime of the user provided timestamp. +/// * `'src_data` - This is the lifetime of the user provided source data. +#[derive(Eq, Debug, Copy, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PusTmCreator<'time, 'src_data> { + pub sp_header: SpHeader, + #[cfg_attr(feature = "serde", serde(borrow))] + pub sec_header: PusTmSecondaryHeader<'time>, + source_data: &'src_data [u8], + /// If this is set to false, a manual call to [Self::calc_own_crc16] or + /// [Self::update_packet_fields] is necessary for the serialized or cached CRC16 to be valid. + pub calc_crc_on_serialization: bool, +} + +impl<'time, 'src_data> PusTmCreator<'time, 'src_data> { + /// Generates a new struct instance. + /// + /// # Arguments + /// + /// * `sp_header` - Space packet header information. The correct packet type and the secondary + /// header flag are set correctly by the constructor. + /// * `sec_header` - Information contained in the secondary header, including the service + /// and subservice type + /// * `source_data` - Custom application data + /// * `set_ccsds_len` - Can be used to automatically update the CCSDS space packet data length + /// field. If this is not set to true, [Self::update_ccsds_data_len] can be called to set + /// the correct value to this field manually + #[inline] + pub fn new( + mut sp_header: SpHeader, + sec_header: PusTmSecondaryHeader<'time>, + source_data: &'src_data [u8], + set_ccsds_len: bool, + ) -> Self { + sp_header.set_packet_type(PacketType::Tm); + sp_header.set_sec_header_flag(); + let mut pus_tm = Self { + sp_header, + source_data, + sec_header, + calc_crc_on_serialization: true, + }; + if set_ccsds_len { + pus_tm.update_ccsds_data_len(); + } + pus_tm + } + + #[inline] + pub fn new_simple( + sp_header: SpHeader, + service: u8, + subservice: u8, + time_provider: &impl TimeWriter, + stamp_buf: &'time mut [u8], + source_data: &'src_data [u8], + set_ccsds_len: bool, + ) -> Result { + let stamp_size = time_provider.write_to_bytes(stamp_buf)?; + let sec_header = + PusTmSecondaryHeader::new_simple(service, subservice, &stamp_buf[0..stamp_size]); + Ok(Self::new(sp_header, sec_header, source_data, set_ccsds_len)) + } + + #[inline] + pub fn new_no_source_data( + sp_header: SpHeader, + sec_header: PusTmSecondaryHeader<'time>, + set_ccsds_len: bool, + ) -> Self { + Self::new(sp_header, sec_header, &[], set_ccsds_len) + } + + #[inline] + pub fn timestamp(&self) -> &[u8] { + self.sec_header.timestamp + } + + #[inline] + pub fn source_data(&self) -> &[u8] { + self.source_data + } + + #[inline] + pub fn set_dest_id(&mut self, dest_id: Option) { + self.sec_header.dest_id = dest_id; + } + + #[inline] + pub fn set_msg_counter(&mut self, msg_counter: Option) { + self.sec_header.msg_counter = msg_counter + } + + sp_header_impls!(); + + /// This is called automatically if the `set_ccsds_len` argument in the [Self::new] call was + /// used. + /// If this was not done or the time stamp or source data is set or changed after construction, + /// this function needs to be called to ensure that the data length field of the CCSDS header + /// is set correctly + #[inline] + pub fn update_ccsds_data_len(&mut self) { + self.sp_header.data_len = + self.len_written() as u16 - size_of::() as u16 - 1; + } + + /// This function should be called before the TM packet is serialized if + /// [Self::calc_crc_on_serialization] is set to False. It will calculate and cache the CRC16. + pub fn calc_own_crc16(&self) -> u16 { + let mut digest = CRC_CCITT_FALSE.digest(); + let sph_zc = crate::zc::SpHeader::from(self.sp_header); + digest.update(sph_zc.as_bytes()); + let mut fixed_header_part: [u8; PUS_TM_MIN_SEC_HEADER_LEN] = [0; PUS_TM_MIN_SEC_HEADER_LEN]; + fixed_header_part[0] = (self.sec_header.pus_version() as u8) << 4; + fixed_header_part[1] = self.sec_header.service; + fixed_header_part[2] = self.sec_header.subservice; + + digest.update(fixed_header_part.as_slice()); + if let Some(msg_counter) = self.sec_header.msg_counter { + digest.update(&[msg_counter]); + } + if let Some(dest_id) = self.sec_header.dest_id { + let mut dest_id_buf: [u8; core::mem::size_of::()] = + [0; core::mem::size_of::()]; + // Unwrap okay, this can never fail because we created a buffer with the largest + // possible size. + let len = dest_id.write_to_be_bytes(&mut dest_id_buf).unwrap(); + digest.update(&dest_id_buf[0..len]); + } + digest.update(self.sec_header.timestamp); + for _ in 0..self.sec_header.spare_bytes { + digest.update(&[0]); + } + digest.update(self.source_data); + digest.finalize() + } + + /// This helper function calls both [Self::update_ccsds_data_len] and [Self::calc_own_crc16] + #[inline] + pub fn update_packet_fields(&mut self) { + self.update_ccsds_data_len(); + } + + /// Write the raw PUS byte representation to a provided buffer. + pub fn write_to_bytes(&self, slice: &mut [u8]) -> Result { + let writer_unfinalized = self.common_write(slice)?; + Ok(writer_unfinalized.finalize()) + } + + /// Write the raw PUS byte representation to a provided buffer. + pub fn write_to_bytes_crc_no_table( + &self, + slice: &mut [u8], + ) -> Result { + let writer_unfinalized = self.common_write(slice)?; + Ok(writer_unfinalized.finalize_crc_no_table()) + } + + /// Write the raw PUS byte representation to a provided buffer. + pub fn write_to_bytes_no_crc(&self, slice: &mut [u8]) -> Result { + let writer_unfinalized = self.common_write(slice)?; + Ok(writer_unfinalized.finalize_no_crc()) + } + + fn common_write<'a>( + &self, + slice: &'a mut [u8], + ) -> Result, ByteConversionError> { + if self.len_written() > slice.len() { + return Err(ByteConversionError::ToSliceTooSmall { + found: slice.len(), + expected: self.len_written(), + }); + } + let mut writer_unfinalized = PusTmCreatorWithReservedSourceData::write_to_bytes_partially( + slice, + self.sp_header, + self.sec_header, + self.source_data.len(), + )?; + writer_unfinalized + .source_data_mut() + .copy_from_slice(self.source_data); + Ok(writer_unfinalized) + } + + /// Append the raw PUS byte representation to a provided [alloc::vec::Vec] + #[cfg(feature = "alloc")] + pub fn append_to_vec(&self, vec: &mut Vec) -> Result { + let sph_zc = crate::zc::SpHeader::from(self.sp_header); + let mut appended_len = PUS_TM_MIN_LEN_WITHOUT_SOURCE_DATA + self.sec_header.timestamp.len(); + appended_len += self.source_data.len(); + let start_idx = vec.len(); + vec.extend_from_slice(sph_zc.as_bytes()); + vec.extend_from_slice(&self.sec_header.to_vec()); + vec.extend_from_slice(self.source_data); + let mut digest = CRC_CCITT_FALSE.digest(); + digest.update(&vec[start_idx..start_idx + appended_len - 2]); + vec.extend_from_slice(&digest.finalize().to_be_bytes()); + Ok(appended_len) + } +} + +impl WritablePusPacket for PusTmCreator<'_, '_> { + #[inline] + fn len_written(&self) -> usize { + CCSDS_HEADER_LEN + self.sec_header.written_len() + self.source_data.len() + 2 + } + /// Write the raw PUS byte representation to a provided buffer. + fn write_to_bytes_no_crc(&self, slice: &mut [u8]) -> Result { + Ok(Self::write_to_bytes_no_crc(self, slice)?) + } + + fn write_to_bytes(&self, slice: &mut [u8]) -> Result { + Ok(Self::write_to_bytes(self, slice)?) + } + + fn write_to_bytes_crc_no_table(&self, slice: &mut [u8]) -> Result { + Ok(Self::write_to_bytes_crc_no_table(self, slice)?) + } +} + +impl PartialEq for PusTmCreator<'_, '_> { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.sp_header == other.sp_header + && self.sec_header == other.sec_header + && self.source_data == other.source_data + } +} + +impl CcsdsPacket for PusTmCreator<'_, '_> { + ccsds_impl!(); +} + +impl PusPacket for PusTmCreator<'_, '_> { + #[inline] + fn pus_version(&self) -> Result { + Ok(self.sec_header.pus_version) + } + + delegate!(to self.sec_header { + #[inline] + fn service(&self) -> u8; + #[inline] + fn subservice(&self) -> u8; + }); + + #[inline] + fn user_data(&self) -> &[u8] { + self.source_data + } + + #[inline] + fn opt_crc16(&self) -> Option { + Some(self.calc_own_crc16()) + } +} + +impl GenericPusTmSecondaryHeader for PusTmCreator<'_, '_> { + delegate!(to self.sec_header { + #[inline] + fn pus_version(&self) -> PusVersion; + #[inline] + fn service(&self) -> u8; + #[inline] + fn subservice(&self) -> u8; + #[inline] + fn dest_id(&self) -> Option; + #[inline] + fn msg_counter(&self) -> Option; + #[inline] + fn spare_bytes(&self) -> usize; + }); +} + +impl IsPusTelemetry for PusTmCreator<'_, '_> {} + +/// A specialized variant of [PusTmCreator] designed for efficiency when handling large source +/// data. +/// +/// Unlike [PusTmCreator], this type does not require the user to provide the source data +/// as a separate slice. Instead, it allows writing the source data directly into the provided +/// serialization buffer. This eliminates the need for an intermediate buffer and the associated +/// memory copy, improving performance, particularly when working with large payloads. +/// +/// **Important:** The total length of the source data must be known and specified in advance +/// to ensure correct serialization behavior. +/// +/// Note that this abstraction intentionally omits certain trait implementations that are available +/// on [PusTmCreator], as they are not applicable in this optimized usage pattern. +pub struct PusTmCreatorWithReservedSourceData<'buf> { + buf: &'buf mut [u8], + source_data_offset: usize, + full_len: usize, +} + +impl<'buf> PusTmCreatorWithReservedSourceData<'buf> { + /// Generates a new instance with reserved space for the user source data. + /// + /// # Arguments + /// + /// * `sp_header` - Space packet header information. The correct packet type and the secondary + /// header flag are set correctly by the constructor. + /// * `sec_header` - Information contained in the secondary header, including the service + /// and subservice type + /// * `src_data_len` - Custom source data length + #[inline] + pub fn new( + buf: &'buf mut [u8], + mut sp_header: SpHeader, + sec_header: PusTmSecondaryHeader, + src_data_len: usize, + ) -> Result { + sp_header.set_packet_type(PacketType::Tm); + sp_header.set_sec_header_flag(); + let len_written = + PUS_TM_MIN_LEN_WITHOUT_SOURCE_DATA + sec_header.timestamp.len() + src_data_len; + if len_written > buf.len() { + return Err(ByteConversionError::ToSliceTooSmall { + found: buf.len(), + expected: len_written, + }); + } + sp_header.data_len = len_written as u16 - size_of::() as u16 - 1; + Self::write_to_bytes_partially(buf, sp_header, sec_header, src_data_len) + } + + fn write_to_bytes_partially( + buf: &'buf mut [u8], + sp_header: SpHeader, + sec_header: PusTmSecondaryHeader, + src_data_len: usize, + ) -> Result { + let mut curr_idx = 0; + sp_header.write_to_be_bytes(&mut buf[0..CCSDS_HEADER_LEN])?; + curr_idx += CCSDS_HEADER_LEN; + curr_idx += sec_header.write_to_be_bytes(&mut buf[CCSDS_HEADER_LEN..])?; + let source_data_offset = curr_idx; + curr_idx += src_data_len; + Ok(Self { + buf, + source_data_offset, + full_len: curr_idx + 2, + }) + } + + #[inline] + pub const fn len_written(&self) -> usize { + self.full_len + } + + /// Mutable access to the source data buffer. + #[inline] + pub fn source_data_mut(&mut self) -> &mut [u8] { + &mut self.buf[self.source_data_offset..self.full_len - 2] + } + + /// Access to the source data buffer. + #[inline] + pub fn source_data(&self) -> &[u8] { + &self.buf[self.source_data_offset..self.full_len - 2] + } + + #[inline] + pub fn source_data_len(&self) -> usize { + self.full_len - 2 - self.source_data_offset + } + + /// Finalize the TM packet by calculating and writing the CRC16. + /// + /// Returns the full packet length. + pub fn finalize(self) -> usize { + let mut digest = CRC_CCITT_FALSE.digest(); + digest.update(&self.buf[0..self.full_len - 2]); + self.buf[self.full_len - 2..self.full_len] + .copy_from_slice(&digest.finalize().to_be_bytes()); + self.full_len + } + + /// Finalize the TM packet by calculating and writing the CRC16 using a table-less + /// implementation. + /// + /// Returns the full packet length. + pub fn finalize_crc_no_table(self) -> usize { + let mut digest = CRC_CCITT_FALSE_NO_TABLE.digest(); + digest.update(&self.buf[0..self.full_len - 2]); + self.buf[self.full_len - 2..self.full_len] + .copy_from_slice(&digest.finalize().to_be_bytes()); + self.full_len + } + + /// Finalize the TM packet without writing the CRC16. + /// + /// Returns the length WITHOUT the CRC16. + #[inline] + pub fn finalize_no_crc(self) -> usize { + self.full_len - 2 + } +} + +/// This class models the PUS C telemetry packet. It is the primary data structure to read +/// a telemetry packet from raw bytes. +/// +/// This class also derives the [serde::Serialize] and [serde::Deserialize] trait if the [serde] +/// feature is used which allows to send around TM packets in a raw byte format using a serde +/// provider like [postcard](https://docs.rs/postcard/latest/postcard/). +/// +/// There is no spare bytes support yet. +/// +/// # Lifetimes +/// +/// * `'raw_data` - Lifetime of the raw slice this class is constructed from. +#[derive(Eq, Debug, Copy, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PusTmReader<'raw_data> { + pub sp_header: SpHeader, + pub sec_header: PusTmSecondaryHeader<'raw_data>, + #[cfg_attr(feature = "serde", serde(skip))] + raw_data: &'raw_data [u8], + source_data: &'raw_data [u8], + crc16: u16, +} + +impl<'raw_data> PusTmReader<'raw_data> { + /// Create a [PusTmReader] instance from a raw slice. On success, it returns a tuple containing + /// the instance and the found byte length of the packet. The timestamp length needs to be + /// known beforehand. + /// + /// This function will check the CRC-16 of the PUS packet and will return an appropriate + /// [PusError] if the check fails. + pub fn new( + slice: &'raw_data [u8], + sec_header_params: &SecondaryHeaderParameters, + ) -> Result { + let tc = Self::new_no_crc_check(slice, sec_header_params)?; + verify_crc16_ccitt_false_from_raw_to_pus_error(tc.raw_data(), tc.crc16)?; + Ok(tc) + } + + /// Like [PusTmReader::new] but uses a table-less CRC implementation. + pub fn new_crc_no_table( + slice: &'raw_data [u8], + sec_header_params: &SecondaryHeaderParameters, + ) -> Result { + let tc = Self::new_no_crc_check(slice, sec_header_params)?; + verify_crc16_ccitt_false_from_raw_to_pus_error_no_table(tc.raw_data(), tc.crc16)?; + Ok(tc) + } + + pub fn new_no_crc_check( + slice: &'raw_data [u8], + sec_header_params: &SecondaryHeaderParameters, + ) -> Result { + let raw_data_len = slice.len(); + if raw_data_len < PUS_TM_MIN_LEN_WITHOUT_SOURCE_DATA { + return Err(ByteConversionError::FromSliceTooSmall { + found: raw_data_len, + expected: PUS_TM_MIN_LEN_WITHOUT_SOURCE_DATA, + } + .into()); + } + let mut current_idx = 0; + let (sp_header, _) = SpHeader::from_be_bytes(&slice[0..CCSDS_HEADER_LEN])?; + current_idx += CCSDS_HEADER_LEN; + let total_len = sp_header.total_len(); + if raw_data_len < total_len { + return Err(ByteConversionError::FromSliceTooSmall { + found: raw_data_len, + expected: total_len, + } + .into()); + } + if total_len < PUS_TM_MIN_LEN_WITHOUT_SOURCE_DATA { + return Err(ByteConversionError::FromSliceTooSmall { + found: total_len, + expected: PUS_TM_MIN_LEN_WITHOUT_SOURCE_DATA, + } + .into()); + } + let sec_header = + PusTmSecondaryHeader::from_bytes(&slice[current_idx..], sec_header_params)?; + current_idx += sec_header.written_len(); + let raw_data = &slice[0..total_len]; + Ok(Self { + sp_header, + sec_header, + raw_data: &slice[0..total_len], + source_data: user_data_from_raw(current_idx, total_len, slice)?, + crc16: crc_from_raw_data(raw_data)?, + }) + } + + #[inline] + pub fn len_packed(&self) -> usize { + self.sp_header.total_len() + } + + #[inline] + pub fn source_data(&self) -> &[u8] { + self.user_data() + } + + #[inline] + pub fn timestamp(&self) -> &[u8] { + self.sec_header.timestamp + } + + #[inline] + pub fn crc16(&self) -> u16 { + self.crc16 + } + + /// This function will return the slice [Self] was constructed from. + #[inline] + pub fn raw_data(&self) -> &[u8] { + self.raw_data + } +} + +impl PartialEq for PusTmReader<'_> { + fn eq(&self, other: &Self) -> bool { + self.sec_header == other.sec_header + && self.source_data == other.source_data + && self.sp_header == other.sp_header + && self.crc16 == other.crc16 + } +} + +impl CcsdsPacket for PusTmReader<'_> { + ccsds_impl!(); +} + +impl PusPacket for PusTmReader<'_> { + #[inline] + fn pus_version(&self) -> Result { + Ok(self.sec_header.pus_version) + } + + delegate!(to self.sec_header { + #[inline] + fn service(&self) -> u8; + #[inline] + fn subservice(&self) -> u8; + }); + + #[inline] + fn user_data(&self) -> &[u8] { + self.source_data + } + + #[inline] + fn opt_crc16(&self) -> Option { + Some(self.crc16()) + } +} + +impl GenericPusTmSecondaryHeader for PusTmReader<'_> { + delegate!(to self.sec_header { + #[inline] + fn pus_version(&self) -> PusVersion; + #[inline] + fn service(&self) -> u8; + #[inline] + fn subservice(&self) -> u8; + #[inline] + fn dest_id(&self) -> Option; + #[inline] + fn msg_counter(&self) -> Option; + #[inline] + fn spare_bytes(&self) -> usize; + }); +} + +impl IsPusTelemetry for PusTmReader<'_> {} + +impl PartialEq> for PusTmReader<'_> { + fn eq(&self, other: &PusTmCreator<'_, '_>) -> bool { + self.sp_header == other.sp_header + && self.sec_header == other.sec_header + && self.source_data == other.source_data + } +} + +impl PartialEq> for PusTmCreator<'_, '_> { + fn eq(&self, other: &PusTmReader<'_>) -> bool { + self.sp_header == other.sp_header + && self.sec_header == other.sec_header + && self.source_data == other.source_data + } +} + +#[derive(Debug, thiserror::Error)] +#[error("this field is not present in the secondary header")] +pub struct SecondaryHeaderFieldNotPresentError; + +#[derive(Debug, thiserror::Error)] +pub enum DestIdOperationError { + #[error("this field is not present in the secondary header")] + FieldNotPresent(#[from] SecondaryHeaderFieldNotPresentError), + #[error("invalid byte field length")] + InvalidFieldLen, + #[error("byte conversion error")] + ByteConversionError(#[from] ByteConversionError), +} + +/// This is a helper class to update certain fields in a raw PUS telemetry packet directly in place. +/// This can be more efficient than creating a full [PusTmReader], modifying the fields and then +/// writing it back to another buffer. +/// +/// Please note that the [Self::finish] method has to be called for the PUS TM CRC16 to be valid +/// after changing fields of the TM packet. Furthermore, the constructor of this class will not +/// do any checks except basic length checks to ensure that all relevant fields can be updated and +/// all methods can be called without a panic. If a full validity check of the PUS TM packet is +/// required, it is recommended to construct a full [PusTmReader] object from the raw bytestream +/// first. +pub struct PusTmZeroCopyWriter<'raw> { + raw_tm: &'raw mut [u8], + sec_header_params: SecondaryHeaderParameters, +} + +impl<'raw> PusTmZeroCopyWriter<'raw> { + /// This function will not do any other checks on the raw data other than a length check + /// for all internal fields which can be updated. + /// + /// It is the responsibility of the user to ensure the raw slice contains a valid telemetry + /// packet. + pub fn new( + raw_tm: &'raw mut [u8], + sec_header_params: &SecondaryHeaderParameters, + ) -> Option { + let raw_tm_len = raw_tm.len(); + if raw_tm_len + < CCSDS_HEADER_LEN + PUS_TM_MIN_SEC_HEADER_LEN + sec_header_params.timestamp_len + { + return None; + } + let sp_header = crate::zc::SpHeader::read_from_bytes(&raw_tm[0..CCSDS_HEADER_LEN]).unwrap(); + if raw_tm_len < sp_header.total_len() { + return None; + } + let writer = Self { + raw_tm: &mut raw_tm[..sp_header.total_len()], + sec_header_params: *sec_header_params, + }; + Some(writer) + } + + /// Set the sequence count. Returns false and does not update the value if the passed value + /// exceeds [MAX_APID]. + #[inline] + pub fn set_apid(&mut self, apid: u16) -> bool { + if apid > MAX_APID { + return false; + } + // Clear APID part of the raw packet ID + let updated_apid = + ((((self.raw_tm[0] as u16) << 8) | self.raw_tm[1] as u16) & !MAX_APID) | apid; + self.raw_tm[0..2].copy_from_slice(&updated_apid.to_be_bytes()); + true + } + + pub fn dest_id(&self) -> Result, ByteConversionError> { + if self.sec_header_params.dest_id_len.is_none() { + return Ok(None); + } + let mut base_idx = 10; + if self.sec_header_params.has_msg_counter { + base_idx += 1; + } + let dest_id_len = self.sec_header_params.dest_id_len.unwrap(); + if self.raw_tm.len() < base_idx + dest_id_len { + return Err(ByteConversionError::FromSliceTooSmall { + found: self.raw_tm.len(), + expected: base_idx + dest_id_len, + }); + } + Ok(Some( + UnsignedByteField::new_from_be_bytes( + dest_id_len, + &self.raw_tm[base_idx..base_idx + dest_id_len], + ) + .unwrap(), + )) + } + + pub fn msg_counter(&self) -> Option { + if !self.sec_header_params.has_msg_counter { + return None; + } + Some(self.raw_tm[9]) + } + + /// This function sets the message counter in the PUS TM secondary header. + /// + /// Please note that usage of this function is only valid if the secondary header has a + /// packet subcounter field, which is a manged parameter which might not be present. + #[inline] + pub fn set_msg_count( + &mut self, + msg_count: u8, + ) -> Result<(), SecondaryHeaderFieldNotPresentError> { + if !self.sec_header_params.has_msg_counter { + return Err(SecondaryHeaderFieldNotPresentError); + } + self.raw_tm[9] = msg_count; + Ok(()) + } + + /// This function sets the destination ID in the PUS TM secondary header. + #[inline] + pub fn set_destination_id( + &mut self, + dest_id: UnsignedByteField, + ) -> Result<(), DestIdOperationError> { + if self.sec_header_params.dest_id_len.is_none() { + return Err(SecondaryHeaderFieldNotPresentError.into()); + } + let dest_id_len = self.sec_header_params.dest_id_len.unwrap(); + if dest_id.size() != dest_id_len { + return Err(DestIdOperationError::InvalidFieldLen); + } + let mut base_idx = 10; + if self.sec_header_params.has_msg_counter { + base_idx += 1; + } + if self.raw_tm.len() < base_idx + dest_id_len { + return Err(DestIdOperationError::ByteConversionError( + ByteConversionError::ToSliceTooSmall { + found: self.raw_tm.len(), + expected: base_idx + dest_id_len, + }, + )); + } + dest_id + .write_to_be_bytes(&mut self.raw_tm[base_idx..base_idx + dest_id_len]) + .unwrap(); + Ok(()) + } + + /// Helper API to generate the space packet header portion of the PUS TM from the raw memory. + #[inline] + pub fn sp_header(&self) -> crate::zc::SpHeader { + // Valid minimum length of packet was checked before. + crate::zc::SpHeader::read_from_bytes(&self.raw_tm[0..CCSDS_HEADER_LEN]).unwrap() + } + + /// Set the sequence count. Returns false and does not update the value if the passed value + /// exceeds [MAX_SEQ_COUNT]. + #[inline] + pub fn set_seq_count(&mut self, seq_count: u16) -> bool { + if seq_count > MAX_SEQ_COUNT { + return false; + } + let new_psc = + (u16::from_be_bytes(self.raw_tm[2..4].try_into().unwrap()) & 0xC000) | seq_count; + self.raw_tm[2..4].copy_from_slice(&new_psc.to_be_bytes()); + true + } + + /// This method has to be called after modifying fields to ensure the CRC16 of the telemetry + /// packet remains valid. + pub fn finish(self) { + let slice_len = self.raw_tm.len(); + let crc16 = calc_pus_crc16(&self.raw_tm[..slice_len - 2]); + self.raw_tm[slice_len - 2..].copy_from_slice(&crc16.to_be_bytes()); + } +} + +impl CcsdsPacket for PusTmZeroCopyWriter<'_> { + #[inline] + fn ccsds_version(&self) -> u8 { + self.sp_header().ccsds_version() + } + + #[inline] + fn packet_id(&self) -> crate::PacketId { + self.sp_header().packet_id() + } + + #[inline] + fn psc(&self) -> crate::PacketSequenceCtrl { + self.sp_header().psc() + } + + #[inline] + fn data_len(&self) -> u16 { + self.sp_header().data_len() + } +} + +impl PusPacket for PusTmZeroCopyWriter<'_> { + #[inline] + fn pus_version(&self) -> Result { + PusVersion::try_from(self.raw_tm[6]) + } + + #[inline] + fn service(&self) -> u8 { + self.raw_tm[7] + } + + #[inline] + fn subservice(&self) -> u8 { + self.raw_tm[8] + } + + #[inline] + fn user_data(&self) -> &[u8] { + &self.raw_tm[CCSDS_HEADER_LEN + + PUS_TM_MIN_SEC_HEADER_LEN + + self.sec_header_params.timestamp_len + ..self.sp_header().total_len() - 2] + } + + #[inline] + fn opt_crc16(&self) -> Option { + Some(u16::from_be_bytes( + self.raw_tm[self.sp_header().total_len() - 2..self.sp_header().total_len()] + .try_into() + .unwrap(), + )) + } +} + +#[cfg(test)] +mod tests { + use alloc::string::ToString; + + use super::*; + use crate::time::cds::CdsTime; + #[cfg(feature = "serde")] + use crate::time::CcsdsTimeProvider; + use crate::SpHeader; + use crate::{ecss::PusVersion::PusC, util::UnsignedByteFieldU16}; + #[cfg(feature = "serde")] + use postcard::{from_bytes, to_allocvec}; + + const DUMMY_DATA: &[u8] = &[0, 1, 2]; + const MIN_SEC_HEADER_PARAMS: SecondaryHeaderParameters = + SecondaryHeaderParameters::new_minimal(7); + + fn ping_reply_no_data<'a, 'b>(timestamp: &'a [u8]) -> PusTmCreator<'a, 'b> { + let sph = SpHeader::new_for_unseg_tm_checked(0x123, 0x234, 0).unwrap(); + let tm_header = PusTmSecondaryHeader::new_simple(17, 2, timestamp); + PusTmCreator::new_no_source_data(sph, tm_header, true) + } + + fn ping_reply_with_data_and_additional_fields<'a, 'b>( + data: &'b [u8], + msg_counter: Option, + dest_id: Option, + timestamp: &'a [u8], + ) -> PusTmCreator<'a, 'b> { + let sph = SpHeader::new_for_unseg_tm_checked(0x123, 0x234, 0).unwrap(); + let tm_header = PusTmSecondaryHeader::new(17, 2, msg_counter, dest_id, timestamp, 0); + PusTmCreator::new(sph, tm_header, data, true) + } + + fn ping_reply_with_data<'a, 'b>(data: &'b [u8], timestamp: &'a [u8]) -> PusTmCreator<'a, 'b> { + let sph = SpHeader::new_for_unseg_tm_checked(0x123, 0x234, 0).unwrap(); + let tm_header = PusTmSecondaryHeader::new_simple(17, 2, timestamp); + PusTmCreator::new(sph, tm_header, data, true) + } + + fn base_hk_reply<'a, 'b>(timestamp: &'a [u8], src_data: &'b [u8]) -> PusTmCreator<'a, 'b> { + let sph = SpHeader::new_for_unseg_tm_checked(0x123, 0x234, 0).unwrap(); + let tc_header = PusTmSecondaryHeader::new_simple(3, 5, timestamp); + PusTmCreator::new(sph, tc_header, src_data, true) + } + + fn dummy_timestamp() -> &'static [u8] { + &[0, 1, 2, 3, 4, 5, 6] + } + + #[test] + fn test_basic() { + let timestamp = dummy_timestamp(); + let pus_tm = ping_reply_no_data(timestamp); + verify_ping_reply(&pus_tm, false, 18, dummy_timestamp(), None, None); + } + + #[test] + fn test_basic_simple_api() { + let sph = SpHeader::new_for_unseg_tm_checked(0x123, 0x234, 0).unwrap(); + let time_provider = CdsTime::new_with_u16_days(0, 0); + let mut stamp_buf: [u8; 8] = [0; 8]; + let pus_tm = + PusTmCreator::new_simple(sph, 17, 2, &time_provider, &mut stamp_buf, &[], true) + .unwrap(); + verify_ping_reply(&pus_tm, false, 18, &[64, 0, 0, 0, 0, 0, 0], None, None); + } + + #[test] + fn test_basic_simple_api_with_dest_id_msg_counter() { + let msg_counter = Some(5); + let dest_id = Some(UnsignedByteFieldU16::new(0x1f1f).into()); + let pus_tm = ping_reply_with_data_and_additional_fields( + &[], + msg_counter, + dest_id, + dummy_timestamp(), + ); + verify_ping_reply(&pus_tm, false, 21, dummy_timestamp(), dest_id, msg_counter); + } + + #[test] + fn test_basic_simple_api_with_dest_id() { + let msg_counter = None; + let dest_id = Some(UnsignedByteFieldU16::new(0x1f1f).into()); + let pus_tm = ping_reply_with_data_and_additional_fields( + &[], + msg_counter, + dest_id, + dummy_timestamp(), + ); + verify_ping_reply(&pus_tm, false, 20, dummy_timestamp(), dest_id, msg_counter); + } + + #[test] + fn test_basic_simple_api_with_msg_counter() { + let msg_counter = Some(5); + let dest_id = None; + let pus_tm = ping_reply_with_data_and_additional_fields( + &[], + msg_counter, + dest_id, + dummy_timestamp(), + ); + verify_ping_reply(&pus_tm, false, 19, dummy_timestamp(), dest_id, msg_counter); + } + + #[test] + fn test_serialization_no_source_data() { + let timestamp = dummy_timestamp(); + let pus_tm = ping_reply_no_data(timestamp); + let mut buf: [u8; 32] = [0; 32]; + let ser_len = pus_tm + .write_to_bytes(&mut buf) + .expect("Serialization failed"); + assert_eq!(ser_len, 18); + verify_raw_ping_reply(pus_tm.opt_crc16(), &buf, ser_len, None, None); + } + + #[test] + fn test_serialization_with_additional_fields() { + let msg_counter = Some(5); + let dest_id = Some(UnsignedByteFieldU16::new(0x1f1f).into()); + let pus_tm = ping_reply_with_data_and_additional_fields( + &[], + msg_counter, + dest_id, + dummy_timestamp(), + ); + let mut buf: [u8; 32] = [0; 32]; + let ser_len = pus_tm + .write_to_bytes(&mut buf) + .expect("Serialization failed"); + assert_eq!(ser_len, 21); + verify_raw_ping_reply(pus_tm.opt_crc16(), &buf, ser_len, msg_counter, dest_id); + } + + #[test] + fn test_serialization_with_dest_id() { + let msg_counter = None; + let dest_id = Some(UnsignedByteFieldU16::new(0x1f1f).into()); + let pus_tm = ping_reply_with_data_and_additional_fields( + &[], + msg_counter, + dest_id, + dummy_timestamp(), + ); + let mut buf: [u8; 32] = [0; 32]; + let ser_len = pus_tm + .write_to_bytes(&mut buf) + .expect("Serialization failed"); + assert_eq!(ser_len, 20); + verify_raw_ping_reply(pus_tm.opt_crc16(), &buf, ser_len, msg_counter, dest_id); + } + + #[test] + fn test_serialization_with_msg_counter() { + let msg_counter = Some(5); + let dest_id = None; + let pus_tm = ping_reply_with_data_and_additional_fields( + &[], + msg_counter, + dest_id, + dummy_timestamp(), + ); + let mut buf: [u8; 32] = [0; 32]; + let ser_len = pus_tm + .write_to_bytes(&mut buf) + .expect("Serialization failed"); + assert_eq!(ser_len, 19); + verify_raw_ping_reply(pus_tm.opt_crc16(), &buf, ser_len, msg_counter, dest_id); + } + + #[test] + fn test_serialization_no_source_data_alt_ctor() { + let timestamp = dummy_timestamp(); + let sph = SpHeader::new_for_unseg_tm_checked(0x123, 0x234, 0).unwrap(); + let tm_header = PusTmSecondaryHeader::new_simple(17, 2, timestamp); + let mut buf: [u8; 32] = [0; 32]; + let mut pus_tm = + PusTmCreatorWithReservedSourceData::new(&mut buf, sph, tm_header, 0).unwrap(); + assert_eq!(pus_tm.source_data_len(), 0); + assert_eq!(pus_tm.source_data(), &[]); + assert_eq!(pus_tm.source_data_mut(), &[]); + let ser_len = pus_tm.finalize(); + assert_eq!(ser_len, 18); + verify_raw_ping_reply(None, &buf, ser_len, None, None); + } + + #[test] + fn test_serialization_no_source_data_alt_ctor_no_crc() { + let timestamp = dummy_timestamp(); + let sph = SpHeader::new_for_unseg_tm_checked(0x123, 0x234, 0).unwrap(); + let tm_header = PusTmSecondaryHeader::new_simple(17, 2, timestamp); + let mut buf: [u8; 32] = [0; 32]; + let mut pus_tm = + PusTmCreatorWithReservedSourceData::new(&mut buf, sph, tm_header, 0).unwrap(); + assert_eq!(pus_tm.source_data_len(), 0); + assert_eq!(pus_tm.source_data(), &[]); + assert_eq!(pus_tm.source_data_mut(), &[]); + let ser_len = pus_tm.finalize_no_crc(); + assert_eq!(ser_len, 16); + verify_raw_ping_reply_no_crc(&buf, None, None); + assert_eq!(buf[16], 0); + assert_eq!(buf[17], 0); + } + + #[test] + fn test_serialization_no_source_data_no_table() { + let timestamp = dummy_timestamp(); + let pus_tm = ping_reply_no_data(timestamp); + let mut buf: [u8; 32] = [0; 32]; + let ser_len = pus_tm + .write_to_bytes_crc_no_table(&mut buf) + .expect("Serialization failed"); + assert_eq!(ser_len, 18); + verify_raw_ping_reply(pus_tm.opt_crc16(), &buf, ser_len, None, None); + } + + #[test] + fn test_serialization_no_source_data_no_crc() { + let timestamp = dummy_timestamp(); + let pus_tm = ping_reply_no_data(timestamp); + let mut buf: [u8; 32] = [0; 32]; + let ser_len = pus_tm + .write_to_bytes_no_crc(&mut buf) + .expect("Serialization failed"); + assert_eq!(ser_len, 16); + assert_eq!(buf[16], 0); + assert_eq!(buf[17], 0); + } + + #[test] + fn test_serialization_with_source_data() { + let src_data = [1, 2, 3]; + let hk_reply = base_hk_reply(dummy_timestamp(), &src_data); + let mut buf: [u8; 32] = [0; 32]; + let ser_len = hk_reply + .write_to_bytes(&mut buf) + .expect("Serialization failed"); + assert_eq!(ser_len, 21); + assert_eq!(buf[16], 1); + assert_eq!(buf[17], 2); + assert_eq!(buf[18], 3); + } + + #[test] + fn test_serialization_with_source_data_alt_ctor() { + let src_data = &[1, 2, 3]; + let mut buf: [u8; 32] = [0; 32]; + let sph = SpHeader::new_for_unseg_tm_checked(0x123, 0x234, 0).unwrap(); + let tc_header = PusTmSecondaryHeader::new_simple(3, 5, dummy_timestamp()); + let mut hk_reply_unwritten = + PusTmCreatorWithReservedSourceData::new(&mut buf, sph, tc_header, 3).unwrap(); + assert_eq!(hk_reply_unwritten.source_data_len(), 3); + assert_eq!(hk_reply_unwritten.source_data(), &[0, 0, 0]); + assert_eq!(hk_reply_unwritten.source_data_mut(), &[0, 0, 0]); + let source_data_mut = hk_reply_unwritten.source_data_mut(); + source_data_mut.copy_from_slice(src_data); + let ser_len = hk_reply_unwritten.finalize(); + assert_eq!(ser_len, 21); + assert_eq!(buf[16], 1); + assert_eq!(buf[17], 2); + assert_eq!(buf[18], 3); + } + + #[test] + fn test_serialization_with_source_data_alt_ctor_no_table() { + let src_data = &[1, 2, 3]; + let mut buf: [u8; 32] = [0; 32]; + let sph = SpHeader::new_for_unseg_tm_checked(0x123, 0x234, 0).unwrap(); + let tc_header = PusTmSecondaryHeader::new_simple(3, 5, dummy_timestamp()); + let mut hk_reply_unwritten = + PusTmCreatorWithReservedSourceData::new(&mut buf, sph, tc_header, 3).unwrap(); + assert_eq!(hk_reply_unwritten.source_data_len(), 3); + assert_eq!(hk_reply_unwritten.source_data(), &[0, 0, 0]); + assert_eq!(hk_reply_unwritten.source_data_mut(), &[0, 0, 0]); + let source_data_mut = hk_reply_unwritten.source_data_mut(); + source_data_mut.copy_from_slice(src_data); + let ser_len = hk_reply_unwritten.finalize_crc_no_table(); + assert_eq!(ser_len, 21); + assert_eq!(buf[16], 1); + assert_eq!(buf[17], 2); + assert_eq!(buf[18], 3); + } + + #[test] + fn test_setters() { + let timestamp = dummy_timestamp(); + let mut pus_tm = ping_reply_no_data(timestamp); + let u16_dest_id = UnsignedByteFieldU16::new(0x7fff).into(); + pus_tm.set_dest_id(Some(u16_dest_id)); + pus_tm.set_msg_counter(Some(0x1f)); + assert_eq!(pus_tm.dest_id(), Some(u16_dest_id)); + assert_eq!(pus_tm.msg_counter(), Some(0x1f)); + assert!(pus_tm.set_apid(0x7ff)); + assert_eq!(pus_tm.apid(), 0x7ff); + } + + #[test] + fn test_write_into_vec() { + let timestamp = dummy_timestamp(); + let pus_tm = ping_reply_no_data(timestamp); + let tm_vec = pus_tm.to_vec().expect("Serialization failed"); + assert_eq!(tm_vec.len(), 18); + let tm_deserialized = PusTmReader::new(tm_vec.as_slice(), &MIN_SEC_HEADER_PARAMS) + .expect("Deserialization failed"); + assert_eq!(tm_vec.len(), tm_deserialized.total_len()); + verify_ping_reply_with_reader(&tm_deserialized, false, 18, dummy_timestamp(), None, None); + } + + #[test] + fn test_deserialization_no_source_data() { + let timestamp = dummy_timestamp(); + let pus_tm = ping_reply_no_data(timestamp); + let mut buf: [u8; 32] = [0; 32]; + let ser_len = pus_tm + .write_to_bytes(&mut buf) + .expect("Serialization failed"); + assert_eq!(ser_len, 18); + let tm_deserialized = + PusTmReader::new(&buf, &MIN_SEC_HEADER_PARAMS).expect("Deserialization failed"); + assert_eq!(ser_len, tm_deserialized.total_len()); + assert_eq!(tm_deserialized.user_data(), tm_deserialized.source_data()); + assert_eq!(tm_deserialized.raw_data(), &buf[..ser_len]); + assert_eq!(tm_deserialized.crc16(), pus_tm.opt_crc16().unwrap()); + verify_ping_reply_with_reader(&tm_deserialized, false, 18, dummy_timestamp(), None, None); + } + + fn generic_test_deserialization_no_source_data_with_additional_fields( + src_data: &[u8], + expected_full_len: usize, + msg_counter: Option, + dest_id: Option, + ) { + let timestamp = dummy_timestamp(); + let pus_tm = + ping_reply_with_data_and_additional_fields(src_data, msg_counter, dest_id, timestamp); + let mut buf: [u8; 32] = [0; 32]; + let ser_len = pus_tm + .write_to_bytes(&mut buf) + .expect("Serialization failed"); + assert_eq!(ser_len, expected_full_len); + let tm_deserialized = PusTmReader::new( + &buf, + &SecondaryHeaderParameters { + timestamp_len: 7, + has_msg_counter: msg_counter.is_some(), + dest_id_len: dest_id.as_ref().map(|id| id.size()), + spare_bytes: 0, + }, + ) + .expect("Deserialization failed"); + assert_eq!(ser_len, tm_deserialized.total_len()); + assert_eq!(tm_deserialized.user_data(), tm_deserialized.source_data()); + assert_eq!(tm_deserialized.raw_data(), &buf[..ser_len]); + assert_eq!(tm_deserialized.crc16(), pus_tm.opt_crc16().unwrap()); + verify_ping_reply_with_reader( + &tm_deserialized, + false, + expected_full_len, + dummy_timestamp(), + dest_id, + msg_counter, + ); + } + + #[test] + fn test_deserialization_with_source_data_dest_id_msg_counter() { + let msg_counter = Some(5); + let dest_id = Some(UnsignedByteFieldU16::new(0x1f1f).into()); + generic_test_deserialization_no_source_data_with_additional_fields( + &[1, 2, 3], + 24, + msg_counter, + dest_id, + ); + } + + #[test] + fn test_deserialization_no_source_data_with_dest_id_msg_counter() { + let msg_counter = Some(5); + let dest_id = Some(UnsignedByteFieldU16::new(0x1f1f).into()); + generic_test_deserialization_no_source_data_with_additional_fields( + &[], + 21, + msg_counter, + dest_id, + ); + } + + #[test] + fn test_deserialization_no_source_data_with_msg_counter() { + let msg_counter = Some(5); + let dest_id = None; + generic_test_deserialization_no_source_data_with_additional_fields( + &[], + 19, + msg_counter, + dest_id, + ); + } + + #[test] + fn test_deserialization_no_source_data_with_dest_id() { + let msg_counter = None; + let dest_id = Some(UnsignedByteFieldU16::new(0x1f1f).into()); + generic_test_deserialization_no_source_data_with_additional_fields( + &[], + 20, + msg_counter, + dest_id, + ); + } + + #[test] + fn test_deserialization_no_source_data_with_trait() { + let timestamp = dummy_timestamp(); + let pus_tm = ping_reply_no_data(timestamp); + let mut buf: [u8; 32] = [0; 32]; + let ser_len = + WritablePusPacket::write_to_bytes(&pus_tm, &mut buf).expect("Serialization failed"); + assert_eq!(ser_len, 18); + let tm_deserialized = + PusTmReader::new(&buf, &MIN_SEC_HEADER_PARAMS).expect("Deserialization failed"); + assert_eq!(ser_len, tm_deserialized.total_len()); + assert_eq!(tm_deserialized.user_data(), tm_deserialized.source_data()); + assert_eq!(tm_deserialized.raw_data(), &buf[..ser_len]); + assert_eq!(tm_deserialized.crc16(), pus_tm.opt_crc16().unwrap()); + verify_ping_reply_with_reader(&tm_deserialized, false, 18, dummy_timestamp(), None, None); + } + + #[test] + fn test_deserialization_no_table() { + let timestamp = dummy_timestamp(); + let pus_tm = ping_reply_no_data(timestamp); + let mut buf: [u8; 32] = [0; 32]; + let ser_len = pus_tm + .write_to_bytes(&mut buf) + .expect("Serialization failed"); + assert_eq!(ser_len, 18); + let tm_deserialized = PusTmReader::new_crc_no_table(&buf, &MIN_SEC_HEADER_PARAMS) + .expect("Deserialization failed"); + assert_eq!(ser_len, tm_deserialized.total_len()); + assert_eq!(tm_deserialized.user_data(), tm_deserialized.source_data()); + assert_eq!(tm_deserialized.raw_data(), &buf[..ser_len]); + assert_eq!(tm_deserialized.crc16(), pus_tm.opt_crc16().unwrap()); + verify_ping_reply_with_reader(&tm_deserialized, false, 18, dummy_timestamp(), None, None); + } + + #[test] + fn test_deserialization_faulty_crc() { + let timestamp = dummy_timestamp(); + let pus_tm = ping_reply_no_data(timestamp); + let mut buf: [u8; 32] = [0; 32]; + let ser_len = pus_tm + .write_to_bytes(&mut buf) + .expect("Serialization failed"); + assert_eq!(ser_len, 18); + buf[ser_len - 2] = 0; + buf[ser_len - 1] = 0; + let tm_error = PusTmReader::new(&buf, &MIN_SEC_HEADER_PARAMS); + assert!(tm_error.is_err()); + let tm_error = tm_error.unwrap_err(); + if let PusError::ChecksumFailure(crc) = tm_error { + assert_eq!(crc, 0); + assert_eq!( + tm_error.to_string(), + "checksum verification for crc16 0x0000 failed" + ); + } + } + + #[test] + fn test_manual_field_update() { + let sph = SpHeader::new_for_unseg_tm_checked(0x123, 0x234, 0).unwrap(); + let tc_header = PusTmSecondaryHeader::new_simple(17, 2, dummy_timestamp()); + let mut tm = PusTmCreator::new_no_source_data(sph, tc_header, false); + tm.calc_crc_on_serialization = false; + assert_eq!(tm.data_len(), 0x00); + let mut buf: [u8; 32] = [0; 32]; + tm.update_ccsds_data_len(); + assert_eq!(tm.data_len(), 11); + tm.calc_own_crc16(); + let res = tm.write_to_bytes(&mut buf); + assert!(res.is_ok()); + tm.sp_header.data_len = 0; + tm.update_packet_fields(); + assert_eq!(tm.data_len(), 11); + } + + #[test] + fn test_target_buf_too_small() { + let timestamp = dummy_timestamp(); + let pus_tm = ping_reply_no_data(timestamp); + let mut buf: [u8; 16] = [0; 16]; + let res = pus_tm.write_to_bytes(&mut buf); + assert!(res.is_err()); + let error = res.unwrap_err(); + if let ByteConversionError::ToSliceTooSmall { found, expected } = error { + assert_eq!(expected, 18); + assert_eq!(found, 16); + } else { + panic!("Invalid error {:?}", error); + } + } + + #[test] + #[cfg(feature = "alloc")] + fn test_append_to_vec() { + let timestamp = dummy_timestamp(); + let pus_tm = ping_reply_no_data(timestamp); + let mut vec = Vec::new(); + let res = pus_tm.append_to_vec(&mut vec); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), 18); + verify_raw_ping_reply(pus_tm.opt_crc16(), vec.as_slice(), res.unwrap(), None, None); + } + + #[test] + #[cfg(feature = "alloc")] + fn test_append_to_vec_with_src_data() { + let src_data = [1, 2, 3]; + let hk_reply = base_hk_reply(dummy_timestamp(), &src_data); + let mut vec = Vec::new(); + vec.push(4); + let res = hk_reply.append_to_vec(&mut vec); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), 21); + assert_eq!(vec.len(), 22); + } + + fn verify_raw_ping_reply_no_crc( + buf: &[u8], + msg_counter: Option, + dest_id: Option, + ) { + // Secondary header is set -> 0b0000_1001 , APID occupies last bit of first byte + assert_eq!(buf[0], 0x09); + // Rest of APID 0x123 + assert_eq!(buf[1], 0x23); + // Unsegmented is the default, and first byte of 0x234 occupies this byte as well + assert_eq!(buf[2], 0xc2); + assert_eq!(buf[3], 0x34); + let mut expected_len = 11; + if let Some(dest_id) = dest_id { + expected_len += dest_id.size(); + } + if msg_counter.is_some() { + expected_len += 1; + } + assert_eq!(((buf[4] as u16) << 8) | buf[5] as u16, expected_len as u16); + assert_eq!(buf[6], (PusC as u8) << 4); + assert_eq!(buf[7], 17); + assert_eq!(buf[8], 2); + let mut current_idx = 9; + if let Some(msg_counter) = msg_counter { + assert_eq!(buf[current_idx], msg_counter); + current_idx += 1; + } + if let Some(dest_id) = dest_id { + let extracted_dest_id = + UnsignedByteField::new_from_be_bytes(dest_id.size(), &buf[current_idx..]) + .expect("Failed to extract destination ID"); + assert_eq!(extracted_dest_id, dest_id); + current_idx += dest_id.size(); + } + // Timestamp + assert_eq!( + &buf[current_idx..current_idx + dummy_timestamp().len()], + dummy_timestamp() + ); + } + + fn verify_raw_ping_reply( + crc16: Option, + buf: &[u8], + exp_full_len: usize, + msg_counter: Option, + dest_id: Option, + ) { + verify_raw_ping_reply_no_crc(buf, msg_counter, dest_id); + let mut digest = CRC_CCITT_FALSE.digest(); + digest.update(&buf[0..exp_full_len - 2]); + let crc16_calced = digest.finalize(); + let crc16_read = u16::from_be_bytes([buf[exp_full_len - 2], buf[exp_full_len - 1]]); + assert_eq!(crc16_read, crc16_calced); + if let Some(crc16) = crc16 { + assert_eq!(((crc16 >> 8) & 0xff) as u8, buf[exp_full_len - 2]); + assert_eq!((crc16 & 0xff) as u8, buf[exp_full_len - 1]); + } + } + + fn verify_ping_reply( + tm: &PusTmCreator, + has_user_data: bool, + exp_full_len: usize, + exp_timestamp: &[u8], + dest_id: Option, + msg_counter: Option, + ) { + assert_eq!(tm.len_written(), exp_full_len); + assert_eq!(tm.timestamp(), exp_timestamp); + assert_eq!(tm.source_data(), tm.user_data()); + verify_ping_reply_generic(tm, has_user_data, exp_full_len, dest_id, msg_counter); + } + + fn verify_ping_reply_with_reader( + tm: &PusTmReader, + has_user_data: bool, + exp_full_len: usize, + exp_timestamp: &[u8], + dest_id: Option, + msg_counter: Option, + ) { + assert_eq!(tm.len_packed(), exp_full_len); + assert_eq!(tm.timestamp(), exp_timestamp); + verify_ping_reply_generic(tm, has_user_data, exp_full_len, dest_id, msg_counter); + } + + fn verify_ping_reply_generic( + tm: &(impl GenericPusTmSecondaryHeader + PusPacket), + has_user_data: bool, + exp_full_len: usize, + dest_id: Option, + msg_counter: Option, + ) { + assert!(tm.is_tm()); + assert_eq!(PusPacket::service(tm), 17); + assert_eq!(GenericPusTmSecondaryHeader::service(tm), 17); + assert_eq!(PusPacket::subservice(tm), 2); + assert_eq!(GenericPusTmSecondaryHeader::subservice(tm), 2); + assert!(tm.sec_header_flag()); + if has_user_data { + assert!(!tm.user_data().is_empty()); + } + assert_eq!(tm.apid(), 0x123); + assert_eq!(tm.seq_count(), 0x234); + assert_eq!(PusPacket::pus_version(tm).unwrap(), PusVersion::PusC); + assert_eq!( + GenericPusTmSecondaryHeader::pus_version(tm), + PusVersion::PusC + ); + assert_eq!(tm.data_len(), exp_full_len as u16 - 7); + assert_eq!(tm.dest_id(), dest_id); + assert_eq!(tm.msg_counter(), msg_counter); + } + + #[test] + fn partial_eq_pus_tm() { + let timestamp = dummy_timestamp(); + let pus_tm_1 = ping_reply_no_data(timestamp); + let pus_tm_2 = ping_reply_no_data(timestamp); + assert_eq!(pus_tm_1, pus_tm_2); + } + + #[test] + fn partial_eq_serialized_vs_derialized() { + let timestamp = dummy_timestamp(); + let pus_tm = ping_reply_no_data(timestamp); + let mut buf = [0; 32]; + pus_tm.write_to_bytes(&mut buf).unwrap(); + assert_eq!( + pus_tm, + PusTmReader::new(&buf, &MIN_SEC_HEADER_PARAMS).unwrap() + ); + } + + #[test] + fn test_zero_copy_writer() { + let ping_tm = ping_reply_no_data(dummy_timestamp()); + let mut buf: [u8; 64] = [0; 64]; + let tm_size = ping_tm + .write_to_bytes(&mut buf) + .expect("writing PUS ping TM failed"); + let mut writer = PusTmZeroCopyWriter::new(&mut buf[..tm_size], &MIN_SEC_HEADER_PARAMS) + .expect("Creating zero copy writer failed"); + writer.set_seq_count(MAX_SEQ_COUNT); + writer.set_apid(MAX_APID); + assert!(!writer.set_apid(MAX_APID + 1)); + assert!(!writer.set_apid(MAX_SEQ_COUNT + 1)); + writer.finish(); + // This performs all necessary checks, including the CRC check. + let tm_read_back = + PusTmReader::new(&buf, &MIN_SEC_HEADER_PARAMS).expect("Re-creating PUS TM failed"); + assert_eq!(tm_read_back.total_len(), tm_size); + assert!(tm_read_back.msg_counter().is_none()); + assert!(tm_read_back.dest_id().is_none()); + assert_eq!(tm_read_back.seq_count(), MAX_SEQ_COUNT); + assert_eq!(tm_read_back.apid(), MAX_APID); + } + + #[test] + fn test_zero_copy_writer_ccsds_api() { + let ping_tm = ping_reply_no_data(dummy_timestamp()); + let mut buf: [u8; 64] = [0; 64]; + let tm_size = ping_tm + .write_to_bytes(&mut buf) + .expect("writing PUS ping TM failed"); + let mut writer = PusTmZeroCopyWriter::new(&mut buf[..tm_size], &MIN_SEC_HEADER_PARAMS) + .expect("Creating zero copy writer failed"); + writer.set_seq_count(MAX_SEQ_COUNT); + writer.set_apid(MAX_APID); + assert_eq!(PusPacket::service(&writer), 17); + assert_eq!(PusPacket::subservice(&writer), 2); + assert_eq!(writer.apid(), MAX_APID); + assert_eq!(writer.seq_count(), MAX_SEQ_COUNT); + } + + #[test] + fn test_zero_copy_pus_api() { + let ping_tm = ping_reply_with_data(DUMMY_DATA, dummy_timestamp()); + let mut buf: [u8; 64] = [0; 64]; + let tm_size = ping_tm + .write_to_bytes(&mut buf) + .expect("writing PUS ping TM failed"); + let crc16_raw = u16::from_be_bytes(buf[tm_size - 2..tm_size].try_into().unwrap()); + let mut writer = PusTmZeroCopyWriter::new(&mut buf[..tm_size], &MIN_SEC_HEADER_PARAMS) + .expect("Creating zero copy writer failed"); + writer.set_seq_count(MAX_SEQ_COUNT); + writer.set_apid(MAX_APID); + assert_eq!(PusPacket::service(&writer), 17); + assert_eq!(PusPacket::subservice(&writer), 2); + assert!(writer.dest_id().unwrap().is_none()); + assert!(writer.msg_counter().is_none()); + assert_eq!(writer.user_data(), DUMMY_DATA); + // Need to check crc16 before finish, because finish will update the CRC. + let crc16 = writer.opt_crc16(); + assert!(crc16.is_some()); + assert_eq!(crc16.unwrap(), crc16_raw); + writer.finish(); + } + + #[test] + fn test_sec_header_without_stamp() { + let sec_header = PusTmSecondaryHeader::new_simple_no_timestamp(17, 1); + assert_eq!(sec_header.timestamp, &[]); + } + + #[test] + fn test_reader_partial_eq() { + let timestamp = dummy_timestamp(); + let pus_tm = ping_reply_no_data(timestamp); + let mut buf = [0; 32]; + pus_tm.write_to_bytes(&mut buf).unwrap(); + let tm_0 = PusTmReader::new(&buf, &MIN_SEC_HEADER_PARAMS).unwrap(); + let tm_1 = PusTmReader::new(&buf, &MIN_SEC_HEADER_PARAMS).unwrap(); + assert_eq!(tm_0, tm_1); + } + #[test] + fn test_reader_buf_too_small_2() { + let timestamp = dummy_timestamp(); + let pus_tm = ping_reply_no_data(timestamp); + let mut buf = [0; 32]; + let written = pus_tm.write_to_bytes(&mut buf).unwrap(); + let tm_error = PusTmReader::new( + &buf[0..PUS_TM_MIN_LEN_WITHOUT_SOURCE_DATA + 1], + &MIN_SEC_HEADER_PARAMS, + ); + assert!(tm_error.is_err()); + let tm_error = tm_error.unwrap_err(); + if let PusError::ByteConversion(ByteConversionError::FromSliceTooSmall { + found, + expected, + }) = tm_error + { + assert_eq!(found, PUS_TM_MIN_LEN_WITHOUT_SOURCE_DATA + 1); + assert_eq!(expected, written); + } else { + panic!("unexpected error {tm_error}") + } + } + #[test] + fn test_reader_buf_too_small() { + let timestamp = dummy_timestamp(); + let pus_tm = ping_reply_no_data(timestamp); + let mut buf = [0; 32]; + pus_tm.write_to_bytes(&mut buf).unwrap(); + let tm_error = PusTmReader::new(&buf[0..5], &MIN_SEC_HEADER_PARAMS); + assert!(tm_error.is_err()); + let tm_error = tm_error.unwrap_err(); + if let PusError::ByteConversion(ByteConversionError::FromSliceTooSmall { + found, + expected, + }) = tm_error + { + assert_eq!(found, 5); + assert_eq!(expected, PUS_TM_MIN_LEN_WITHOUT_SOURCE_DATA); + } else { + panic!("unexpected error {tm_error}") + } + } + + #[test] + #[cfg(feature = "serde")] + fn test_serialization_creator_serde() { + let sph = SpHeader::new_for_unseg_tm_checked(0x123, 0x234, 0).unwrap(); + let time_provider = CdsTime::new_with_u16_days(0, 0); + let mut stamp_buf: [u8; 8] = [0; 8]; + let pus_tm = + PusTmCreator::new_simple(sph, 17, 2, &time_provider, &mut stamp_buf, &[], true) + .unwrap(); + + let output = to_allocvec(&pus_tm).unwrap(); + let output_converted_back: PusTmCreator = from_bytes(&output).unwrap(); + assert_eq!(output_converted_back, pus_tm); + } + + #[test] + #[cfg(feature = "serde")] + fn test_serialization_reader_serde() { + let sph = SpHeader::new_for_unseg_tm_checked(0x123, 0x234, 0).unwrap(); + let time_provider = CdsTime::new_with_u16_days(0, 0); + let mut stamp_buf: [u8; 8] = [0; 8]; + let pus_tm = + PusTmCreator::new_simple(sph, 17, 2, &time_provider, &mut stamp_buf, &[], true) + .unwrap(); + let pus_tm_vec = pus_tm.to_vec().unwrap(); + let tm_reader = PusTmReader::new(&pus_tm_vec, &MIN_SEC_HEADER_PARAMS).unwrap(); + let output = to_allocvec(&tm_reader).unwrap(); + let output_converted_back: PusTmReader = from_bytes(&output).unwrap(); + assert_eq!(output_converted_back, tm_reader); + } +} -- 2.43.0