diff --git a/CHANGELOG.md b/CHANGELOG.md index e978fa0..c5cd617 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Unreleased + +### Added +- TC Frame support (CCSDS 232.0-B-4) +- CLTU Generation only with BCH encoding (CCSDS 231.0-B-4) +- TM Frame support (CCSDS 132.0-B-3) +- Randomization support (CCSDS 131.0-B-5, CCSDS 231.0-B-4) + ## v0.1.0 ### Added diff --git a/Cargo.toml b/Cargo.toml index 1d03afc..93d4c9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" rust-version = "1.63" license-file = "LICENSE" + license = "MIT" categories = [ "aerospace::space-protocols" ] keywords = [ "ccsds", "bytes", "parser", "packets" ] exclude = [ ".github" ] @@ -18,6 +19,7 @@ async-codec = [ "asynchronous-codec", "bytes" ] tokio-codec = [ "bytes", "tokio-util/codec" ] crc = [ "dep:crc" ] + tctm = [ "dep:lazy_static" ] # docs.rs-specific configuration [package.metadata.docs.rs] @@ -31,9 +33,11 @@ byteorder = "~1.4" bytes = { version = "~1.4", optional = true } crc = { version = "3.0", optional = true } + lazy_static = { version = "1.5.0", optional = true } tokio-util = { version = "~0.7", optional = true, features = [ "codec" ] } [dev-dependencies] - rstest = "~0.15" - futures = "~0.3" + rstest = "~0.15" + futures = "~0.3" + spacepacket = { path = ".", features = [ "async-codec", "crc", "tctm" ] } diff --git a/README.md b/README.md index a21d72e..b31bda9 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The full packet is parsed into a SpacePacket struct, all overhead for the payloa This crate was created after viewing other rust-based CCSDS crates and wanting a more friendly user interface for packet interaction. -Currently this crate assumes Big Endian for all byte streams. Though this may change to be generic over endian in the future. +Currently this crate assumes Big Endian for all byte streams. Though this may change to be generic over endianness in the future. ## Optional Features @@ -17,6 +17,12 @@ This crate provides data validation via CRC-16 calculation through the [crc crat #### Sink/Stream Support Another optional feature this crate provides is support for for sapcepacket I/O via sinks and stream through the async-codec and tokio-codec features. This allows users to easily create asynchronous listeners for spacepackets with optional sync markers and CRC support. +#### TC/TM Support and CLTU Generation +TeleComamand (TC) and Telemetry (TM) Frames are supported when the `tctm` feature is enabled. + +Communications Link Transmission Unit (CLTU) packets can also be constructed +when this feature is enabled. Currently only BCH and Randomized BCH +encoding is supported however LDPC encoding is planned as a future enhancement. # Examples diff --git a/src/lib.rs b/src/lib.rs index dc0b389..c5708d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,10 @@ use byteorder::{BigEndian, ReadBytesExt}; #[cfg_attr(docsrs, doc(cfg(feature = "crc")))] use crc::Crc; +#[cfg(feature = "tctm")] +#[cfg_attr(docsrs, doc(cfg(feature = "tctm")))] +pub mod tctm; + #[cfg(feature = "crc")] use std::fmt::Display; diff --git a/src/tctm.rs b/src/tctm.rs new file mode 100644 index 0000000..1ca7678 --- /dev/null +++ b/src/tctm.rs @@ -0,0 +1,7 @@ +//! TeleCommand (TC; CCSDS 231.0-B-4 ) +//! and Telemetry (TM; CCSDS 132.0-B-3 ) Transfer Frame +//! definitions, en/de-coding. +pub mod cltu; +pub(crate) mod randomizer; +pub mod tc; +pub mod tm; diff --git a/src/tctm/cltu.rs b/src/tctm/cltu.rs new file mode 100644 index 0000000..c3093f5 --- /dev/null +++ b/src/tctm/cltu.rs @@ -0,0 +1,104 @@ +//! Generate Communications Link Transmission Unit (CLTU) packets +//! as defined in CCSDS 231.0-B-4 + +use crate::tctm::randomizer::{apply_randomization, Randomization}; + +mod bch; + +#[derive(Debug, Clone, Copy)] +/// Possible CCSDS 231.0-B-4 CLTU encoding types +pub enum EncodingScheme { + /// A modified (63, 56) Bose-Chaudhuri-Hocquenghem code. + /// Generates 7 parity bits for every 56 data bits + BCH, + /// The same as the [Self::BCH] but with CCSDS 231.0-B-4 + /// randomized applied to the TC frame before BCH encoding. + BCHRandomized, +} + +/// Generates a Communications Link Transmission Unit (CLTU) from an input +/// byte stream using the chosen encoding scheme. +pub fn generate_ctlu>(bytes: P, encoding: EncodingScheme) -> Vec { + let bytes = bytes.as_ref(); + match encoding { + EncodingScheme::BCH => bch::encode_bch_ctlu(bytes), + EncodingScheme::BCHRandomized => { + bch::encode_bch_ctlu(apply_randomization(bytes, Randomization::TC).as_slice()) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + use rstest::rstest; + + // test values derived from https://github.com/yamcs/yamcs/blob/78b9553caf3c9f7ef7a6e6897d236a69aeed8190/yamcs-core/src/test/java/org/yamcs/tctm/ccsds/error/BchCltuGeneratorTest.java + // and by extension from SpacePyLibrary + // https://github.com/Stefan-Korner/SpacePyLibrary/blob/master/UnitTest/testData.py + const TC_FRAME_01: &[u8] = &[ + 0x22, 0xF6, 0x00, 0xFF, 0x00, 0x42, 0x1A, 0x8C, 0xC0, 0x0E, 0x01, 0x0D, 0x19, 0x06, 0x02, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, + 0x00, 0x0F, 0x00, 0x01, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x02, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x0F, 0x00, 0x03, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x04, 0xFF, 0x00, + 0x00, 0x00, 0x00, 0x0F, 0x00, 0x05, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x06, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x07, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x08, + 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x09, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, + 0x0A, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x0B, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, + 0x00, 0x0C, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x0D, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0x0F, 0x00, 0x0E, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x0F, 0xFF, 0x00, 0x00, 0x00, + 0x00, 0x0F, 0x00, 0x10, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x11, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x0F, 0x00, 0x12, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x13, 0xFF, 0x00, + 0x00, 0x00, 0x00, 0x0F, 0x00, 0x14, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x15, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x16, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x17, + 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x18, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, + 0x19, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x1A, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, + 0x00, 0x1B, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x1C, 0xFF, 0x00, 0x00, 0x00, 0xAD, + 0x1A, + ]; + + const CLTU_01: &[u8] = &[ + 0xEB, 0x90, 0x22, 0xF6, 0x00, 0xFF, 0x00, 0x42, 0x1A, 0x12, 0x8C, 0xC0, 0x0E, 0x01, 0x0D, + 0x19, 0x06, 0x5A, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x8A, 0x00, 0x01, 0x00, 0x00, + 0x00, 0xFF, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x01, 0xFF, 0x28, 0x00, 0x00, 0x00, + 0x00, 0x0F, 0x00, 0x02, 0x5A, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x92, 0x03, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x0F, 0xD6, 0x00, 0x04, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xD4, 0x0F, + 0x00, 0x05, 0xFF, 0x00, 0x00, 0x00, 0xA8, 0x00, 0x0F, 0x00, 0x06, 0xFF, 0x00, 0x00, 0xC8, + 0x00, 0x00, 0x0F, 0x00, 0x07, 0xFF, 0x00, 0xCA, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x08, 0xFF, + 0x66, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x09, 0xA8, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, + 0x00, 0x92, 0x0A, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xF4, 0x00, 0x0B, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x5A, 0x0F, 0x00, 0x0C, 0xFF, 0x00, 0x00, 0x00, 0xB0, 0x00, 0x0F, 0x00, 0x0D, + 0xFF, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x0F, 0x00, 0x0E, 0xFF, 0x00, 0x96, 0x00, 0x00, 0x00, + 0x0F, 0x00, 0x0F, 0xFF, 0xDA, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x10, 0x82, 0xFF, 0x00, + 0x00, 0x00, 0x00, 0x0F, 0x00, 0x92, 0x11, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x92, 0x00, + 0x12, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x2A, 0x0F, 0x00, 0x13, 0xFF, 0x00, 0x00, 0x00, 0x24, + 0x00, 0x0F, 0x00, 0x14, 0xFF, 0x00, 0x00, 0x4A, 0x00, 0x00, 0x0F, 0x00, 0x15, 0xFF, 0x00, + 0x72, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x16, 0xFF, 0x2E, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, + 0x17, 0x20, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x92, 0x18, 0xFF, 0x00, 0x00, 0x00, + 0x00, 0x0F, 0xB0, 0x00, 0x19, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x90, 0x0F, 0x00, 0x1A, 0xFF, + 0x00, 0x00, 0x00, 0x3C, 0x00, 0x0F, 0x00, 0x1B, 0xFF, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x0F, + 0x00, 0x1C, 0xFF, 0x00, 0x2E, 0x00, 0x00, 0xAD, 0x1A, 0x55, 0x55, 0x55, 0xEC, 0xC5, 0xC5, + 0xC5, 0xC5, 0xC5, 0xC5, 0xC5, 0x79, + ]; + + const TC_FRAME_02: &[u8] = &[ + 0x22, 0xF6, 0x00, 0x23, 0x00, 0x82, 0x00, 0x0F, 0x00, 0x1D, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0x0F, 0x00, 0x1E, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x1F, 0xFF, 0x00, 0x00, 0x00, + 0x00, 0x0F, 0xAC, 0x8F, 0x00, 0x68, + ]; + + const CLTU_02: &[u8] = &[ + 0xEB, 0x90, 0x22, 0xF6, 0x00, 0x23, 0x00, 0x82, 0x00, 0x24, 0x0F, 0x00, 0x1D, 0xFF, 0x00, + 0x00, 0x00, 0x34, 0x00, 0x0F, 0x00, 0x1E, 0xFF, 0x00, 0x00, 0x10, 0x00, 0x00, 0x0F, 0x00, + 0x1F, 0xFF, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x0F, 0xAC, 0x8F, 0x00, 0x90, 0x68, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x06, 0xC5, 0xC5, 0xC5, 0xC5, 0xC5, 0xC5, 0xC5, 0x79, + ]; + + #[rstest] + #[case(TC_FRAME_01, CLTU_01)] + #[case(TC_FRAME_02, CLTU_02)] + fn cltu_gen(#[case] tc_frame: &[u8], #[case] cltu: &[u8]) { + assert_eq!(cltu, generate_ctlu(tc_frame, EncodingScheme::BCH)) + } +} diff --git a/src/tctm/cltu/bch.rs b/src/tctm/cltu/bch.rs new file mode 100644 index 0000000..3fa5246 --- /dev/null +++ b/src/tctm/cltu/bch.rs @@ -0,0 +1,82 @@ +use lazy_static::lazy_static; +/// CCSDS BCH polynomial x^7 + x^6 + x^2 + 1 +/// is then left shifted 1 bit +const CCSDS_POLYNOMIAL: u8 = 0x8A_u8; +const START_SEQUNCE: &[u8] = &[0xEB, 0x90]; +const TAIL_SEQUENCE: &[u8] = &[0xC5, 0xC5, 0xC5, 0xC5, 0xC5, 0xC5, 0xC5, 0x79]; + +lazy_static! { + static ref LOOKUP_TALBE: [u8; 256] = (0_u8..=255) + .map(|val| { + (0..8_u8).fold(val, |val, _| { + if val & 0x80 == 0 { + val << 1 + } else { + (val << 1) ^ CCSDS_POLYNOMIAL + } + }) + }) + .collect::>() + .try_into() + .unwrap(); +} + +/// Compute BCH codeword as defined in CCSDS 232.0-B-4 with polynomial +/// polynomial x^7 + x^6 + x^2 + 1 +pub fn compute_bch_parity(bytes: &[u8; 7]) -> u8 { + // bch encoding takes 7 byte chunks of data then computes 1 parity byte + + let mut remainder = bytes + .iter() + .fold(0, |acc, val| LOOKUP_TALBE[(val ^ acc) as usize]); + // logical complement of the remainder + remainder ^= 0xFF; + // force the 0th byte to be 0 since there are only 7 parity bits. + remainder &= 0xFE; + remainder +} + +pub(crate) fn encode_bch_ctlu(bytes: &[u8]) -> Vec { + let mut output = START_SEQUNCE.to_vec(); + + let mut iter = bytes.chunks_exact(7); + + (&mut iter).for_each(|chunk| { + output.extend_from_slice(chunk); + // unwraping is safe here because we have forced chunks of length 7 + output.push(compute_bch_parity(chunk.try_into().unwrap())); + }); + + // handle any remainder by resizing to 7-bytes chunk + if !iter.remainder().is_empty() { + let mut remainder = iter.remainder().to_vec(); + // pad with bits of alternating 0 and 1s starting with 0 + remainder.resize(7, 0x55_u8); + output.extend_from_slice(&remainder); + // unwraping is safe here because we have forced a length of 7 + output.push(compute_bch_parity(remainder.as_slice().try_into().unwrap())); + } + output.extend_from_slice(TAIL_SEQUENCE); + + output +} + +#[cfg(test)] +mod test { + use super::*; + + use rstest::rstest; + + // test values derived from https://github.com/yamcs/yamcs/blob/78b9553caf3c9f7ef7a6e6897d236a69aeed8190/yamcs-core/src/test/java/org/yamcs/tctm/ccsds/error/BchCltuGeneratorTest.java + // and by extension from SpacePyLibrary + // https://github.com/Stefan-Korner/SpacePyLibrary/blob/master/UnitTest/testData.py + + #[rstest] + #[case([0x22, 0xF6, 0x00, 0xFF, 0x00, 0x42, 0x1A], 0x12)] + #[case([0x8C, 0xC0, 0x0E, 0x01, 0x0D, 0x19, 0x06], 0x5A)] + #[case([0x30, 0x1B, 0x00, 0x09, 0x00, 0x82, 0x00], 0x54)] + #[case([0x10, 0xE4, 0xC1, 0x55, 0x55, 0x55, 0x55], 0x3E)] + fn bch_encoding(#[case] input: [u8; 7], #[case] parity: u8) { + assert_eq!(parity, compute_bch_parity(&input)) + } +} diff --git a/src/tctm/randomizer.rs b/src/tctm/randomizer.rs new file mode 100644 index 0000000..aeee84e --- /dev/null +++ b/src/tctm/randomizer.rs @@ -0,0 +1,164 @@ +use lazy_static::lazy_static; +lazy_static! { + // CCSDS 131.0-B-5 TC randomizer with generator polynomial + // h(x) = x^8 + x^6 + x^4 + x^3 + x^2 + x + 1 + pub(crate) static ref TC_RANDOMIZER: Box<[u8]> = { + let mut lfsr = 0xFF_u8; + let mut extra_bit = 0_u8; + + [0_u8; 255] + .into_iter() + .map(|mut val| { + (0..8).for_each(|_| { + val = (val << 1) | (lfsr & 1); + extra_bit = (lfsr + ^ (lfsr >> 1) + ^ (lfsr >> 2) + ^ (lfsr >> 3) + ^ (lfsr >> 4) + ^ (lfsr >> 6)) + & 1; + lfsr = (lfsr >> 1) | (extra_bit << 7); + }); + val + }) + .collect::>() + .into_boxed_slice() + }; + + + // legacy 255 byte TM randomizer with generator polynomial + // h(x) = x^8 + x^7 + x^5 + x^3 + 1 + pub(crate) static ref TM_RANDOMIZER_255: Box<[u8]> ={ + let mut lfsr = 0xFF_u8; + let mut extra_bit = 0_u8; + + [0_u8; 255].into_iter().map(|mut val| { + (0..8).for_each(|_|{ + val = (val <<1) | (lfsr & 1); + extra_bit = ( + lfsr + ^ (lfsr >> 3) + ^ (lfsr >> 5) + ^ (lfsr >> 7) + ) & 1; + + lfsr = (lfsr >> 1) | (extra_bit << 7); + }); + val + }).collect::>() + .into_boxed_slice() + + }; + + // Recommended 131071 length repeater with generator polynomial + // h(x) = x^17 + x^14 + 1 + pub(crate) static ref TM_RANDOMIZER_131071: Box<[u8]> ={ + let mut lfsr = 0x18E38_u32; + let mut extra_bit = 0x0_u32; + + [0_u8; 131071].into_iter().map(|mut val|{ + (0..8).for_each(|_| { + // accumulate the output bits into the output + // register + val = (val << 1) | ((lfsr & 1) as u8); + + // perform xor output on the taps + extra_bit = (lfsr ^ (lfsr >> 14)) & 1; + + // polynomial depth is 17 bits, so shift by depth - 1 + lfsr = (lfsr >> 1) | (extra_bit << 16); + }); + val + }).collect::>().into_boxed_slice() + }; +} + +#[cfg_attr(test, derive(Clone, Copy))] +pub(crate) enum Randomization { + TC, + Tm255, + Tm131071, +} + +pub(crate) fn apply_randomization>(bytes: P, randomizer: Randomization) -> Vec { + let randomization_generator = match randomizer { + Randomization::TC => &(*TC_RANDOMIZER), + Randomization::Tm255 => &(*TM_RANDOMIZER_255), + Randomization::Tm131071 => &(*TM_RANDOMIZER_131071), + }; + bytes + .as_ref() + .iter() + .zip(randomization_generator.iter().cycle()) + .map(|(val, rand)| val ^ rand) + .collect() +} + +#[cfg(test)] +mod test { + use super::*; + + use rstest::rstest; + + #[test] + fn tc_randomizer() { + let expected_seq = [ + 0b1111_1111_u8, + 0b0011_1001, + 0b1001_1110, + 0b0101_1010, + 0b0110_1000, + ]; + + let seq: [u8; 5] = TC_RANDOMIZER[..5].try_into().unwrap(); + + assert_eq!(expected_seq, seq) + } + + #[test] + fn tm_randomizer_255() { + let expected_seq = [ + 0b1111_1111_u8, + 0b0100_1000, + 0b0000_1110, + 0b1100_0000, + 0b1001_1010, + ]; + + let seq: [u8; 5] = TM_RANDOMIZER_255[..5].try_into().unwrap(); + + assert_eq!(expected_seq, seq) + } + + #[test] + fn tm_randomizer_131071() { + let expected_eq = [ + 0b0001_1100_u8, + 0b0111_0001, + 0b1011_1001, + 0b0001_1011, + 0b1010_1001, + ]; + + let seq: [u8; 5] = TM_RANDOMIZER_131071[..5].try_into().unwrap(); + + assert_eq!(expected_eq, seq) + } + + #[rstest] + fn apply_randomness( + #[values(Randomization::TC, Randomization::Tm255, Randomization::Tm131071)] + randomization: Randomization, + ) { + let input_bytes: Vec = (0..131_071_u32).map(|val| (val % 256) as u8).collect(); + + let random_bytes = apply_randomization(&input_bytes, randomization); + + assert_ne!(input_bytes, random_bytes); + + let recovered_bytes = apply_randomization(random_bytes, randomization); + + assert_eq!(input_bytes, recovered_bytes) + } +} diff --git a/src/tctm/tc.rs b/src/tctm/tc.rs new file mode 100644 index 0000000..ca32d4b --- /dev/null +++ b/src/tctm/tc.rs @@ -0,0 +1,331 @@ +//! Implementation of the TC Space Data Link Protocool +//! as defined in CCSDS 232.0-B-4 +//! + +use std::io::{Error, ErrorKind, Read}; + +use byteorder::{BigEndian, ReadBytesExt}; + +/// The Bypass Flag is used to control the types of +/// Frame Acceptanc Check performed by the receiving entity. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BypassFlag { + /// This type of frame indicates the normal acceptance + /// checks shall be performed + TypeA = 0, + /// Under Type-B acceptance chcecks are bypassed + TypeB = 1, +} +impl BypassFlag { + pub fn from_u8(val: u8) -> Result { + match val { + 0 => Ok(Self::TypeA), + 1 => Ok(Self::TypeB), + val => Err(Error::new( + ErrorKind::InvalidData, + format!("Invalid BypassFlag value {val:}. Can only be 1 bit."), + )), + } + } +} + +/// Control Command Flag indicates if the packet contains +/// data (Type-D) or control information to set up the +/// Frame Acceptance and Reporting Mechanism (FARM) +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ControlFlag { + /// This type of frame contains Data + TypeD = 0, + /// A control frame with parameters + /// to configure FARM to accept data. + TypeC = 1, +} +impl ControlFlag { + pub fn from_u8(val: u8) -> Result { + match val { + 0 => Ok(Self::TypeD), + 1 => Ok(Self::TypeC), + val => Err(Error::new( + ErrorKind::InvalidData, + format!("Invalid ControlFlag value {val:}. Can only be 1 bit."), + )), + } + } +} + +/// Primary Header for a TC Transfer Frame +/// This Header is only meant to be used with a [TCTransferFrame] +/// as the length of the payload is calculated at encoding time. +// When calclulating Length of this header, only 10 bits are allowed. +// Additionally it is considered length -1 consistent with SpacePackets +// this leaves a maximum size of 1024 bytes per packet +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TCPrimaryHeader { + /// Transfer Frame Version number. + /// Currently fixed to '00' + /// Encoded in 2 bits + pub tfvn: u8, + + /// Bypass Flag determines the type of Frame Acceptance Checks + /// applied by receiving entity + pub bypass_flag: BypassFlag, + + /// Control Command Flag indicates if the packet contains + /// data (Type-D) or control information to set up the + /// Frame Acceptance and Reporting Mechanism (FARM) + pub control_flag: ControlFlag, + + /// 10-bit unique identifier for the spacecraft + pub scid: u16, + + /// The identifier of the virtual channel to which this + /// packet belongs. 6-bits maximum. + pub vcid: u8, + + /// Sequence number of this frame, used by Type-A + /// FARMs to check frames are received sequentially + pub sequence_number: u8, +} +impl TCPrimaryHeader { + /// Validate header values which require bit masks will fit in the + /// desginate bit-depth + /// + /// # Errors + /// + /// This function errors under the following circumstances + /// - [Self::tfvn] > 3 + /// - [Self::scid] > 1023 + /// - [Self::vcid] > 63 + pub fn validate(&self) -> Result<(), Error> { + if self.tfvn > 3 { + return Err(Error::new( + ErrorKind::InvalidData, + format!( + "Transfer frame version number must be <=3 but found {}", + self.tfvn + ), + )); + } + + if self.scid > 1023 { + return Err(Error::new( + ErrorKind::InvalidData, + format!("Spacecraft ID must be <=1023 but found {}", self.scid), + )); + } + + if self.vcid > 63 { + return Err(Error::new( + ErrorKind::InvalidData, + format!("Virtual Channel ID must be <=63 but found {}", self.vcid), + )); + } + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +/// A TeleCommand (TC) Transfer Frame per CCSDS 232.0-B-4 +pub struct TCTransferFrame { + /// Primary Header information with exception of the payload + /// length. + header: TCPrimaryHeader, + + /// Packet payload has a maximum length of 1019 bytes + payload: Vec, +} +impl TCTransferFrame { + /// Initialize a new TC Transfer Frame. + /// + /// # Errors + /// + /// This function errors under the following circumstances + /// - payload length is > 1019 bytes + /// - [TCPrimaryHeader::tfvn] > 3 + /// - [TCPrimaryHeader::scid] > 1023 + /// - [TCPrimaryHeader::vcid] > 63 + pub fn new(header: TCPrimaryHeader, payload: Vec) -> Result { + header.validate()?; + + if payload.len() > 1019 { + return Err(Error::new( + ErrorKind::InvalidData, + format!( + "Payload length must be <=1019 bytes but supplied payload has length {}", + payload.len() + ), + )); + } + + Ok(Self { header, payload }) + } + + /// Retrieve the meta-data header information for this packet. + /// Header information does not include length of the payload. + pub fn header(&self) -> TCPrimaryHeader { + self.header + } + + /// Borrow the payload of this packet. The payload has a maximum possible length of 1019 bytes + pub fn payload(&self) -> &[u8] { + self.payload.as_slice() + } + + /// Encode the Transfer frame into a byte stream. + /// Assumes Big Endian byte order + pub fn encode(mut self) -> Vec { + let TCPrimaryHeader { + tfvn, + bypass_flag, + control_flag, + scid, + vcid, + sequence_number, + } = self.header; + + let first_word = { + (tfvn as u16 & 0x3_u16) << 14 + | (bypass_flag as u16 & 0x1_u16) << 13 + | (control_flag as u16 & 0x1_u16) << 12 + // two spare bits here reserved + | (scid & 0x3ff_u16) + }; + + // Add 5 to account for the header length as well + let encoded_len = (self.payload.len() - 1 + 5) as u16; + let second_word = { ((vcid as u16 & 0x3f_u16) << 10) | (encoded_len & 0x3ff_u16) }; + + let mut message = first_word.to_be_bytes().to_vec(); + + message.extend_from_slice(&second_word.to_be_bytes()); + message.push(sequence_number); + + message.append(&mut self.payload); + + message + } + + /// Decode a transfer frame from a byte stream. + /// Assumes Big Endian byte order + pub fn decode(buffer: &mut R) -> Result { + let first_word = buffer.read_u16::()?; + let second_word = buffer.read_u16::()?; + + // subtract 5 to accound for the length of the Primary Header + let payload_len = (second_word & 0x3ff_u16) + 1 - 5; + + let header = TCPrimaryHeader { + tfvn: ((first_word >> 14) & 0x3_u16) as u8, + bypass_flag: BypassFlag::from_u8(((first_word >> 13) & 0x1_u16) as u8)?, + control_flag: ControlFlag::from_u8(((first_word >> 12) & 0x1_u16) as u8)?, + scid: first_word & 0x3ff_u16, + vcid: ((second_word >> 10) & 0x3f_u16) as u8, + sequence_number: buffer.read_u8()?, + }; + + let mut payload = vec![0_u8; payload_len as usize]; + + buffer.read_exact(&mut payload)?; + + Self::new(header, payload) + } +} + +#[cfg(test)] +mod test { + + use super::*; + + use rstest::rstest; + + #[rstest] + #[case(0, 5, 2)] + #[should_panic] + // tfvn out of bounds + #[case(4, 5, 33)] + #[should_panic] + // scid out of bounds + #[case(0, 1024, 62)] + #[should_panic] + // vcid out of bounds + #[case(0, 7, 65)] + fn header_validation(#[case] tfvn: u8, #[case] scid: u16, #[case] vcid: u8) { + let header = TCPrimaryHeader { + tfvn, + bypass_flag: BypassFlag::TypeA, + control_flag: ControlFlag::TypeC, + scid, + vcid, + sequence_number: 23, + }; + + assert!(header.validate().is_ok()) + } + + #[rstest] + #[case(b"some bytes foo bar baz".to_vec())] + #[should_panic] + #[case(vec![0_u8; 2048])] + fn frame_roundtrip( + #[values(BypassFlag::TypeA, BypassFlag::TypeB)] bypass_flag: BypassFlag, + #[values(ControlFlag::TypeD, ControlFlag::TypeD)] control_flag: ControlFlag, + #[case] payload: Vec, + #[values(0, 33, 1023)] scid: u16, + #[values(0, 3, 7)] vcid: u8, + ) { + let expected = TCTransferFrame::new( + TCPrimaryHeader { + tfvn: 0, + bypass_flag, + control_flag, + scid, + vcid, + sequence_number: 23, + }, + payload, + ) + .unwrap(); + + let buffer = expected.clone().encode(); + + let recovered = TCTransferFrame::decode(&mut buffer.as_slice()) + .expect("Should be able to roundtrip TCTransferFrame"); + + assert_eq!(expected, recovered) + } + + #[test] + fn tc_compare_spacepy() { + // test data from https://github.com/Stefan-Korner/SpacePyLibrary/blob/master/UnitTest/testData.py + // TC_FRAME_02 + let mut input_bytes: &[u8] = &[ + 0x22, 0xF6, 0x00, 0x23, 0x00, 0x82, 0x00, 0x0F, 0x00, 0x1D, 0xFF, 0x00, 0x00, 0x00, + 0x00, 0x0F, 0x00, 0x1E, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x1F, 0xFF, 0x00, + 0x00, 0x00, 0x00, 0x0F, 0xAC, 0x8F, 0x00, 0x68, + ]; + + let expected = TCTransferFrame::new( + TCPrimaryHeader { + tfvn: 0, + bypass_flag: BypassFlag::TypeB, + control_flag: ControlFlag::TypeD, + scid: 758, + vcid: 0, + sequence_number: 0, + }, + vec![ + 0x82, 0x00, 0x0F, 0x00, 0x1D, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x1E, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x1F, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xAC, + 0x8F, 0x00, 0x68, + ], + ) + .expect("Unable to make test transfer frame."); + + let parsed_tc = + TCTransferFrame::decode(&mut input_bytes).expect("unable to parse input bytes."); + + assert_eq!(expected, parsed_tc) + } +} diff --git a/src/tctm/tm.rs b/src/tctm/tm.rs new file mode 100644 index 0000000..936eb7a --- /dev/null +++ b/src/tctm/tm.rs @@ -0,0 +1,838 @@ +//! Implementation of the Telemetry Frame (TM) as defined in CCSDS 132.0-B-3 + +use std::io::{Error, ErrorKind, Read}; + +use byteorder::{BigEndian, ReadBytesExt}; +#[cfg(feature = "crc")] +use crc::Crc; + +use crate::GroupingFlag; + +use crate::tctm::randomizer::{apply_randomization, Randomization}; + +/// Randomization Schemes for TM Transfer Frames as defined CCSDS in 131.0-B-5 +#[derive(Debug, Clone, Copy)] +pub enum TMRandomization { + /// No randomization is applied to the TM Frame + None, + /// 255 Bit repeating randomization with polynomial h(x) = x^8 + x^7 + x^5 + x^3 + 1 + Tm255, + /// 131071 Bit repeating randomization with polynomial h(x) = x^17 + x^14 + 1 + Tm131071, +} + +// apply randomization to bytestream + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// Flag to indicate if the associated field is present in a TM Tranfser Frame. +pub enum BooleanFieldFlag { + NotPresent = 0, + Present = 1, +} +impl BooleanFieldFlag { + pub fn from_u8(val: u8) -> Result { + match val { + 0 => Ok(Self::NotPresent), + 1 => Ok(Self::Present), + val => Err(Error::new( + ErrorKind::InvalidData, + format!("Invalid BooleanFieldFlag value {val:}. Can only be 1 bit."), + )), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// Flag to identify type type of data in the TM Transfer Frame Data Field. +pub enum SynchronizationFlag { + /// Indicates the presence of octet-synchronized and forward-ordered + /// Packets or Idle Data + Nominal = 0, + /// Indicates the presence of Virtual Channel Access Service Data Unit (VCA_SDU) + /// in the data field + VcaSdu = 1, +} +impl SynchronizationFlag { + pub fn from_u8(val: u8) -> Result { + match val { + 0 => Ok(Self::Nominal), + 1 => Ok(Self::VcaSdu), + val => Err(Error::new( + ErrorKind::InvalidData, + format!("Invalid SynchronizationFlag value {val:}. Can only be 1 bit."), + )), + } + } +} + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// When the [TMDataFieldStatus::synchronization_flag] is set to [SynchronizationFlag::VcaSdu] +/// this field is undefined. +pub enum SegmentLength { + /// This flag has no meaning when synchronization is set to [SynchronizationFlag::VcaSdu] + Undefined(u8), + /// Value is fixed to `0b11` to align with non-use of Source Packet Segments in previous standards. + /// when synchronization is set to [SynchronizationFlag::Nominal] + Unsegmented = 0b11, +} +impl SegmentLength { + pub fn from_u8(value: u8) -> Result { + match value { + val if val < 0b11 => Ok(Self::Undefined(val)), + 0b11 => Ok(Self::Unsegmented), + val => Err(Error::new( + ErrorKind::InvalidData, + format!("SegmentLength can must be less than 4, found {val}"), + )), + } + } + + pub fn into_u8(self) -> u8 { + match self { + SegmentLength::Undefined(val) => val & 0x3, + SegmentLength::Unsegmented => 0b11, + } + } +} + +#[repr(u16)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FirstHeaderPointer { + /// Index of the first header must be less than 2046 + ByteIndex(u16), + /// This packet contains only IDLE data + OnlyIdleData = 0b111_1111_1110, + /// This frame is entirely a continuation of a previous packet + /// No new packet header starts inside of this frame. + NoPacketStart = 0b111_1111_1111, +} +impl FirstHeaderPointer { + pub fn into_u16(self) -> u16 { + match self { + FirstHeaderPointer::ByteIndex(value) => value & 0x7ff, + FirstHeaderPointer::OnlyIdleData => 0b111_1111_1110, + FirstHeaderPointer::NoPacketStart => 0b111_1111_1111, + } + } + + pub fn from_u16(value: u16) -> Result { + match value { + val if val < 2046 => Ok(Self::ByteIndex(val)), + 0b111_1111_1110 => Ok(Self::OnlyIdleData), + 0b111_1111_1111 => Ok(Self::NoPacketStart), + val => Err(Error::new( + ErrorKind::InvalidData, + format!( + "Invalid FristHeaderPointer value. \ + Value must be less than 2048 but received {val}." + ), + )), + } + } + + pub fn validate(&self) -> Result<(), Error> { + match self { + Self::ByteIndex(index) => { + if index < &2046_u16 { + Ok(()) + } else { + Err(Error::new( + ErrorKind::InvalidData, + format!("First Header byte index must be less than 2046 found {index}"), + )) + } + } + Self::OnlyIdleData => Ok(()), + Self::NoPacketStart => Ok(()), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// Data Field status is a 16-bit value of meta-data flags +/// used to identify how to decode the Data Field +pub struct TMDataFieldStatus { + /// Indicated if a seconday header is attached to this TM Transfer Field + pub secondary_header_flag: BooleanFieldFlag, + + /// Flag state is used to discern the type of data in the TM Data Field. + pub synchronization_flag: SynchronizationFlag, + + /// as Defined in CCSDS 132.0-B-3: If the Synchronization Flag is set to ‘0’, + /// the Packet Order Flag is reserved for future use by the CCSDS and is set to ‘0’. + /// If the Synchronization Flag is set to [SynchronizationFlag::VcaSdu], the + /// use of the Packet Order Flag is undefined. + pub packet_order: bool, + + /// When [Self::synchronization_flag] is [SynchronizationFlag::Nominal] + /// this flag is set to a fixed value [GroupingFlag::Unsegm] to align with a previous standard version + /// which allowed for segemneted Packets. + /// + /// When [Self::synchronization_flag] is [SynchronizationFlag::VcaSdu] + /// this flag is undefined. + pub segment_length: GroupingFlag, + + /// Identifies the byte offset of the start of the next packet inside + /// the data field + pub first_header_pointer: FirstHeaderPointer, +} +impl TMDataFieldStatus { + /// Validate Data Field status values + /// + /// # Errors + /// + /// This function errors under the following circumstances + /// - [Self::first_header_pointer] is of type [FirstHeaderPointer::ByteIndex] with index > 2046 + pub fn validate(&self) -> Result<(), Error> { + self.first_header_pointer.validate() + } + + /// Encode into a byte stream + pub fn encode(self) -> Vec { + let Self { + secondary_header_flag, + synchronization_flag, + packet_order, + segment_length, + first_header_pointer, + } = self; + + let word = (secondary_header_flag as u16) << 15 + | (synchronization_flag as u16) << 14 + | (packet_order as u16) << 13 + | (segment_length as u16) << 11 + | first_header_pointer.into_u16(); + + word.to_be_bytes().to_vec() + } + + /// Decode the Status field from a byte stream + pub fn decode(buffer: &mut R) -> Result { + let first_word = buffer.read_u16::()?; + + Ok(Self { + secondary_header_flag: BooleanFieldFlag::from_u8((first_word >> 15) as u8 & 0x1_u8)?, + synchronization_flag: SynchronizationFlag::from_u8((first_word >> 14) as u8 & 0x1_u8)?, + packet_order: (first_word >> 13) & 0x1 == 1, + segment_length: GroupingFlag::from_2bits((first_word >> 11) as u8 & 0x3), + first_header_pointer: FirstHeaderPointer::from_u16(first_word & 0x7FF_u16)?, + }) + } +} + +/// Primary Header for a TM transfer Frame. +/// This header is meant to be with a [TMTransferFrame] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TMPrimaryHeader { + /// Transfer Frame Version number. + /// Currently fixed to '00' + /// Encoded in 2 bits + pub tfvn: u8, + + /// 10-bit unique identifier for the spacecraft + pub scid: u16, + + /// The identifier of the virtual channel to which this + /// packet belongs. 6-bits maximum. + pub vcid: u8, + + /// Flag to identify whether the Operational Control Field + /// is present or not in the TM Transfer Frame. + pub ocf_flag: BooleanFieldFlag, + + /// Sequence count (modulo 256) of frames in the master channel. + pub mc_frame_count: u8, + + /// Sequence coutn (modulo 256) of frames in the virtual channel. + pub vc_frame_count: u8, + + /// Metadata about the Transfer Frame Data Field used to decode underlying packets. + pub data_field_status: TMDataFieldStatus, +} +impl TMPrimaryHeader { + /// Validate header values which require bit masks will fit in the + /// desginate bit-depth + /// + /// # Errors + /// + /// This function errors under the following circumstances + /// - [Self::tfvn] > 3 + /// - [Self::scid] > 1023 + /// - [Self::vcid] > 7 + pub fn validate(&self) -> Result<(), Error> { + if self.tfvn > 3 { + return Err(Error::new( + ErrorKind::InvalidData, + format!( + "Transfer frame version number must be <=3 but found {}", + self.tfvn + ), + )); + } + + if self.scid > 1023 { + return Err(Error::new( + ErrorKind::InvalidData, + format!("Spacecraft ID must be <=1023 but found {}", self.scid), + )); + } + + if self.vcid > 7 { + return Err(Error::new( + ErrorKind::InvalidData, + format!("Virtual Channel ID must be <=7 but found {}", self.vcid), + )); + } + + self.data_field_status.validate()?; + + Ok(()) + } + + /// Encode self into a byte steam + pub fn encode(self) -> Vec { + let Self { + tfvn, + scid, + vcid, + ocf_flag, + mc_frame_count, + vc_frame_count, + data_field_status, + } = self; + + let first_word = { + (tfvn as u16 & 0x3_u16) << 14 + | (scid & 0x3ff_u16) << 4 + | (vcid as u16 & 0x7_u16) << 1 + | ocf_flag as u16 + }; + + let mut message = first_word.to_be_bytes().to_vec(); + + message.push(mc_frame_count); + message.push(vc_frame_count); + message.extend_from_slice(&data_field_status.encode()); + + message + } + + /// Decode from a byte steam + pub fn decode(buffer: &mut R) -> Result { + let first_word = buffer.read_u16::()?; + + Ok(Self { + tfvn: (first_word >> 14) as u8 & 0x3_u8, + scid: (first_word >> 4) & 0x3ff_u16, + vcid: (first_word >> 1) as u8 & 0x7_u8, + ocf_flag: BooleanFieldFlag::from_u8((first_word & 0x1_u16) as u8)?, + mc_frame_count: buffer.read_u8()?, + vc_frame_count: buffer.read_u8()?, + data_field_status: TMDataFieldStatus::decode(buffer)?, + }) + } +} + +/// A flexible Platform for the Secondary Header in a TM Transfer Frame. +/// This secondary header computes the length of the Secondary Header Payload +/// at en/de-coding time, as such it should only be used along with a [TMTransferFrame] +/// to ensure correctness. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TMSecondaryHeader { + /// The version number of the secondary header + /// CCSDS 132.0-B-3 recognizes only one version, which is + /// Version 1, the binary encoded Version Number of which is ‘00’. + pub tfvn: u8, + + /// The data Field of the secondary header contains mission specific + /// information. Maximum length is 63 bytes. + pub data_field: Vec, +} +impl TMSecondaryHeader { + /// Validate header values which require bit masks will fit in the + /// desginate bit-depth. + /// + /// Errors: + /// - if [Self::tfvn] > 3 + /// - if [Self::data_field] has length > 63 + pub fn validate(&self) -> Result<(), Error> { + if self.tfvn > 3 { + return Err(Error::new( + ErrorKind::InvalidData, + format!( + "Transfer frame version number must be <=3 but found {}", + self.tfvn + ), + )); + } + + if self.data_field.len() > 63 { + return Err(Error::new( + ErrorKind::InvalidData, + format!( + "Secondary Header data field must have length <=63. Found {}", + self.data_field.len() + ), + )); + } + + Ok(()) + } + + /// Encode to a byte steam + pub fn encode(self) -> Vec { + let Self { + tfvn, + mut data_field, + } = self; + + // the secondary header is 1 byte + // and the encoded length is total length -1 + // so we need to take len() +1 -1 or just len + let packet_len = (data_field.len()) as u8; + let mut message = vec![{ (tfvn & 0x3_u8) << 6 | packet_len }]; + message.append(&mut data_field); + message + } + + /// Decode from a byte steam + pub fn decode(buffer: &mut R) -> Result { + let first_byte = buffer.read_u8()?; + + // the length field is total length -1, add 1 to get the length of the header too + // then subtract again to get length of the data array + let data_len = (first_byte & 0x3f) as usize; + Ok(Self { + tfvn: (first_byte >> 6) & 0x3, + data_field: { + let mut tmp = vec![0; data_len]; + buffer.read_exact(&mut tmp)?; + tmp + }, + }) + } +} + +/// A Telemetry (TM) Transfer Frame used in telemetry downlink defined in CCSDS 132.0-B-3 +/// Operational Control Field and Frame Error Control Field are not automatically +/// decoded and are left in the data_field of this structure. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TMTransferFrame { + /// TM primary header meta-data + pub primary_header: TMPrimaryHeader, + + /// The Data Field of telemetry values. May have a [TMSecondaryHeader] + /// at the beginning. This is indicated by the presence of [TMDataFieldStatus::secondary_header_flag] + /// + /// The length of this field is fixed on a per physical channel basis. + /// As such it is impossible to decode one without apriori knowledge of the + /// length. + pub data_field: Vec, +} +impl TMTransferFrame { + fn _encode_helper(self) -> Vec { + let Self { + primary_header, + mut data_field, + } = self; + let mut message = primary_header.encode(); + message.append(&mut data_field); + message + } + /// Encode this packet into a byte stream + pub fn encode(self, randomization: TMRandomization) -> Vec { + match randomization { + TMRandomization::None => self._encode_helper(), + TMRandomization::Tm255 => { + apply_randomization(self._encode_helper().as_slice(), Randomization::Tm255) + } + TMRandomization::Tm131071 => { + apply_randomization(self._encode_helper().as_slice(), Randomization::Tm131071) + } + } + } + + fn _decode_helper( + mut buffer: R, + length: usize, + randomization: TMRandomization, + ) -> Result, Error> { + let byte_array = { + let mut tmp = vec![0_u8; length]; + buffer.read_exact(&mut tmp)?; + tmp + }; + + let buffer_vec = match randomization { + TMRandomization::None => byte_array, + TMRandomization::Tm255 => { + apply_randomization(byte_array.as_slice(), Randomization::Tm255) + } + TMRandomization::Tm131071 => { + apply_randomization(byte_array.as_slice(), Randomization::Tm131071) + } + }; + Ok(buffer_vec) + } + + /// Decode a Transfer Frame from a byte stream. + /// Frame lengths are fixed on per-mission physical channel basis. + /// As such it is impossible to decode one without apriori knowledge of the + /// length. + /// + /// The length parameter to this function is the length of the entire Frame: + /// - Primary Header [6-bytes] + /// - Secondary Header (<= 64 bytes, if present) + /// - Trailer (2, 4, or 6 bytes, if present) + pub fn decode( + buffer: R, + length: usize, + randomization: TMRandomization, + ) -> Result { + let buffer = Self::_decode_helper(buffer, length, randomization)?; + let mut buffer = buffer.as_slice(); + + Ok(Self { + primary_header: TMPrimaryHeader::decode(&mut buffer)?, + data_field: { + let mut tmp = vec![0_u8; length - 6]; + buffer.read_exact(&mut tmp)?; + tmp + }, + }) + } + + #[cfg(feature = "crc")] + #[cfg_attr(docsrs, doc(cfg(feature = "crc")))] + /// Encode the TM Tansfer Frame and append a CRC-16 value using the provied [Crc]. + pub fn encode_crc(self, crc: &Crc, randomization: TMRandomization) -> Vec { + let mut message = self._encode_helper(); + message.extend(crc.checksum(message.as_slice()).to_be_bytes()); + match randomization { + TMRandomization::None => message, + TMRandomization::Tm255 => apply_randomization(message.as_slice(), Randomization::Tm255), + TMRandomization::Tm131071 => { + apply_randomization(message.as_slice(), Randomization::Tm131071) + } + } + } + + #[cfg(feature = "crc")] + #[cfg_attr(docsrs, doc(cfg(feature = "crc")))] + /// Decode a CCSDS packet with an appended a CRC-16 value using the provied [Crc]. + /// This method assumes the length of the CRC should be **included** in the payload length of the Transfer Frame. + /// The crc is stripped from the byte stream and not included in the returned packet. + /// Error if the packet's CRC is not valid. + pub fn decode_crc( + buffer: &mut R, + length: usize, + randomization: TMRandomization, + crc: &Crc, + ) -> Result { + let buffer = Self::_decode_helper(buffer, length, randomization)?; + let mut buffer = buffer.as_slice(); + // subtract 2 to accound for the CRC + let mut msg_buffer = vec![0_u8; length - 2]; + buffer.read_exact(&mut msg_buffer)?; + + let attached_crc = buffer.read_u16::()?; + let computed_crc = crc.checksum(msg_buffer.as_slice()); + + if computed_crc != attached_crc { + return Err(Error::new( + ErrorKind::InvalidData, + format!( + "CRC failure on TM Transfer Frame. \ + Exepcted {attached_crc:#04X} Computer {computed_crc:#04X}" + ), + )); + } + Ok(Self { + primary_header: TMPrimaryHeader::decode(&mut buffer)?, + data_field: { + let mut tmp = vec![0_u8; length - 6]; + buffer.read_exact(&mut tmp)?; + tmp + }, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + use rstest::rstest; + + #[rstest] + #[case(0, 5, 2, 12)] + #[case(0, 1023, 7, 2045)] + #[should_panic] + // tfvn out of bounds + #[case(4, 5, 2, 2045)] + #[should_panic] + // scid out of bounds + #[case(0, 1024, 5, 1023)] + #[should_panic] + // vcid out of bounds + #[case(0, 7, 8, 1023)] + #[should_panic] + // first header index out of bounds + #[case(0, 100, 3, 2049)] + fn tm_header_validation( + #[case] tfvn: u8, + #[case] scid: u16, + #[case] vcid: u8, + #[case] index: u16, + ) { + let header = TMPrimaryHeader { + tfvn, + scid, + vcid, + ocf_flag: BooleanFieldFlag::Present, + mc_frame_count: 7, + vc_frame_count: 244, + data_field_status: TMDataFieldStatus { + secondary_header_flag: BooleanFieldFlag::NotPresent, + synchronization_flag: SynchronizationFlag::Nominal, + packet_order: false, + segment_length: GroupingFlag::Unsegm, + first_header_pointer: FirstHeaderPointer::ByteIndex(index), + }, + }; + + assert!(header.validate().is_ok()) + } + + #[rstest] + fn tm_primary_header( + #[values(0, 5, 1023)] scid: u16, + #[values(0, 3, 7)] vcid: u8, + #[values(BooleanFieldFlag::NotPresent, BooleanFieldFlag::Present)] + ocf_flag: BooleanFieldFlag, + #[values(BooleanFieldFlag::NotPresent, BooleanFieldFlag::Present)] + secondary_header_flag: BooleanFieldFlag, + #[values(SynchronizationFlag::Nominal, SynchronizationFlag::VcaSdu)] + synchronization_flag: SynchronizationFlag, + #[values(GroupingFlag::First, GroupingFlag::Unsegm)] segment_length: GroupingFlag, + #[values(0, 2045, 2046, 2047)] index: u16, + ) { + let expected = TMPrimaryHeader { + tfvn: 0b11, + scid, + vcid, + ocf_flag, + mc_frame_count: 77, + vc_frame_count: 128, + data_field_status: TMDataFieldStatus { + secondary_header_flag, + synchronization_flag, + packet_order: false, + segment_length, + first_header_pointer: FirstHeaderPointer::from_u16(index) + .expect("Bad index number"), + }, + }; + + let bytes = expected.encode(); + + let recovered = TMPrimaryHeader::decode(&mut bytes.as_slice()) + .expect("Unable to decode TMPrimaryHeader"); + + assert_eq!(expected, recovered) + } + + #[test] + fn tm_compare_spacepy() { + // test data from https://github.com/Stefan-Korner/SpacePyLibrary/blob/master/UnitTest/testData.py + // TM_FRAME_01 + let mut input_bytes: &[u8] = &[ + 0x2F, 0x61, 0x00, 0x00, 0x18, 0x00, 0x0C, 0xD2, 0xC0, 0x00, 0x00, 0x1A, 0x10, 0x03, + 0x19, 0x16, 0x92, 0x5E, 0x92, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x89, 0xE4, 0x07, 0xFF, 0xC0, + 0x00, 0x04, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xC9, 0x48, 0x01, 0x00, 0x00, 0x00, 0xBB, 0xD6, + ]; + + let expected = TMTransferFrame { + primary_header: TMPrimaryHeader { + tfvn: 0, + scid: 758, + vcid: 0, + ocf_flag: BooleanFieldFlag::Present, + mc_frame_count: 0, + vc_frame_count: 0, + data_field_status: TMDataFieldStatus { + secondary_header_flag: BooleanFieldFlag::NotPresent, + synchronization_flag: SynchronizationFlag::Nominal, + packet_order: false, + segment_length: GroupingFlag::Unsegm, + first_header_pointer: FirstHeaderPointer::ByteIndex(0), + }, + }, + data_field: vec![ + 0x0C, 0xD2, 0xC0, 0x00, 0x00, 0x1A, 0x10, 0x03, 0x19, 0x16, 0x92, 0x5E, 0x92, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x89, 0xE4, 0x07, 0xFF, 0xC0, 0x00, 0x04, 0x27, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC9, 0x48, 0x01, 0x00, 0x00, + 0x00, 0xBB, 0xD6, + ], + }; + + let parsed_tm = TMTransferFrame::decode(&mut input_bytes, 1115, TMRandomization::None) + .expect("Unable to parse TM Frame."); + + assert_eq!(expected, parsed_tm) + } +}