From c60ebc2a6e82ebd1c2f717a129ec5bdabf567535 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Thu, 8 Dec 2022 00:29:07 +0100 Subject: [PATCH 01/23] start baseline CUC Impl --- src/ecss.rs | 8 ++++ src/time.rs | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) diff --git a/src/ecss.rs b/src/ecss.rs index f06b4c7..47159d0 100644 --- a/src/ecss.rs +++ b/src/ecss.rs @@ -224,6 +224,14 @@ pub trait ToBeBytes { fn to_be_bytes(&self) -> Self::ByteArray; } +impl ToBeBytes for () { + type ByteArray = [u8; 0]; + + fn to_be_bytes(&self) -> Self::ByteArray { + [] + } +} + impl ToBeBytes for u8 { type ByteArray = [u8; 1]; diff --git a/src/time.rs b/src/time.rs index b26fe15..0710662 100644 --- a/src/time.rs +++ b/src/time.rs @@ -814,6 +814,125 @@ pub mod cds { } } +pub mod cuc { + use core::fmt::Debug; + use super::*; + + #[derive(Copy, Clone, PartialEq, Eq, Debug)] + #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + pub struct WidthCounterPair(u8, u32); + + /// This provider uses the CCSDS epoch. Furthermore the preamble field only has one byte, + /// which allows a time code representation through the year 2094. + /// + /// More specifically, only having a preamble field of one byte limits the width of the counter + /// type (generally seconds) to 4 bytes and the width of the fractions type to 3 bytes. + #[derive(Copy, Clone, PartialEq, Eq, Debug)] + #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + pub struct TimeProviderCcsdsEpoch { + pfield: u8, + counter: WidthCounterPair, + fractions: Option + } + + #[inline] + pub fn pfield_len(pfield: u8) -> usize { + if ((pfield >> 7) & 0b1) == 1 { + return 2; + } + return 1 + } + + impl TimeProviderCcsdsEpoch { + fn build_p_field(counter_width: u8, fractions_width: Option) -> u8 { + let mut pfield = (CcsdsTimeCodes::CucCcsdsEpoch as u8) << 4; + if counter_width < 1 || counter_width > 4 { + panic!("invalid counter width {} for cuc timestamp", counter_width); + } + pfield |= counter_width << 3; + if let Some(fractions_width) = fractions_width { + if fractions_width < 1 || fractions_width > 3 { + panic!("invalid fractions width {} for cuc timestamp", fractions_width); + } + pfield |= fractions_width; + } + pfield + } + + + pub fn len_packed(&self) -> usize { + // TODO: Implement + todo!() + } + } + + impl TimeProviderCcsdsEpoch { + pub fn new_default(counter: u32) -> Self { + Self::new(WidthCounterPair(4, counter), None) + } + + pub fn new(counter: WidthCounterPair, fractions: Option) -> Self { + let fractions_width = match fractions { + None => 0, + Some(fractions) => fractions.0 + }; + Self { + pfield: Self::build_p_field(counter.0.into(), fractions_width.into()), + counter, + fractions + } + } + } + + impl TimeReader for TimeProviderCcsdsEpoch { + fn from_bytes(_buf: &[u8]) -> Result where Self: Sized { + todo!() + } + } + + impl TimeWriter for TimeProviderCcsdsEpoch { + fn write_to_bytes(&self, bytes: &mut [u8]) -> Result { + // Cross check the sizes of the counters against byte widths in the ctor + if bytes.len() < self.len_packed() { + return Err(TimestampError::ByteConversionError(ByteConversionError::ToSliceTooSmall(SizeMissmatch { + found: bytes.len(), + expected: self.len_packed() + }))) + } + bytes[0] = self.pfield; + let mut current_idx: usize = 1; + match self.counter.0 { + 1 => { + bytes[1] = self.counter.1 as u8; + }, + 2 => { + bytes[1..3].copy_from_slice(&(self.counter.1 as u16).to_be_bytes()); + }, + 3 => { + bytes[1..4].copy_from_slice(&self.counter.1.to_be_bytes()[1..4]); + }, + 4 => { + bytes[1..5].copy_from_slice(&self.counter.1.to_be_bytes()); + }, + // Should never happen + _ => panic!("invalid counter width value") + } + current_idx += self.counter.0 as usize; + if let Some(fractions) = self.fractions { + match fractions.0 { + 1 => bytes[current_idx] = fractions.1 as u8, + 2 => bytes[current_idx..current_idx + 2].copy_from_slice(&(fractions.1 as u16).to_be_bytes()), + 3 => bytes[current_idx..current_idx + 3].copy_from_slice(&fractions.1.to_be_bytes()[1..4]), + // Should also never happen + _ => panic!("invalid fractions value") + } + current_idx += fractions.0 as usize; + } + Ok(current_idx) + } + } + +} /// Module to generate the ASCII timecodes specified in /// [CCSDS 301.0-B-4](https://public.ccsds.org/Pubs/301x0b4e1.pdf) section 3.5 . /// See [chrono::DateTime::format] for a usage example of the generated -- 2.34.1 From c4f8eee8daf354524d7ebdd273b006818fc0e6c8 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Thu, 8 Dec 2022 14:59:12 +0100 Subject: [PATCH 02/23] base line cuc impl done --- src/time.rs | 340 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 260 insertions(+), 80 deletions(-) diff --git a/src/time.rs b/src/time.rs index 923066d..894295e 100644 --- a/src/time.rs +++ b/src/time.rs @@ -7,15 +7,14 @@ use core::fmt::{Display, Formatter}; #[cfg(not(feature = "std"))] use num_traits::float::FloatCore; +use crate::time::cuc::CucError; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; #[cfg(feature = "std")] use std::error::Error; #[cfg(feature = "std")] use std::time::{SystemTime, SystemTimeError}; -use crate::time::cds::LengthOfDaySegment; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - pub const DAYS_CCSDS_TO_UNIX: i32 = -4383; pub const SECONDS_PER_DAY: u32 = 86400; @@ -51,36 +50,6 @@ pub fn ccsds_time_code_from_p_field(pfield: u8) -> Result { CcsdsTimeCodes::try_from(raw_bits).map_err(|_| raw_bits) } -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum CdsError { - /// CCSDS days value exceeds maximum allowed size or is negative - InvalidCcsdsDays(i64), - /// There are distinct constructors depending on the days field width detected in the preamble - /// field. This error will be returned if there is a missmatch. - InvalidCtorForDaysOfLenInPreamble(LengthOfDaySegment), -} - -impl Display for CdsError { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - match self { - CdsError::InvalidCcsdsDays(days) => { - write!(f, "invalid ccsds days {}", days) - } - CdsError::InvalidCtorForDaysOfLenInPreamble(length_of_day) => { - write!( - f, - "wrong constructor for length of day {:?} detected in preamble", - length_of_day - ) - } - } - } -} - -#[cfg(feature = "std")] -impl Error for CdsError {} - #[derive(Debug, PartialEq, Eq, Copy, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum TimestampError { @@ -88,13 +57,20 @@ pub enum TimestampError { /// value is the found raw value InvalidTimeCode(CcsdsTimeCodes, u8), ByteConversionError(ByteConversionError), - CdsError(CdsError), + CdsError(cds::CdsError), + CucError(cuc::CucError), CustomEpochNotSupported, } -impl From for TimestampError { - fn from(v: CdsError) -> Self { - TimestampError::CdsError(v) +impl From for TimestampError { + fn from(e: cds::CdsError) -> Self { + TimestampError::CdsError(e) + } +} + +impl From for TimestampError { + fn from(e: CucError) -> Self { + TimestampError::CucError(e) } } @@ -133,6 +109,9 @@ impl Display for TimestampError { TimestampError::CdsError(e) => { write!(f, "cds error {}", e) } + TimestampError::CucError(e) => { + write!(f, "cuc error {}", e) + } TimestampError::ByteConversionError(e) => { write!(f, "byte conversion error {}", e) } @@ -149,6 +128,7 @@ impl Error for TimestampError { match self { TimestampError::ByteConversionError(e) => Some(e), TimestampError::CdsError(e) => Some(e), + TimestampError::CucError(e) => Some(e), _ => None, } } @@ -270,6 +250,35 @@ pub mod cds { Reserved, } + #[derive(Debug, PartialEq, Eq, Copy, Clone)] + #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + pub enum CdsError { + /// CCSDS days value exceeds maximum allowed size or is negative + InvalidCcsdsDays(i64), + /// There are distinct constructors depending on the days field width detected in the preamble + /// field. This error will be returned if there is a missmatch. + InvalidCtorForDaysOfLenInPreamble(LengthOfDaySegment), + } + + impl Display for CdsError { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + match self { + CdsError::InvalidCcsdsDays(days) => { + write!(f, "invalid ccsds days {}", days) + } + CdsError::InvalidCtorForDaysOfLenInPreamble(length_of_day) => { + write!( + f, + "wrong constructor for length of day {:?} detected in preamble", + length_of_day + ) + } + } + } + } + + #[cfg(feature = "std")] + impl Error for CdsError {} pub fn length_of_day_segment_from_pfield(pfield: u8) -> LengthOfDaySegment { if (pfield >> 2) & 0b1 == 1 { return LengthOfDaySegment::Long24Bits; @@ -816,8 +825,41 @@ pub mod cds { } pub mod cuc { - use core::fmt::Debug; use super::*; + use core::fmt::Debug; + + const MIN_CUC_LEN: usize = 2; + + #[derive(Copy, Clone, PartialEq, Eq, Debug)] + #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + pub enum CucError { + InvalidCounterWidth(u8), + InvalidFractionWidth(u8), + InvalidCounter(u8, u64), + InvalidFractions(u8, u64), + } + + impl Display for CucError { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + match self { + CucError::InvalidCounterWidth(w) => { + write!(f, "invalid cuc counter byte width {}", w) + } + CucError::InvalidFractionWidth(w) => { + write!(f, "invalid cuc fractional part byte width {}", w) + } + CucError::InvalidCounter(w, c) => { + write!(f, "invalid cuc counter {} for width {}", c, w) + } + CucError::InvalidFractions(w, c) => { + write!(f, "invalid cuc fractional part {} for width {}", c, w) + } + } + } + } + + #[cfg(feature = "std")] + impl Error for CucError {} #[derive(Copy, Clone, PartialEq, Eq, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -833,7 +875,7 @@ pub mod cuc { pub struct TimeProviderCcsdsEpoch { pfield: u8, counter: WidthCounterPair, - fractions: Option + fractions: Option, } #[inline] @@ -841,53 +883,183 @@ pub mod cuc { if ((pfield >> 7) & 0b1) == 1 { return 2; } - return 1 + 1 } impl TimeProviderCcsdsEpoch { fn build_p_field(counter_width: u8, fractions_width: Option) -> u8 { let mut pfield = (CcsdsTimeCodes::CucCcsdsEpoch as u8) << 4; - if counter_width < 1 || counter_width > 4 { + if !(1..=4).contains(&counter_width) { + // Okay to panic here, this function is private and all input values should + // have been sanitized panic!("invalid counter width {} for cuc timestamp", counter_width); } - pfield |= counter_width << 3; + pfield |= (counter_width - 1) << 3; if let Some(fractions_width) = fractions_width { - if fractions_width < 1 || fractions_width > 3 { - panic!("invalid fractions width {} for cuc timestamp", fractions_width); + if !(1..=3).contains(&fractions_width) { + // Okay to panic here, this function is private and all input values should + // have been sanitized + panic!( + "invalid fractions width {} for cuc timestamp", + fractions_width + ); } pfield |= fractions_width; } pfield } - pub fn len_packed(&self) -> usize { - // TODO: Implement - todo!() + Self::len_packed_from_pfield(self.pfield) + } + + #[inline] + pub fn len_cntr_from_pfield(pfield: u8) -> u8 { + ((pfield >> 2) & 0b11) + 1 + } + + #[inline] + pub fn len_fractions_from_pfield(pfield: u8) -> u8 { + pfield & 0b11 + } + + /// This returns the length of the individual components of the CUC timestamp in addition + /// to the total size. + /// + /// This function will return a tuple where the first value is the byte width of the + /// counter, the second value is the byte width of the fractional part, and the third + /// components is the total size. + pub fn len_components_and_total_from_pfield(pfield: u8) -> (u8, u8, usize) { + let base_len: usize = 1; + let cntr_len = Self::len_cntr_from_pfield(pfield); + let fractions_len = Self::len_fractions_from_pfield(pfield); + ( + cntr_len, + fractions_len, + base_len + cntr_len as usize + fractions_len as usize, + ) + } + + pub fn len_packed_from_pfield(pfield: u8) -> usize { + let mut base_len: usize = 1; + base_len += Self::len_cntr_from_pfield(pfield) as usize; + base_len += Self::len_fractions_from_pfield(pfield) as usize; + base_len + } + + /// Verifies the raw width parameter and returns the actual length, which is the raw + /// value plus 1. + fn verify_counter_width(width: u8) -> Result<(), CucError> { + if width == 0 || width > 4 { + return Err(CucError::InvalidCounterWidth(width)); + } + Ok(()) + } + + fn verify_fractions_width(width: u8) -> Result<(), CucError> { + if width > 3 { + return Err(CucError::InvalidFractionWidth(width)); + } + Ok(()) } } impl TimeProviderCcsdsEpoch { pub fn new_default(counter: u32) -> Self { - Self::new(WidthCounterPair(4, counter), None) + // These values are definitely valid, so it is okay to unwrap here. + Self::new(WidthCounterPair(4, counter), None).unwrap() + } + pub fn new_u16_counter(counter: u16) -> Self { + // These values are definitely valid, so it is okay to unwrap here. + Self::new(WidthCounterPair(2, counter as u32), None).unwrap() } - pub fn new(counter: WidthCounterPair, fractions: Option) -> Self { - let fractions_width = match fractions { - None => 0, - Some(fractions) => fractions.0 - }; - Self { - pfield: Self::build_p_field(counter.0.into(), fractions_width.into()), - counter, - fractions + pub fn new( + counter: WidthCounterPair, + fractions: Option, + ) -> Result { + Self::verify_counter_width(counter.0)?; + if counter.1 > 2u32.pow(counter.0 as u32 * 8) - 1 { + return Err(CucError::InvalidCounter(counter.0, counter.1 as u64)); } + if let Some(fractions) = fractions { + Self::verify_fractions_width(fractions.0)?; + if fractions.1 > 2u32.pow((fractions.0 as u32) * 8) - 1 { + return Err(CucError::InvalidFractions(fractions.0, fractions.1 as u64)); + } + } + Ok(Self { + pfield: Self::build_p_field(counter.0, fractions.map(|v| v.0)), + counter, + fractions, + }) } } impl TimeReader for TimeProviderCcsdsEpoch { - fn from_bytes(_buf: &[u8]) -> Result where Self: Sized { - todo!() + fn from_bytes(buf: &[u8]) -> Result + where + Self: Sized, + { + if buf.len() < MIN_CUC_LEN { + return Err(TimestampError::ByteConversionError( + ByteConversionError::FromSliceTooSmall(SizeMissmatch { + expected: MIN_CUC_LEN, + found: buf.len(), + }), + )); + } + let (cntr_len, fractions_len, total_len) = + Self::len_components_and_total_from_pfield(buf[0]); + if buf.len() < total_len { + return Err(TimestampError::ByteConversionError( + ByteConversionError::FromSliceTooSmall(SizeMissmatch { + expected: total_len, + found: buf.len(), + }), + )); + } + let mut current_idx = 1; + let counter = + match cntr_len { + 1 => buf[current_idx] as u32, + 2 => u16::from_be_bytes(buf[current_idx..current_idx + 2].try_into().unwrap()) + as u32, + 3 => { + let mut tmp_buf: [u8; 4] = [0; 4]; + tmp_buf[1..4].copy_from_slice(&buf[current_idx..current_idx + 3]); + u32::from_be_bytes(tmp_buf) as u32 + } + 4 => u32::from_be_bytes(buf[current_idx..current_idx + 4].try_into().unwrap()) + as u32, + _ => panic!("unreachable match arm"), + }; + current_idx += cntr_len as usize; + let mut fractions = None; + if fractions_len > 0 { + match fractions_len { + 1 => fractions = Some(WidthCounterPair(fractions_len, buf[current_idx] as u32)), + 2 => { + fractions = Some(WidthCounterPair( + fractions_len, + u16::from_be_bytes( + buf[current_idx..current_idx + 2].try_into().unwrap(), + ) as u32, + )) + } + 3 => { + let mut tmp_buf: [u8; 4] = [0; 4]; + tmp_buf[1..4].copy_from_slice(&buf[current_idx..current_idx + 3]); + fractions = Some(WidthCounterPair( + fractions_len, + u32::from_be_bytes(tmp_buf) as u32, + )) + } + _ => panic!("unreachable match arm"), + } + } + let provider = Self::new(WidthCounterPair(cntr_len, counter), fractions)?; + Ok(provider) } } @@ -895,45 +1067,52 @@ pub mod cuc { fn write_to_bytes(&self, bytes: &mut [u8]) -> Result { // Cross check the sizes of the counters against byte widths in the ctor if bytes.len() < self.len_packed() { - return Err(TimestampError::ByteConversionError(ByteConversionError::ToSliceTooSmall(SizeMissmatch { - found: bytes.len(), - expected: self.len_packed() - }))) + return Err(TimestampError::ByteConversionError( + ByteConversionError::ToSliceTooSmall(SizeMissmatch { + found: bytes.len(), + expected: self.len_packed(), + }), + )); } bytes[0] = self.pfield; let mut current_idx: usize = 1; match self.counter.0 { 1 => { - bytes[1] = self.counter.1 as u8; - }, + bytes[current_idx] = self.counter.1 as u8; + } 2 => { - bytes[1..3].copy_from_slice(&(self.counter.1 as u16).to_be_bytes()); - }, + bytes[current_idx..current_idx + 2] + .copy_from_slice(&(self.counter.1 as u16).to_be_bytes()); + } 3 => { - bytes[1..4].copy_from_slice(&self.counter.1.to_be_bytes()[1..4]); - }, + bytes[current_idx..current_idx + 3] + .copy_from_slice(&self.counter.1.to_be_bytes()[1..4]); + } 4 => { - bytes[1..5].copy_from_slice(&self.counter.1.to_be_bytes()); - }, + bytes[current_idx..current_idx + 4] + .copy_from_slice(&self.counter.1.to_be_bytes()); + } // Should never happen - _ => panic!("invalid counter width value") + _ => panic!("invalid counter width value"), } current_idx += self.counter.0 as usize; if let Some(fractions) = self.fractions { match fractions.0 { 1 => bytes[current_idx] = fractions.1 as u8, - 2 => bytes[current_idx..current_idx + 2].copy_from_slice(&(fractions.1 as u16).to_be_bytes()), - 3 => bytes[current_idx..current_idx + 3].copy_from_slice(&fractions.1.to_be_bytes()[1..4]), + 2 => bytes[current_idx..current_idx + 2] + .copy_from_slice(&(fractions.1 as u16).to_be_bytes()), + 3 => bytes[current_idx..current_idx + 3] + .copy_from_slice(&fractions.1.to_be_bytes()[1..4]), // Should also never happen - _ => panic!("invalid fractions value") + _ => panic!("invalid fractions value"), } current_idx += fractions.0 as usize; } Ok(current_idx) } } - } + /// Module to generate the ASCII timecodes specified in /// [CCSDS 301.0-B-4](https://public.ccsds.org/Pubs/301x0b4e1.pdf) section 3.5 . /// See [chrono::DateTime::format] for a usage example of the generated @@ -1006,7 +1185,7 @@ pub mod ascii { mod tests { use super::cds::TimeProvider; use super::*; - use crate::time::cds::{DaysLen16Bits, DaysLen24Bits, SubmillisPrecision}; + use crate::time::cds::{DaysLen16Bits, DaysLen24Bits, LengthOfDaySegment, SubmillisPrecision}; use crate::time::TimestampError::{ByteConversionError, InvalidTimeCode}; use crate::ByteConversionError::{FromSliceTooSmall, ToSliceTooSmall}; use alloc::format; @@ -1163,8 +1342,9 @@ mod tests { let faulty_ctor = TimeProvider::::from_bytes(&buf); assert!(faulty_ctor.is_err()); let error = faulty_ctor.unwrap_err(); - if let TimestampError::CdsError(CdsError::InvalidCtorForDaysOfLenInPreamble(len_of_day)) = - error + if let TimestampError::CdsError(cds::CdsError::InvalidCtorForDaysOfLenInPreamble( + len_of_day, + )) = error { assert_eq!(len_of_day, LengthOfDaySegment::Long24Bits); } else { -- 2.34.1 From 4d643946373a86b45e71d6cda0d535e29005f9ba Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Thu, 8 Dec 2022 15:22:19 +0100 Subject: [PATCH 03/23] split up large source file --- src/time.rs | 1596 --------------------------------------------- src/time/ascii.rs | 128 ++++ src/time/cds.rs | 956 +++++++++++++++++++++++++++ src/time/cuc.rs | 301 +++++++++ src/time/mod.rs | 221 +++++++ 5 files changed, 1606 insertions(+), 1596 deletions(-) delete mode 100644 src/time.rs create mode 100644 src/time/ascii.rs create mode 100644 src/time/cds.rs create mode 100644 src/time/cuc.rs create mode 100644 src/time/mod.rs diff --git a/src/time.rs b/src/time.rs deleted file mode 100644 index 894295e..0000000 --- a/src/time.rs +++ /dev/null @@ -1,1596 +0,0 @@ -//! CCSDS Time Code Formats according to [CCSDS 301.0-B-4](https://public.ccsds.org/Pubs/301x0b4e1.pdf) -use crate::{ByteConversionError, SizeMissmatch}; -use chrono::{DateTime, LocalResult, TimeZone, Utc}; -use core::fmt::{Display, Formatter}; - -#[allow(unused_imports)] -#[cfg(not(feature = "std"))] -use num_traits::float::FloatCore; - -use crate::time::cuc::CucError; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; -#[cfg(feature = "std")] -use std::error::Error; -#[cfg(feature = "std")] -use std::time::{SystemTime, SystemTimeError}; - -pub const DAYS_CCSDS_TO_UNIX: i32 = -4383; -pub const SECONDS_PER_DAY: u32 = 86400; - -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum CcsdsTimeCodes { - CucCcsdsEpoch = 0b001, - CucAgencyEpoch = 0b010, - Cds = 0b100, - Ccs = 0b101, - AgencyDefined = 0b110, -} - -impl TryFrom for CcsdsTimeCodes { - type Error = (); - - fn try_from(value: u8) -> Result { - match value { - x if x == CcsdsTimeCodes::CucCcsdsEpoch as u8 => Ok(CcsdsTimeCodes::CucCcsdsEpoch), - x if x == CcsdsTimeCodes::CucAgencyEpoch as u8 => Ok(CcsdsTimeCodes::CucAgencyEpoch), - x if x == CcsdsTimeCodes::Cds as u8 => Ok(CcsdsTimeCodes::Cds), - x if x == CcsdsTimeCodes::Ccs as u8 => Ok(CcsdsTimeCodes::Ccs), - x if x == CcsdsTimeCodes::AgencyDefined as u8 => Ok(CcsdsTimeCodes::AgencyDefined), - _ => Err(()), - } - } -} - -/// Retrieve the CCSDS time code from the p-field. If no valid time code identifier is found, the -/// value of the raw time code identification field is returned. -pub fn ccsds_time_code_from_p_field(pfield: u8) -> Result { - let raw_bits = (pfield >> 4) & 0b111; - CcsdsTimeCodes::try_from(raw_bits).map_err(|_| raw_bits) -} - -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum TimestampError { - /// Contains tuple where first value is the expected time code and the second - /// value is the found raw value - InvalidTimeCode(CcsdsTimeCodes, u8), - ByteConversionError(ByteConversionError), - CdsError(cds::CdsError), - CucError(cuc::CucError), - CustomEpochNotSupported, -} - -impl From for TimestampError { - fn from(e: cds::CdsError) -> Self { - TimestampError::CdsError(e) - } -} - -impl From for TimestampError { - fn from(e: CucError) -> Self { - TimestampError::CucError(e) - } -} - -#[cfg(feature = "std")] -#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] -#[derive(Debug, Clone)] -pub enum StdTimestampError { - SystemTimeError(SystemTimeError), - TimestampError(TimestampError), -} - -#[cfg(feature = "std")] -impl From for StdTimestampError { - fn from(v: TimestampError) -> Self { - Self::TimestampError(v) - } -} - -#[cfg(feature = "std")] -impl From for StdTimestampError { - fn from(v: SystemTimeError) -> Self { - Self::SystemTimeError(v) - } -} - -impl Display for TimestampError { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - match self { - TimestampError::InvalidTimeCode(time_code, raw_val) => { - write!( - f, - "invalid raw time code value {} for time code {:?}", - raw_val, time_code - ) - } - TimestampError::CdsError(e) => { - write!(f, "cds error {}", e) - } - TimestampError::CucError(e) => { - write!(f, "cuc error {}", e) - } - TimestampError::ByteConversionError(e) => { - write!(f, "byte conversion error {}", e) - } - TimestampError::CustomEpochNotSupported => { - write!(f, "custom epochs are not supported") - } - } - } -} - -#[cfg(feature = "std")] -impl Error for TimestampError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - match self { - TimestampError::ByteConversionError(e) => Some(e), - TimestampError::CdsError(e) => Some(e), - TimestampError::CucError(e) => Some(e), - _ => None, - } - } -} - -#[cfg(feature = "std")] -#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] -pub fn seconds_since_epoch() -> f64 { - SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("System time generation failed") - .as_secs_f64() -} - -/// Convert UNIX days to CCSDS days -/// -/// - CCSDS epoch: 1958 January 1 -/// - UNIX Epoch: 1970 January 1 -pub const fn unix_to_ccsds_days(unix_days: i64) -> i64 { - unix_days - DAYS_CCSDS_TO_UNIX as i64 -} - -/// Convert CCSDS days to UNIX days -/// -/// - CCSDS epoch: 1958 January 1 -/// - UNIX Epoch: 1970 January 1 -pub const fn ccsds_to_unix_days(ccsds_days: i64) -> i64 { - ccsds_days + DAYS_CCSDS_TO_UNIX as i64 -} - -#[cfg(feature = "std")] -#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] -pub fn ms_of_day_using_sysclock() -> u32 { - ms_of_day(seconds_since_epoch()) -} - -pub fn ms_of_day(seconds_since_epoch: f64) -> u32 { - let fraction_ms = seconds_since_epoch - seconds_since_epoch.floor(); - let ms_of_day: u32 = (((seconds_since_epoch.floor() as u32 % SECONDS_PER_DAY) * 1000) as f64 - + fraction_ms) - .floor() as u32; - ms_of_day -} - -pub trait TimeWriter { - /// Generic function to convert write a timestamp into a raw buffer. - /// Returns the number of written bytes on success. - fn write_to_bytes(&self, bytes: &mut [u8]) -> Result; -} - -pub trait TimeReader { - fn from_bytes(buf: &[u8]) -> Result - where - Self: Sized; -} - -/// Trait for generic CCSDS time providers. -pub trait CcsdsTimeProvider { - fn len_as_bytes(&self) -> usize; - - /// Returns the pfield of the time provider. The pfield can have one or two bytes depending - /// on the extension bit (first bit). The time provider should returns a tuple where the first - /// entry denotes the length of the pfield and the second entry is the value of the pfield - /// in big endian format. - fn p_field(&self) -> (usize, [u8; 2]); - fn ccdsd_time_code(&self) -> CcsdsTimeCodes; - fn unix_seconds(&self) -> i64; - fn date_time(&self) -> Option>; -} - -/// Module to generate or read CDS timestamps as specified in -/// [CCSDS 301.0-B-4](https://public.ccsds.org/Pubs/301x0b4e1.pdf) section 3.3 . -/// -/// The core data structure to do this is the [cds::TimeProvider] struct. -pub mod cds { - use super::*; - use crate::private::Sealed; - use core::fmt::Debug; - - const CDS_SHORT_P_FIELD: u8 = (CcsdsTimeCodes::Cds as u8) << 4; - pub const MIN_CDS_FIELD_LEN: usize = 7; - - /// Generic trait implemented by token structs to specify the length of day field at type - /// level. This trait is only meant to be implemented in this crate and therefore sealed. - pub trait ProvidesDaysLength: Sealed { - type FieldType: Copy + Clone + TryFrom; - } - - /// Type level token to be used as a generic parameter to [TimeProvider]. - #[derive(Debug, PartialEq, Eq, Default)] - pub struct DaysLen16Bits {} - - impl Sealed for DaysLen16Bits {} - impl ProvidesDaysLength for DaysLen16Bits { - type FieldType = u16; - } - - /// Type level token to be used as a generic parameter to [TimeProvider]. - #[derive(Debug, PartialEq, Eq, Default)] - pub struct DaysLen24Bits {} - impl Sealed for DaysLen24Bits {} - impl ProvidesDaysLength for DaysLen24Bits { - type FieldType = u32; - } - - #[derive(Debug, PartialEq, Eq, Copy, Clone)] - #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] - pub enum LengthOfDaySegment { - Short16Bits = 0, - Long24Bits = 1, - } - - #[derive(Debug, Copy, Clone, PartialEq, Eq)] - #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] - pub enum SubmillisPrecision { - Absent, - Microseconds(u16), - Picoseconds(u32), - Reserved, - } - - #[derive(Debug, PartialEq, Eq, Copy, Clone)] - #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] - pub enum CdsError { - /// CCSDS days value exceeds maximum allowed size or is negative - InvalidCcsdsDays(i64), - /// There are distinct constructors depending on the days field width detected in the preamble - /// field. This error will be returned if there is a missmatch. - InvalidCtorForDaysOfLenInPreamble(LengthOfDaySegment), - } - - impl Display for CdsError { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - match self { - CdsError::InvalidCcsdsDays(days) => { - write!(f, "invalid ccsds days {}", days) - } - CdsError::InvalidCtorForDaysOfLenInPreamble(length_of_day) => { - write!( - f, - "wrong constructor for length of day {:?} detected in preamble", - length_of_day - ) - } - } - } - } - - #[cfg(feature = "std")] - impl Error for CdsError {} - pub fn length_of_day_segment_from_pfield(pfield: u8) -> LengthOfDaySegment { - if (pfield >> 2) & 0b1 == 1 { - return LengthOfDaySegment::Long24Bits; - } - LengthOfDaySegment::Short16Bits - } - pub fn precision_from_pfield(pfield: u8) -> SubmillisPrecision { - match pfield & 0b11 { - 0b01 => SubmillisPrecision::Microseconds(0), - 0b10 => SubmillisPrecision::Picoseconds(0), - 0b00 => SubmillisPrecision::Absent, - 0b11 => SubmillisPrecision::Reserved, - _ => panic!("pfield to SubmillisPrecision failed"), - } - } - - /// This object is the abstraction for the CCSDS Day Segmented Time Code (CDS). - /// - /// It has the capability to generate and read timestamps as specified in the CCSDS 301.0-B-4 - /// section 3.3 . The width of the days field is configured at compile time via the generic - /// [ProvidesDaysLength] trait which is implemented by [DaysLen16Bits] and [DaysLen24Bits]. - /// - /// Custom epochs are not supported yet. - /// Furthermore, the preamble field (p-field) is explicitly conveyed. - /// That means it will always be present when writing the time stamp to a raw buffer, and it - /// must be present when reading a CDS timestamp from a raw buffer. - /// - /// # Example - /// - /// ``` - /// use spacepackets::time::cds::{TimeProvider, DaysLen16Bits}; - /// use spacepackets::time::{TimeWriter, CcsdsTimeCodes, TimeReader, CcsdsTimeProvider}; - /// - /// let timestamp_now = TimeProvider::from_now_with_u16_days().unwrap(); - /// let mut raw_stamp = [0; 7]; - /// { - /// let written = timestamp_now.write_to_bytes(&mut raw_stamp).unwrap(); - /// assert_eq!((raw_stamp[0] >> 4) & 0b111, CcsdsTimeCodes::Cds as u8); - /// assert_eq!(written, 7); - /// } - /// { - /// let read_result = TimeProvider::::from_bytes(&raw_stamp); - /// assert!(read_result.is_ok()); - /// let stamp_deserialized = read_result.unwrap(); - /// assert_eq!(stamp_deserialized.len_as_bytes(), 7); - /// } - /// ``` - #[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] - #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] - pub struct TimeProvider { - pfield: u8, - ccsds_days: DaysLen::FieldType, - ms_of_day: u32, - submillis_precision: Option, - unix_seconds: i64, - } - - #[cfg(feature = "std")] - struct ConversionFromNow { - ccsds_days: i32, - ms_of_day: u64, - unix_days_seconds: u64, - submillis_prec: Option, - } - - #[cfg(feature = "std")] - impl ConversionFromNow { - fn new() -> Result { - Self::new_generic(None) - } - - fn new_with_submillis_us_prec() -> Result { - Self::new_generic(Some(SubmillisPrecision::Microseconds(0))) - } - - fn new_with_submillis_ps_prec() -> Result { - Self::new_generic(Some(SubmillisPrecision::Picoseconds(0))) - } - - fn new_generic(mut prec: Option) -> Result { - let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; - let epoch = now.as_secs(); - let secs_of_day = epoch % SECONDS_PER_DAY as u64; - let unix_days_seconds = epoch - secs_of_day; - if let Some(submilli_prec) = prec { - match submilli_prec { - SubmillisPrecision::Microseconds(_) => { - prec = Some(SubmillisPrecision::Microseconds( - (now.subsec_micros() % 1000) as u16, - )); - } - SubmillisPrecision::Picoseconds(_) => { - prec = Some(SubmillisPrecision::Microseconds( - (now.subsec_nanos() * 1000) as u16, - )); - } - _ => (), - } - } - Ok(Self { - ms_of_day: secs_of_day * 1000 + now.subsec_millis() as u64, - ccsds_days: unix_to_ccsds_days((unix_days_seconds / SECONDS_PER_DAY as u64) as i64) - as i32, - unix_days_seconds, - submillis_prec: prec, - }) - } - } - - impl TimeProvider { - pub fn set_submillis_precision(&mut self, prec: SubmillisPrecision) { - self.pfield &= !(0b11); - if let SubmillisPrecision::Absent = prec { - self.submillis_precision = None; - return; - } - self.submillis_precision = Some(prec); - match prec { - SubmillisPrecision::Microseconds(_) => { - self.pfield |= 0b01; - } - SubmillisPrecision::Picoseconds(_) => { - self.pfield |= 0b10; - } - _ => (), - } - } - - pub fn clear_submillis_precision(&mut self) { - self.pfield &= !(0b11); - self.submillis_precision = None; - } - - pub fn ccsds_days(&self) -> ProvidesDaysLen::FieldType { - self.ccsds_days - } - - pub fn submillis_precision(&self) -> Option { - self.submillis_precision - } - - pub fn ms_of_day(&self) -> u32 { - self.ms_of_day - } - - fn generic_raw_read_checks( - buf: &[u8], - days_len: LengthOfDaySegment, - ) -> Result { - if buf.len() < MIN_CDS_FIELD_LEN { - return Err(TimestampError::ByteConversionError( - ByteConversionError::FromSliceTooSmall(SizeMissmatch { - expected: MIN_CDS_FIELD_LEN, - found: buf.len(), - }), - )); - } - let pfield = buf[0]; - match CcsdsTimeCodes::try_from(pfield >> 4 & 0b111) { - Ok(cds_type) => match cds_type { - CcsdsTimeCodes::Cds => (), - _ => { - return Err(TimestampError::InvalidTimeCode( - CcsdsTimeCodes::Cds, - cds_type as u8, - )) - } - }, - _ => { - return Err(TimestampError::InvalidTimeCode( - CcsdsTimeCodes::Cds, - pfield >> 4 & 0b111, - )) - } - }; - if ((pfield >> 3) & 0b1) == 1 { - return Err(TimestampError::CustomEpochNotSupported); - } - let days_len_from_pfield = length_of_day_segment_from_pfield(pfield); - if days_len_from_pfield != days_len { - return Err( - CdsError::InvalidCtorForDaysOfLenInPreamble(days_len_from_pfield).into(), - ); - } - let stamp_len = Self::calc_stamp_len(pfield); - if buf.len() < stamp_len { - return Err(TimestampError::ByteConversionError( - ByteConversionError::FromSliceTooSmall(SizeMissmatch { - expected: stamp_len, - found: buf.len(), - }), - )); - } - Ok(precision_from_pfield(pfield)) - } - - fn calc_stamp_len(pfield: u8) -> usize { - let mut init_len = 7; - if length_of_day_segment_from_pfield(pfield) == LengthOfDaySegment::Long24Bits { - init_len += 1 - } - match pfield & 0b11 { - 0b01 => { - init_len += 2; - } - 0b10 => { - init_len += 4; - } - _ => (), - } - init_len - } - - fn setup(&mut self, unix_days_seconds: i64, ms_of_day: u64) { - self.calc_unix_seconds(unix_days_seconds, ms_of_day); - } - - fn calc_unix_seconds(&mut self, unix_days_seconds: i64, ms_of_day: u64) { - self.unix_seconds = unix_days_seconds; - let seconds_of_day = (ms_of_day / 1000) as i64; - if self.unix_seconds < 0 { - self.unix_seconds -= seconds_of_day; - } else { - self.unix_seconds += seconds_of_day; - } - } - - fn calc_date_time(&self, ms_since_last_second: u32) -> Option> { - assert!(ms_since_last_second < 1000, "Invalid MS since last second"); - let ns_since_last_sec = ms_since_last_second * 1e6 as u32; - if let LocalResult::Single(val) = - Utc.timestamp_opt(self.unix_seconds, ns_since_last_sec) - { - return Some(val); - } - None - } - - fn length_check(&self, buf: &[u8], len_as_bytes: usize) -> Result<(), TimestampError> { - if buf.len() < len_as_bytes { - return Err(TimestampError::ByteConversionError( - ByteConversionError::ToSliceTooSmall(SizeMissmatch { - expected: len_as_bytes, - found: buf.len(), - }), - )); - } - Ok(()) - } - - fn generic_new( - days_len: LengthOfDaySegment, - ccsds_days: ProvidesDaysLen::FieldType, - ms_of_day: u32, - ) -> Result - where - i64: From, - { - let mut provider = Self { - pfield: Self::generate_p_field(days_len, None), - ccsds_days, - ms_of_day, - unix_seconds: 0, - submillis_precision: None, - }; - let unix_days_seconds = - ccsds_to_unix_days(ccsds_days.into()) as i64 * SECONDS_PER_DAY as i64; - provider.setup(unix_days_seconds as i64, ms_of_day.into()); - Ok(provider) - } - - #[cfg(feature = "std")] - fn generic_from_now( - days_len: LengthOfDaySegment, - conversion_from_now: ConversionFromNow, - ) -> Result - where - >::Error: Debug, - { - let ccsds_days: ProvidesDaysLen::FieldType = - conversion_from_now.ccsds_days.try_into().map_err(|_| { - StdTimestampError::TimestampError( - CdsError::InvalidCcsdsDays(conversion_from_now.ccsds_days.into()).into(), - ) - })?; - let mut provider = Self { - pfield: Self::generate_p_field(days_len, conversion_from_now.submillis_prec), - ccsds_days, - ms_of_day: conversion_from_now.ms_of_day as u32, - unix_seconds: 0, - submillis_precision: conversion_from_now.submillis_prec, - }; - provider.setup( - conversion_from_now.unix_days_seconds as i64, - conversion_from_now.ms_of_day, - ); - Ok(provider) - } - - #[cfg(feature = "std")] - fn generic_conversion_from_now(&self) -> Result { - Ok(match self.submillis_precision { - None => ConversionFromNow::new()?, - Some(prec) => match prec { - SubmillisPrecision::Microseconds(_) => { - ConversionFromNow::new_with_submillis_us_prec()? - } - SubmillisPrecision::Picoseconds(_) => { - ConversionFromNow::new_with_submillis_ps_prec()? - } - _ => ConversionFromNow::new()?, - }, - }) - } - - fn generate_p_field( - day_seg_len: LengthOfDaySegment, - submillis_prec: Option, - ) -> u8 { - let mut pfield = CDS_SHORT_P_FIELD | ((day_seg_len as u8) << 2); - if let Some(submillis_prec) = submillis_prec { - match submillis_prec { - SubmillisPrecision::Microseconds(_) => pfield |= 0b01, - SubmillisPrecision::Picoseconds(_) => pfield |= 0b10, - SubmillisPrecision::Reserved => pfield |= 0b11, - _ => (), - } - } - pfield - } - - #[cfg(feature = "std")] - #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] - pub fn update_from_now(&mut self) -> Result<(), StdTimestampError> - where - >::Error: Debug, - { - let conversion_from_now = self.generic_conversion_from_now()?; - let ccsds_days: ProvidesDaysLen::FieldType = - conversion_from_now.ccsds_days.try_into().map_err(|_| { - StdTimestampError::TimestampError( - CdsError::InvalidCcsdsDays(conversion_from_now.ccsds_days as i64).into(), - ) - })?; - self.ccsds_days = ccsds_days; - self.ms_of_day = conversion_from_now.ms_of_day as u32; - self.setup( - conversion_from_now.unix_days_seconds as i64, - conversion_from_now.ms_of_day, - ); - Ok(()) - } - } - - impl TimeProvider { - /// Generate a new timestamp provider with the days field width set to 24 bits - pub fn new_with_u24_days(ccsds_days: u32, ms_of_day: u32) -> Result { - if ccsds_days > 2_u32.pow(24) { - return Err(CdsError::InvalidCcsdsDays(ccsds_days.into())); - } - Self::generic_new(LengthOfDaySegment::Long24Bits, ccsds_days, ms_of_day) - } - - /// Generate a time stamp from the current time using the system clock. - #[cfg(feature = "std")] - #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] - pub fn from_now_with_u24_days() -> Result { - let conversion_from_now = ConversionFromNow::new()?; - Self::generic_from_now(LengthOfDaySegment::Long24Bits, conversion_from_now) - } - - /// Like [Self::from_now_with_u24_days] but with microsecond sub-millisecond precision. - #[cfg(feature = "std")] - #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] - pub fn from_now_with_u24_days_and_us_prec() -> Result { - let conversion_from_now = ConversionFromNow::new_with_submillis_us_prec()?; - Self::generic_from_now(LengthOfDaySegment::Long24Bits, conversion_from_now) - } - - /// Like [Self::from_now_with_u24_days] but with picoseconds sub-millisecond precision. - #[cfg(feature = "std")] - #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] - pub fn from_now_with_u24_days_ps_submillis_prec() -> Result { - let conversion_from_now = ConversionFromNow::new_with_submillis_ps_prec()?; - Self::generic_from_now(LengthOfDaySegment::Long24Bits, conversion_from_now) - } - - fn from_bytes_24_bit_days(buf: &[u8]) -> Result { - let submillis_precision = - Self::generic_raw_read_checks(buf, LengthOfDaySegment::Long24Bits)?; - let mut temp_buf: [u8; 4] = [0; 4]; - temp_buf[1..4].copy_from_slice(&buf[1..4]); - let cccsds_days: u32 = u32::from_be_bytes(temp_buf); - let ms_of_day: u32 = u32::from_be_bytes(buf[4..8].try_into().unwrap()); - let mut provider = Self::new_with_u24_days(cccsds_days, ms_of_day)?; - match submillis_precision { - SubmillisPrecision::Microseconds(_) => { - provider.set_submillis_precision(SubmillisPrecision::Microseconds( - u16::from_be_bytes(buf[8..10].try_into().unwrap()), - )) - } - SubmillisPrecision::Picoseconds(_) => { - provider.set_submillis_precision(SubmillisPrecision::Picoseconds( - u32::from_be_bytes(buf[8..12].try_into().unwrap()), - )) - } - _ => (), - } - Ok(provider) - } - } - - impl TimeProvider { - /// Generate a new timestamp provider with the days field width set to 16 bits - pub fn new_with_u16_days(ccsds_days: u16, ms_of_day: u32) -> Self { - // This should never fail, type system ensures CCSDS can not be negative or too large - Self::generic_new(LengthOfDaySegment::Short16Bits, ccsds_days, ms_of_day).unwrap() - } - - /// Generate a time stamp from the current time using the system clock. - #[cfg(feature = "std")] - #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] - pub fn from_now_with_u16_days() -> Result { - let conversion_from_now = ConversionFromNow::new()?; - Self::generic_from_now(LengthOfDaySegment::Short16Bits, conversion_from_now) - } - - /// Like [Self::from_now_with_u16_days] but with microsecond sub-millisecond precision. - #[cfg(feature = "std")] - #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] - pub fn from_now_with_u16_days_and_us_prec() -> Result { - let conversion_from_now = ConversionFromNow::new_with_submillis_us_prec()?; - Self::generic_from_now(LengthOfDaySegment::Short16Bits, conversion_from_now) - } - - /// Like [Self::from_now_with_u16_days] but with picosecond sub-millisecond precision. - #[cfg(feature = "std")] - #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] - pub fn from_now_with_u16_days_and_ps_prec() -> Result { - let conversion_from_now = ConversionFromNow::new_with_submillis_ps_prec()?; - Self::generic_from_now(LengthOfDaySegment::Short16Bits, conversion_from_now) - } - - fn from_bytes_16_bit_days(buf: &[u8]) -> Result { - let submillis_precision = - Self::generic_raw_read_checks(buf, LengthOfDaySegment::Short16Bits)?; - let ccsds_days: u16 = u16::from_be_bytes(buf[1..3].try_into().unwrap()); - let ms_of_day: u32 = u32::from_be_bytes(buf[3..7].try_into().unwrap()); - let mut provider = Self::new_with_u16_days(ccsds_days, ms_of_day); - provider.pfield = buf[0]; - match submillis_precision { - SubmillisPrecision::Microseconds(_) => { - provider.set_submillis_precision(SubmillisPrecision::Microseconds( - u16::from_be_bytes(buf[7..9].try_into().unwrap()), - )) - } - SubmillisPrecision::Picoseconds(_) => { - provider.set_submillis_precision(SubmillisPrecision::Picoseconds( - u32::from_be_bytes(buf[7..11].try_into().unwrap()), - )) - } - _ => (), - } - Ok(provider) - } - } - - impl CcsdsTimeProvider for TimeProvider { - fn len_as_bytes(&self) -> usize { - Self::calc_stamp_len(self.pfield) - } - - fn p_field(&self) -> (usize, [u8; 2]) { - (1, [self.pfield, 0]) - } - - fn ccdsd_time_code(&self) -> CcsdsTimeCodes { - CcsdsTimeCodes::Cds - } - - fn unix_seconds(&self) -> i64 { - self.unix_seconds - } - - fn date_time(&self) -> Option> { - self.calc_date_time((self.ms_of_day % 1000) as u32) - } - } - - impl TimeReader for TimeProvider { - fn from_bytes(buf: &[u8]) -> Result { - Self::from_bytes_16_bit_days(buf) - } - } - - impl TimeReader for TimeProvider { - fn from_bytes(buf: &[u8]) -> Result { - Self::from_bytes_24_bit_days(buf) - } - } - - impl TimeWriter for TimeProvider { - fn write_to_bytes(&self, buf: &mut [u8]) -> Result { - self.length_check(buf, self.len_as_bytes())?; - buf[0] = self.pfield; - buf[1..3].copy_from_slice(self.ccsds_days.to_be_bytes().as_slice()); - buf[3..7].copy_from_slice(self.ms_of_day.to_be_bytes().as_slice()); - if let Some(submillis_prec) = self.submillis_precision { - match submillis_prec { - SubmillisPrecision::Microseconds(ms) => { - buf[7..9].copy_from_slice(ms.to_be_bytes().as_slice()); - } - SubmillisPrecision::Picoseconds(ps) => { - buf[7..11].copy_from_slice(ps.to_be_bytes().as_slice()); - } - _ => (), - } - } - Ok(self.len_as_bytes()) - } - } - - impl TimeWriter for TimeProvider { - fn write_to_bytes(&self, buf: &mut [u8]) -> Result { - self.length_check(buf, self.len_as_bytes())?; - buf[0] = self.pfield; - let be_days = self.ccsds_days.to_be_bytes(); - buf[1..4].copy_from_slice(&be_days[1..4]); - buf[4..8].copy_from_slice(self.ms_of_day.to_be_bytes().as_slice()); - if let Some(submillis_prec) = self.submillis_precision { - match submillis_prec { - SubmillisPrecision::Microseconds(ms) => { - buf[8..10].copy_from_slice(ms.to_be_bytes().as_slice()); - } - SubmillisPrecision::Picoseconds(ps) => { - buf[8..12].copy_from_slice(ps.to_be_bytes().as_slice()); - } - _ => (), - } - } - Ok(self.len_as_bytes()) - } - } -} - -pub mod cuc { - use super::*; - use core::fmt::Debug; - - const MIN_CUC_LEN: usize = 2; - - #[derive(Copy, Clone, PartialEq, Eq, Debug)] - #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] - pub enum CucError { - InvalidCounterWidth(u8), - InvalidFractionWidth(u8), - InvalidCounter(u8, u64), - InvalidFractions(u8, u64), - } - - impl Display for CucError { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - match self { - CucError::InvalidCounterWidth(w) => { - write!(f, "invalid cuc counter byte width {}", w) - } - CucError::InvalidFractionWidth(w) => { - write!(f, "invalid cuc fractional part byte width {}", w) - } - CucError::InvalidCounter(w, c) => { - write!(f, "invalid cuc counter {} for width {}", c, w) - } - CucError::InvalidFractions(w, c) => { - write!(f, "invalid cuc fractional part {} for width {}", c, w) - } - } - } - } - - #[cfg(feature = "std")] - impl Error for CucError {} - - #[derive(Copy, Clone, PartialEq, Eq, Debug)] - #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] - pub struct WidthCounterPair(u8, u32); - - /// This provider uses the CCSDS epoch. Furthermore the preamble field only has one byte, - /// which allows a time code representation through the year 2094. - /// - /// More specifically, only having a preamble field of one byte limits the width of the counter - /// type (generally seconds) to 4 bytes and the width of the fractions type to 3 bytes. - #[derive(Copy, Clone, PartialEq, Eq, Debug)] - #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] - pub struct TimeProviderCcsdsEpoch { - pfield: u8, - counter: WidthCounterPair, - fractions: Option, - } - - #[inline] - pub fn pfield_len(pfield: u8) -> usize { - if ((pfield >> 7) & 0b1) == 1 { - return 2; - } - 1 - } - - impl TimeProviderCcsdsEpoch { - fn build_p_field(counter_width: u8, fractions_width: Option) -> u8 { - let mut pfield = (CcsdsTimeCodes::CucCcsdsEpoch as u8) << 4; - if !(1..=4).contains(&counter_width) { - // Okay to panic here, this function is private and all input values should - // have been sanitized - panic!("invalid counter width {} for cuc timestamp", counter_width); - } - pfield |= (counter_width - 1) << 3; - if let Some(fractions_width) = fractions_width { - if !(1..=3).contains(&fractions_width) { - // Okay to panic here, this function is private and all input values should - // have been sanitized - panic!( - "invalid fractions width {} for cuc timestamp", - fractions_width - ); - } - pfield |= fractions_width; - } - pfield - } - - pub fn len_packed(&self) -> usize { - Self::len_packed_from_pfield(self.pfield) - } - - #[inline] - pub fn len_cntr_from_pfield(pfield: u8) -> u8 { - ((pfield >> 2) & 0b11) + 1 - } - - #[inline] - pub fn len_fractions_from_pfield(pfield: u8) -> u8 { - pfield & 0b11 - } - - /// This returns the length of the individual components of the CUC timestamp in addition - /// to the total size. - /// - /// This function will return a tuple where the first value is the byte width of the - /// counter, the second value is the byte width of the fractional part, and the third - /// components is the total size. - pub fn len_components_and_total_from_pfield(pfield: u8) -> (u8, u8, usize) { - let base_len: usize = 1; - let cntr_len = Self::len_cntr_from_pfield(pfield); - let fractions_len = Self::len_fractions_from_pfield(pfield); - ( - cntr_len, - fractions_len, - base_len + cntr_len as usize + fractions_len as usize, - ) - } - - pub fn len_packed_from_pfield(pfield: u8) -> usize { - let mut base_len: usize = 1; - base_len += Self::len_cntr_from_pfield(pfield) as usize; - base_len += Self::len_fractions_from_pfield(pfield) as usize; - base_len - } - - /// Verifies the raw width parameter and returns the actual length, which is the raw - /// value plus 1. - fn verify_counter_width(width: u8) -> Result<(), CucError> { - if width == 0 || width > 4 { - return Err(CucError::InvalidCounterWidth(width)); - } - Ok(()) - } - - fn verify_fractions_width(width: u8) -> Result<(), CucError> { - if width > 3 { - return Err(CucError::InvalidFractionWidth(width)); - } - Ok(()) - } - } - - impl TimeProviderCcsdsEpoch { - pub fn new_default(counter: u32) -> Self { - // These values are definitely valid, so it is okay to unwrap here. - Self::new(WidthCounterPair(4, counter), None).unwrap() - } - pub fn new_u16_counter(counter: u16) -> Self { - // These values are definitely valid, so it is okay to unwrap here. - Self::new(WidthCounterPair(2, counter as u32), None).unwrap() - } - - pub fn new( - counter: WidthCounterPair, - fractions: Option, - ) -> Result { - Self::verify_counter_width(counter.0)?; - if counter.1 > 2u32.pow(counter.0 as u32 * 8) - 1 { - return Err(CucError::InvalidCounter(counter.0, counter.1 as u64)); - } - if let Some(fractions) = fractions { - Self::verify_fractions_width(fractions.0)?; - if fractions.1 > 2u32.pow((fractions.0 as u32) * 8) - 1 { - return Err(CucError::InvalidFractions(fractions.0, fractions.1 as u64)); - } - } - Ok(Self { - pfield: Self::build_p_field(counter.0, fractions.map(|v| v.0)), - counter, - fractions, - }) - } - } - - impl TimeReader for TimeProviderCcsdsEpoch { - fn from_bytes(buf: &[u8]) -> Result - where - Self: Sized, - { - if buf.len() < MIN_CUC_LEN { - return Err(TimestampError::ByteConversionError( - ByteConversionError::FromSliceTooSmall(SizeMissmatch { - expected: MIN_CUC_LEN, - found: buf.len(), - }), - )); - } - let (cntr_len, fractions_len, total_len) = - Self::len_components_and_total_from_pfield(buf[0]); - if buf.len() < total_len { - return Err(TimestampError::ByteConversionError( - ByteConversionError::FromSliceTooSmall(SizeMissmatch { - expected: total_len, - found: buf.len(), - }), - )); - } - let mut current_idx = 1; - let counter = - match cntr_len { - 1 => buf[current_idx] as u32, - 2 => u16::from_be_bytes(buf[current_idx..current_idx + 2].try_into().unwrap()) - as u32, - 3 => { - let mut tmp_buf: [u8; 4] = [0; 4]; - tmp_buf[1..4].copy_from_slice(&buf[current_idx..current_idx + 3]); - u32::from_be_bytes(tmp_buf) as u32 - } - 4 => u32::from_be_bytes(buf[current_idx..current_idx + 4].try_into().unwrap()) - as u32, - _ => panic!("unreachable match arm"), - }; - current_idx += cntr_len as usize; - let mut fractions = None; - if fractions_len > 0 { - match fractions_len { - 1 => fractions = Some(WidthCounterPair(fractions_len, buf[current_idx] as u32)), - 2 => { - fractions = Some(WidthCounterPair( - fractions_len, - u16::from_be_bytes( - buf[current_idx..current_idx + 2].try_into().unwrap(), - ) as u32, - )) - } - 3 => { - let mut tmp_buf: [u8; 4] = [0; 4]; - tmp_buf[1..4].copy_from_slice(&buf[current_idx..current_idx + 3]); - fractions = Some(WidthCounterPair( - fractions_len, - u32::from_be_bytes(tmp_buf) as u32, - )) - } - _ => panic!("unreachable match arm"), - } - } - let provider = Self::new(WidthCounterPair(cntr_len, counter), fractions)?; - Ok(provider) - } - } - - impl TimeWriter for TimeProviderCcsdsEpoch { - fn write_to_bytes(&self, bytes: &mut [u8]) -> Result { - // Cross check the sizes of the counters against byte widths in the ctor - if bytes.len() < self.len_packed() { - return Err(TimestampError::ByteConversionError( - ByteConversionError::ToSliceTooSmall(SizeMissmatch { - found: bytes.len(), - expected: self.len_packed(), - }), - )); - } - bytes[0] = self.pfield; - let mut current_idx: usize = 1; - match self.counter.0 { - 1 => { - bytes[current_idx] = self.counter.1 as u8; - } - 2 => { - bytes[current_idx..current_idx + 2] - .copy_from_slice(&(self.counter.1 as u16).to_be_bytes()); - } - 3 => { - bytes[current_idx..current_idx + 3] - .copy_from_slice(&self.counter.1.to_be_bytes()[1..4]); - } - 4 => { - bytes[current_idx..current_idx + 4] - .copy_from_slice(&self.counter.1.to_be_bytes()); - } - // Should never happen - _ => panic!("invalid counter width value"), - } - current_idx += self.counter.0 as usize; - if let Some(fractions) = self.fractions { - match fractions.0 { - 1 => bytes[current_idx] = fractions.1 as u8, - 2 => bytes[current_idx..current_idx + 2] - .copy_from_slice(&(fractions.1 as u16).to_be_bytes()), - 3 => bytes[current_idx..current_idx + 3] - .copy_from_slice(&fractions.1.to_be_bytes()[1..4]), - // Should also never happen - _ => panic!("invalid fractions value"), - } - current_idx += fractions.0 as usize; - } - Ok(current_idx) - } - } -} - -/// Module to generate the ASCII timecodes specified in -/// [CCSDS 301.0-B-4](https://public.ccsds.org/Pubs/301x0b4e1.pdf) section 3.5 . -/// See [chrono::DateTime::format] for a usage example of the generated -/// [chrono::format::DelayedFormat] structs. -pub mod ascii { - #[cfg(feature = "alloc")] - use chrono::format::{DelayedFormat, StrftimeItems}; - use chrono::{DateTime, Utc}; - - /// Tuple of format string and formatted size for time code A. - /// - /// Format: YYYY-MM-DDThh:mm:ss.ddd - /// - /// Three digits are used for the decimal fraction - pub const FMT_STR_CODE_A_WITH_SIZE: (&str, usize) = ("%FT%T%.3f", 23); - /// Tuple of format string and formatted size for time code A. - /// - /// Format: YYYY-MM-DDThh:mm:ss.dddZ - /// - /// Three digits are used for the decimal fraction and a terminator is added at the end. - pub const FMT_STR_CODE_A_TERMINATED_WITH_SIZE: (&str, usize) = ("%FT%T%.3fZ", 24); - - /// Tuple of format string and formatted size for time code A. - /// - /// Format: YYYY-DDDThh:mm:ss.ddd - /// - /// Three digits are used for the decimal fraction - pub const FMT_STR_CODE_B_WITH_SIZE: (&str, usize) = ("%Y-%jT%T%.3f", 21); - /// Tuple of format string and formatted size for time code A. - /// - /// Format: YYYY-DDDThh:mm:ss.dddZ - /// - /// Three digits are used for the decimal fraction and a terminator is added at the end. - pub const FMT_STR_CODE_B_TERMINATED_WITH_SIZE: (&str, usize) = ("%Y-%jT%T%.3fZ", 22); - - /// Generates a time code formatter using the [FMT_STR_CODE_A_WITH_SIZE] format. - #[cfg(feature = "alloc")] - #[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] - pub fn generate_time_code_a(date: &DateTime) -> DelayedFormat> { - date.format(FMT_STR_CODE_A_WITH_SIZE.0) - } - - /// Generates a time code formatter using the [FMT_STR_CODE_A_TERMINATED_WITH_SIZE] format. - #[cfg(feature = "alloc")] - #[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] - pub fn generate_time_code_a_terminated( - date: &DateTime, - ) -> DelayedFormat> { - date.format(FMT_STR_CODE_A_TERMINATED_WITH_SIZE.0) - } - - /// Generates a time code formatter using the [FMT_STR_CODE_B_WITH_SIZE] format. - #[cfg(feature = "alloc")] - #[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] - pub fn generate_time_code_b(date: &DateTime) -> DelayedFormat> { - date.format(FMT_STR_CODE_B_WITH_SIZE.0) - } - - /// Generates a time code formatter using the [FMT_STR_CODE_B_TERMINATED_WITH_SIZE] format. - #[cfg(feature = "alloc")] - #[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] - pub fn generate_time_code_b_terminated( - date: &DateTime, - ) -> DelayedFormat> { - date.format(FMT_STR_CODE_B_TERMINATED_WITH_SIZE.0) - } -} - -#[cfg(all(test, feature = "std"))] -mod tests { - use super::cds::TimeProvider; - use super::*; - use crate::time::cds::{DaysLen16Bits, DaysLen24Bits, LengthOfDaySegment, SubmillisPrecision}; - use crate::time::TimestampError::{ByteConversionError, InvalidTimeCode}; - use crate::ByteConversionError::{FromSliceTooSmall, ToSliceTooSmall}; - use alloc::format; - use chrono::{Datelike, Timelike}; - #[cfg(feature = "serde")] - use postcard::{from_bytes, to_allocvec}; - - #[test] - fn test_ascii_timestamp_a_unterminated() { - let date = Utc::now(); - let stamp_formatter = ascii::generate_time_code_a(&date); - let stamp = format!("{}", stamp_formatter); - let t_sep = stamp.find("T"); - assert!(t_sep.is_some()); - assert_eq!(t_sep.unwrap(), 10); - assert_eq!(stamp.len(), ascii::FMT_STR_CODE_A_WITH_SIZE.1); - } - - #[test] - fn test_ascii_timestamp_a_terminated() { - let date = Utc::now(); - let stamp_formatter = ascii::generate_time_code_a_terminated(&date); - let stamp = format!("{}", stamp_formatter); - let t_sep = stamp.find("T"); - assert!(t_sep.is_some()); - assert_eq!(t_sep.unwrap(), 10); - let z_terminator = stamp.find("Z"); - assert!(z_terminator.is_some()); - assert_eq!( - z_terminator.unwrap(), - ascii::FMT_STR_CODE_A_TERMINATED_WITH_SIZE.1 - 1 - ); - assert_eq!(stamp.len(), ascii::FMT_STR_CODE_A_TERMINATED_WITH_SIZE.1); - } - - #[test] - fn test_ascii_timestamp_b_unterminated() { - let date = Utc::now(); - let stamp_formatter = ascii::generate_time_code_b(&date); - let stamp = format!("{}", stamp_formatter); - let t_sep = stamp.find("T"); - assert!(t_sep.is_some()); - assert_eq!(t_sep.unwrap(), 8); - assert_eq!(stamp.len(), ascii::FMT_STR_CODE_B_WITH_SIZE.1); - } - - #[test] - fn test_ascii_timestamp_b_terminated() { - let date = Utc::now(); - let stamp_formatter = ascii::generate_time_code_b_terminated(&date); - let stamp = format!("{}", stamp_formatter); - let t_sep = stamp.find("T"); - assert!(t_sep.is_some()); - assert_eq!(t_sep.unwrap(), 8); - let z_terminator = stamp.find("Z"); - assert!(z_terminator.is_some()); - assert_eq!( - z_terminator.unwrap(), - ascii::FMT_STR_CODE_B_TERMINATED_WITH_SIZE.1 - 1 - ); - assert_eq!(stamp.len(), ascii::FMT_STR_CODE_B_TERMINATED_WITH_SIZE.1); - } - - #[test] - fn test_days_conversion() { - assert_eq!(unix_to_ccsds_days(DAYS_CCSDS_TO_UNIX.into()), 0); - assert_eq!(ccsds_to_unix_days(0), DAYS_CCSDS_TO_UNIX.into()); - } - - #[test] - fn test_get_current_time() { - let sec_floats = seconds_since_epoch(); - assert!(sec_floats > 0.0); - } - - #[test] - fn test_time_stamp_zero_args() { - let time_stamper = TimeProvider::new_with_u16_days(0, 0); - assert_eq!( - time_stamper.unix_seconds(), - (DAYS_CCSDS_TO_UNIX * SECONDS_PER_DAY as i32) as i64 - ); - assert_eq!(time_stamper.submillis_precision(), None); - assert_eq!(time_stamper.ccdsd_time_code(), CcsdsTimeCodes::Cds); - assert_eq!( - time_stamper.p_field(), - (1, [(CcsdsTimeCodes::Cds as u8) << 4, 0]) - ); - let date_time = time_stamper.date_time().unwrap(); - assert_eq!(date_time.year(), 1958); - assert_eq!(date_time.month(), 1); - assert_eq!(date_time.day(), 1); - assert_eq!(date_time.hour(), 0); - assert_eq!(date_time.minute(), 0); - assert_eq!(date_time.second(), 0); - } - - #[test] - fn test_time_stamp_unix_epoch() { - let time_stamper = TimeProvider::new_with_u16_days((-DAYS_CCSDS_TO_UNIX) as u16, 0); - assert_eq!(time_stamper.unix_seconds(), 0); - assert_eq!(time_stamper.submillis_precision(), None); - let date_time = time_stamper.date_time().unwrap(); - assert_eq!(date_time.year(), 1970); - assert_eq!(date_time.month(), 1); - assert_eq!(date_time.day(), 1); - assert_eq!(date_time.hour(), 0); - assert_eq!(date_time.minute(), 0); - assert_eq!(date_time.second(), 0); - } - - #[test] - fn test_large_days_field_write() { - let time_stamper = TimeProvider::new_with_u24_days(0x108020, 0); - assert!(time_stamper.is_ok()); - let time_stamper = time_stamper.unwrap(); - assert_eq!(time_stamper.len_as_bytes(), 8); - let mut buf = [0; 16]; - let written = time_stamper.write_to_bytes(&mut buf); - assert!(written.is_ok()); - let written = written.unwrap(); - assert_eq!(written, 8); - assert_eq!(buf[1], 0x10); - assert_eq!(buf[2], 0x80); - assert_eq!(buf[3], 0x20); - let ms = u32::from_be_bytes(buf[4..8].try_into().unwrap()); - assert_eq!(ms, 0); - assert_eq!((buf[0] >> 2) & 0b1, 1); - } - - #[test] - fn test_large_days_field_read() { - let time_stamper = TimeProvider::new_with_u24_days(0x108020, 0); - assert!(time_stamper.is_ok()); - let time_stamper = time_stamper.unwrap(); - let mut buf = [0; 16]; - let written = time_stamper.write_to_bytes(&mut buf); - assert!(written.is_ok()); - let provider = TimeProvider::::from_bytes(&buf); - assert!(provider.is_ok()); - let provider = provider.unwrap(); - assert_eq!(provider.ccsds_days(), 0x108020); - assert_eq!(provider.ms_of_day(), 0); - } - - #[test] - fn test_large_days_field_read_invalid_ctor() { - let time_stamper = TimeProvider::new_with_u24_days(0x108020, 0); - assert!(time_stamper.is_ok()); - let time_stamper = time_stamper.unwrap(); - let mut buf = [0; 16]; - let written = time_stamper.write_to_bytes(&mut buf); - assert!(written.is_ok()); - let faulty_ctor = TimeProvider::::from_bytes(&buf); - assert!(faulty_ctor.is_err()); - let error = faulty_ctor.unwrap_err(); - if let TimestampError::CdsError(cds::CdsError::InvalidCtorForDaysOfLenInPreamble( - len_of_day, - )) = error - { - assert_eq!(len_of_day, LengthOfDaySegment::Long24Bits); - } else { - panic!("Wrong error type"); - } - } - - #[test] - fn test_write() { - let mut buf = [0; 16]; - let time_stamper_0 = TimeProvider::new_with_u16_days(0, 0); - let mut res = time_stamper_0.write_to_bytes(&mut buf); - assert!(res.is_ok()); - assert_eq!(buf[0], (CcsdsTimeCodes::Cds as u8) << 4); - assert_eq!( - u16::from_be_bytes(buf[1..3].try_into().expect("Byte conversion failed")), - 0 - ); - assert_eq!( - u32::from_be_bytes(buf[3..7].try_into().expect("Byte conversion failed")), - 0 - ); - let time_stamper_1 = TimeProvider::new_with_u16_days(u16::MAX - 1, u32::MAX - 1); - res = time_stamper_1.write_to_bytes(&mut buf); - assert!(res.is_ok()); - assert_eq!(buf[0], (CcsdsTimeCodes::Cds as u8) << 4); - assert_eq!( - u16::from_be_bytes(buf[1..3].try_into().expect("Byte conversion failed")), - u16::MAX - 1 - ); - assert_eq!( - u32::from_be_bytes(buf[3..7].try_into().expect("Byte conversion failed")), - u32::MAX - 1 - ); - } - - #[test] - fn test_faulty_write_buf_too_small() { - let mut buf = [0; 7]; - let time_stamper = TimeProvider::new_with_u16_days(u16::MAX - 1, u32::MAX - 1); - for i in 0..6 { - let res = time_stamper.write_to_bytes(&mut buf[0..i]); - assert!(res.is_err()); - match res.unwrap_err() { - ByteConversionError(ToSliceTooSmall(missmatch)) => { - assert_eq!(missmatch.found, i); - assert_eq!(missmatch.expected, 7); - } - _ => panic!( - "{}", - format!("Invalid error {:?} detected", res.unwrap_err()) - ), - } - } - } - - #[test] - fn test_faulty_read_buf_too_small() { - let buf = [0; 7]; - for i in 0..6 { - let res = TimeProvider::::from_bytes(&buf[0..i]); - assert!(res.is_err()); - let err = res.unwrap_err(); - match err { - ByteConversionError(e) => match e { - FromSliceTooSmall(missmatch) => { - assert_eq!(missmatch.found, i); - assert_eq!(missmatch.expected, 7); - } - _ => panic!("{}", format!("Invalid error {:?} detected", e)), - }, - _ => { - panic!("Unexpected error {:?}", err); - } - } - } - } - - #[test] - fn test_faulty_invalid_pfield() { - let mut buf = [0; 16]; - let time_stamper_0 = TimeProvider::new_with_u16_days(0, 0); - let res = time_stamper_0.write_to_bytes(&mut buf); - assert!(res.is_ok()); - buf[0] = 0; - let res = TimeProvider::::from_bytes(&buf); - assert!(res.is_err()); - let err = res.unwrap_err(); - match err { - InvalidTimeCode(code, raw) => { - assert_eq!(code, CcsdsTimeCodes::Cds); - assert_eq!(raw, 0); - } - _ => {} - } - } - - #[test] - fn test_reading() { - let mut buf = [0; 16]; - let time_stamper = TimeProvider::new_with_u16_days(u16::MAX - 1, u32::MAX - 1); - let res = time_stamper.write_to_bytes(&mut buf); - assert!(res.is_ok()); - assert_eq!(buf[0], (CcsdsTimeCodes::Cds as u8) << 4); - assert_eq!( - u16::from_be_bytes(buf[1..3].try_into().expect("Byte conversion failed")), - u16::MAX - 1 - ); - assert_eq!( - u32::from_be_bytes(buf[3..7].try_into().expect("Byte conversion failed")), - u32::MAX - 1 - ); - - let read_stamp: TimeProvider = - TimeProvider::from_bytes(&buf).expect("Reading timestamp failed"); - assert_eq!(read_stamp.ccsds_days(), u16::MAX - 1); - assert_eq!(read_stamp.ms_of_day(), u32::MAX - 1); - } - - #[test] - fn test_time_now() { - let timestamp_now = TimeProvider::from_now_with_u16_days().unwrap(); - let compare_stamp = Utc::now(); - let dt = timestamp_now.date_time().unwrap(); - if compare_stamp.year() > dt.year() { - assert_eq!(compare_stamp.year() - dt.year(), 1); - } else { - assert_eq!(dt.year(), compare_stamp.year()); - } - generic_dt_property_equality_check(dt.month(), compare_stamp.month(), 1, 12); - - assert_eq!(dt.day(), compare_stamp.day()); - if compare_stamp.day() < dt.day() { - assert!(dt.day() >= 28); - assert_eq!(compare_stamp.day(), 1); - } else if compare_stamp.day() > dt.day() { - assert_eq!(compare_stamp.day() - dt.day(), 1); - } else { - assert_eq!(compare_stamp.day(), dt.day()); - } - generic_dt_property_equality_check(dt.hour(), compare_stamp.hour(), 0, 23); - generic_dt_property_equality_check(dt.minute(), compare_stamp.minute(), 0, 59); - } - - #[test] - fn test_submillis_precision_micros() { - let mut time_stamper = TimeProvider::new_with_u16_days(0, 0); - time_stamper.set_submillis_precision(SubmillisPrecision::Microseconds(500)); - assert!(time_stamper.submillis_precision().is_some()); - if let SubmillisPrecision::Microseconds(micros) = - time_stamper.submillis_precision().unwrap() - { - assert_eq!(micros, 500); - } else { - panic!("Submillis precision was not set properly"); - } - let mut write_buf: [u8; 16] = [0; 16]; - let written = time_stamper - .write_to_bytes(&mut write_buf) - .expect("Writing timestamp failed"); - assert_eq!(written, 9); - let cross_check: u16 = 500; - assert_eq!(write_buf[7..9], cross_check.to_be_bytes()); - } - - #[test] - fn test_submillis_precision_picos() { - let mut time_stamper = TimeProvider::new_with_u16_days(0, 0); - time_stamper.set_submillis_precision(SubmillisPrecision::Picoseconds(5e8 as u32)); - assert!(time_stamper.submillis_precision().is_some()); - if let SubmillisPrecision::Picoseconds(ps) = time_stamper.submillis_precision().unwrap() { - assert_eq!(ps, 5e8 as u32); - } else { - panic!("Submillis precision was not set properly"); - } - let mut write_buf: [u8; 16] = [0; 16]; - let written = time_stamper - .write_to_bytes(&mut write_buf) - .expect("Writing timestamp failed"); - assert_eq!(written, 11); - let cross_check: u32 = 5e8 as u32; - assert_eq!(write_buf[7..11], cross_check.to_be_bytes()); - } - - #[test] - fn read_stamp_with_ps_submillis_precision() { - let mut time_stamper = TimeProvider::new_with_u16_days(0, 0); - time_stamper.set_submillis_precision(SubmillisPrecision::Picoseconds(5e8 as u32)); - let mut write_buf: [u8; 16] = [0; 16]; - let written = time_stamper - .write_to_bytes(&mut write_buf) - .expect("Writing timestamp failed"); - assert_eq!(written, 11); - let stamp_deserialized = TimeProvider::::from_bytes(&write_buf); - assert!(stamp_deserialized.is_ok()); - let stamp_deserialized = stamp_deserialized.unwrap(); - assert_eq!(stamp_deserialized.len_as_bytes(), 11); - assert!(stamp_deserialized.submillis_precision().is_some()); - let submillis_rec = stamp_deserialized.submillis_precision().unwrap(); - if let SubmillisPrecision::Picoseconds(ps) = submillis_rec { - assert_eq!(ps, 5e8 as u32); - } else { - panic!("Wrong precision field detected"); - } - } - - #[test] - fn read_stamp_with_us_submillis_precision() { - let mut time_stamper = TimeProvider::new_with_u16_days(0, 0); - time_stamper.set_submillis_precision(SubmillisPrecision::Microseconds(500)); - let mut write_buf: [u8; 16] = [0; 16]; - let written = time_stamper - .write_to_bytes(&mut write_buf) - .expect("Writing timestamp failed"); - assert_eq!(written, 9); - let stamp_deserialized = TimeProvider::::from_bytes(&write_buf); - assert!(stamp_deserialized.is_ok()); - let stamp_deserialized = stamp_deserialized.unwrap(); - assert_eq!(stamp_deserialized.len_as_bytes(), 9); - assert!(stamp_deserialized.submillis_precision().is_some()); - let submillis_rec = stamp_deserialized.submillis_precision().unwrap(); - if let SubmillisPrecision::Microseconds(us) = submillis_rec { - assert_eq!(us, 500); - } else { - panic!("Wrong precision field detected"); - } - } - - #[test] - #[cfg(feature = "serde")] - fn test_serialization() { - let stamp_now = TimeProvider::from_now_with_u16_days().expect("Error retrieving time"); - let val = to_allocvec(&stamp_now).expect("Serializing timestamp failed"); - assert!(val.len() > 0); - let stamp_deser: TimeProvider = from_bytes(&val).expect("Stamp deserialization failed"); - assert_eq!(stamp_deser, stamp_now); - } - - fn generic_dt_property_equality_check(first: u32, second: u32, start: u32, end: u32) { - if second < first { - assert_eq!(second, start); - assert_eq!(first, end); - } else if second > first { - assert_eq!(second - first, 1); - } else { - assert_eq!(first, second); - } - } -} diff --git a/src/time/ascii.rs b/src/time/ascii.rs new file mode 100644 index 0000000..9a028c1 --- /dev/null +++ b/src/time/ascii.rs @@ -0,0 +1,128 @@ +//! Module to generate the ASCII timecodes specified in +//! [CCSDS 301.0-B-4](https://public.ccsds.org/Pubs/301x0b4e1.pdf) section 3.5 . +//! See [chrono::DateTime::format] for a usage example of the generated +//! [chrono::format::DelayedFormat] structs. +#[cfg(feature = "alloc")] +use chrono::format::{DelayedFormat, StrftimeItems}; +use chrono::{DateTime, Utc}; + +/// Tuple of format string and formatted size for time code A. +/// +/// Format: YYYY-MM-DDThh:mm:ss.ddd +/// +/// Three digits are used for the decimal fraction +pub const FMT_STR_CODE_A_WITH_SIZE: (&str, usize) = ("%FT%T%.3f", 23); +/// Tuple of format string and formatted size for time code A. +/// +/// Format: YYYY-MM-DDThh:mm:ss.dddZ +/// +/// Three digits are used for the decimal fraction and a terminator is added at the end. +pub const FMT_STR_CODE_A_TERMINATED_WITH_SIZE: (&str, usize) = ("%FT%T%.3fZ", 24); + +/// Tuple of format string and formatted size for time code A. +/// +/// Format: YYYY-DDDThh:mm:ss.ddd +/// +/// Three digits are used for the decimal fraction +pub const FMT_STR_CODE_B_WITH_SIZE: (&str, usize) = ("%Y-%jT%T%.3f", 21); +/// Tuple of format string and formatted size for time code A. +/// +/// Format: YYYY-DDDThh:mm:ss.dddZ +/// +/// Three digits are used for the decimal fraction and a terminator is added at the end. +pub const FMT_STR_CODE_B_TERMINATED_WITH_SIZE: (&str, usize) = ("%Y-%jT%T%.3fZ", 22); + +/// Generates a time code formatter using the [FMT_STR_CODE_A_WITH_SIZE] format. +#[cfg(feature = "alloc")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] +pub fn generate_time_code_a(date: &DateTime) -> DelayedFormat> { + date.format(FMT_STR_CODE_A_WITH_SIZE.0) +} + +/// Generates a time code formatter using the [FMT_STR_CODE_A_TERMINATED_WITH_SIZE] format. +#[cfg(feature = "alloc")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] +pub fn generate_time_code_a_terminated( + date: &DateTime, +) -> DelayedFormat> { + date.format(FMT_STR_CODE_A_TERMINATED_WITH_SIZE.0) +} + +/// Generates a time code formatter using the [FMT_STR_CODE_B_WITH_SIZE] format. +#[cfg(feature = "alloc")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] +pub fn generate_time_code_b(date: &DateTime) -> DelayedFormat> { + date.format(FMT_STR_CODE_B_WITH_SIZE.0) +} + +/// Generates a time code formatter using the [FMT_STR_CODE_B_TERMINATED_WITH_SIZE] format. +#[cfg(feature = "alloc")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] +pub fn generate_time_code_b_terminated( + date: &DateTime, +) -> DelayedFormat> { + date.format(FMT_STR_CODE_B_TERMINATED_WITH_SIZE.0) +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::Utc; + use std::format; + + #[test] + fn test_ascii_timestamp_a_unterminated() { + let date = Utc::now(); + let stamp_formatter = generate_time_code_a(&date); + let stamp = format!("{}", stamp_formatter); + let t_sep = stamp.find("T"); + assert!(t_sep.is_some()); + assert_eq!(t_sep.unwrap(), 10); + assert_eq!(stamp.len(), FMT_STR_CODE_A_WITH_SIZE.1); + } + + #[test] + fn test_ascii_timestamp_a_terminated() { + let date = Utc::now(); + let stamp_formatter = generate_time_code_a_terminated(&date); + let stamp = format!("{}", stamp_formatter); + let t_sep = stamp.find("T"); + assert!(t_sep.is_some()); + assert_eq!(t_sep.unwrap(), 10); + let z_terminator = stamp.find("Z"); + assert!(z_terminator.is_some()); + assert_eq!( + z_terminator.unwrap(), + FMT_STR_CODE_A_TERMINATED_WITH_SIZE.1 - 1 + ); + assert_eq!(stamp.len(), FMT_STR_CODE_A_TERMINATED_WITH_SIZE.1); + } + + #[test] + fn test_ascii_timestamp_b_unterminated() { + let date = Utc::now(); + let stamp_formatter = generate_time_code_b(&date); + let stamp = format!("{}", stamp_formatter); + let t_sep = stamp.find("T"); + assert!(t_sep.is_some()); + assert_eq!(t_sep.unwrap(), 8); + assert_eq!(stamp.len(), FMT_STR_CODE_B_WITH_SIZE.1); + } + + #[test] + fn test_ascii_timestamp_b_terminated() { + let date = Utc::now(); + let stamp_formatter = generate_time_code_b_terminated(&date); + let stamp = format!("{}", stamp_formatter); + let t_sep = stamp.find("T"); + assert!(t_sep.is_some()); + assert_eq!(t_sep.unwrap(), 8); + let z_terminator = stamp.find("Z"); + assert!(z_terminator.is_some()); + assert_eq!( + z_terminator.unwrap(), + FMT_STR_CODE_B_TERMINATED_WITH_SIZE.1 - 1 + ); + assert_eq!(stamp.len(), FMT_STR_CODE_B_TERMINATED_WITH_SIZE.1); + } +} diff --git a/src/time/cds.rs b/src/time/cds.rs new file mode 100644 index 0000000..3a44046 --- /dev/null +++ b/src/time/cds.rs @@ -0,0 +1,956 @@ +//! Module to generate or read CDS timestamps as specified in +//! [CCSDS 301.0-B-4](https://public.ccsds.org/Pubs/301x0b4e1.pdf) section 3.3 . +//! +//! The core data structure to do this is the [cds::TimeProvider] struct. +use super::*; +use crate::private::Sealed; +use core::fmt::Debug; + +const CDS_SHORT_P_FIELD: u8 = (CcsdsTimeCodes::Cds as u8) << 4; +pub const MIN_CDS_FIELD_LEN: usize = 7; + +/// Generic trait implemented by token structs to specify the length of day field at type +/// level. This trait is only meant to be implemented in this crate and therefore sealed. +pub trait ProvidesDaysLength: Sealed { + type FieldType: Copy + Clone + TryFrom; +} + +/// Type level token to be used as a generic parameter to [TimeProvider]. +#[derive(Debug, PartialEq, Eq, Default)] +pub struct DaysLen16Bits {} + +impl Sealed for DaysLen16Bits {} +impl ProvidesDaysLength for DaysLen16Bits { + type FieldType = u16; +} + +/// Type level token to be used as a generic parameter to [TimeProvider]. +#[derive(Debug, PartialEq, Eq, Default)] +pub struct DaysLen24Bits {} +impl Sealed for DaysLen24Bits {} +impl ProvidesDaysLength for DaysLen24Bits { + type FieldType = u32; +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum LengthOfDaySegment { + Short16Bits = 0, + Long24Bits = 1, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum SubmillisPrecision { + Absent, + Microseconds(u16), + Picoseconds(u32), + Reserved, +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum CdsError { + /// CCSDS days value exceeds maximum allowed size or is negative + InvalidCcsdsDays(i64), + /// There are distinct constructors depending on the days field width detected in the preamble + /// field. This error will be returned if there is a missmatch. + InvalidCtorForDaysOfLenInPreamble(LengthOfDaySegment), +} + +impl Display for CdsError { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + match self { + CdsError::InvalidCcsdsDays(days) => { + write!(f, "invalid ccsds days {}", days) + } + CdsError::InvalidCtorForDaysOfLenInPreamble(length_of_day) => { + write!( + f, + "wrong constructor for length of day {:?} detected in preamble", + length_of_day + ) + } + } + } +} + +#[cfg(feature = "std")] +impl Error for CdsError {} +pub fn length_of_day_segment_from_pfield(pfield: u8) -> LengthOfDaySegment { + if (pfield >> 2) & 0b1 == 1 { + return LengthOfDaySegment::Long24Bits; + } + LengthOfDaySegment::Short16Bits +} +pub fn precision_from_pfield(pfield: u8) -> SubmillisPrecision { + match pfield & 0b11 { + 0b01 => SubmillisPrecision::Microseconds(0), + 0b10 => SubmillisPrecision::Picoseconds(0), + 0b00 => SubmillisPrecision::Absent, + 0b11 => SubmillisPrecision::Reserved, + _ => panic!("pfield to SubmillisPrecision failed"), + } +} + +/// This object is the abstraction for the CCSDS Day Segmented Time Code (CDS). +/// +/// It has the capability to generate and read timestamps as specified in the CCSDS 301.0-B-4 +/// section 3.3 . The width of the days field is configured at compile time via the generic +/// [ProvidesDaysLength] trait which is implemented by [DaysLen16Bits] and [DaysLen24Bits]. +/// +/// Custom epochs are not supported yet. +/// Furthermore, the preamble field (p-field) is explicitly conveyed. +/// That means it will always be present when writing the time stamp to a raw buffer, and it +/// must be present when reading a CDS timestamp from a raw buffer. +/// +/// # Example +/// +/// ``` +/// use spacepackets::time::cds::{TimeProvider, DaysLen16Bits}; +/// use spacepackets::time::{TimeWriter, CcsdsTimeCodes, TimeReader, CcsdsTimeProvider}; +/// +/// let timestamp_now = TimeProvider::from_now_with_u16_days().unwrap(); +/// let mut raw_stamp = [0; 7]; +/// { +/// let written = timestamp_now.write_to_bytes(&mut raw_stamp).unwrap(); +/// assert_eq!((raw_stamp[0] >> 4) & 0b111, CcsdsTimeCodes::Cds as u8); +/// assert_eq!(written, 7); +/// } +/// { +/// let read_result = TimeProvider::::from_bytes(&raw_stamp); +/// assert!(read_result.is_ok()); +/// let stamp_deserialized = read_result.unwrap(); +/// assert_eq!(stamp_deserialized.len_as_bytes(), 7); +/// } +/// ``` +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct TimeProvider { + pfield: u8, + ccsds_days: DaysLen::FieldType, + ms_of_day: u32, + submillis_precision: Option, + unix_seconds: i64, +} + +#[cfg(feature = "std")] +struct ConversionFromNow { + ccsds_days: i32, + ms_of_day: u64, + unix_days_seconds: u64, + submillis_prec: Option, +} + +#[cfg(feature = "std")] +impl ConversionFromNow { + fn new() -> Result { + Self::new_generic(None) + } + + fn new_with_submillis_us_prec() -> Result { + Self::new_generic(Some(SubmillisPrecision::Microseconds(0))) + } + + fn new_with_submillis_ps_prec() -> Result { + Self::new_generic(Some(SubmillisPrecision::Picoseconds(0))) + } + + fn new_generic(mut prec: Option) -> Result { + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; + let epoch = now.as_secs(); + let secs_of_day = epoch % SECONDS_PER_DAY as u64; + let unix_days_seconds = epoch - secs_of_day; + if let Some(submilli_prec) = prec { + match submilli_prec { + SubmillisPrecision::Microseconds(_) => { + prec = Some(SubmillisPrecision::Microseconds( + (now.subsec_micros() % 1000) as u16, + )); + } + SubmillisPrecision::Picoseconds(_) => { + prec = Some(SubmillisPrecision::Microseconds( + (now.subsec_nanos() * 1000) as u16, + )); + } + _ => (), + } + } + Ok(Self { + ms_of_day: secs_of_day * 1000 + now.subsec_millis() as u64, + ccsds_days: unix_to_ccsds_days((unix_days_seconds / SECONDS_PER_DAY as u64) as i64) + as i32, + unix_days_seconds, + submillis_prec: prec, + }) + } +} + +impl TimeProvider { + pub fn set_submillis_precision(&mut self, prec: SubmillisPrecision) { + self.pfield &= !(0b11); + if let SubmillisPrecision::Absent = prec { + self.submillis_precision = None; + return; + } + self.submillis_precision = Some(prec); + match prec { + SubmillisPrecision::Microseconds(_) => { + self.pfield |= 0b01; + } + SubmillisPrecision::Picoseconds(_) => { + self.pfield |= 0b10; + } + _ => (), + } + } + + pub fn clear_submillis_precision(&mut self) { + self.pfield &= !(0b11); + self.submillis_precision = None; + } + + pub fn ccsds_days(&self) -> ProvidesDaysLen::FieldType { + self.ccsds_days + } + + pub fn submillis_precision(&self) -> Option { + self.submillis_precision + } + + pub fn ms_of_day(&self) -> u32 { + self.ms_of_day + } + + fn generic_raw_read_checks( + buf: &[u8], + days_len: LengthOfDaySegment, + ) -> Result { + if buf.len() < MIN_CDS_FIELD_LEN { + return Err(TimestampError::ByteConversionError( + ByteConversionError::FromSliceTooSmall(SizeMissmatch { + expected: MIN_CDS_FIELD_LEN, + found: buf.len(), + }), + )); + } + let pfield = buf[0]; + match CcsdsTimeCodes::try_from(pfield >> 4 & 0b111) { + Ok(cds_type) => match cds_type { + CcsdsTimeCodes::Cds => (), + _ => { + return Err(TimestampError::InvalidTimeCode( + CcsdsTimeCodes::Cds, + cds_type as u8, + )) + } + }, + _ => { + return Err(TimestampError::InvalidTimeCode( + CcsdsTimeCodes::Cds, + pfield >> 4 & 0b111, + )) + } + }; + if ((pfield >> 3) & 0b1) == 1 { + return Err(TimestampError::CustomEpochNotSupported); + } + let days_len_from_pfield = length_of_day_segment_from_pfield(pfield); + if days_len_from_pfield != days_len { + return Err(CdsError::InvalidCtorForDaysOfLenInPreamble(days_len_from_pfield).into()); + } + let stamp_len = Self::calc_stamp_len(pfield); + if buf.len() < stamp_len { + return Err(TimestampError::ByteConversionError( + ByteConversionError::FromSliceTooSmall(SizeMissmatch { + expected: stamp_len, + found: buf.len(), + }), + )); + } + Ok(precision_from_pfield(pfield)) + } + + fn calc_stamp_len(pfield: u8) -> usize { + let mut init_len = 7; + if length_of_day_segment_from_pfield(pfield) == LengthOfDaySegment::Long24Bits { + init_len += 1 + } + match pfield & 0b11 { + 0b01 => { + init_len += 2; + } + 0b10 => { + init_len += 4; + } + _ => (), + } + init_len + } + + fn setup(&mut self, unix_days_seconds: i64, ms_of_day: u64) { + self.calc_unix_seconds(unix_days_seconds, ms_of_day); + } + + fn calc_unix_seconds(&mut self, unix_days_seconds: i64, ms_of_day: u64) { + self.unix_seconds = unix_days_seconds; + let seconds_of_day = (ms_of_day / 1000) as i64; + if self.unix_seconds < 0 { + self.unix_seconds -= seconds_of_day; + } else { + self.unix_seconds += seconds_of_day; + } + } + + fn calc_date_time(&self, ms_since_last_second: u32) -> Option> { + assert!(ms_since_last_second < 1000, "Invalid MS since last second"); + let ns_since_last_sec = ms_since_last_second * 1e6 as u32; + if let LocalResult::Single(val) = Utc.timestamp_opt(self.unix_seconds, ns_since_last_sec) { + return Some(val); + } + None + } + + fn length_check(&self, buf: &[u8], len_as_bytes: usize) -> Result<(), TimestampError> { + if buf.len() < len_as_bytes { + return Err(TimestampError::ByteConversionError( + ByteConversionError::ToSliceTooSmall(SizeMissmatch { + expected: len_as_bytes, + found: buf.len(), + }), + )); + } + Ok(()) + } + + fn generic_new( + days_len: LengthOfDaySegment, + ccsds_days: ProvidesDaysLen::FieldType, + ms_of_day: u32, + ) -> Result + where + i64: From, + { + let mut provider = Self { + pfield: Self::generate_p_field(days_len, None), + ccsds_days, + ms_of_day, + unix_seconds: 0, + submillis_precision: None, + }; + let unix_days_seconds = + ccsds_to_unix_days(ccsds_days.into()) as i64 * SECONDS_PER_DAY as i64; + provider.setup(unix_days_seconds as i64, ms_of_day.into()); + Ok(provider) + } + + #[cfg(feature = "std")] + fn generic_from_now( + days_len: LengthOfDaySegment, + conversion_from_now: ConversionFromNow, + ) -> Result + where + >::Error: Debug, + { + let ccsds_days: ProvidesDaysLen::FieldType = + conversion_from_now.ccsds_days.try_into().map_err(|_| { + StdTimestampError::TimestampError( + CdsError::InvalidCcsdsDays(conversion_from_now.ccsds_days.into()).into(), + ) + })?; + let mut provider = Self { + pfield: Self::generate_p_field(days_len, conversion_from_now.submillis_prec), + ccsds_days, + ms_of_day: conversion_from_now.ms_of_day as u32, + unix_seconds: 0, + submillis_precision: conversion_from_now.submillis_prec, + }; + provider.setup( + conversion_from_now.unix_days_seconds as i64, + conversion_from_now.ms_of_day, + ); + Ok(provider) + } + + #[cfg(feature = "std")] + fn generic_conversion_from_now(&self) -> Result { + Ok(match self.submillis_precision { + None => ConversionFromNow::new()?, + Some(prec) => match prec { + SubmillisPrecision::Microseconds(_) => { + ConversionFromNow::new_with_submillis_us_prec()? + } + SubmillisPrecision::Picoseconds(_) => { + ConversionFromNow::new_with_submillis_ps_prec()? + } + _ => ConversionFromNow::new()?, + }, + }) + } + + fn generate_p_field( + day_seg_len: LengthOfDaySegment, + submillis_prec: Option, + ) -> u8 { + let mut pfield = CDS_SHORT_P_FIELD | ((day_seg_len as u8) << 2); + if let Some(submillis_prec) = submillis_prec { + match submillis_prec { + SubmillisPrecision::Microseconds(_) => pfield |= 0b01, + SubmillisPrecision::Picoseconds(_) => pfield |= 0b10, + SubmillisPrecision::Reserved => pfield |= 0b11, + _ => (), + } + } + pfield + } + + #[cfg(feature = "std")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] + pub fn update_from_now(&mut self) -> Result<(), StdTimestampError> + where + >::Error: Debug, + { + let conversion_from_now = self.generic_conversion_from_now()?; + let ccsds_days: ProvidesDaysLen::FieldType = + conversion_from_now.ccsds_days.try_into().map_err(|_| { + StdTimestampError::TimestampError( + CdsError::InvalidCcsdsDays(conversion_from_now.ccsds_days as i64).into(), + ) + })?; + self.ccsds_days = ccsds_days; + self.ms_of_day = conversion_from_now.ms_of_day as u32; + self.setup( + conversion_from_now.unix_days_seconds as i64, + conversion_from_now.ms_of_day, + ); + Ok(()) + } +} + +impl TimeProvider { + /// Generate a new timestamp provider with the days field width set to 24 bits + pub fn new_with_u24_days(ccsds_days: u32, ms_of_day: u32) -> Result { + if ccsds_days > 2_u32.pow(24) { + return Err(CdsError::InvalidCcsdsDays(ccsds_days.into())); + } + Self::generic_new(LengthOfDaySegment::Long24Bits, ccsds_days, ms_of_day) + } + + /// Generate a time stamp from the current time using the system clock. + #[cfg(feature = "std")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] + pub fn from_now_with_u24_days() -> Result { + let conversion_from_now = ConversionFromNow::new()?; + Self::generic_from_now(LengthOfDaySegment::Long24Bits, conversion_from_now) + } + + /// Like [Self::from_now_with_u24_days] but with microsecond sub-millisecond precision. + #[cfg(feature = "std")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] + pub fn from_now_with_u24_days_and_us_prec() -> Result { + let conversion_from_now = ConversionFromNow::new_with_submillis_us_prec()?; + Self::generic_from_now(LengthOfDaySegment::Long24Bits, conversion_from_now) + } + + /// Like [Self::from_now_with_u24_days] but with picoseconds sub-millisecond precision. + #[cfg(feature = "std")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] + pub fn from_now_with_u24_days_ps_submillis_prec() -> Result { + let conversion_from_now = ConversionFromNow::new_with_submillis_ps_prec()?; + Self::generic_from_now(LengthOfDaySegment::Long24Bits, conversion_from_now) + } + + fn from_bytes_24_bit_days(buf: &[u8]) -> Result { + let submillis_precision = + Self::generic_raw_read_checks(buf, LengthOfDaySegment::Long24Bits)?; + let mut temp_buf: [u8; 4] = [0; 4]; + temp_buf[1..4].copy_from_slice(&buf[1..4]); + let cccsds_days: u32 = u32::from_be_bytes(temp_buf); + let ms_of_day: u32 = u32::from_be_bytes(buf[4..8].try_into().unwrap()); + let mut provider = Self::new_with_u24_days(cccsds_days, ms_of_day)?; + match submillis_precision { + SubmillisPrecision::Microseconds(_) => { + provider.set_submillis_precision(SubmillisPrecision::Microseconds( + u16::from_be_bytes(buf[8..10].try_into().unwrap()), + )) + } + SubmillisPrecision::Picoseconds(_) => provider.set_submillis_precision( + SubmillisPrecision::Picoseconds(u32::from_be_bytes(buf[8..12].try_into().unwrap())), + ), + _ => (), + } + Ok(provider) + } +} + +impl TimeProvider { + /// Generate a new timestamp provider with the days field width set to 16 bits + pub fn new_with_u16_days(ccsds_days: u16, ms_of_day: u32) -> Self { + // This should never fail, type system ensures CCSDS can not be negative or too large + Self::generic_new(LengthOfDaySegment::Short16Bits, ccsds_days, ms_of_day).unwrap() + } + + /// Generate a time stamp from the current time using the system clock. + #[cfg(feature = "std")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] + pub fn from_now_with_u16_days() -> Result { + let conversion_from_now = ConversionFromNow::new()?; + Self::generic_from_now(LengthOfDaySegment::Short16Bits, conversion_from_now) + } + + /// Like [Self::from_now_with_u16_days] but with microsecond sub-millisecond precision. + #[cfg(feature = "std")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] + pub fn from_now_with_u16_days_and_us_prec() -> Result { + let conversion_from_now = ConversionFromNow::new_with_submillis_us_prec()?; + Self::generic_from_now(LengthOfDaySegment::Short16Bits, conversion_from_now) + } + + /// Like [Self::from_now_with_u16_days] but with picosecond sub-millisecond precision. + #[cfg(feature = "std")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] + pub fn from_now_with_u16_days_and_ps_prec() -> Result { + let conversion_from_now = ConversionFromNow::new_with_submillis_ps_prec()?; + Self::generic_from_now(LengthOfDaySegment::Short16Bits, conversion_from_now) + } + + fn from_bytes_16_bit_days(buf: &[u8]) -> Result { + let submillis_precision = + Self::generic_raw_read_checks(buf, LengthOfDaySegment::Short16Bits)?; + let ccsds_days: u16 = u16::from_be_bytes(buf[1..3].try_into().unwrap()); + let ms_of_day: u32 = u32::from_be_bytes(buf[3..7].try_into().unwrap()); + let mut provider = Self::new_with_u16_days(ccsds_days, ms_of_day); + provider.pfield = buf[0]; + match submillis_precision { + SubmillisPrecision::Microseconds(_) => provider.set_submillis_precision( + SubmillisPrecision::Microseconds(u16::from_be_bytes(buf[7..9].try_into().unwrap())), + ), + SubmillisPrecision::Picoseconds(_) => provider.set_submillis_precision( + SubmillisPrecision::Picoseconds(u32::from_be_bytes(buf[7..11].try_into().unwrap())), + ), + _ => (), + } + Ok(provider) + } +} + +impl CcsdsTimeProvider for TimeProvider { + fn len_as_bytes(&self) -> usize { + Self::calc_stamp_len(self.pfield) + } + + fn p_field(&self) -> (usize, [u8; 2]) { + (1, [self.pfield, 0]) + } + + fn ccdsd_time_code(&self) -> CcsdsTimeCodes { + CcsdsTimeCodes::Cds + } + + fn unix_seconds(&self) -> i64 { + self.unix_seconds + } + + fn date_time(&self) -> Option> { + self.calc_date_time((self.ms_of_day % 1000) as u32) + } +} + +impl TimeReader for TimeProvider { + fn from_bytes(buf: &[u8]) -> Result { + Self::from_bytes_16_bit_days(buf) + } +} + +impl TimeReader for TimeProvider { + fn from_bytes(buf: &[u8]) -> Result { + Self::from_bytes_24_bit_days(buf) + } +} + +impl TimeWriter for TimeProvider { + fn write_to_bytes(&self, buf: &mut [u8]) -> Result { + self.length_check(buf, self.len_as_bytes())?; + buf[0] = self.pfield; + buf[1..3].copy_from_slice(self.ccsds_days.to_be_bytes().as_slice()); + buf[3..7].copy_from_slice(self.ms_of_day.to_be_bytes().as_slice()); + if let Some(submillis_prec) = self.submillis_precision { + match submillis_prec { + SubmillisPrecision::Microseconds(ms) => { + buf[7..9].copy_from_slice(ms.to_be_bytes().as_slice()); + } + SubmillisPrecision::Picoseconds(ps) => { + buf[7..11].copy_from_slice(ps.to_be_bytes().as_slice()); + } + _ => (), + } + } + Ok(self.len_as_bytes()) + } +} + +impl TimeWriter for TimeProvider { + fn write_to_bytes(&self, buf: &mut [u8]) -> Result { + self.length_check(buf, self.len_as_bytes())?; + buf[0] = self.pfield; + let be_days = self.ccsds_days.to_be_bytes(); + buf[1..4].copy_from_slice(&be_days[1..4]); + buf[4..8].copy_from_slice(self.ms_of_day.to_be_bytes().as_slice()); + if let Some(submillis_prec) = self.submillis_precision { + match submillis_prec { + SubmillisPrecision::Microseconds(ms) => { + buf[8..10].copy_from_slice(ms.to_be_bytes().as_slice()); + } + SubmillisPrecision::Picoseconds(ps) => { + buf[8..12].copy_from_slice(ps.to_be_bytes().as_slice()); + } + _ => (), + } + } + Ok(self.len_as_bytes()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::time::TimestampError::{ByteConversionError, InvalidTimeCode}; + use crate::ByteConversionError::{FromSliceTooSmall, ToSliceTooSmall}; + use chrono::{Datelike, Timelike}; + #[cfg(feature = "serde")] + use postcard::{from_bytes, to_allocvec}; + use std::format; + + #[test] + fn test_time_stamp_zero_args() { + let time_stamper = TimeProvider::new_with_u16_days(0, 0); + assert_eq!( + time_stamper.unix_seconds(), + (DAYS_CCSDS_TO_UNIX * SECONDS_PER_DAY as i32) as i64 + ); + assert_eq!(time_stamper.submillis_precision(), None); + assert_eq!(time_stamper.ccdsd_time_code(), CcsdsTimeCodes::Cds); + assert_eq!( + time_stamper.p_field(), + (1, [(CcsdsTimeCodes::Cds as u8) << 4, 0]) + ); + let date_time = time_stamper.date_time().unwrap(); + assert_eq!(date_time.year(), 1958); + assert_eq!(date_time.month(), 1); + assert_eq!(date_time.day(), 1); + assert_eq!(date_time.hour(), 0); + assert_eq!(date_time.minute(), 0); + assert_eq!(date_time.second(), 0); + } + + #[test] + fn test_time_stamp_unix_epoch() { + let time_stamper = TimeProvider::new_with_u16_days((-DAYS_CCSDS_TO_UNIX) as u16, 0); + assert_eq!(time_stamper.unix_seconds(), 0); + assert_eq!(time_stamper.submillis_precision(), None); + let date_time = time_stamper.date_time().unwrap(); + assert_eq!(date_time.year(), 1970); + assert_eq!(date_time.month(), 1); + assert_eq!(date_time.day(), 1); + assert_eq!(date_time.hour(), 0); + assert_eq!(date_time.minute(), 0); + assert_eq!(date_time.second(), 0); + } + + #[test] + fn test_large_days_field_write() { + let time_stamper = TimeProvider::new_with_u24_days(0x108020, 0); + assert!(time_stamper.is_ok()); + let time_stamper = time_stamper.unwrap(); + assert_eq!(time_stamper.len_as_bytes(), 8); + let mut buf = [0; 16]; + let written = time_stamper.write_to_bytes(&mut buf); + assert!(written.is_ok()); + let written = written.unwrap(); + assert_eq!(written, 8); + assert_eq!(buf[1], 0x10); + assert_eq!(buf[2], 0x80); + assert_eq!(buf[3], 0x20); + let ms = u32::from_be_bytes(buf[4..8].try_into().unwrap()); + assert_eq!(ms, 0); + assert_eq!((buf[0] >> 2) & 0b1, 1); + } + + #[test] + fn test_large_days_field_read() { + let time_stamper = TimeProvider::new_with_u24_days(0x108020, 0); + assert!(time_stamper.is_ok()); + let time_stamper = time_stamper.unwrap(); + let mut buf = [0; 16]; + let written = time_stamper.write_to_bytes(&mut buf); + assert!(written.is_ok()); + let provider = TimeProvider::::from_bytes(&buf); + assert!(provider.is_ok()); + let provider = provider.unwrap(); + assert_eq!(provider.ccsds_days(), 0x108020); + assert_eq!(provider.ms_of_day(), 0); + } + + #[test] + fn test_large_days_field_read_invalid_ctor() { + let time_stamper = TimeProvider::new_with_u24_days(0x108020, 0); + assert!(time_stamper.is_ok()); + let time_stamper = time_stamper.unwrap(); + let mut buf = [0; 16]; + let written = time_stamper.write_to_bytes(&mut buf); + assert!(written.is_ok()); + let faulty_ctor = TimeProvider::::from_bytes(&buf); + assert!(faulty_ctor.is_err()); + let error = faulty_ctor.unwrap_err(); + if let TimestampError::CdsError(cds::CdsError::InvalidCtorForDaysOfLenInPreamble( + len_of_day, + )) = error + { + assert_eq!(len_of_day, LengthOfDaySegment::Long24Bits); + } else { + panic!("Wrong error type"); + } + } + + #[test] + fn test_write() { + let mut buf = [0; 16]; + let time_stamper_0 = TimeProvider::new_with_u16_days(0, 0); + let mut res = time_stamper_0.write_to_bytes(&mut buf); + assert!(res.is_ok()); + assert_eq!(buf[0], (CcsdsTimeCodes::Cds as u8) << 4); + assert_eq!( + u16::from_be_bytes(buf[1..3].try_into().expect("Byte conversion failed")), + 0 + ); + assert_eq!( + u32::from_be_bytes(buf[3..7].try_into().expect("Byte conversion failed")), + 0 + ); + let time_stamper_1 = TimeProvider::new_with_u16_days(u16::MAX - 1, u32::MAX - 1); + res = time_stamper_1.write_to_bytes(&mut buf); + assert!(res.is_ok()); + assert_eq!(buf[0], (CcsdsTimeCodes::Cds as u8) << 4); + assert_eq!( + u16::from_be_bytes(buf[1..3].try_into().expect("Byte conversion failed")), + u16::MAX - 1 + ); + assert_eq!( + u32::from_be_bytes(buf[3..7].try_into().expect("Byte conversion failed")), + u32::MAX - 1 + ); + } + + #[test] + fn test_faulty_write_buf_too_small() { + let mut buf = [0; 7]; + let time_stamper = TimeProvider::new_with_u16_days(u16::MAX - 1, u32::MAX - 1); + for i in 0..6 { + let res = time_stamper.write_to_bytes(&mut buf[0..i]); + assert!(res.is_err()); + match res.unwrap_err() { + ByteConversionError(ToSliceTooSmall(missmatch)) => { + assert_eq!(missmatch.found, i); + assert_eq!(missmatch.expected, 7); + } + _ => panic!( + "{}", + format!("Invalid error {:?} detected", res.unwrap_err()) + ), + } + } + } + + #[test] + fn test_faulty_read_buf_too_small() { + let buf = [0; 7]; + for i in 0..6 { + let res = TimeProvider::::from_bytes(&buf[0..i]); + assert!(res.is_err()); + let err = res.unwrap_err(); + match err { + ByteConversionError(e) => match e { + FromSliceTooSmall(missmatch) => { + assert_eq!(missmatch.found, i); + assert_eq!(missmatch.expected, 7); + } + _ => panic!("{}", format!("Invalid error {:?} detected", e)), + }, + _ => { + panic!("Unexpected error {:?}", err); + } + } + } + } + + #[test] + fn test_faulty_invalid_pfield() { + let mut buf = [0; 16]; + let time_stamper_0 = TimeProvider::new_with_u16_days(0, 0); + let res = time_stamper_0.write_to_bytes(&mut buf); + assert!(res.is_ok()); + buf[0] = 0; + let res = TimeProvider::::from_bytes(&buf); + assert!(res.is_err()); + let err = res.unwrap_err(); + match err { + InvalidTimeCode(code, raw) => { + assert_eq!(code, CcsdsTimeCodes::Cds); + assert_eq!(raw, 0); + } + _ => {} + } + } + + #[test] + fn test_reading() { + let mut buf = [0; 16]; + let time_stamper = TimeProvider::new_with_u16_days(u16::MAX - 1, u32::MAX - 1); + let res = time_stamper.write_to_bytes(&mut buf); + assert!(res.is_ok()); + assert_eq!(buf[0], (CcsdsTimeCodes::Cds as u8) << 4); + assert_eq!( + u16::from_be_bytes(buf[1..3].try_into().expect("Byte conversion failed")), + u16::MAX - 1 + ); + assert_eq!( + u32::from_be_bytes(buf[3..7].try_into().expect("Byte conversion failed")), + u32::MAX - 1 + ); + + let read_stamp: TimeProvider = + TimeProvider::from_bytes(&buf).expect("Reading timestamp failed"); + assert_eq!(read_stamp.ccsds_days(), u16::MAX - 1); + assert_eq!(read_stamp.ms_of_day(), u32::MAX - 1); + } + + #[test] + fn test_time_now() { + let timestamp_now = TimeProvider::from_now_with_u16_days().unwrap(); + let compare_stamp = Utc::now(); + let dt = timestamp_now.date_time().unwrap(); + if compare_stamp.year() > dt.year() { + assert_eq!(compare_stamp.year() - dt.year(), 1); + } else { + assert_eq!(dt.year(), compare_stamp.year()); + } + generic_dt_property_equality_check(dt.month(), compare_stamp.month(), 1, 12); + + assert_eq!(dt.day(), compare_stamp.day()); + if compare_stamp.day() < dt.day() { + assert!(dt.day() >= 28); + assert_eq!(compare_stamp.day(), 1); + } else if compare_stamp.day() > dt.day() { + assert_eq!(compare_stamp.day() - dt.day(), 1); + } else { + assert_eq!(compare_stamp.day(), dt.day()); + } + generic_dt_property_equality_check(dt.hour(), compare_stamp.hour(), 0, 23); + generic_dt_property_equality_check(dt.minute(), compare_stamp.minute(), 0, 59); + } + + #[test] + fn test_submillis_precision_micros() { + let mut time_stamper = TimeProvider::new_with_u16_days(0, 0); + time_stamper.set_submillis_precision(SubmillisPrecision::Microseconds(500)); + assert!(time_stamper.submillis_precision().is_some()); + if let SubmillisPrecision::Microseconds(micros) = + time_stamper.submillis_precision().unwrap() + { + assert_eq!(micros, 500); + } else { + panic!("Submillis precision was not set properly"); + } + let mut write_buf: [u8; 16] = [0; 16]; + let written = time_stamper + .write_to_bytes(&mut write_buf) + .expect("Writing timestamp failed"); + assert_eq!(written, 9); + let cross_check: u16 = 500; + assert_eq!(write_buf[7..9], cross_check.to_be_bytes()); + } + + #[test] + fn test_submillis_precision_picos() { + let mut time_stamper = TimeProvider::new_with_u16_days(0, 0); + time_stamper.set_submillis_precision(SubmillisPrecision::Picoseconds(5e8 as u32)); + assert!(time_stamper.submillis_precision().is_some()); + if let SubmillisPrecision::Picoseconds(ps) = time_stamper.submillis_precision().unwrap() { + assert_eq!(ps, 5e8 as u32); + } else { + panic!("Submillis precision was not set properly"); + } + let mut write_buf: [u8; 16] = [0; 16]; + let written = time_stamper + .write_to_bytes(&mut write_buf) + .expect("Writing timestamp failed"); + assert_eq!(written, 11); + let cross_check: u32 = 5e8 as u32; + assert_eq!(write_buf[7..11], cross_check.to_be_bytes()); + } + + #[test] + fn read_stamp_with_ps_submillis_precision() { + let mut time_stamper = TimeProvider::new_with_u16_days(0, 0); + time_stamper.set_submillis_precision(SubmillisPrecision::Picoseconds(5e8 as u32)); + let mut write_buf: [u8; 16] = [0; 16]; + let written = time_stamper + .write_to_bytes(&mut write_buf) + .expect("Writing timestamp failed"); + assert_eq!(written, 11); + let stamp_deserialized = TimeProvider::::from_bytes(&write_buf); + assert!(stamp_deserialized.is_ok()); + let stamp_deserialized = stamp_deserialized.unwrap(); + assert_eq!(stamp_deserialized.len_as_bytes(), 11); + assert!(stamp_deserialized.submillis_precision().is_some()); + let submillis_rec = stamp_deserialized.submillis_precision().unwrap(); + if let SubmillisPrecision::Picoseconds(ps) = submillis_rec { + assert_eq!(ps, 5e8 as u32); + } else { + panic!("Wrong precision field detected"); + } + } + + #[test] + fn read_stamp_with_us_submillis_precision() { + let mut time_stamper = TimeProvider::new_with_u16_days(0, 0); + time_stamper.set_submillis_precision(SubmillisPrecision::Microseconds(500)); + let mut write_buf: [u8; 16] = [0; 16]; + let written = time_stamper + .write_to_bytes(&mut write_buf) + .expect("Writing timestamp failed"); + assert_eq!(written, 9); + let stamp_deserialized = TimeProvider::::from_bytes(&write_buf); + assert!(stamp_deserialized.is_ok()); + let stamp_deserialized = stamp_deserialized.unwrap(); + assert_eq!(stamp_deserialized.len_as_bytes(), 9); + assert!(stamp_deserialized.submillis_precision().is_some()); + let submillis_rec = stamp_deserialized.submillis_precision().unwrap(); + if let SubmillisPrecision::Microseconds(us) = submillis_rec { + assert_eq!(us, 500); + } else { + panic!("Wrong precision field detected"); + } + } + + #[test] + #[cfg(feature = "serde")] + fn test_serialization() { + let stamp_now = TimeProvider::from_now_with_u16_days().expect("Error retrieving time"); + let val = to_allocvec(&stamp_now).expect("Serializing timestamp failed"); + assert!(val.len() > 0); + let stamp_deser: TimeProvider = from_bytes(&val).expect("Stamp deserialization failed"); + assert_eq!(stamp_deser, stamp_now); + } + + fn generic_dt_property_equality_check(first: u32, second: u32, start: u32, end: u32) { + if second < first { + assert_eq!(second, start); + assert_eq!(first, end); + } else if second > first { + assert_eq!(second - first, 1); + } else { + assert_eq!(first, second); + } + } +} diff --git a/src/time/cuc.rs b/src/time/cuc.rs new file mode 100644 index 0000000..387a36f --- /dev/null +++ b/src/time/cuc.rs @@ -0,0 +1,301 @@ +use super::*; +use core::fmt::Debug; + +const MIN_CUC_LEN: usize = 2; + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum CucError { + InvalidCounterWidth(u8), + InvalidFractionWidth(u8), + InvalidCounter(u8, u64), + InvalidFractions(u8, u64), +} + +impl Display for CucError { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + match self { + CucError::InvalidCounterWidth(w) => { + write!(f, "invalid cuc counter byte width {}", w) + } + CucError::InvalidFractionWidth(w) => { + write!(f, "invalid cuc fractional part byte width {}", w) + } + CucError::InvalidCounter(w, c) => { + write!(f, "invalid cuc counter {} for width {}", c, w) + } + CucError::InvalidFractions(w, c) => { + write!(f, "invalid cuc fractional part {} for width {}", c, w) + } + } + } +} + +#[cfg(feature = "std")] +impl Error for CucError {} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct WidthCounterPair(u8, u32); + +/// This provider uses the CCSDS epoch. Furthermore the preamble field only has one byte, +/// which allows a time code representation through the year 2094. +/// +/// More specifically, only having a preamble field of one byte limits the width of the counter +/// type (generally seconds) to 4 bytes and the width of the fractions type to 3 bytes. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct TimeProviderCcsdsEpoch { + pfield: u8, + counter: WidthCounterPair, + fractions: Option, +} + +#[inline] +pub fn pfield_len(pfield: u8) -> usize { + if ((pfield >> 7) & 0b1) == 1 { + return 2; + } + 1 +} + +impl TimeProviderCcsdsEpoch { + fn build_p_field(counter_width: u8, fractions_width: Option) -> u8 { + let mut pfield = (CcsdsTimeCodes::CucCcsdsEpoch as u8) << 4; + if !(1..=4).contains(&counter_width) { + // Okay to panic here, this function is private and all input values should + // have been sanitized + panic!("invalid counter width {} for cuc timestamp", counter_width); + } + pfield |= (counter_width - 1) << 3; + if let Some(fractions_width) = fractions_width { + if !(1..=3).contains(&fractions_width) { + // Okay to panic here, this function is private and all input values should + // have been sanitized + panic!( + "invalid fractions width {} for cuc timestamp", + fractions_width + ); + } + pfield |= fractions_width; + } + pfield + } + + fn update_p_field_fractions(&mut self) { + self.pfield &= !(0b11); + if let Some(fractions) = self.fractions { + self.pfield |= fractions.0; + } + } + + pub fn len_packed(&self) -> usize { + Self::len_packed_from_pfield(self.pfield) + } + + #[inline] + pub fn len_cntr_from_pfield(pfield: u8) -> u8 { + ((pfield >> 2) & 0b11) + 1 + } + + #[inline] + pub fn len_fractions_from_pfield(pfield: u8) -> u8 { + pfield & 0b11 + } + + /// This returns the length of the individual components of the CUC timestamp in addition + /// to the total size. + /// + /// This function will return a tuple where the first value is the byte width of the + /// counter, the second value is the byte width of the fractional part, and the third + /// components is the total size. + pub fn len_components_and_total_from_pfield(pfield: u8) -> (u8, u8, usize) { + let base_len: usize = 1; + let cntr_len = Self::len_cntr_from_pfield(pfield); + let fractions_len = Self::len_fractions_from_pfield(pfield); + ( + cntr_len, + fractions_len, + base_len + cntr_len as usize + fractions_len as usize, + ) + } + + pub fn len_packed_from_pfield(pfield: u8) -> usize { + let mut base_len: usize = 1; + base_len += Self::len_cntr_from_pfield(pfield) as usize; + base_len += Self::len_fractions_from_pfield(pfield) as usize; + base_len + } + + /// Verifies the raw width parameter and returns the actual length, which is the raw + /// value plus 1. + fn verify_counter_width(width: u8) -> Result<(), CucError> { + if width == 0 || width > 4 { + return Err(CucError::InvalidCounterWidth(width)); + } + Ok(()) + } + + fn verify_fractions_width(width: u8) -> Result<(), CucError> { + if width > 3 { + return Err(CucError::InvalidFractionWidth(width)); + } + Ok(()) + } + + fn verify_fractions_value(val: WidthCounterPair) -> Result<(), CucError> { + if val.1 > 2u32.pow((val.0 as u32) * 8) - 1 { + return Err(CucError::InvalidFractions(val.0, val.1 as u64)); + } + Ok(()) + } +} + +impl TimeProviderCcsdsEpoch { + pub fn new_default(counter: u32) -> Self { + // These values are definitely valid, so it is okay to unwrap here. + Self::new(WidthCounterPair(4, counter), None).unwrap() + } + pub fn new_u16_counter(counter: u16) -> Self { + // These values are definitely valid, so it is okay to unwrap here. + Self::new(WidthCounterPair(2, counter as u32), None).unwrap() + } + + pub fn set_fractions(&mut self, fractions: WidthCounterPair) -> Result<(), CucError> { + Self::verify_fractions_width(fractions.0)?; + Self::verify_fractions_value(fractions)?; + self.fractions = Some(fractions); + self.update_p_field_fractions(); + Ok(()) + } + + pub fn new( + counter: WidthCounterPair, + fractions: Option, + ) -> Result { + Self::verify_counter_width(counter.0)?; + if counter.1 > 2u32.pow(counter.0 as u32 * 8) - 1 { + return Err(CucError::InvalidCounter(counter.0, counter.1 as u64)); + } + if let Some(fractions) = fractions { + Self::verify_fractions_width(fractions.0)?; + Self::verify_fractions_value(fractions)?; + } + Ok(Self { + pfield: Self::build_p_field(counter.0, fractions.map(|v| v.0)), + counter, + fractions, + }) + } +} + +impl TimeReader for TimeProviderCcsdsEpoch { + fn from_bytes(buf: &[u8]) -> Result + where + Self: Sized, + { + if buf.len() < MIN_CUC_LEN { + return Err(TimestampError::ByteConversionError( + ByteConversionError::FromSliceTooSmall(SizeMissmatch { + expected: MIN_CUC_LEN, + found: buf.len(), + }), + )); + } + let (cntr_len, fractions_len, total_len) = + Self::len_components_and_total_from_pfield(buf[0]); + if buf.len() < total_len { + return Err(TimestampError::ByteConversionError( + ByteConversionError::FromSliceTooSmall(SizeMissmatch { + expected: total_len, + found: buf.len(), + }), + )); + } + let mut current_idx = 1; + let counter = match cntr_len { + 1 => buf[current_idx] as u32, + 2 => u16::from_be_bytes(buf[current_idx..current_idx + 2].try_into().unwrap()) as u32, + 3 => { + let mut tmp_buf: [u8; 4] = [0; 4]; + tmp_buf[1..4].copy_from_slice(&buf[current_idx..current_idx + 3]); + u32::from_be_bytes(tmp_buf) as u32 + } + 4 => u32::from_be_bytes(buf[current_idx..current_idx + 4].try_into().unwrap()) as u32, + _ => panic!("unreachable match arm"), + }; + current_idx += cntr_len as usize; + let mut fractions = None; + if fractions_len > 0 { + match fractions_len { + 1 => fractions = Some(WidthCounterPair(fractions_len, buf[current_idx] as u32)), + 2 => { + fractions = Some(WidthCounterPair( + fractions_len, + u16::from_be_bytes(buf[current_idx..current_idx + 2].try_into().unwrap()) + as u32, + )) + } + 3 => { + let mut tmp_buf: [u8; 4] = [0; 4]; + tmp_buf[1..4].copy_from_slice(&buf[current_idx..current_idx + 3]); + fractions = Some(WidthCounterPair( + fractions_len, + u32::from_be_bytes(tmp_buf) as u32, + )) + } + _ => panic!("unreachable match arm"), + } + } + let provider = Self::new(WidthCounterPair(cntr_len, counter), fractions)?; + Ok(provider) + } +} + +impl TimeWriter for TimeProviderCcsdsEpoch { + fn write_to_bytes(&self, bytes: &mut [u8]) -> Result { + // Cross check the sizes of the counters against byte widths in the ctor + if bytes.len() < self.len_packed() { + return Err(TimestampError::ByteConversionError( + ByteConversionError::ToSliceTooSmall(SizeMissmatch { + found: bytes.len(), + expected: self.len_packed(), + }), + )); + } + bytes[0] = self.pfield; + let mut current_idx: usize = 1; + match self.counter.0 { + 1 => { + bytes[current_idx] = self.counter.1 as u8; + } + 2 => { + bytes[current_idx..current_idx + 2] + .copy_from_slice(&(self.counter.1 as u16).to_be_bytes()); + } + 3 => { + bytes[current_idx..current_idx + 3] + .copy_from_slice(&self.counter.1.to_be_bytes()[1..4]); + } + 4 => { + bytes[current_idx..current_idx + 4].copy_from_slice(&self.counter.1.to_be_bytes()); + } + // Should never happen + _ => panic!("invalid counter width value"), + } + current_idx += self.counter.0 as usize; + if let Some(fractions) = self.fractions { + match fractions.0 { + 1 => bytes[current_idx] = fractions.1 as u8, + 2 => bytes[current_idx..current_idx + 2] + .copy_from_slice(&(fractions.1 as u16).to_be_bytes()), + 3 => bytes[current_idx..current_idx + 3] + .copy_from_slice(&fractions.1.to_be_bytes()[1..4]), + // Should also never happen + _ => panic!("invalid fractions value"), + } + current_idx += fractions.0 as usize; + } + Ok(current_idx) + } +} diff --git a/src/time/mod.rs b/src/time/mod.rs new file mode 100644 index 0000000..b1f9661 --- /dev/null +++ b/src/time/mod.rs @@ -0,0 +1,221 @@ +//! CCSDS Time Code Formats according to [CCSDS 301.0-B-4](https://public.ccsds.org/Pubs/301x0b4e1.pdf) +use crate::{ByteConversionError, SizeMissmatch}; +use chrono::{DateTime, LocalResult, TimeZone, Utc}; +use core::fmt::{Display, Formatter}; + +#[allow(unused_imports)] +#[cfg(not(feature = "std"))] +use num_traits::float::FloatCore; + +use crate::time::cuc::CucError; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +#[cfg(feature = "std")] +use std::error::Error; +#[cfg(feature = "std")] +use std::time::{SystemTime, SystemTimeError}; + +pub mod ascii; +pub mod cds; +pub mod cuc; + +pub const DAYS_CCSDS_TO_UNIX: i32 = -4383; +pub const SECONDS_PER_DAY: u32 = 86400; + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum CcsdsTimeCodes { + CucCcsdsEpoch = 0b001, + CucAgencyEpoch = 0b010, + Cds = 0b100, + Ccs = 0b101, + AgencyDefined = 0b110, +} + +impl TryFrom for CcsdsTimeCodes { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + x if x == CcsdsTimeCodes::CucCcsdsEpoch as u8 => Ok(CcsdsTimeCodes::CucCcsdsEpoch), + x if x == CcsdsTimeCodes::CucAgencyEpoch as u8 => Ok(CcsdsTimeCodes::CucAgencyEpoch), + x if x == CcsdsTimeCodes::Cds as u8 => Ok(CcsdsTimeCodes::Cds), + x if x == CcsdsTimeCodes::Ccs as u8 => Ok(CcsdsTimeCodes::Ccs), + x if x == CcsdsTimeCodes::AgencyDefined as u8 => Ok(CcsdsTimeCodes::AgencyDefined), + _ => Err(()), + } + } +} + +/// Retrieve the CCSDS time code from the p-field. If no valid time code identifier is found, the +/// value of the raw time code identification field is returned. +pub fn ccsds_time_code_from_p_field(pfield: u8) -> Result { + let raw_bits = (pfield >> 4) & 0b111; + CcsdsTimeCodes::try_from(raw_bits).map_err(|_| raw_bits) +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum TimestampError { + /// Contains tuple where first value is the expected time code and the second + /// value is the found raw value + InvalidTimeCode(CcsdsTimeCodes, u8), + ByteConversionError(ByteConversionError), + CdsError(cds::CdsError), + CucError(cuc::CucError), + CustomEpochNotSupported, +} + +impl From for TimestampError { + fn from(e: cds::CdsError) -> Self { + TimestampError::CdsError(e) + } +} + +impl From for TimestampError { + fn from(e: CucError) -> Self { + TimestampError::CucError(e) + } +} + +#[cfg(feature = "std")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] +#[derive(Debug, Clone)] +pub enum StdTimestampError { + SystemTimeError(SystemTimeError), + TimestampError(TimestampError), +} + +#[cfg(feature = "std")] +impl From for StdTimestampError { + fn from(v: TimestampError) -> Self { + Self::TimestampError(v) + } +} + +#[cfg(feature = "std")] +impl From for StdTimestampError { + fn from(v: SystemTimeError) -> Self { + Self::SystemTimeError(v) + } +} + +impl Display for TimestampError { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + match self { + TimestampError::InvalidTimeCode(time_code, raw_val) => { + write!( + f, + "invalid raw time code value {} for time code {:?}", + raw_val, time_code + ) + } + TimestampError::CdsError(e) => { + write!(f, "cds error {}", e) + } + TimestampError::CucError(e) => { + write!(f, "cuc error {}", e) + } + TimestampError::ByteConversionError(e) => { + write!(f, "byte conversion error {}", e) + } + TimestampError::CustomEpochNotSupported => { + write!(f, "custom epochs are not supported") + } + } + } +} + +#[cfg(feature = "std")] +impl Error for TimestampError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + TimestampError::ByteConversionError(e) => Some(e), + TimestampError::CdsError(e) => Some(e), + TimestampError::CucError(e) => Some(e), + _ => None, + } + } +} + +#[cfg(feature = "std")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] +pub fn seconds_since_epoch() -> f64 { + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("System time generation failed") + .as_secs_f64() +} + +/// Convert UNIX days to CCSDS days +/// +/// - CCSDS epoch: 1958 January 1 +/// - UNIX Epoch: 1970 January 1 +pub const fn unix_to_ccsds_days(unix_days: i64) -> i64 { + unix_days - DAYS_CCSDS_TO_UNIX as i64 +} + +/// Convert CCSDS days to UNIX days +/// +/// - CCSDS epoch: 1958 January 1 +/// - UNIX Epoch: 1970 January 1 +pub const fn ccsds_to_unix_days(ccsds_days: i64) -> i64 { + ccsds_days + DAYS_CCSDS_TO_UNIX as i64 +} + +#[cfg(feature = "std")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] +pub fn ms_of_day_using_sysclock() -> u32 { + ms_of_day(seconds_since_epoch()) +} + +pub fn ms_of_day(seconds_since_epoch: f64) -> u32 { + let fraction_ms = seconds_since_epoch - seconds_since_epoch.floor(); + let ms_of_day: u32 = (((seconds_since_epoch.floor() as u32 % SECONDS_PER_DAY) * 1000) as f64 + + fraction_ms) + .floor() as u32; + ms_of_day +} + +pub trait TimeWriter { + /// Generic function to convert write a timestamp into a raw buffer. + /// Returns the number of written bytes on success. + fn write_to_bytes(&self, bytes: &mut [u8]) -> Result; +} + +pub trait TimeReader { + fn from_bytes(buf: &[u8]) -> Result + where + Self: Sized; +} + +/// Trait for generic CCSDS time providers. +pub trait CcsdsTimeProvider { + fn len_as_bytes(&self) -> usize; + + /// Returns the pfield of the time provider. The pfield can have one or two bytes depending + /// on the extension bit (first bit). The time provider should returns a tuple where the first + /// entry denotes the length of the pfield and the second entry is the value of the pfield + /// in big endian format. + fn p_field(&self) -> (usize, [u8; 2]); + fn ccdsd_time_code(&self) -> CcsdsTimeCodes; + fn unix_seconds(&self) -> i64; + fn date_time(&self) -> Option>; +} + +#[cfg(all(test, feature = "std"))] +mod tests { + use super::*; + + #[test] + fn test_days_conversion() { + assert_eq!(unix_to_ccsds_days(DAYS_CCSDS_TO_UNIX.into()), 0); + assert_eq!(ccsds_to_unix_days(0), DAYS_CCSDS_TO_UNIX.into()); + } + + #[test] + fn test_get_current_time() { + let sec_floats = seconds_since_epoch(); + assert!(sec_floats > 0.0); + } +} -- 2.34.1 From 5958d19eb4563f01fe96741cecfecc980a5aed74 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 9 Dec 2022 13:50:04 +0100 Subject: [PATCH 04/23] continue cuc impl --- src/time/cuc.rs | 163 ++++++++++++++++++++++++++++++++++++++++-------- src/time/mod.rs | 19 ++++++ 2 files changed, 157 insertions(+), 25 deletions(-) diff --git a/src/time/cuc.rs b/src/time/cuc.rs index 387a36f..f2ba7d9 100644 --- a/src/time/cuc.rs +++ b/src/time/cuc.rs @@ -3,13 +3,53 @@ use core::fmt::Debug; const MIN_CUC_LEN: usize = 2; +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum FractionalResolution { + /// No fractional part, only second resolution + Seconds = 0, + /// 256 fractional parts, resulting in 1/255 ~= 4 ms fractional resolution + FourMs = 1, + /// 65535 fractional parts, resulting in 1/65535 ~= 15 us fractional resolution + FifteenUs = 2, + /// 16777215 fractional parts, resulting in 1/16777215 ~= 60 ns fractional resolution + SixtyNs = 3, +} + +impl TryFrom for FractionalResolution { + type Error = (); + + fn try_from(v: u8) -> Result { + match v { + 0 => Ok(FractionalResolution::Seconds), + 1 => Ok(FractionalResolution::FourMs), + 2 => Ok(FractionalResolution::FifteenUs), + 3 => Ok(FractionalResolution::SixtyNs), + _ => Err(()), + } + } +} + +/// Please note that this function will panic if the fractional value is not smaller than +/// the maximum number of fractions allowed for the particular resolution. +/// (e.g. passing 270 when the resolution only allows 255 values). +pub fn convert_fractional_part_to_ns(fractional_part: FractionalPart) -> u64 { + let div = fractional_res_to_div(fractional_part.0); + assert!(fractional_part.1 < div); + 10_u64.pow(9) * fractional_part.1 as u64 / div as u64 +} + +pub const fn fractional_res_to_div(res: FractionalResolution) -> u32 { + 2_u32.pow(8 * res as u32) - 1 +} + #[derive(Copy, Clone, PartialEq, Eq, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum CucError { InvalidCounterWidth(u8), - InvalidFractionWidth(u8), + InvalidFractionResolution(FractionalResolution), InvalidCounter(u8, u64), - InvalidFractions(u8, u64), + InvalidFractions(FractionalResolution, u64), } impl Display for CucError { @@ -18,14 +58,14 @@ impl Display for CucError { CucError::InvalidCounterWidth(w) => { write!(f, "invalid cuc counter byte width {}", w) } - CucError::InvalidFractionWidth(w) => { - write!(f, "invalid cuc fractional part byte width {}", w) + CucError::InvalidFractionResolution(w) => { + write!(f, "invalid cuc fractional part byte width {:?}", w) } CucError::InvalidCounter(w, c) => { write!(f, "invalid cuc counter {} for width {}", c, w) } CucError::InvalidFractions(w, c) => { - write!(f, "invalid cuc fractional part {} for width {}", c, w) + write!(f, "invalid cuc fractional part {} for width {:?}", c, w) } } } @@ -37,6 +77,9 @@ impl Error for CucError {} #[derive(Copy, Clone, PartialEq, Eq, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct WidthCounterPair(u8, u32); +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct FractionalPart(FractionalResolution, u32); /// This provider uses the CCSDS epoch. Furthermore the preamble field only has one byte, /// which allows a time code representation through the year 2094. @@ -48,7 +91,7 @@ pub struct WidthCounterPair(u8, u32); pub struct TimeProviderCcsdsEpoch { pfield: u8, counter: WidthCounterPair, - fractions: Option, + fractions: Option, } #[inline] @@ -60,7 +103,7 @@ pub fn pfield_len(pfield: u8) -> usize { } impl TimeProviderCcsdsEpoch { - fn build_p_field(counter_width: u8, fractions_width: Option) -> u8 { + fn build_p_field(counter_width: u8, fractions_width: Option) -> u8 { let mut pfield = (CcsdsTimeCodes::CucCcsdsEpoch as u8) << 4; if !(1..=4).contains(&counter_width) { // Okay to panic here, this function is private and all input values should @@ -69,15 +112,15 @@ impl TimeProviderCcsdsEpoch { } pfield |= (counter_width - 1) << 3; if let Some(fractions_width) = fractions_width { - if !(1..=3).contains(&fractions_width) { + if !(1..=3).contains(&(fractions_width as u8)) { // Okay to panic here, this function is private and all input values should // have been sanitized panic!( - "invalid fractions width {} for cuc timestamp", + "invalid fractions width {:?} for cuc timestamp", fractions_width ); } - pfield |= fractions_width; + pfield |= fractions_width as u8; } pfield } @@ -85,7 +128,7 @@ impl TimeProviderCcsdsEpoch { fn update_p_field_fractions(&mut self) { self.pfield &= !(0b11); if let Some(fractions) = self.fractions { - self.pfield |= fractions.0; + self.pfield |= fractions.0 as u8; } } @@ -136,14 +179,14 @@ impl TimeProviderCcsdsEpoch { Ok(()) } - fn verify_fractions_width(width: u8) -> Result<(), CucError> { - if width > 3 { - return Err(CucError::InvalidFractionWidth(width)); + fn verify_fractions_width(width: FractionalResolution) -> Result<(), CucError> { + if width as u8 > 3 { + return Err(CucError::InvalidFractionResolution(width)); } Ok(()) } - fn verify_fractions_value(val: WidthCounterPair) -> Result<(), CucError> { + fn verify_fractions_value(val: FractionalPart) -> Result<(), CucError> { if val.1 > 2u32.pow((val.0 as u32) * 8) - 1 { return Err(CucError::InvalidFractions(val.0, val.1 as u64)); } @@ -156,12 +199,41 @@ impl TimeProviderCcsdsEpoch { // These values are definitely valid, so it is okay to unwrap here. Self::new(WidthCounterPair(4, counter), None).unwrap() } + pub fn new_u16_counter(counter: u16) -> Self { // These values are definitely valid, so it is okay to unwrap here. Self::new(WidthCounterPair(2, counter as u32), None).unwrap() } - pub fn set_fractions(&mut self, fractions: WidthCounterPair) -> Result<(), CucError> { + /// This function will return the current time as a CUC timestamp. + /// The counter width will always be set to 4 bytes because the normal CCSDS epoch will overflow + /// when using less than that. + #[cfg(feature = "std")] + pub fn from_now(fraction_resolution: FractionalResolution) -> Result { + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; + let ccsds_epoch = unix_epoch_to_ccsds_epoch(now.as_secs()); + let mut fractions = None; + if fraction_resolution != FractionalResolution::Seconds { + let fractional_part = 10_u64.pow(9) * now.subsec_nanos() as u64 + / fractional_res_to_div(fraction_resolution) as u64; + fractions = Some(FractionalPart(fraction_resolution, fractional_part as u32)); + } + Ok(Self { + pfield: 0, + counter: WidthCounterPair(4, ccsds_epoch as u32), + fractions, + }) + } + + pub fn width_counter_pair(&self) -> WidthCounterPair { + self.counter + } + + pub fn width_fractions_pair(&self) -> Option { + self.fractions + } + + pub fn set_fractions(&mut self, fractions: FractionalPart) -> Result<(), CucError> { Self::verify_fractions_width(fractions.0)?; Self::verify_fractions_value(fractions)?; self.fractions = Some(fractions); @@ -171,7 +243,7 @@ impl TimeProviderCcsdsEpoch { pub fn new( counter: WidthCounterPair, - fractions: Option, + fractions: Option, ) -> Result { Self::verify_counter_width(counter.0)?; if counter.1 > 2u32.pow(counter.0 as u32 * 8) - 1 { @@ -228,10 +300,15 @@ impl TimeReader for TimeProviderCcsdsEpoch { let mut fractions = None; if fractions_len > 0 { match fractions_len { - 1 => fractions = Some(WidthCounterPair(fractions_len, buf[current_idx] as u32)), + 1 => { + fractions = Some(FractionalPart( + fractions_len.try_into().unwrap(), + buf[current_idx] as u32, + )) + } 2 => { - fractions = Some(WidthCounterPair( - fractions_len, + fractions = Some(FractionalPart( + fractions_len.try_into().unwrap(), u16::from_be_bytes(buf[current_idx..current_idx + 2].try_into().unwrap()) as u32, )) @@ -239,8 +316,8 @@ impl TimeReader for TimeProviderCcsdsEpoch { 3 => { let mut tmp_buf: [u8; 4] = [0; 4]; tmp_buf[1..4].copy_from_slice(&buf[current_idx..current_idx + 3]); - fractions = Some(WidthCounterPair( - fractions_len, + fractions = Some(FractionalPart( + fractions_len.try_into().unwrap(), u32::from_be_bytes(tmp_buf) as u32, )) } @@ -286,10 +363,10 @@ impl TimeWriter for TimeProviderCcsdsEpoch { current_idx += self.counter.0 as usize; if let Some(fractions) = self.fractions { match fractions.0 { - 1 => bytes[current_idx] = fractions.1 as u8, - 2 => bytes[current_idx..current_idx + 2] + FractionalResolution::FourMs => bytes[current_idx] = fractions.1 as u8, + FractionalResolution::FifteenUs => bytes[current_idx..current_idx + 2] .copy_from_slice(&(fractions.1 as u16).to_be_bytes()), - 3 => bytes[current_idx..current_idx + 3] + FractionalResolution::SixtyNs => bytes[current_idx..current_idx + 3] .copy_from_slice(&fractions.1.to_be_bytes()[1..4]), // Should also never happen _ => panic!("invalid fractions value"), @@ -299,3 +376,39 @@ impl TimeWriter for TimeProviderCcsdsEpoch { Ok(current_idx) } } + +#[cfg(test)] +mod tests { + use crate::time::cuc::{convert_fractional_part_to_ns, FractionalPart, FractionalResolution}; + + #[test] + fn test_basic() {} + + #[test] + fn test_fractional_converter() { + let ns = convert_fractional_part_to_ns(FractionalPart(FractionalResolution::FourMs, 2)); + // The formula for this is 2/255 * 10e9 = 7.843.137. + assert_eq!(ns, 7843137); + // This is the largest value we should be able to pass without this function panicking. + let ns = convert_fractional_part_to_ns(FractionalPart( + FractionalResolution::SixtyNs, + 2_u32.pow(24) - 2, + )); + assert_eq!(ns, 999999940); + } + + #[test] + #[should_panic] + fn test_fractional_converter_invalid_input() { + convert_fractional_part_to_ns(FractionalPart(FractionalResolution::FourMs, 256)); + } + + #[test] + #[should_panic] + fn test_fractional_converter_invalid_input_2() { + convert_fractional_part_to_ns(FractionalPart( + FractionalResolution::SixtyNs, + 2_u32.pow(32) - 1, + )); + } +} diff --git a/src/time/mod.rs b/src/time/mod.rs index b1f9661..f851880 100644 --- a/src/time/mod.rs +++ b/src/time/mod.rs @@ -163,6 +163,12 @@ pub const fn ccsds_to_unix_days(ccsds_days: i64) -> i64 { ccsds_days + DAYS_CCSDS_TO_UNIX as i64 } +/// Similar to [unix_to_ccsds_days] but converts the epoch instead, which is the number of elpased +/// seconds since the CCSDS and UNIX epoch times. +pub const fn unix_epoch_to_ccsds_epoch(unix_epoch: u64) -> u64 { + (unix_epoch as i64 - (DAYS_CCSDS_TO_UNIX as i64 * SECONDS_PER_DAY as i64)) as u64 +} + #[cfg(feature = "std")] #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] pub fn ms_of_day_using_sysclock() -> u32 { @@ -218,4 +224,17 @@ mod tests { let sec_floats = seconds_since_epoch(); assert!(sec_floats > 0.0); } + + #[test] + fn test_ccsds_epoch() { + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let unix_epoch = now.as_secs(); + let ccsds_epoch = unix_epoch_to_ccsds_epoch(now.as_secs()); + assert!(ccsds_epoch > unix_epoch); + assert_eq!((ccsds_epoch - unix_epoch) % SECONDS_PER_DAY as u64, 0); + let days_diff = (ccsds_epoch - unix_epoch) / SECONDS_PER_DAY as u64; + assert_eq!(days_diff, -DAYS_CCSDS_TO_UNIX as u64); + } } -- 2.34.1 From 8d0de0dce449ef5f864775af113083eccb6ec4c0 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 9 Dec 2022 16:51:48 +0100 Subject: [PATCH 05/23] continued cuc impl --- src/time/cuc.rs | 136 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 125 insertions(+), 11 deletions(-) diff --git a/src/time/cuc.rs b/src/time/cuc.rs index f2ba7d9..272e538 100644 --- a/src/time/cuc.rs +++ b/src/time/cuc.rs @@ -43,6 +43,28 @@ pub const fn fractional_res_to_div(res: FractionalResolution) -> u32 { 2_u32.pow(8 * res as u32) - 1 } +/// Calculate the fractional part for a given resolution and subsecond nanoseconds. +/// Please note that this function will panic if the passed nanoseconds exceeds 1 second +/// as a nanosecond (10 to the power of 9). Furthermore, it will return [None] if the +/// given resolution is [FractionalResolution::Seconds]. +pub fn fractional_part_from_subsec_ns( + res: FractionalResolution, + ns: u64, +) -> Option { + if res == FractionalResolution::Seconds { + return None; + } + let sec_as_ns = 10_u64.pow(9); + if ns > sec_as_ns { + panic!("passed nanosecond value larger than 1 second"); + } + // First determine the nanoseconds for the smallest segment given the resolution. + // Then divide by that to find out the fractional part. An integer division floors + // which is what we want here. + let fractional_part = ns / (sec_as_ns / fractional_res_to_div(res) as u64); + Some(FractionalPart(res, fractional_part as u32)) +} + #[derive(Copy, Clone, PartialEq, Eq, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum CucError { @@ -195,14 +217,55 @@ impl TimeProviderCcsdsEpoch { } impl TimeProviderCcsdsEpoch { - pub fn new_default(counter: u32) -> Self { + pub fn new(counter: u32) -> Self { // These values are definitely valid, so it is okay to unwrap here. - Self::new(WidthCounterPair(4, counter), None).unwrap() + Self::new_generic(WidthCounterPair(4, counter), None).unwrap() + } + + pub fn new_with_coarse_fractions(counter: u32, subsec_fractions: u8) -> Self { + // These values are definitely valid, so it is okay to unwrap here. + Self::new_generic( + WidthCounterPair(4, counter), + Some(FractionalPart( + FractionalResolution::FourMs, + subsec_fractions as u32, + )), + ) + .unwrap() + } + + pub fn new_with_submillis_fractions(counter: u32, subsec_fractions: u16) -> Self { + // These values are definitely valid, so it is okay to unwrap here. + Self::new_generic( + WidthCounterPair(4, counter), + Some(FractionalPart( + FractionalResolution::FifteenUs, + subsec_fractions as u32, + )), + ) + .unwrap() + } + + pub fn new_with_fine_fractions(counter: u32, subsec_fractions: u32) -> Result { + Self::new_generic( + WidthCounterPair(4, counter), + Some(FractionalPart( + FractionalResolution::SixtyNs, + subsec_fractions as u32, + )), + ) + } + + pub fn new_with_fractions( + counter: u32, + fractions: Option, + ) -> Result { + Self::new_generic(WidthCounterPair(4, counter), fractions) } pub fn new_u16_counter(counter: u16) -> Self { // These values are definitely valid, so it is okay to unwrap here. - Self::new(WidthCounterPair(2, counter as u32), None).unwrap() + Self::new_generic(WidthCounterPair(2, counter as u32), None).unwrap() } /// This function will return the current time as a CUC timestamp. @@ -212,12 +275,8 @@ impl TimeProviderCcsdsEpoch { pub fn from_now(fraction_resolution: FractionalResolution) -> Result { let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; let ccsds_epoch = unix_epoch_to_ccsds_epoch(now.as_secs()); - let mut fractions = None; - if fraction_resolution != FractionalResolution::Seconds { - let fractional_part = 10_u64.pow(9) * now.subsec_nanos() as u64 - / fractional_res_to_div(fraction_resolution) as u64; - fractions = Some(FractionalPart(fraction_resolution, fractional_part as u32)); - } + let fractions = + fractional_part_from_subsec_ns(fraction_resolution, now.subsec_nanos() as u64); Ok(Self { pfield: 0, counter: WidthCounterPair(4, ccsds_epoch as u32), @@ -225,6 +284,19 @@ impl TimeProviderCcsdsEpoch { }) } + #[cfg(feature = "std")] + pub fn update_from_now(&mut self) -> Result<(), StdTimestampError> { + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; + self.counter.1 = unix_epoch_to_ccsds_epoch(now.as_secs()) as u32; + if self.fractions.is_some() { + self.fractions = fractional_part_from_subsec_ns( + self.fractions.unwrap().0, + now.subsec_nanos() as u64, + ); + } + Ok(()) + } + pub fn width_counter_pair(&self) -> WidthCounterPair { self.counter } @@ -241,7 +313,20 @@ impl TimeProviderCcsdsEpoch { Ok(()) } - pub fn new( + /// Set a fractional resolution. Please note that this function will reset the fractional value + /// to 0 if the resolution changes. + pub fn set_fractional_resolution(&mut self, res: FractionalResolution) { + if res == FractionalResolution::Seconds { + self.fractions = None; + } + if let Some(existing_fractions) = self.fractions { + if existing_fractions.0 != res { + self.fractions = Some(FractionalPart(res, 0)); + } + } + } + + pub fn new_generic( counter: WidthCounterPair, fractions: Option, ) -> Result { @@ -324,7 +409,7 @@ impl TimeReader for TimeProviderCcsdsEpoch { _ => panic!("unreachable match arm"), } } - let provider = Self::new(WidthCounterPair(cntr_len, counter), fractions)?; + let provider = Self::new_generic(WidthCounterPair(cntr_len, counter), fractions)?; Ok(provider) } } @@ -379,6 +464,7 @@ impl TimeWriter for TimeProviderCcsdsEpoch { #[cfg(test)] mod tests { + use super::*; use crate::time::cuc::{convert_fractional_part_to_ns, FractionalPart, FractionalResolution}; #[test] @@ -411,4 +497,32 @@ mod tests { 2_u32.pow(32) - 1, )); } + + #[test] + fn fractional_part_formula() { + let fractional_part = + 7843137 / (10_u64.pow(9) / fractional_res_to_div(FractionalResolution::FourMs) as u64); + assert_eq!(fractional_part, 2); + } + + #[test] + fn fractional_part_formula_2() { + let fractional_part = + 12000000 / (10_u64.pow(9) / fractional_res_to_div(FractionalResolution::FourMs) as u64); + assert_eq!(fractional_part, 3); + } + + #[test] + fn fractional_part_formula_3() { + let one_fraction_with_width_two_in_ns = 10_u64.pow(9) / (2_u32.pow(8 * 2) - 1) as u64; + assert_eq!(one_fraction_with_width_two_in_ns, 15259); + let hundred_fractions_and_some = 100 * one_fraction_with_width_two_in_ns + 7000; + let fractional_part = hundred_fractions_and_some + / (10_u64.pow(9) / fractional_res_to_div(FractionalResolution::FifteenUs) as u64); + assert_eq!(fractional_part, 100); + let hundred_and_one_fractions = 101 * one_fraction_with_width_two_in_ns; + let fractional_part = hundred_and_one_fractions + / (10_u64.pow(9) / fractional_res_to_div(FractionalResolution::FifteenUs) as u64); + assert_eq!(fractional_part, 101); + } } -- 2.34.1 From bccbdf65a3ab820bcbaf1aa9538ce3eb3bc7d6e7 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 9 Dec 2022 17:21:45 +0100 Subject: [PATCH 06/23] basic impl done, add first unittests --- src/time/cuc.rs | 84 +++++++++++++++++++++++++++++++++++++++++++++---- src/time/mod.rs | 4 +++ 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/src/time/cuc.rs b/src/time/cuc.rs index 272e538..ecb1893 100644 --- a/src/time/cuc.rs +++ b/src/time/cuc.rs @@ -132,7 +132,7 @@ impl TimeProviderCcsdsEpoch { // have been sanitized panic!("invalid counter width {} for cuc timestamp", counter_width); } - pfield |= (counter_width - 1) << 3; + pfield |= (counter_width - 1) << 2; if let Some(fractions_width) = fractions_width { if !(1..=3).contains(&(fractions_width as u8)) { // Okay to panic here, this function is private and all input values should @@ -192,8 +192,7 @@ impl TimeProviderCcsdsEpoch { base_len } - /// Verifies the raw width parameter and returns the actual length, which is the raw - /// value plus 1. + /// Verifies the raw width parameter. fn verify_counter_width(width: u8) -> Result<(), CucError> { if width == 0 || width > 4 { return Err(CucError::InvalidCounterWidth(width)); @@ -331,7 +330,7 @@ impl TimeProviderCcsdsEpoch { fractions: Option, ) -> Result { Self::verify_counter_width(counter.0)?; - if counter.1 > 2u32.pow(counter.0 as u32 * 8) - 1 { + if counter.1 > (2u64.pow(counter.0 as u32 * 8) - 1) as u32 { return Err(CucError::InvalidCounter(counter.0, counter.1 as u64)); } if let Some(fractions) = fractions { @@ -462,13 +461,86 @@ impl TimeWriter for TimeProviderCcsdsEpoch { } } +impl CcsdsTimeProvider for TimeProviderCcsdsEpoch { + fn len_as_bytes(&self) -> usize { + self.len_packed() + } + + fn p_field(&self) -> (usize, [u8; 2]) { + (1, [self.pfield, 0]) + } + + fn ccdsd_time_code(&self) -> CcsdsTimeCodes { + CcsdsTimeCodes::CucCcsdsEpoch + } + + /// Please note that this function only works as intended if the time counter resolution + /// is one second. + fn unix_seconds(&self) -> i64 { + ccsds_epoch_to_unix_epoch(self.counter.1 as u64) as i64 + } + + fn date_time(&self) -> Option> { + let unix_seconds = self.unix_seconds(); + let ns = if let Some(fractional_part) = self.fractions { + convert_fractional_part_to_ns(fractional_part) + } else { + 0 + }; + if let LocalResult::Single(res) = Utc.timestamp_opt(unix_seconds, ns as u32) { + return Some(res); + } + None + } +} + #[cfg(test)] mod tests { use super::*; - use crate::time::cuc::{convert_fractional_part_to_ns, FractionalPart, FractionalResolution}; + use chrono::{Datelike, Timelike}; + #[allow(unused_imports)] + use std::println; #[test] - fn test_basic() {} + fn test_basic_zero_epoch() { + let zero_cuc = TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(4, 0), None); + assert!(zero_cuc.is_ok()); + let zero_cuc = zero_cuc.unwrap(); + let counter = zero_cuc.width_counter_pair(); + assert_eq!(counter.0, 4); + assert_eq!(counter.1, 0); + let fractions = zero_cuc.width_fractions_pair(); + assert!(fractions.is_none()); + let dt = zero_cuc.date_time(); + assert!(dt.is_some()); + let dt = dt.unwrap(); + assert_eq!(dt.year(), 1958); + assert_eq!(dt.month(), 1); + assert_eq!(dt.day(), 1); + assert_eq!(dt.hour(), 0); + assert_eq!(dt.minute(), 0); + assert_eq!(dt.second(), 0); + } + + #[test] + fn test_write() { + let mut buf: [u8; 16] = [0; 16]; + let zero_cuc = TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(4, 0x20102030), None); + assert!(zero_cuc.is_ok()); + let zero_cuc = zero_cuc.unwrap(); + let res = zero_cuc.write_to_bytes(&mut buf); + assert!(res.is_ok()); + let written = res.unwrap(); + assert_eq!(written, 5); + assert_eq!((buf[0] >> 7) & 0b1, 0); + let time_code = ccsds_time_code_from_p_field(buf[0]); + assert!(time_code.is_ok()); + assert_eq!(time_code.unwrap(), CcsdsTimeCodes::CucCcsdsEpoch); + assert_eq!((buf[0] >> 2) & 0b11, 0b11); + assert_eq!(buf[0] & 0b11, 0); + let raw_counter = u32::from_be_bytes(buf[1..5].try_into().unwrap()); + assert_eq!(raw_counter, 0x20102030); + } #[test] fn test_fractional_converter() { diff --git a/src/time/mod.rs b/src/time/mod.rs index f851880..37c9449 100644 --- a/src/time/mod.rs +++ b/src/time/mod.rs @@ -169,6 +169,10 @@ pub const fn unix_epoch_to_ccsds_epoch(unix_epoch: u64) -> u64 { (unix_epoch as i64 - (DAYS_CCSDS_TO_UNIX as i64 * SECONDS_PER_DAY as i64)) as u64 } +pub const fn ccsds_epoch_to_unix_epoch(ccsds_epoch: u64) -> u64 { + (ccsds_epoch as i64 + (DAYS_CCSDS_TO_UNIX as i64 * SECONDS_PER_DAY as i64)) as u64 +} + #[cfg(feature = "std")] #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] pub fn ms_of_day_using_sysclock() -> u32 { -- 2.34.1 From 7615e40e433e00ffa15ac24b38a7b2bf45a34add Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 9 Dec 2022 17:33:04 +0100 Subject: [PATCH 07/23] basic docs --- src/time/cds.rs | 2 +- src/time/cuc.rs | 21 +++++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/time/cds.rs b/src/time/cds.rs index 3a44046..dd88215 100644 --- a/src/time/cds.rs +++ b/src/time/cds.rs @@ -1,4 +1,4 @@ -//! Module to generate or read CDS timestamps as specified in +//! Module to generate or read CCSDS Day Segmented (CDS) timestamps as specified in //! [CCSDS 301.0-B-4](https://public.ccsds.org/Pubs/301x0b4e1.pdf) section 3.3 . //! //! The core data structure to do this is the [cds::TimeProvider] struct. diff --git a/src/time/cuc.rs b/src/time/cuc.rs index ecb1893..1c79b23 100644 --- a/src/time/cuc.rs +++ b/src/time/cuc.rs @@ -1,7 +1,13 @@ +//! Module to generate or read CCSDS Unsegmented (CUC) timestamps as specified in +//! [CCSDS 301.0-B-4](https://public.ccsds.org/Pubs/301x0b4e1.pdf) section 3.2 . +//! +//! The core data structure to do this is the [TimeProviderCcsdsEpoch] struct. use super::*; use core::fmt::Debug; const MIN_CUC_LEN: usize = 2; +/// Maximum length if the preamble field is not extended. +pub const MAX_CUC_LEN_SMALL_PREAMBLE: usize = 8; #[derive(Copy, Clone, PartialEq, Eq, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -103,11 +109,18 @@ pub struct WidthCounterPair(u8, u32); #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct FractionalPart(FractionalResolution, u32); -/// This provider uses the CCSDS epoch. Furthermore the preamble field only has one byte, -/// which allows a time code representation through the year 2094. +/// This object is the abstraction for the CCSDS Unsegmented Time Code (CUC) using the CCSDS epoch +/// and a small preamble field. /// -/// More specifically, only having a preamble field of one byte limits the width of the counter -/// type (generally seconds) to 4 bytes and the width of the fractions type to 3 bytes. +/// It has the capability to generate and read timestamps as specified in the CCSDS 301.0-B-4 +/// section 3.2 . The preamble field only has one byte, which allows a time code representation +/// through the year 2094. The time is represented as a simple binary counter starting from the +/// fixed CCSDS epoch (1958-01-01 00:00:00). It is possible to provide subsecond accuracy using the +/// fractional field with various available resolutions. +/// +/// Having a preamble field of one byte limits the width of the counter +/// type (generally seconds) to 4 bytes and the width of the fractions type to 3 bytes. This limits +/// the maximum time stamp size to [MAX_CUC_LEN_SMALL_PREAMBLE] (8 bytes). #[derive(Copy, Clone, PartialEq, Eq, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct TimeProviderCcsdsEpoch { -- 2.34.1 From e155ddbcb0705871d4cb170bced83e88022a57ce Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 10 Dec 2022 16:35:00 +0100 Subject: [PATCH 08/23] add more tests and additional check --- src/time/cuc.rs | 124 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 122 insertions(+), 2 deletions(-) diff --git a/src/time/cuc.rs b/src/time/cuc.rs index 1c79b23..afd5c50 100644 --- a/src/time/cuc.rs +++ b/src/time/cuc.rs @@ -234,6 +234,7 @@ impl TimeProviderCcsdsEpoch { Self::new_generic(WidthCounterPair(4, counter), None).unwrap() } + /// Fractions with a resolution of ~ 4 ms pub fn new_with_coarse_fractions(counter: u32, subsec_fractions: u8) -> Self { // These values are definitely valid, so it is okay to unwrap here. Self::new_generic( @@ -246,7 +247,8 @@ impl TimeProviderCcsdsEpoch { .unwrap() } - pub fn new_with_submillis_fractions(counter: u32, subsec_fractions: u16) -> Self { + /// Fractions with a resolution of ~ 16 us + pub fn new_with_medium_fractions(counter: u32, subsec_fractions: u16) -> Self { // These values are definitely valid, so it is okay to unwrap here. Self::new_generic( WidthCounterPair(4, counter), @@ -258,6 +260,7 @@ impl TimeProviderCcsdsEpoch { .unwrap() } + /// Fractions with a resolution of ~ 60 ns pub fn new_with_fine_fractions(counter: u32, subsec_fractions: u32) -> Result { Self::new_generic( WidthCounterPair(4, counter), @@ -371,6 +374,22 @@ impl TimeReader for TimeProviderCcsdsEpoch { }), )); } + match ccsds_time_code_from_p_field(buf[0]) { + Ok(code) => { + if code != CcsdsTimeCodes::CucCcsdsEpoch { + return Err(TimestampError::InvalidTimeCode( + CcsdsTimeCodes::CucCcsdsEpoch, + code as u8, + )); + } + } + Err(raw) => { + return Err(TimestampError::InvalidTimeCode( + CcsdsTimeCodes::CucCcsdsEpoch, + raw, + )) + } + } let (cntr_len, fractions_len, total_len) = Self::len_components_and_total_from_pfield(buf[0]); if buf.len() < total_len { @@ -536,7 +555,7 @@ mod tests { } #[test] - fn test_write() { + fn test_write_no_fractions() { let mut buf: [u8; 16] = [0; 16]; let zero_cuc = TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(4, 0x20102030), None); assert!(zero_cuc.is_ok()); @@ -553,8 +572,109 @@ mod tests { assert_eq!(buf[0] & 0b11, 0); let raw_counter = u32::from_be_bytes(buf[1..5].try_into().unwrap()); assert_eq!(raw_counter, 0x20102030); + assert_eq!(buf[5], 0); } + #[test] + fn test_read_no_fractions() { + let mut buf: [u8; 16] = [0; 16]; + let zero_cuc = + TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(4, 0x20102030), None).unwrap(); + zero_cuc.write_to_bytes(&mut buf).unwrap(); + let cuc_read_back = + TimeProviderCcsdsEpoch::from_bytes(&buf).expect("reading cuc timestamp failed"); + assert_eq!(cuc_read_back, zero_cuc); + assert_eq!(cuc_read_back.width_counter_pair().1, 0x20102030); + assert_eq!(cuc_read_back.width_fractions_pair(), None); + } + + #[test] + fn invalid_read_len() { + let buf: [u8; 8] = [0; 8]; + for i in 0..2 { + let res = TimeProviderCcsdsEpoch::from_bytes(&buf[0..i]); + assert!(res.is_err()); + let err = res.unwrap_err(); + if let TimestampError::ByteConversionError(ByteConversionError::FromSliceTooSmall(e)) = + err + { + assert_eq!(e.found, i); + assert_eq!(e.expected, 2); + } + } + } + + #[test] + fn invalid_ccsds_stamp_type() { + let mut buf: [u8; 16] = [0; 16]; + buf[0] |= (CcsdsTimeCodes::CucAgencyEpoch as u8) << 4; + let res = TimeProviderCcsdsEpoch::from_bytes(&buf); + assert!(res.is_err()); + let err = res.unwrap_err(); + if let TimestampError::InvalidTimeCode(code, raw) = err { + assert_eq!(code, CcsdsTimeCodes::CucCcsdsEpoch); + assert_eq!(raw, CcsdsTimeCodes::CucAgencyEpoch as u8); + } else { + panic!("unexpected error: {}", err); + } + } + + #[test] + fn test_write_with_coarse_fractions() { + let mut buf: [u8; 16] = [0; 16]; + let cuc = TimeProviderCcsdsEpoch::new_with_coarse_fractions(0x30201060, 120); + assert!(cuc.fractions.is_some()); + assert_eq!(cuc.fractions.unwrap().1, 120); + assert_eq!(cuc.fractions.unwrap().0, FractionalResolution::FourMs); + let res = cuc.write_to_bytes(&mut buf); + assert!(res.is_ok()); + let written = res.unwrap(); + assert_eq!(written, 6); + assert_eq!(buf[5], 120); + assert_eq!(buf[6], 0); + assert_eq!(u32::from_be_bytes(buf[1..5].try_into().unwrap()), 0x30201060); + } + + #[test] + fn test_read_with_coarse_fractions() { + let mut buf: [u8; 16] = [0; 16]; + let cuc = TimeProviderCcsdsEpoch::new_with_coarse_fractions(0x30201060, 120); + let res = cuc.write_to_bytes(&mut buf); + assert!(res.is_ok()); + let res= TimeProviderCcsdsEpoch::from_bytes(&buf); + assert!(res.is_ok()); + let read_back = res.unwrap(); + assert_eq!(read_back, cuc); + } + + #[test] + fn test_write_with_medium_fractions() { + let mut buf: [u8; 16] = [0; 16]; + let cuc = TimeProviderCcsdsEpoch::new_with_medium_fractions(0x30303030, 30000); + let res = cuc.write_to_bytes(&mut buf); + assert!(res.is_ok()); + let written = res.unwrap(); + assert_eq!(written, 7); + assert_eq!(u16::from_be_bytes(buf[5..7].try_into().unwrap()), 30000); + assert_eq!(buf[7], 0); + } + + #[test] + fn test_write_with_fine_fractions() { + let mut buf: [u8; 16] = [0; 16]; + let cuc = TimeProviderCcsdsEpoch::new_with_fine_fractions(0x30303030, u16::MAX as u32 + 60000); + assert!(cuc.is_ok()); + let cuc = cuc.unwrap(); + let res = cuc.write_to_bytes(&mut buf); + let written = res.unwrap(); + assert_eq!(written, 8); + let mut dummy_buf: [u8; 4] = [0; 4]; + dummy_buf[1..4].copy_from_slice(&buf[5..8]); + assert_eq!(u32::from_be_bytes(dummy_buf), u16::MAX as u32 + 60000); + assert_eq!(buf[8], 0); + } + + #[test] fn test_fractional_converter() { let ns = convert_fractional_part_to_ns(FractionalPart(FractionalResolution::FourMs, 2)); -- 2.34.1 From d889826b7943261862f9aa2ee306c19e3fc1b2c2 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 10 Dec 2022 16:42:24 +0100 Subject: [PATCH 09/23] remove duplicate function --- src/time/cuc.rs | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/src/time/cuc.rs b/src/time/cuc.rs index afd5c50..a949616 100644 --- a/src/time/cuc.rs +++ b/src/time/cuc.rs @@ -167,10 +167,6 @@ impl TimeProviderCcsdsEpoch { } } - pub fn len_packed(&self) -> usize { - Self::len_packed_from_pfield(self.pfield) - } - #[inline] pub fn len_cntr_from_pfield(pfield: u8) -> u8 { ((pfield >> 2) & 0b11) + 1 @@ -448,11 +444,11 @@ impl TimeReader for TimeProviderCcsdsEpoch { impl TimeWriter for TimeProviderCcsdsEpoch { fn write_to_bytes(&self, bytes: &mut [u8]) -> Result { // Cross check the sizes of the counters against byte widths in the ctor - if bytes.len() < self.len_packed() { + if bytes.len() < self.len_as_bytes() { return Err(TimestampError::ByteConversionError( ByteConversionError::ToSliceTooSmall(SizeMissmatch { found: bytes.len(), - expected: self.len_packed(), + expected: self.len_as_bytes(), }), )); } @@ -495,7 +491,7 @@ impl TimeWriter for TimeProviderCcsdsEpoch { impl CcsdsTimeProvider for TimeProviderCcsdsEpoch { fn len_as_bytes(&self) -> usize { - self.len_packed() + Self::len_packed_from_pfield(self.pfield) } fn p_field(&self) -> (usize, [u8; 2]) { @@ -659,6 +655,18 @@ mod tests { assert_eq!(buf[7], 0); } + #[test] + fn test_read_with_medium_fractions() { + let mut buf: [u8; 16] = [0; 16]; + let cuc = TimeProviderCcsdsEpoch::new_with_medium_fractions(0x30303030, 30000); + let res = cuc.write_to_bytes(&mut buf); + assert!(res.is_ok()); + let res = TimeProviderCcsdsEpoch::from_bytes(&buf); + assert!(res.is_ok()); + let cuc_read_back = res.unwrap(); + assert_eq!(cuc_read_back, cuc); + } + #[test] fn test_write_with_fine_fractions() { let mut buf: [u8; 16] = [0; 16]; @@ -674,6 +682,19 @@ mod tests { assert_eq!(buf[8], 0); } + #[test] + fn test_read_with_fine_fractions() { + let mut buf: [u8; 16] = [0; 16]; + let cuc = TimeProviderCcsdsEpoch::new_with_fine_fractions(0x30303030, u16::MAX as u32 + 60000); + assert!(cuc.is_ok()); + let cuc = cuc.unwrap(); + let res = cuc.write_to_bytes(&mut buf); + assert!(res.is_ok()); + let res = TimeProviderCcsdsEpoch::from_bytes(&buf); + assert!(res.is_ok()); + let cuc_read_back = res.unwrap(); + assert_eq!(cuc_read_back, cuc); + } #[test] fn test_fractional_converter() { -- 2.34.1 From 1d9329ad6358f27c1550e707924da8dcfc07095c Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 10 Dec 2022 17:39:15 +0100 Subject: [PATCH 10/23] this should cover most basic cases --- src/time/cuc.rs | 128 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 120 insertions(+), 8 deletions(-) diff --git a/src/time/cuc.rs b/src/time/cuc.rs index a949616..6a5c491 100644 --- a/src/time/cuc.rs +++ b/src/time/cuc.rs @@ -531,9 +531,9 @@ mod tests { #[test] fn test_basic_zero_epoch() { - let zero_cuc = TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(4, 0), None); - assert!(zero_cuc.is_ok()); - let zero_cuc = zero_cuc.unwrap(); + let zero_cuc = TimeProviderCcsdsEpoch::new(0); + assert_eq!(zero_cuc.len_as_bytes(), 5); + assert_eq!(zero_cuc.ccdsd_time_code(), CcsdsTimeCodes::CucCcsdsEpoch); let counter = zero_cuc.width_counter_pair(); assert_eq!(counter.0, 4); assert_eq!(counter.1, 0); @@ -558,6 +558,8 @@ mod tests { let zero_cuc = zero_cuc.unwrap(); let res = zero_cuc.write_to_bytes(&mut buf); assert!(res.is_ok()); + assert_eq!(zero_cuc.len_as_bytes(), 5); + assert_eq!(pfield_len(buf[0]), 1); let written = res.unwrap(); assert_eq!(written, 5); assert_eq!((buf[0] >> 7) & 0b1, 0); @@ -571,6 +573,21 @@ mod tests { assert_eq!(buf[5], 0); } + #[test] + fn test_datetime_now() { + let now = Utc::now(); + let cuc_now = TimeProviderCcsdsEpoch::from_now(FractionalResolution::SixtyNs); + assert!(cuc_now.is_ok()); + let cuc_now = cuc_now.unwrap(); + let dt_opt = cuc_now.date_time(); + assert!(dt_opt.is_some()); + let dt = dt_opt.unwrap(); + let diff = dt - now; + assert!(diff.num_milliseconds() < 1000); + println!("datetime from cuc: {}", dt); + println!("datetime now: {}", now); + } + #[test] fn test_read_no_fractions() { let mut buf: [u8; 16] = [0; 16]; @@ -586,7 +603,7 @@ mod tests { #[test] fn invalid_read_len() { - let buf: [u8; 8] = [0; 8]; + let mut buf: [u8; 16] = [0; 16]; for i in 0..2 { let res = TimeProviderCcsdsEpoch::from_bytes(&buf[0..i]); assert!(res.is_err()); @@ -598,8 +615,98 @@ mod tests { assert_eq!(e.expected, 2); } } + let large_stamp = TimeProviderCcsdsEpoch::new_with_fine_fractions(22, 300).unwrap(); + large_stamp.write_to_bytes(&mut buf).unwrap(); + for i in 2..large_stamp.len_as_bytes() - 1 { + let res = TimeProviderCcsdsEpoch::from_bytes(&buf[0..i]); + assert!(res.is_err()); + let err = res.unwrap_err(); + if let TimestampError::ByteConversionError(ByteConversionError::FromSliceTooSmall(e)) = + err + { + assert_eq!(e.found, i); + assert_eq!(e.expected, large_stamp.len_as_bytes()); + } + } } + #[test] + fn write_and_read_tiny_stamp() { + let mut buf = [0; 2]; + let cuc = TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(1, 200), None); + assert!(cuc.is_ok()); + let cuc = cuc.unwrap(); + assert_eq!(cuc.len_as_bytes(), 2); + let res = cuc.write_to_bytes(&mut buf); + assert!(res.is_ok()); + let written = res.unwrap(); + assert_eq!(written, 2); + assert_eq!(buf[1], 200); + let cuc_read_back = TimeProviderCcsdsEpoch::from_bytes(&buf); + assert!(cuc_read_back.is_ok()); + let cuc_read_back = cuc_read_back.unwrap(); + assert_eq!(cuc_read_back, cuc); + } + + #[test] + fn write_slightly_larger_stamp() { + let mut buf = [0; 4]; + let cuc = TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(2, 40000), None); + assert!(cuc.is_ok()); + let cuc = cuc.unwrap(); + assert_eq!(cuc.len_as_bytes(), 3); + let res = cuc.write_to_bytes(&mut buf); + assert!(res.is_ok()); + let written = res.unwrap(); + assert_eq!(written, 3); + assert_eq!(u16::from_be_bytes(buf[1..3].try_into().unwrap()), 40000); + let cuc_read_back = TimeProviderCcsdsEpoch::from_bytes(&buf); + assert!(cuc_read_back.is_ok()); + let cuc_read_back = cuc_read_back.unwrap(); + assert_eq!(cuc_read_back, cuc); + } + + #[test] + fn invalid_buf_len_for_read() {} + #[test] + fn write_read_three_byte_cntr_stamp() { + let mut buf = [0; 4]; + let cuc = TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(3, 2_u32.pow(24) - 2), None); + assert!(cuc.is_ok()); + let cuc = cuc.unwrap(); + assert_eq!(cuc.len_as_bytes(), 4); + let res = cuc.write_to_bytes(&mut buf); + assert!(res.is_ok()); + let written = res.unwrap(); + assert_eq!(written, 4); + let mut temp_buf = [0; 4]; + temp_buf[1..4].copy_from_slice(&buf[1..4]); + assert_eq!(u32::from_be_bytes(temp_buf), 2_u32.pow(24) - 2); + let cuc_read_back = TimeProviderCcsdsEpoch::from_bytes(&buf); + assert!(cuc_read_back.is_ok()); + let cuc_read_back = cuc_read_back.unwrap(); + assert_eq!(cuc_read_back, cuc); + } + + #[test] + fn test_write_invalid_buf() { + let mut buf: [u8; 16] = [0; 16]; + let res = TimeProviderCcsdsEpoch::new_with_fine_fractions(0, 0); + let cuc = res.unwrap(); + for i in 0..cuc.len_as_bytes() - 1 { + let err = cuc.write_to_bytes(&mut buf[0..i]); + assert!(err.is_err()); + let err = err.unwrap_err(); + if let TimestampError::ByteConversionError(ByteConversionError::ToSliceTooSmall(e)) = + err + { + assert_eq!(e.expected, cuc.len_as_bytes()); + assert_eq!(e.found, i); + } else { + panic!("unexpected error: {}", err); + } + } + } #[test] fn invalid_ccsds_stamp_type() { let mut buf: [u8; 16] = [0; 16]; @@ -628,7 +735,10 @@ mod tests { assert_eq!(written, 6); assert_eq!(buf[5], 120); assert_eq!(buf[6], 0); - assert_eq!(u32::from_be_bytes(buf[1..5].try_into().unwrap()), 0x30201060); + assert_eq!( + u32::from_be_bytes(buf[1..5].try_into().unwrap()), + 0x30201060 + ); } #[test] @@ -637,7 +747,7 @@ mod tests { let cuc = TimeProviderCcsdsEpoch::new_with_coarse_fractions(0x30201060, 120); let res = cuc.write_to_bytes(&mut buf); assert!(res.is_ok()); - let res= TimeProviderCcsdsEpoch::from_bytes(&buf); + let res = TimeProviderCcsdsEpoch::from_bytes(&buf); assert!(res.is_ok()); let read_back = res.unwrap(); assert_eq!(read_back, cuc); @@ -670,7 +780,8 @@ mod tests { #[test] fn test_write_with_fine_fractions() { let mut buf: [u8; 16] = [0; 16]; - let cuc = TimeProviderCcsdsEpoch::new_with_fine_fractions(0x30303030, u16::MAX as u32 + 60000); + let cuc = + TimeProviderCcsdsEpoch::new_with_fine_fractions(0x30303030, u16::MAX as u32 + 60000); assert!(cuc.is_ok()); let cuc = cuc.unwrap(); let res = cuc.write_to_bytes(&mut buf); @@ -685,7 +796,8 @@ mod tests { #[test] fn test_read_with_fine_fractions() { let mut buf: [u8; 16] = [0; 16]; - let cuc = TimeProviderCcsdsEpoch::new_with_fine_fractions(0x30303030, u16::MAX as u32 + 60000); + let cuc = + TimeProviderCcsdsEpoch::new_with_fine_fractions(0x30303030, u16::MAX as u32 + 60000); assert!(cuc.is_ok()); let cuc = cuc.unwrap(); let res = cuc.write_to_bytes(&mut buf); -- 2.34.1 From 6341cf35d3c0ac3978471e8243cefc092ea56ca7 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 10 Dec 2022 18:09:00 +0100 Subject: [PATCH 11/23] added doc test / example as well --- src/time/cuc.rs | 61 +++++++++++++++++++++++++++++++++++++++++++------ src/time/mod.rs | 3 +-- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/time/cuc.rs b/src/time/cuc.rs index 6a5c491..fb1438c 100644 --- a/src/time/cuc.rs +++ b/src/time/cuc.rs @@ -121,6 +121,29 @@ pub struct FractionalPart(FractionalResolution, u32); /// Having a preamble field of one byte limits the width of the counter /// type (generally seconds) to 4 bytes and the width of the fractions type to 3 bytes. This limits /// the maximum time stamp size to [MAX_CUC_LEN_SMALL_PREAMBLE] (8 bytes). +/// +/// # Example +/// +/// ``` +/// use spacepackets::time::cuc::{FractionalResolution, TimeProviderCcsdsEpoch}; +/// use spacepackets::time::{TimeWriter, CcsdsTimeCodes, TimeReader, CcsdsTimeProvider}; +/// +/// // Highest fractional resolution +/// let timestamp_now = TimeProviderCcsdsEpoch::from_now(FractionalResolution::SixtyNs).expect("creating cuc stamp failed"); +/// let mut raw_stamp = [0; 16]; +/// { +/// let written = timestamp_now.write_to_bytes(&mut raw_stamp).expect("writing timestamp failed"); +/// assert_eq!((raw_stamp[0] >> 4) & 0b111, CcsdsTimeCodes::CucCcsdsEpoch as u8); +/// // 1 byte preamble + 4 byte counter + 3 byte fractional part +/// assert_eq!(written, 8); +/// } +/// { +/// let read_result = TimeProviderCcsdsEpoch::from_bytes(&raw_stamp); +/// assert!(read_result.is_ok()); +/// let stamp_deserialized = read_result.unwrap(); +/// assert_eq!(stamp_deserialized, timestamp_now); +/// } +/// ``` #[derive(Copy, Clone, PartialEq, Eq, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct TimeProviderCcsdsEpoch { @@ -288,11 +311,8 @@ impl TimeProviderCcsdsEpoch { let ccsds_epoch = unix_epoch_to_ccsds_epoch(now.as_secs()); let fractions = fractional_part_from_subsec_ns(fraction_resolution, now.subsec_nanos() as u64); - Ok(Self { - pfield: 0, - counter: WidthCounterPair(4, ccsds_epoch as u32), - fractions, - }) + Self::new_with_fractions(ccsds_epoch as u32, fractions) + .map_err(|e| TimestampError::CucError(e).into()) } #[cfg(feature = "std")] @@ -330,10 +350,14 @@ impl TimeProviderCcsdsEpoch { if res == FractionalResolution::Seconds { self.fractions = None; } + let mut update_fractions = true; if let Some(existing_fractions) = self.fractions { - if existing_fractions.0 != res { - self.fractions = Some(FractionalPart(res, 0)); + if existing_fractions.0 == res { + update_fractions = false; } + }; + if update_fractions { + self.fractions = Some(FractionalPart(res, 0)); } } @@ -863,4 +887,27 @@ mod tests { / (10_u64.pow(9) / fractional_res_to_div(FractionalResolution::FifteenUs) as u64); assert_eq!(fractional_part, 101); } + + #[test] + fn update_fractions() { + let mut stamp = TimeProviderCcsdsEpoch::new(2000); + let res = stamp.set_fractions(FractionalPart(FractionalResolution::SixtyNs, 5000)); + assert!(res.is_ok()); + assert!(stamp.fractions.is_some()); + let fractions = stamp.fractions.unwrap(); + assert_eq!(fractions.0, FractionalResolution::SixtyNs); + assert_eq!(fractions.1, 5000); + } + + #[test] + fn set_fract_resolution() { + let mut stamp = TimeProviderCcsdsEpoch::new(2000); + stamp.set_fractional_resolution(FractionalResolution::SixtyNs); + assert!(stamp.fractions.is_some()); + let fractions = stamp.fractions.unwrap(); + assert_eq!(fractions.0, FractionalResolution::SixtyNs); + assert_eq!(fractions.1, 0); + let res = stamp.update_from_now(); + assert!(res.is_ok()); + } } diff --git a/src/time/mod.rs b/src/time/mod.rs index 37c9449..26d74b2 100644 --- a/src/time/mod.rs +++ b/src/time/mod.rs @@ -7,7 +7,6 @@ use core::fmt::{Display, Formatter}; #[cfg(not(feature = "std"))] use num_traits::float::FloatCore; -use crate::time::cuc::CucError; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; #[cfg(feature = "std")] @@ -73,7 +72,7 @@ impl From for TimestampError { } impl From for TimestampError { - fn from(e: CucError) -> Self { + fn from(e: cuc::CucError) -> Self { TimestampError::CucError(e) } } -- 2.34.1 From f73edd71fdba179a71deb386469ccda27e792a7b Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 10 Dec 2022 18:18:41 +0100 Subject: [PATCH 12/23] better structure --- src/time/cuc.rs | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/time/cuc.rs b/src/time/cuc.rs index fb1438c..3f7bbb7 100644 --- a/src/time/cuc.rs +++ b/src/time/cuc.rs @@ -116,7 +116,7 @@ pub struct FractionalPart(FractionalResolution, u32); /// section 3.2 . The preamble field only has one byte, which allows a time code representation /// through the year 2094. The time is represented as a simple binary counter starting from the /// fixed CCSDS epoch (1958-01-01 00:00:00). It is possible to provide subsecond accuracy using the -/// fractional field with various available resolutions. +/// fractional field with various available [resolutions][FractionalResolution]. /// /// Having a preamble field of one byte limits the width of the counter /// type (generally seconds) to 4 bytes and the width of the fractions type to 3 bytes. This limits @@ -248,11 +248,17 @@ impl TimeProviderCcsdsEpoch { } impl TimeProviderCcsdsEpoch { + /// Create a time provider with a four byte counter and no fractional part. pub fn new(counter: u32) -> Self { // These values are definitely valid, so it is okay to unwrap here. Self::new_generic(WidthCounterPair(4, counter), None).unwrap() } + /// Like [TimeProviderCcsdsEpoch::new] but allow to supply a fractional part as well. + pub fn new_with_fractions(counter: u32, fractions: FractionalPart) -> Result { + Self::new_generic(WidthCounterPair(4, counter), Some(fractions)) + } + /// Fractions with a resolution of ~ 4 ms pub fn new_with_coarse_fractions(counter: u32, subsec_fractions: u8) -> Self { // These values are definitely valid, so it is okay to unwrap here. @@ -290,18 +296,6 @@ impl TimeProviderCcsdsEpoch { ) } - pub fn new_with_fractions( - counter: u32, - fractions: Option, - ) -> Result { - Self::new_generic(WidthCounterPair(4, counter), fractions) - } - - pub fn new_u16_counter(counter: u16) -> Self { - // These values are definitely valid, so it is okay to unwrap here. - Self::new_generic(WidthCounterPair(2, counter as u32), None).unwrap() - } - /// This function will return the current time as a CUC timestamp. /// The counter width will always be set to 4 bytes because the normal CCSDS epoch will overflow /// when using less than that. @@ -309,10 +303,13 @@ impl TimeProviderCcsdsEpoch { pub fn from_now(fraction_resolution: FractionalResolution) -> Result { let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; let ccsds_epoch = unix_epoch_to_ccsds_epoch(now.as_secs()); + if fraction_resolution == FractionalResolution::Seconds { + return Ok(Self::new(ccsds_epoch as u32)); + } let fractions = fractional_part_from_subsec_ns(fraction_resolution, now.subsec_nanos() as u64); - Self::new_with_fractions(ccsds_epoch as u32, fractions) - .map_err(|e| TimestampError::CucError(e).into()) + Self::new_with_fractions(ccsds_epoch as u32, fractions.unwrap()) + .map_err(|e| StdTimestampError::TimestampError(e.into())) } #[cfg(feature = "std")] @@ -328,6 +325,11 @@ impl TimeProviderCcsdsEpoch { Ok(()) } + pub fn new_u16_counter(counter: u16) -> Self { + // These values are definitely valid, so it is okay to unwrap here. + Self::new_generic(WidthCounterPair(2, counter as u32), None).unwrap() + } + pub fn width_counter_pair(&self) -> WidthCounterPair { self.counter } -- 2.34.1 From 93159dae4510e8a60593330ca04860585732858f Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 10 Dec 2022 18:23:47 +0100 Subject: [PATCH 13/23] some more docs --- src/time/cuc.rs | 274 ++++++++++++++++++++++++------------------------ 1 file changed, 138 insertions(+), 136 deletions(-) diff --git a/src/time/cuc.rs b/src/time/cuc.rs index 3f7bbb7..9ccd439 100644 --- a/src/time/cuc.rs +++ b/src/time/cuc.rs @@ -161,6 +161,144 @@ pub fn pfield_len(pfield: u8) -> usize { } impl TimeProviderCcsdsEpoch { + /// Create a time provider with a four byte counter and no fractional part. + pub fn new(counter: u32) -> Self { + // These values are definitely valid, so it is okay to unwrap here. + Self::new_generic(WidthCounterPair(4, counter), None).unwrap() + } + + /// Like [TimeProviderCcsdsEpoch::new] but allow to supply a fractional part as well. + pub fn new_with_fractions(counter: u32, fractions: FractionalPart) -> Result { + Self::new_generic(WidthCounterPair(4, counter), Some(fractions)) + } + + /// Fractions with a resolution of ~ 4 ms + pub fn new_with_coarse_fractions(counter: u32, subsec_fractions: u8) -> Self { + // These values are definitely valid, so it is okay to unwrap here. + Self::new_generic( + WidthCounterPair(4, counter), + Some(FractionalPart( + FractionalResolution::FourMs, + subsec_fractions as u32, + )), + ) + .unwrap() + } + + /// Fractions with a resolution of ~ 16 us + pub fn new_with_medium_fractions(counter: u32, subsec_fractions: u16) -> Self { + // These values are definitely valid, so it is okay to unwrap here. + Self::new_generic( + WidthCounterPair(4, counter), + Some(FractionalPart( + FractionalResolution::FifteenUs, + subsec_fractions as u32, + )), + ) + .unwrap() + } + + /// Fractions with a resolution of ~ 60 ns. The fractional part value is limited by the + /// 24 bits of the fractional field, so this function will fail with + /// [CucError::InvalidFractions] if the fractional value exceeds the value. + pub fn new_with_fine_fractions(counter: u32, subsec_fractions: u32) -> Result { + Self::new_generic( + WidthCounterPair(4, counter), + Some(FractionalPart( + FractionalResolution::SixtyNs, + subsec_fractions as u32, + )), + ) + } + + /// This function will return the current time as a CUC timestamp. + /// The counter width will always be set to 4 bytes because the normal CCSDS epoch will overflow + /// when using less than that. + #[cfg(feature = "std")] + pub fn from_now(fraction_resolution: FractionalResolution) -> Result { + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; + let ccsds_epoch = unix_epoch_to_ccsds_epoch(now.as_secs()); + if fraction_resolution == FractionalResolution::Seconds { + return Ok(Self::new(ccsds_epoch as u32)); + } + let fractions = + fractional_part_from_subsec_ns(fraction_resolution, now.subsec_nanos() as u64); + Self::new_with_fractions(ccsds_epoch as u32, fractions.unwrap()) + .map_err(|e| StdTimestampError::TimestampError(e.into())) + } + + /// Updates the current time stamp from the current time. The fractional field width remains + /// the same and will be updated accordingly. + #[cfg(feature = "std")] + pub fn update_from_now(&mut self) -> Result<(), StdTimestampError> { + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; + self.counter.1 = unix_epoch_to_ccsds_epoch(now.as_secs()) as u32; + if self.fractions.is_some() { + self.fractions = fractional_part_from_subsec_ns( + self.fractions.unwrap().0, + now.subsec_nanos() as u64, + ); + } + Ok(()) + } + + pub fn new_u16_counter(counter: u16) -> Self { + // These values are definitely valid, so it is okay to unwrap here. + Self::new_generic(WidthCounterPair(2, counter as u32), None).unwrap() + } + + pub fn width_counter_pair(&self) -> WidthCounterPair { + self.counter + } + + pub fn width_fractions_pair(&self) -> Option { + self.fractions + } + + pub fn set_fractions(&mut self, fractions: FractionalPart) -> Result<(), CucError> { + Self::verify_fractions_width(fractions.0)?; + Self::verify_fractions_value(fractions)?; + self.fractions = Some(fractions); + self.update_p_field_fractions(); + Ok(()) + } + + /// Set a fractional resolution. Please note that this function will reset the fractional value + /// to 0 if the resolution changes. + pub fn set_fractional_resolution(&mut self, res: FractionalResolution) { + if res == FractionalResolution::Seconds { + self.fractions = None; + } + let mut update_fractions = true; + if let Some(existing_fractions) = self.fractions { + if existing_fractions.0 == res { + update_fractions = false; + } + }; + if update_fractions { + self.fractions = Some(FractionalPart(res, 0)); + } + } + + pub fn new_generic( + counter: WidthCounterPair, + fractions: Option, + ) -> Result { + Self::verify_counter_width(counter.0)?; + if counter.1 > (2u64.pow(counter.0 as u32 * 8) - 1) as u32 { + return Err(CucError::InvalidCounter(counter.0, counter.1 as u64)); + } + if let Some(fractions) = fractions { + Self::verify_fractions_width(fractions.0)?; + Self::verify_fractions_value(fractions)?; + } + Ok(Self { + pfield: Self::build_p_field(counter.0, fractions.map(|v| v.0)), + counter, + fractions, + }) + } + fn build_p_field(counter_width: u8, fractions_width: Option) -> u8 { let mut pfield = (CcsdsTimeCodes::CucCcsdsEpoch as u8) << 4; if !(1..=4).contains(&counter_width) { @@ -247,142 +385,6 @@ impl TimeProviderCcsdsEpoch { } } -impl TimeProviderCcsdsEpoch { - /// Create a time provider with a four byte counter and no fractional part. - pub fn new(counter: u32) -> Self { - // These values are definitely valid, so it is okay to unwrap here. - Self::new_generic(WidthCounterPair(4, counter), None).unwrap() - } - - /// Like [TimeProviderCcsdsEpoch::new] but allow to supply a fractional part as well. - pub fn new_with_fractions(counter: u32, fractions: FractionalPart) -> Result { - Self::new_generic(WidthCounterPair(4, counter), Some(fractions)) - } - - /// Fractions with a resolution of ~ 4 ms - pub fn new_with_coarse_fractions(counter: u32, subsec_fractions: u8) -> Self { - // These values are definitely valid, so it is okay to unwrap here. - Self::new_generic( - WidthCounterPair(4, counter), - Some(FractionalPart( - FractionalResolution::FourMs, - subsec_fractions as u32, - )), - ) - .unwrap() - } - - /// Fractions with a resolution of ~ 16 us - pub fn new_with_medium_fractions(counter: u32, subsec_fractions: u16) -> Self { - // These values are definitely valid, so it is okay to unwrap here. - Self::new_generic( - WidthCounterPair(4, counter), - Some(FractionalPart( - FractionalResolution::FifteenUs, - subsec_fractions as u32, - )), - ) - .unwrap() - } - - /// Fractions with a resolution of ~ 60 ns - pub fn new_with_fine_fractions(counter: u32, subsec_fractions: u32) -> Result { - Self::new_generic( - WidthCounterPair(4, counter), - Some(FractionalPart( - FractionalResolution::SixtyNs, - subsec_fractions as u32, - )), - ) - } - - /// This function will return the current time as a CUC timestamp. - /// The counter width will always be set to 4 bytes because the normal CCSDS epoch will overflow - /// when using less than that. - #[cfg(feature = "std")] - pub fn from_now(fraction_resolution: FractionalResolution) -> Result { - let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; - let ccsds_epoch = unix_epoch_to_ccsds_epoch(now.as_secs()); - if fraction_resolution == FractionalResolution::Seconds { - return Ok(Self::new(ccsds_epoch as u32)); - } - let fractions = - fractional_part_from_subsec_ns(fraction_resolution, now.subsec_nanos() as u64); - Self::new_with_fractions(ccsds_epoch as u32, fractions.unwrap()) - .map_err(|e| StdTimestampError::TimestampError(e.into())) - } - - #[cfg(feature = "std")] - pub fn update_from_now(&mut self) -> Result<(), StdTimestampError> { - let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; - self.counter.1 = unix_epoch_to_ccsds_epoch(now.as_secs()) as u32; - if self.fractions.is_some() { - self.fractions = fractional_part_from_subsec_ns( - self.fractions.unwrap().0, - now.subsec_nanos() as u64, - ); - } - Ok(()) - } - - pub fn new_u16_counter(counter: u16) -> Self { - // These values are definitely valid, so it is okay to unwrap here. - Self::new_generic(WidthCounterPair(2, counter as u32), None).unwrap() - } - - pub fn width_counter_pair(&self) -> WidthCounterPair { - self.counter - } - - pub fn width_fractions_pair(&self) -> Option { - self.fractions - } - - pub fn set_fractions(&mut self, fractions: FractionalPart) -> Result<(), CucError> { - Self::verify_fractions_width(fractions.0)?; - Self::verify_fractions_value(fractions)?; - self.fractions = Some(fractions); - self.update_p_field_fractions(); - Ok(()) - } - - /// Set a fractional resolution. Please note that this function will reset the fractional value - /// to 0 if the resolution changes. - pub fn set_fractional_resolution(&mut self, res: FractionalResolution) { - if res == FractionalResolution::Seconds { - self.fractions = None; - } - let mut update_fractions = true; - if let Some(existing_fractions) = self.fractions { - if existing_fractions.0 == res { - update_fractions = false; - } - }; - if update_fractions { - self.fractions = Some(FractionalPart(res, 0)); - } - } - - pub fn new_generic( - counter: WidthCounterPair, - fractions: Option, - ) -> Result { - Self::verify_counter_width(counter.0)?; - if counter.1 > (2u64.pow(counter.0 as u32 * 8) - 1) as u32 { - return Err(CucError::InvalidCounter(counter.0, counter.1 as u64)); - } - if let Some(fractions) = fractions { - Self::verify_fractions_width(fractions.0)?; - Self::verify_fractions_value(fractions)?; - } - Ok(Self { - pfield: Self::build_p_field(counter.0, fractions.map(|v| v.0)), - counter, - fractions, - }) - } -} - impl TimeReader for TimeProviderCcsdsEpoch { fn from_bytes(buf: &[u8]) -> Result where -- 2.34.1 From ef963187acf54464dee5cb97351ddc84745743db Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 10 Dec 2022 18:27:13 +0100 Subject: [PATCH 14/23] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16e7e04..1ca80c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added `std::error::Error` implementation for all error enumerations if the `std` feature is enabled. +- CUC timestamp implementation as specified in CCSDS 301.0-B-4 section 3.2. - ACII timestamps as specified in CCSDS 301.0-B-4 section 3.5. - Added MSRV in `Cargo.toml` with the `rust-version` field set to Rust 1.60. - `serde` `Serialize` and `Deserialize` added to all types. -- 2.34.1 From 177ddba9c55261a32dcd7700715a1fdbd9bf39ff Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 19 Dec 2022 00:01:07 +0100 Subject: [PATCH 15/23] clippy fixes --- src/ecss.rs | 6 +++--- src/time/cds.rs | 7 +++---- src/time/cuc.rs | 8 ++++---- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/ecss.rs b/src/ecss.rs index 47159d0..ea2896f 100644 --- a/src/ecss.rs +++ b/src/ecss.rs @@ -286,13 +286,13 @@ impl EcssEnumeration for GenericEcssEnumWrapper { } fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<(), ByteConversionError> { - if buf.len() < self.byte_width() as usize { + if buf.len() < self.byte_width() { return Err(ByteConversionError::ToSliceTooSmall(SizeMissmatch { found: buf.len(), - expected: self.byte_width() as usize, + expected: self.byte_width(), })); } - buf[0..self.byte_width() as usize].copy_from_slice(self.val.to_be_bytes().as_ref()); + buf[0..self.byte_width()].copy_from_slice(self.val.to_be_bytes().as_ref()); Ok(()) } } diff --git a/src/time/cds.rs b/src/time/cds.rs index dd88215..b22015b 100644 --- a/src/time/cds.rs +++ b/src/time/cds.rs @@ -338,9 +338,8 @@ impl TimeProvider { unix_seconds: 0, submillis_precision: None, }; - let unix_days_seconds = - ccsds_to_unix_days(ccsds_days.into()) as i64 * SECONDS_PER_DAY as i64; - provider.setup(unix_days_seconds as i64, ms_of_day.into()); + let unix_days_seconds = ccsds_to_unix_days(ccsds_days.into()) * SECONDS_PER_DAY as i64; + provider.setup(unix_days_seconds, ms_of_day.into()); Ok(provider) } @@ -552,7 +551,7 @@ impl CcsdsTimeProvider for TimeProvider Option> { - self.calc_date_time((self.ms_of_day % 1000) as u32) + self.calc_date_time(self.ms_of_day % 1000) } } diff --git a/src/time/cuc.rs b/src/time/cuc.rs index 9ccd439..7347921 100644 --- a/src/time/cuc.rs +++ b/src/time/cuc.rs @@ -206,7 +206,7 @@ impl TimeProviderCcsdsEpoch { WidthCounterPair(4, counter), Some(FractionalPart( FractionalResolution::SixtyNs, - subsec_fractions as u32, + subsec_fractions, )), ) } @@ -431,9 +431,9 @@ impl TimeReader for TimeProviderCcsdsEpoch { 3 => { let mut tmp_buf: [u8; 4] = [0; 4]; tmp_buf[1..4].copy_from_slice(&buf[current_idx..current_idx + 3]); - u32::from_be_bytes(tmp_buf) as u32 + u32::from_be_bytes(tmp_buf) } - 4 => u32::from_be_bytes(buf[current_idx..current_idx + 4].try_into().unwrap()) as u32, + 4 => u32::from_be_bytes(buf[current_idx..current_idx + 4].try_into().unwrap()), _ => panic!("unreachable match arm"), }; current_idx += cntr_len as usize; @@ -458,7 +458,7 @@ impl TimeReader for TimeProviderCcsdsEpoch { tmp_buf[1..4].copy_from_slice(&buf[current_idx..current_idx + 3]); fractions = Some(FractionalPart( fractions_len.try_into().unwrap(), - u32::from_be_bytes(tmp_buf) as u32, + u32::from_be_bytes(tmp_buf), )) } _ => panic!("unreachable match arm"), -- 2.34.1 From f641248ac2595f22519e8d229298cd4c83e3e82b Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 20 Dec 2022 16:28:10 +0100 Subject: [PATCH 16/23] add PR link --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7eadb2..5d6e482 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## Added +CUC PR: https://egit.irs.uni-stuttgart.de/rust/spacepackets/pulls/4/files + - Added PFC enumerations: `ecss::UnsignedPfc` and `ecss::RealPfc`. PR: https://egit.irs.uni-stuttgart.de/rust/spacepackets/pulls/5 - Added `std::error::Error` implementation for all error enumerations if the `std` feature -- 2.34.1 From 3828a98c76103d67d7bac99d18a428152af4629a Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 21 Dec 2022 01:01:05 +0100 Subject: [PATCH 17/23] important bugfix --- src/time/cuc.rs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/time/cuc.rs b/src/time/cuc.rs index 7347921..bf87064 100644 --- a/src/time/cuc.rs +++ b/src/time/cuc.rs @@ -64,10 +64,16 @@ pub fn fractional_part_from_subsec_ns( if ns > sec_as_ns { panic!("passed nanosecond value larger than 1 second"); } + let resolution = fractional_res_to_div(res) as u64; + // Use integer division because this can reduce code size of really small systems. // First determine the nanoseconds for the smallest segment given the resolution. - // Then divide by that to find out the fractional part. An integer division floors - // which is what we want here. - let fractional_part = ns / (sec_as_ns / fractional_res_to_div(res) as u64); + // Then divide by that to find out the fractional part. For the calculation of the smallest + // fraction, we perform a ceiling division. This is because if we would use the default + // flooring division, we would divide by a smaller value, thereby allowing the calculation to + // invalid fractional parts which are too large. For the division of the nanoseconds by the + // smallest fraction, a flooring division is correct. + // The multiplication with 100000 is necessary to avoid precision loss during integer division. + let fractional_part = ns * 100000 / ((sec_as_ns * 100000 + resolution) / resolution); Some(FractionalPart(res, fractional_part as u32)) } @@ -914,4 +920,12 @@ mod tests { let res = stamp.update_from_now(); assert!(res.is_ok()); } + + #[test] + fn assert_largest_fractions() { + let fractions = fractional_part_from_subsec_ns(FractionalResolution::SixtyNs, 10u64.pow(9) - 1).unwrap(); + // The value can not be larger than representable by 3 bytes + // Assert that the maximum resolution can be reached + assert_eq!(fractions.1, 2_u32.pow(3 * 8) - 2); + } } -- 2.34.1 From 14fa1bad9263f14d4c823dd9f93ccb599a99756f Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 21 Dec 2022 01:03:26 +0100 Subject: [PATCH 18/23] add TODO --- src/time/cuc.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/time/cuc.rs b/src/time/cuc.rs index bf87064..a7660e2 100644 --- a/src/time/cuc.rs +++ b/src/time/cuc.rs @@ -73,6 +73,8 @@ pub fn fractional_part_from_subsec_ns( // invalid fractional parts which are too large. For the division of the nanoseconds by the // smallest fraction, a flooring division is correct. // The multiplication with 100000 is necessary to avoid precision loss during integer division. + // TODO: Floating point division might actually be faster option, but requires additional + // code on small embedded systems.. let fractional_part = ns * 100000 / ((sec_as_ns * 100000 + resolution) / resolution); Some(FractionalPart(res, fractional_part as u32)) } -- 2.34.1 From 472bfa9964f3c0cd218e829f9c93d8564fcb7ecc Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 21 Dec 2022 01:17:36 +0100 Subject: [PATCH 19/23] add floating point division code --- src/time/cuc.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/time/cuc.rs b/src/time/cuc.rs index a7660e2..bae59dc 100644 --- a/src/time/cuc.rs +++ b/src/time/cuc.rs @@ -76,6 +76,8 @@ pub fn fractional_part_from_subsec_ns( // TODO: Floating point division might actually be faster option, but requires additional // code on small embedded systems.. let fractional_part = ns * 100000 / ((sec_as_ns * 100000 + resolution) / resolution); + // Floating point division. + //let fractional_part = (ns as f64 / ((sec_as_ns as f64) / resolution as f64)).floor() as u32; Some(FractionalPart(res, fractional_part as u32)) } @@ -930,4 +932,17 @@ mod tests { // Assert that the maximum resolution can be reached assert_eq!(fractions.1, 2_u32.pow(3 * 8) - 2); } + + // extern crate test; + // use test::Bencher; + // + // #[bench] + // fn speed_test(b: &mut Bencher) { + // let ns = 10_u32.pow(9) - 1; + // let sec_as_ns = ns + 1; + // let resolution = 2_u32.pow(3 * 8) - 1; + // b.iter(|| { + // ns * 100000 / ((sec_as_ns * 100000 + resolution) / resolution) + // }); + // } } -- 2.34.1 From 83e2cad75377969b1e74379acff2c0dcf858d4cf Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 21 Dec 2022 09:47:09 +0100 Subject: [PATCH 20/23] cargo fmt --- src/time/cuc.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/time/cuc.rs b/src/time/cuc.rs index bae59dc..438293b 100644 --- a/src/time/cuc.rs +++ b/src/time/cuc.rs @@ -927,7 +927,9 @@ mod tests { #[test] fn assert_largest_fractions() { - let fractions = fractional_part_from_subsec_ns(FractionalResolution::SixtyNs, 10u64.pow(9) - 1).unwrap(); + let fractions = + fractional_part_from_subsec_ns(FractionalResolution::SixtyNs, 10u64.pow(9) - 1) + .unwrap(); // The value can not be larger than representable by 3 bytes // Assert that the maximum resolution can be reached assert_eq!(fractions.1, 2_u32.pow(3 * 8) - 2); -- 2.34.1 From f137bd2549ef4da5bbf4408171766d6956087e2e Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Thu, 22 Dec 2022 23:45:15 +0100 Subject: [PATCH 21/23] improve tests --- src/time/cuc.rs | 51 +++++++++++++++++++++++-------------------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/src/time/cuc.rs b/src/time/cuc.rs index 438293b..6807769 100644 --- a/src/time/cuc.rs +++ b/src/time/cuc.rs @@ -877,29 +877,39 @@ mod tests { #[test] fn fractional_part_formula() { let fractional_part = - 7843137 / (10_u64.pow(9) / fractional_res_to_div(FractionalResolution::FourMs) as u64); - assert_eq!(fractional_part, 2); + fractional_part_from_subsec_ns(FractionalResolution::FourMs, 7843138).unwrap(); + assert_eq!(fractional_part.1, 2); } #[test] fn fractional_part_formula_2() { let fractional_part = - 12000000 / (10_u64.pow(9) / fractional_res_to_div(FractionalResolution::FourMs) as u64); - assert_eq!(fractional_part, 3); + fractional_part_from_subsec_ns(FractionalResolution::FourMs, 12000000).unwrap(); + assert_eq!(fractional_part.1, 3); } #[test] fn fractional_part_formula_3() { - let one_fraction_with_width_two_in_ns = 10_u64.pow(9) / (2_u32.pow(8 * 2) - 1) as u64; - assert_eq!(one_fraction_with_width_two_in_ns, 15259); - let hundred_fractions_and_some = 100 * one_fraction_with_width_two_in_ns + 7000; - let fractional_part = hundred_fractions_and_some - / (10_u64.pow(9) / fractional_res_to_div(FractionalResolution::FifteenUs) as u64); - assert_eq!(fractional_part, 100); - let hundred_and_one_fractions = 101 * one_fraction_with_width_two_in_ns; - let fractional_part = hundred_and_one_fractions - / (10_u64.pow(9) / fractional_res_to_div(FractionalResolution::FifteenUs) as u64); - assert_eq!(fractional_part, 101); + let one_fraction_with_width_two_in_ns = + 10_u64.pow(9) as f64 / (2_u32.pow(8 * 2) - 1) as f64; + assert_eq!(one_fraction_with_width_two_in_ns.ceil(), 15260.0); + let hundred_fractions_and_some = + (100.0 * one_fraction_with_width_two_in_ns).floor() as u64 + 7000; + let fractional_part = fractional_part_from_subsec_ns( + FractionalResolution::FifteenUs, + hundred_fractions_and_some, + ) + .unwrap(); + assert_eq!(fractional_part.1, 100); + // Using exactly 101.0 can yield values which will later be rounded down to 100 + let hundred_and_one_fractions = + (101.001 * one_fraction_with_width_two_in_ns).floor() as u64; + let fractional_part = fractional_part_from_subsec_ns( + FractionalResolution::FifteenUs, + hundred_and_one_fractions, + ) + .unwrap(); + assert_eq!(fractional_part.1, 101); } #[test] @@ -934,17 +944,4 @@ mod tests { // Assert that the maximum resolution can be reached assert_eq!(fractions.1, 2_u32.pow(3 * 8) - 2); } - - // extern crate test; - // use test::Bencher; - // - // #[bench] - // fn speed_test(b: &mut Bencher) { - // let ns = 10_u32.pow(9) - 1; - // let sec_as_ns = ns + 1; - // let resolution = 2_u32.pow(3 * 8) - 1; - // b.iter(|| { - // ns * 100000 / ((sec_as_ns * 100000 + resolution) / resolution) - // }); - // } } -- 2.34.1 From 192e2f2c7639e7923641b47816e4d64d2500f6fa Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 4 Jan 2023 23:45:07 +0100 Subject: [PATCH 22/23] make pfield public --- src/time/cds.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/time/cds.rs b/src/time/cds.rs index b22015b..1a843ec 100644 --- a/src/time/cds.rs +++ b/src/time/cds.rs @@ -6,7 +6,7 @@ use super::*; use crate::private::Sealed; use core::fmt::Debug; -const CDS_SHORT_P_FIELD: u8 = (CcsdsTimeCodes::Cds as u8) << 4; +pub const CDS_SHORT_P_FIELD: u8 = (CcsdsTimeCodes::Cds as u8) << 4; pub const MIN_CDS_FIELD_LEN: usize = 7; /// Generic trait implemented by token structs to specify the length of day field at type -- 2.34.1 From ec8a2e1d2401da93d5a63010c5b6e2a3aad4ea14 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 9 Jan 2023 11:07:43 +0100 Subject: [PATCH 23/23] rename pfield preamble constant, add for CUC --- src/time/cds.rs | 5 +++-- src/time/cuc.rs | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/time/cds.rs b/src/time/cds.rs index 1a843ec..9c00352 100644 --- a/src/time/cds.rs +++ b/src/time/cds.rs @@ -6,7 +6,8 @@ use super::*; use crate::private::Sealed; use core::fmt::Debug; -pub const CDS_SHORT_P_FIELD: u8 = (CcsdsTimeCodes::Cds as u8) << 4; +/// Base value for the preamble field for a time field parser to determine the time field type. +pub const P_FIELD_BASE: u8 = (CcsdsTimeCodes::Cds as u8) << 4; pub const MIN_CDS_FIELD_LEN: usize = 7; /// Generic trait implemented by token structs to specify the length of day field at type @@ -391,7 +392,7 @@ impl TimeProvider { day_seg_len: LengthOfDaySegment, submillis_prec: Option, ) -> u8 { - let mut pfield = CDS_SHORT_P_FIELD | ((day_seg_len as u8) << 2); + let mut pfield = P_FIELD_BASE | ((day_seg_len as u8) << 2); if let Some(submillis_prec) = submillis_prec { match submillis_prec { SubmillisPrecision::Microseconds(_) => pfield |= 0b01, diff --git a/src/time/cuc.rs b/src/time/cuc.rs index 6807769..029a241 100644 --- a/src/time/cuc.rs +++ b/src/time/cuc.rs @@ -6,6 +6,9 @@ use super::*; use core::fmt::Debug; const MIN_CUC_LEN: usize = 2; + +/// Base value for the preamble field for a time field parser to determine the time field type. +pub const P_FIELD_BASE: u8 = (CcsdsTimeCodes::CucCcsdsEpoch as u8) << 4; /// Maximum length if the preamble field is not extended. pub const MAX_CUC_LEN_SMALL_PREAMBLE: usize = 8; @@ -310,7 +313,7 @@ impl TimeProviderCcsdsEpoch { } fn build_p_field(counter_width: u8, fractions_width: Option) -> u8 { - let mut pfield = (CcsdsTimeCodes::CucCcsdsEpoch as u8) << 4; + let mut pfield = P_FIELD_BASE; if !(1..=4).contains(&counter_width) { // Okay to panic here, this function is private and all input values should // have been sanitized -- 2.34.1