21 Commits

Author SHA1 Message Date
6e2c35e0c0 Merge pull request 'prepare next release' (#131) from prep-v0.15.0 into main
Reviewed-on: #131
2025-07-18 19:32:28 +02:00
Robin Mueller
026e1a50b9 prepare next release 2025-07-18 19:31:55 +02:00
440b836b70 Merge pull request 'allow arbitrary crc minor version' (#130) from allow-arbitrary-crc-minor-version into main
Reviewed-on: #130
2025-07-18 19:28:31 +02:00
Robin Mueller
00e28e4a96 allow arbitrary crc minor version 2025-07-18 19:27:59 +02:00
4c1cad5b72 Merge pull request 'reserved data variants for ECSS TM and TC' (#129) from ecss-tm-tc-reserved-data-variants into main
Reviewed-on: #129
2025-05-16 19:06:08 +02:00
5cd5c1ce6d reserved data variants for ECSS TM and TC 2025-05-16 19:04:23 +02:00
de99bb926a Merge pull request 'small changelog tweak' (#128) from small-changelog-tweak into main
Reviewed-on: #128
2025-05-10 15:08:18 +02:00
167f53cac7 small changelog tweak 2025-05-10 15:07:58 +02:00
172227b843 Merge pull request 'update MSRV check' (#127) from update-msrv-check into main
Reviewed-on: #127
2025-05-10 15:04:21 +02:00
1bbca6866b update MSRV check 2025-05-10 15:03:05 +02:00
b569208d45 Merge pull request 'prepare v0.14.0' (#126) from prepare-release into main
Reviewed-on: #126
2025-05-10 14:58:45 +02:00
d9709ffd6c prepare v0.14.0 2025-05-10 14:54:27 +02:00
243dc64a78 Merge pull request 'remove badge' (#125) from remove-badge into main
Reviewed-on: #125
2025-05-10 14:38:23 +02:00
a6dc173f7f Merge branch 'main' into remove-badge 2025-05-10 14:38:19 +02:00
86dddbeef5 remove badge 2025-05-10 14:36:00 +02:00
17d112e838 Merge pull request 'one more test fix' (#124) from one-more-test-fix into main
Reviewed-on: #124
2025-05-10 14:31:53 +02:00
9c8467ccfe one more test fix 2025-05-10 14:30:00 +02:00
217a8c2cc7 Merge pull request 'formatting' (#123) from formatting into main
Reviewed-on: #123
2025-05-10 14:26:27 +02:00
349e34bed6 formatting 2025-05-10 14:25:44 +02:00
d6a76ca360 Merge pull request 'CRC handling and dependency update' (#122) from msp430-tweak into main
Reviewed-on: #122
2025-05-10 14:24:54 +02:00
8f4351771b API variants which use table-less CRC 2025-05-10 13:58:10 +02:00
7 changed files with 546 additions and 123 deletions

View File

@@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.81.0 - uses: dtolnay/rust-toolchain@1.70.0
- run: cargo check --release - run: cargo check --release
cross-check: cross-check:

View File

@@ -8,6 +8,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
# [unreleased] # [unreleased]
# [v0.15.0] 2025-07-18
## Added
- `PusTcCreatorWithReservedAppData` and `PusTmCreatorWithReservedSourceData` constructor variants
which allow writing source/app data into the serialization buffer directly without
requiring an extra buffer.
# [v0.14.0] 2025-05-10
## Changed ## Changed
- Moved CRC constants/implementations to dedicated `crc` module. - Moved CRC constants/implementations to dedicated `crc` module.
@@ -583,6 +593,9 @@ The timestamp of `PusTm` is now optional. See Added and Changed section for deta
Initial release with CCSDS Space Packet Primary Header implementation and basic PUS TC and TM Initial release with CCSDS Space Packet Primary Header implementation and basic PUS TC and TM
implementations. implementations.
[unreleased]: https://egit.irs.uni-stuttgart.de/rust/spacepackets/compare/v0.15.0...HEAD
[v0.15.0]: https://egit.irs.uni-stuttgart.de/rust/spacepackets/compare/v0.14.0...v0.15.0
[v0.14.0]: https://egit.irs.uni-stuttgart.de/rust/spacepackets/compare/v0.13.1...v0.14.0
[v0.13.1]: https://egit.irs.uni-stuttgart.de/rust/spacepackets/compare/v0.13.0...v0.13.1 [v0.13.1]: https://egit.irs.uni-stuttgart.de/rust/spacepackets/compare/v0.13.0...v0.13.1
[v0.13.0]: https://egit.irs.uni-stuttgart.de/rust/spacepackets/compare/v0.12.0...v0.13.0 [v0.13.0]: https://egit.irs.uni-stuttgart.de/rust/spacepackets/compare/v0.12.0...v0.13.0
[v0.12.0]: https://egit.irs.uni-stuttgart.de/rust/spacepackets/compare/v0.11.2...v0.12.0 [v0.12.0]: https://egit.irs.uni-stuttgart.de/rust/spacepackets/compare/v0.11.2...v0.12.0

View File

@@ -1,8 +1,8 @@
[package] [package]
name = "spacepackets" name = "spacepackets"
version = "0.13.1" version = "0.15.0"
edition = "2021" edition = "2021"
rust-version = "1.81.0" rust-version = "1.70.0"
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"] authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
description = "Generic implementations for various CCSDS and ECSS packet standards" description = "Generic implementations for various CCSDS and ECSS packet standards"
homepage = "https://egit.irs.uni-stuttgart.de/rust/spacepackets" homepage = "https://egit.irs.uni-stuttgart.de/rust/spacepackets"
@@ -13,45 +13,18 @@ categories = ["aerospace", "aerospace::space-protocols", "no-std", "hardware-sup
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
crc = "3.3" crc = "3"
delegate = ">=0.8, <=0.13" delegate = ">=0.8, <=0.13"
paste = "1" paste = "1"
zerocopy = { version = "0.8", features = ["derive"] }
thiserror = { version = "2", default-features = false }
num_enum = { version = ">0.5, <=0.7", default-features = false }
num-traits = { version = "0.2", default-features = false }
serde = { version = "1", optional = true, default-features = false, features = ["derive"] }
[dependencies.zerocopy] time = { version = "0.3", default-features = false, optional = true }
version = "0.8" chrono = { version = "0.4", default-features = false, optional = true }
features = ["derive"] defmt = { version = "1", default-features = false, optional = true }
[dependencies.thiserror]
version = "2"
default-features = false
[dependencies.num_enum]
version = ">0.5, <=0.7"
default-features = false
[dependencies.serde]
version = "1"
optional = true
default-features = false
features = ["derive"]
[dependencies.time]
version = "0.3"
default-features = false
optional = true
[dependencies.chrono]
version = "0.4"
default-features = false
optional = true
[dependencies.num-traits]
version = "0.2"
default-features = false
[dependencies.defmt]
version = "1"
optional = true
[features] [features]
default = ["std"] default = ["std"]

View File

@@ -1,7 +1,6 @@
[![Crates.io](https://img.shields.io/crates/v/spacepackets)](https://crates.io/crates/spacepackets) [![Crates.io](https://img.shields.io/crates/v/spacepackets)](https://crates.io/crates/spacepackets)
[![docs.rs](https://img.shields.io/docsrs/spacepackets)](https://docs.rs/spacepackets) [![docs.rs](https://img.shields.io/docsrs/spacepackets)](https://docs.rs/spacepackets)
[![ci](https://github.com/us-irs/spacepackets-rs/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/us-irs/spacepackets-rs/actions/workflows/ci.yml) [![ci](https://github.com/us-irs/spacepackets-rs/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/us-irs/spacepackets-rs/actions/workflows/ci.yml)
[![coverage](https://shields.io/endpoint?url=https://absatsw.irs.uni-stuttgart.de/projects/spacepackets/coverage-rs/latest/coverage.json)](https://absatsw.irs.uni-stuttgart.de/projects/spacepackets/coverage-rs/latest/index.html)
ECSS and CCSDS Spacepackets ECSS and CCSDS Spacepackets
====== ======

View File

@@ -377,7 +377,7 @@ pub trait WritablePusPacket {
fn write_to_bytes_no_crc(&self, slice: &mut [u8]) -> Result<usize, PusError>; fn write_to_bytes_no_crc(&self, slice: &mut [u8]) -> Result<usize, PusError>;
/// First uses [Self::write_to_bytes_no_crc] to write the packet to the given slice and then /// First uses [Self::write_to_bytes_no_crc] to write the packet to the given slice and then
/// uses the [CRC_CCITT_FALS] to calculate the CRC and write it to the slice. /// uses the [CRC_CCITT_FALSE] to calculate the CRC and write it to the slice.
fn write_to_bytes(&self, slice: &mut [u8]) -> Result<usize, PusError> { fn write_to_bytes(&self, slice: &mut [u8]) -> Result<usize, PusError> {
let mut curr_idx = self.write_to_bytes_no_crc(slice)?; let mut curr_idx = self.write_to_bytes_no_crc(slice)?;
let mut digest = CRC_CCITT_FALSE.digest(); let mut digest = CRC_CCITT_FALSE.digest();

View File

@@ -372,6 +372,49 @@ impl<'app_data> PusTcCreator<'app_data> {
vec.extend_from_slice(&digest.finalize().to_be_bytes()); vec.extend_from_slice(&digest.finalize().to_be_bytes());
appended_len appended_len
} }
/// Write the raw PUS byte representation to a provided buffer.
pub fn write_to_bytes(&self, slice: &mut [u8]) -> Result<usize, ByteConversionError> {
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<usize, ByteConversionError> {
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<usize, ByteConversionError> {
let writer_unfinalized = self.common_write(slice)?;
Ok(writer_unfinalized.finalize_no_crc())
}
fn common_write<'a>(
&self,
slice: &'a mut [u8],
) -> Result<PusTcCreatorWithReservedAppData<'a>, 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<'_> { impl WritablePusPacket for PusTcCreator<'_> {
@@ -380,31 +423,17 @@ impl WritablePusPacket for PusTcCreator<'_> {
PUS_TC_MIN_LEN_WITHOUT_APP_DATA + self.app_data.len() PUS_TC_MIN_LEN_WITHOUT_APP_DATA + self.app_data.len()
} }
/// Writes the packet to the given slice without writing the CRC. /// Write the raw PUS byte representation to a provided buffer.
///
/// The returned size is the written size WITHOUT the CRC.
fn write_to_bytes_no_crc(&self, slice: &mut [u8]) -> Result<usize, PusError> { fn write_to_bytes_no_crc(&self, slice: &mut [u8]) -> Result<usize, PusError> {
let mut curr_idx = 0; Ok(Self::write_to_bytes_no_crc(self, slice)?)
let tc_header_len = size_of::<zc::PusTcSecondaryHeader>(); }
let total_size = self.len_written();
if total_size > slice.len() {
return Err(ByteConversionError::ToSliceTooSmall {
found: slice.len(),
expected: total_size,
}
.into());
}
self.sp_header.write_to_be_bytes(slice)?;
curr_idx += CCSDS_HEADER_LEN;
let sec_header = zc::PusTcSecondaryHeader::try_from(self.sec_header).unwrap();
sec_header
.write_to(&mut slice[curr_idx..curr_idx + tc_header_len])
.map_err(|_| ByteConversionError::ZeroCopyToError)?;
curr_idx += tc_header_len; fn write_to_bytes(&self, slice: &mut [u8]) -> Result<usize, PusError> {
slice[curr_idx..curr_idx + self.app_data.len()].copy_from_slice(self.app_data); Ok(Self::write_to_bytes(self, slice)?)
curr_idx += self.app_data.len(); }
Ok(curr_idx)
fn write_to_bytes_crc_no_table(&self, slice: &mut [u8]) -> Result<usize, PusError> {
Ok(Self::write_to_bytes_crc_no_table(self, slice)?)
} }
} }
@@ -450,6 +479,133 @@ impl GenericPusTcSecondaryHeader for PusTcCreator<'_> {
impl IsPusTelecommand for PusTcCreator<'_> {} 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<Self, ByteConversionError> {
sp_header.set_packet_type(PacketType::Tc);
sp_header.set_sec_header_flag();
let len_written = PUS_TC_MIN_LEN_WITHOUT_APP_DATA + app_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::<crate::zc::SpHeader>() 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<Self, ByteConversionError> {
let mut curr_idx = 0;
sp_header.write_to_be_bytes(&mut buf[0..CCSDS_HEADER_LEN])?;
curr_idx += CCSDS_HEADER_LEN;
let sec_header_len = size_of::<zc::PusTcSecondaryHeader>();
let sec_header_zc = zc::PusTcSecondaryHeader::try_from(sec_header).unwrap();
sec_header_zc
.write_to(&mut buf[curr_idx..curr_idx + sec_header_len])
.map_err(|_| ByteConversionError::ZeroCopyToError)?;
curr_idx += sec_header_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 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 /// This class also derives the [serde::Serialize] and [serde::Deserialize] trait if the
@@ -627,8 +783,6 @@ impl PartialEq<PusTcReader<'_>> for PusTcCreator<'_> {
#[cfg(all(test, feature = "std"))] #[cfg(all(test, feature = "std"))]
mod tests { mod tests {
use std::error::Error;
use super::*; use super::*;
use crate::ecss::PusVersion::PusC; use crate::ecss::PusVersion::PusC;
use crate::ecss::{PusError, PusPacket, WritablePusPacket}; use crate::ecss::{PusError, PusPacket, WritablePusPacket};
@@ -675,6 +829,32 @@ mod tests {
); );
} }
#[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, 13);
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, 13);
assert_eq!(
pus_tc.opt_crc16().unwrap(),
u16::from_be_bytes(test_buf[size - 2..size].try_into().unwrap())
);
}
#[test] #[test]
fn test_serialization_crc_no_table() { fn test_serialization_crc_no_table() {
let pus_tc = base_ping_tc_simple_ctor(); let pus_tc = base_ping_tc_simple_ctor();
@@ -701,6 +881,17 @@ mod tests {
assert_eq!(test_buf[12], 0); assert_eq!(test_buf[12], 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, 11);
assert_eq!(test_buf[11], 0);
assert_eq!(test_buf[12], 0);
}
#[test] #[test]
fn test_deserialization() { fn test_deserialization() {
let pus_tc = base_ping_tc_simple_ctor(); let pus_tc = base_ping_tc_simple_ctor();
@@ -716,7 +907,28 @@ mod tests {
assert!(tc_from_raw.user_data().is_empty()); assert!(tc_from_raw.user_data().is_empty());
verify_test_tc_raw(&test_buf); verify_test_tc_raw(&test_buf);
verify_crc_no_app_data(&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(), 13);
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, 13);
let tc_from_raw =
PusTcReader::new(&test_buf).expect("Creating PUS TC struct from raw buffer failed");
assert_eq!(tc_from_raw.total_len(), 13);
verify_test_tc_with_reader(&tc_from_raw, false, 13);
assert!(tc_from_raw.user_data().is_empty());
verify_test_tc_raw(&test_buf);
verify_crc_no_app_data(&test_buf);
} }
#[test] #[test]
@@ -727,8 +939,8 @@ mod tests {
.write_to_bytes(test_buf.as_mut_slice()) .write_to_bytes(test_buf.as_mut_slice())
.expect("Error writing TC to buffer"); .expect("Error writing TC to buffer");
assert_eq!(size, 13); assert_eq!(size, 13);
let tc_from_raw = let tc_from_raw = PusTcReader::new_crc_no_table(&test_buf)
PusTcReader::new_crc_no_table(&test_buf).expect("Creating PUS TC struct from raw buffer failed"); .expect("Creating PUS TC struct from raw buffer failed");
assert_eq!(tc_from_raw.total_len(), 13); assert_eq!(tc_from_raw.total_len(), 13);
verify_test_tc_with_reader(&tc_from_raw, false, 13); verify_test_tc_with_reader(&tc_from_raw, false, 13);
assert!(tc_from_raw.user_data().is_empty()); assert!(tc_from_raw.user_data().is_empty());
@@ -758,6 +970,7 @@ mod tests {
tc.update_ccsds_data_len(); tc.update_ccsds_data_len();
assert_eq!(tc.data_len(), 6); assert_eq!(tc.data_len(), 6);
} }
#[test] #[test]
fn test_deserialization_with_app_data() { fn test_deserialization_with_app_data() {
let pus_tc = base_ping_tc_simple_ctor_with_app_data(&[1, 2, 3]); let pus_tc = base_ping_tc_simple_ctor_with_app_data(&[1, 2, 3]);
@@ -860,22 +1073,17 @@ mod tests {
let res = pus_tc.write_to_bytes(test_buf.as_mut_slice()); let res = pus_tc.write_to_bytes(test_buf.as_mut_slice());
assert!(res.is_err()); assert!(res.is_err());
let err = res.unwrap_err(); let err = res.unwrap_err();
if let PusError::ByteConversion(e) = err { assert_eq!(
assert_eq!( err,
e, ByteConversionError::ToSliceTooSmall {
ByteConversionError::ToSliceTooSmall { found: 12,
found: 12, expected: 13
expected: 13 }
} );
); assert_eq!(
assert_eq!( err.to_string(),
err.to_string(), "target slice with size 12 is too small, expected size of at least 13"
"pus error: target slice with size 12 is too small, expected size of at least 13" );
);
assert_eq!(err.source().unwrap().to_string(), e.to_string());
} else {
panic!("unexpected error {err}");
}
} }
#[test] #[test]

View File

@@ -392,12 +392,8 @@ impl<'time, 'src_data> PusTmCreator<'time, 'src_data> {
/// Write the raw PUS byte representation to a provided buffer. /// Write the raw PUS byte representation to a provided buffer.
pub fn write_to_bytes(&self, slice: &mut [u8]) -> Result<usize, ByteConversionError> { pub fn write_to_bytes(&self, slice: &mut [u8]) -> Result<usize, ByteConversionError> {
let mut curr_idx = self.write_to_bytes_no_crc(slice)?; let writer_unfinalized = self.common_write(slice)?;
let mut digest = CRC_CCITT_FALSE.digest(); Ok(writer_unfinalized.finalize())
digest.update(&slice[0..curr_idx]);
slice[curr_idx..curr_idx + 2].copy_from_slice(&digest.finalize().to_be_bytes());
curr_idx += 2;
Ok(curr_idx)
} }
/// Write the raw PUS byte representation to a provided buffer. /// Write the raw PUS byte representation to a provided buffer.
@@ -405,39 +401,36 @@ impl<'time, 'src_data> PusTmCreator<'time, 'src_data> {
&self, &self,
slice: &mut [u8], slice: &mut [u8],
) -> Result<usize, ByteConversionError> { ) -> Result<usize, ByteConversionError> {
let mut curr_idx = self.write_to_bytes_no_crc(slice)?; let writer_unfinalized = self.common_write(slice)?;
let mut digest = CRC_CCITT_FALSE_NO_TABLE.digest(); Ok(writer_unfinalized.finalize_crc_no_table())
digest.update(&slice[0..curr_idx]);
slice[curr_idx..curr_idx + 2].copy_from_slice(&digest.finalize().to_be_bytes());
curr_idx += 2;
Ok(curr_idx)
} }
/// Write the raw PUS byte representation to a provided buffer. /// Write the raw PUS byte representation to a provided buffer.
pub fn write_to_bytes_no_crc(&self, slice: &mut [u8]) -> Result<usize, ByteConversionError> { pub fn write_to_bytes_no_crc(&self, slice: &mut [u8]) -> Result<usize, ByteConversionError> {
let mut curr_idx = 0; let writer_unfinalized = self.common_write(slice)?;
let total_size = self.len_written(); Ok(writer_unfinalized.finalize_no_crc())
if total_size > slice.len() { }
fn common_write<'a>(
&self,
slice: &'a mut [u8],
) -> Result<PusTmCreatorWithReservedSourceData<'a>, ByteConversionError> {
if self.len_written() > slice.len() {
return Err(ByteConversionError::ToSliceTooSmall { return Err(ByteConversionError::ToSliceTooSmall {
found: slice.len(), found: slice.len(),
expected: total_size, expected: self.len_written(),
}); });
} }
self.sp_header let mut writer_unfinalized = PusTmCreatorWithReservedSourceData::write_to_bytes_partially(
.write_to_be_bytes(&mut slice[0..CCSDS_HEADER_LEN])?; slice,
curr_idx += CCSDS_HEADER_LEN; self.sp_header,
let sec_header_len = size_of::<zc::PusTmSecHeaderWithoutTimestamp>(); self.sec_header,
let sec_header = zc::PusTmSecHeaderWithoutTimestamp::try_from(self.sec_header).unwrap(); self.source_data.len(),
sec_header )?;
.write_to(&mut slice[curr_idx..curr_idx + sec_header_len]) writer_unfinalized
.map_err(|_| ByteConversionError::ZeroCopyToError)?; .source_data_mut()
curr_idx += sec_header_len; .copy_from_slice(self.source_data);
slice[curr_idx..curr_idx + self.sec_header.timestamp.len()] Ok(writer_unfinalized)
.copy_from_slice(self.sec_header.timestamp);
curr_idx += self.sec_header.timestamp.len();
slice[curr_idx..curr_idx + self.source_data.len()].copy_from_slice(self.source_data);
curr_idx += self.source_data.len();
Ok(curr_idx)
} }
/// Append the raw PUS byte representation to a provided [alloc::vec::Vec] /// Append the raw PUS byte representation to a provided [alloc::vec::Vec]
@@ -471,9 +464,18 @@ impl WritablePusPacket for PusTmCreator<'_, '_> {
fn write_to_bytes_no_crc(&self, slice: &mut [u8]) -> Result<usize, PusError> { fn write_to_bytes_no_crc(&self, slice: &mut [u8]) -> Result<usize, PusError> {
Ok(Self::write_to_bytes_no_crc(self, slice)?) Ok(Self::write_to_bytes_no_crc(self, slice)?)
} }
fn write_to_bytes(&self, slice: &mut [u8]) -> Result<usize, PusError> {
Ok(Self::write_to_bytes(self, slice)?)
}
fn write_to_bytes_crc_no_table(&self, slice: &mut [u8]) -> Result<usize, PusError> {
Ok(Self::write_to_bytes_crc_no_table(self, slice)?)
}
} }
impl PartialEq for PusTmCreator<'_, '_> { impl PartialEq for PusTmCreator<'_, '_> {
#[inline]
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.sp_header == other.sp_header self.sp_header == other.sp_header
&& self.sec_header == other.sec_header && self.sec_header == other.sec_header
@@ -525,6 +527,136 @@ impl GenericPusTmSecondaryHeader for PusTmCreator<'_, '_> {
impl IsPusTelemetry for PusTmCreator<'_, '_> {} 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<Self, ByteConversionError> {
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::<crate::zc::SpHeader>() 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<Self, ByteConversionError> {
let mut curr_idx = 0;
sp_header.write_to_be_bytes(&mut buf[0..CCSDS_HEADER_LEN])?;
curr_idx += CCSDS_HEADER_LEN;
let sec_header_len = size_of::<zc::PusTmSecHeaderWithoutTimestamp>();
let sec_header_zc = zc::PusTmSecHeaderWithoutTimestamp::try_from(sec_header).unwrap();
sec_header_zc
.write_to(&mut buf[curr_idx..curr_idx + sec_header_len])
.map_err(|_| ByteConversionError::ZeroCopyToError)?;
curr_idx += sec_header_len;
buf[curr_idx..curr_idx + sec_header.timestamp.len()].copy_from_slice(sec_header.timestamp);
curr_idx += sec_header.timestamp.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 /// This class models the PUS C telemetry packet. It is the primary data structure to read
/// a telemetry packet from raw bytes. /// a telemetry packet from raw bytes.
/// ///
@@ -962,7 +1094,41 @@ mod tests {
.write_to_bytes(&mut buf) .write_to_bytes(&mut buf)
.expect("Serialization failed"); .expect("Serialization failed");
assert_eq!(ser_len, 22); assert_eq!(ser_len, 22);
verify_raw_ping_reply(pus_tm.opt_crc16().unwrap(), &buf); verify_raw_ping_reply(pus_tm.opt_crc16(), &buf);
}
#[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, 22);
verify_raw_ping_reply(None, &buf);
}
#[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, 20);
verify_raw_ping_reply_no_crc(&buf);
assert_eq!(buf[20], 0);
assert_eq!(buf[21], 0);
} }
#[test] #[test]
@@ -974,7 +1140,7 @@ mod tests {
.write_to_bytes_crc_no_table(&mut buf) .write_to_bytes_crc_no_table(&mut buf)
.expect("Serialization failed"); .expect("Serialization failed");
assert_eq!(ser_len, 22); assert_eq!(ser_len, 22);
verify_raw_ping_reply(pus_tm.opt_crc16().unwrap(), &buf); verify_raw_ping_reply(pus_tm.opt_crc16(), &buf);
} }
#[test] #[test]
@@ -1004,6 +1170,46 @@ mod tests {
assert_eq!(buf[22], 3); assert_eq!(buf[22], 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, 25);
assert_eq!(buf[20], 1);
assert_eq!(buf[21], 2);
assert_eq!(buf[22], 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, 25);
assert_eq!(buf[20], 1);
assert_eq!(buf[21], 2);
assert_eq!(buf[22], 3);
}
#[test] #[test]
fn test_setters() { fn test_setters() {
let timestamp = dummy_timestamp(); let timestamp = dummy_timestamp();
@@ -1029,6 +1235,7 @@ mod tests {
assert_eq!(tm_vec.len(), tm_deserialized.total_len()); assert_eq!(tm_vec.len(), tm_deserialized.total_len());
verify_ping_reply_with_reader(&tm_deserialized, false, 22, dummy_timestamp()); verify_ping_reply_with_reader(&tm_deserialized, false, 22, dummy_timestamp());
} }
#[test] #[test]
fn test_deserialization_no_source_data() { fn test_deserialization_no_source_data() {
let timestamp = dummy_timestamp(); let timestamp = dummy_timestamp();
@@ -1046,6 +1253,22 @@ mod tests {
verify_ping_reply_with_reader(&tm_deserialized, false, 22, dummy_timestamp()); verify_ping_reply_with_reader(&tm_deserialized, false, 22, dummy_timestamp());
} }
#[test]
fn test_deserialization_no_source_data_with_trait() {
let timestamp = dummy_timestamp();
let pus_tm = base_ping_reply_full_ctor(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, 22);
let tm_deserialized = PusTmReader::new(&buf, 7).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, 22, dummy_timestamp());
}
#[test] #[test]
fn test_deserialization_no_table() { fn test_deserialization_no_table() {
let timestamp = dummy_timestamp(); let timestamp = dummy_timestamp();
@@ -1130,7 +1353,7 @@ mod tests {
let res = pus_tm.append_to_vec(&mut vec); let res = pus_tm.append_to_vec(&mut vec);
assert!(res.is_ok()); assert!(res.is_ok());
assert_eq!(res.unwrap(), 22); assert_eq!(res.unwrap(), 22);
verify_raw_ping_reply(pus_tm.opt_crc16().unwrap(), vec.as_slice()); verify_raw_ping_reply(pus_tm.opt_crc16(), vec.as_slice());
} }
#[test] #[test]
@@ -1146,7 +1369,7 @@ mod tests {
assert_eq!(vec.len(), 26); assert_eq!(vec.len(), 26);
} }
fn verify_raw_ping_reply(crc16: u16, buf: &[u8]) { fn verify_raw_ping_reply_no_crc(buf: &[u8]) {
// Secondary header is set -> 0b0000_1001 , APID occupies last bit of first byte // Secondary header is set -> 0b0000_1001 , APID occupies last bit of first byte
assert_eq!(buf[0], 0x09); assert_eq!(buf[0], 0x09);
// Rest of APID 0x123 // Rest of APID 0x123
@@ -1167,12 +1390,19 @@ mod tests {
assert_eq!(buf[12], 0x00); assert_eq!(buf[12], 0x00);
// Timestamp // Timestamp
assert_eq!(&buf[13..20], dummy_timestamp()); assert_eq!(&buf[13..20], dummy_timestamp());
}
fn verify_raw_ping_reply(crc16: Option<u16>, buf: &[u8]) {
verify_raw_ping_reply_no_crc(buf);
let mut digest = CRC_CCITT_FALSE.digest(); let mut digest = CRC_CCITT_FALSE.digest();
digest.update(&buf[0..20]); digest.update(&buf[0..20]);
let crc16_calced = digest.finalize(); let crc16_calced = digest.finalize();
assert_eq!(((crc16 >> 8) & 0xff) as u8, buf[20]); let crc16_read = u16::from_be_bytes([buf[20], buf[21]]);
assert_eq!((crc16 & 0xff) as u8, buf[21]); assert_eq!(crc16_read, crc16_calced);
assert_eq!(crc16, crc16_calced); if let Some(crc16) = crc16 {
assert_eq!(((crc16 >> 8) & 0xff) as u8, buf[20]);
assert_eq!((crc16 & 0xff) as u8, buf[21]);
}
} }
fn verify_ping_reply( fn verify_ping_reply(
@@ -1400,7 +1630,7 @@ mod tests {
PusTmCreator::new_simple(sph, 17, 2, &time_provider, &mut stamp_buf, &[], true) PusTmCreator::new_simple(sph, 17, 2, &time_provider, &mut stamp_buf, &[], true)
.unwrap(); .unwrap();
let pus_tm_vec = pus_tm.to_vec().unwrap(); let pus_tm_vec = pus_tm.to_vec().unwrap();
let (tm_reader, _) = PusTmReader::new(&pus_tm_vec, time_provider.len_as_bytes()).unwrap(); let tm_reader = PusTmReader::new(&pus_tm_vec, time_provider.len_as_bytes()).unwrap();
let output = to_allocvec(&tm_reader).unwrap(); let output = to_allocvec(&tm_reader).unwrap();
let output_converted_back: PusTmReader = from_bytes(&output).unwrap(); let output_converted_back: PusTmReader = from_bytes(&output).unwrap();
assert_eq!(output_converted_back, tm_reader); assert_eq!(output_converted_back, tm_reader);