From de1dc16e60b66472f25379ab299c164fb64677a7 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 13 Mar 2024 17:37:51 +0100 Subject: [PATCH 1/2] this works, need to think about where this is the best solution --- CHANGELOG.md | 6 ++ src/time/cds.rs | 4 +- src/time/cuc.rs | 265 ++++++++++++++++++++++++++++++++++-------------- src/time/mod.rs | 14 ++- 4 files changed, 207 insertions(+), 82 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6578ca4..a5d650b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). # [unreleased] +## Fixed + +- CUC timestamp was fixed to include leap second corrections because it is based on the TAI + time reference. Changed the `TimeReader` trait to allow specifying leap seconds which might + be necessary for some timestamps to enable other API. + ## Added - `From<$EcssEnum$TY> from $TY` for the ECSS enum type definitions. diff --git a/src/time/cds.rs b/src/time/cds.rs index 52bf7cf..96932aa 100644 --- a/src/time/cds.rs +++ b/src/time/cds.rs @@ -1171,13 +1171,13 @@ impl CcsdsTimeProvider for TimeProvider { - fn from_bytes(buf: &[u8]) -> Result { + fn from_bytes_generic(buf: &[u8], _leap_seconds: Option) -> Result { Self::from_bytes_with_u16_days(buf) } } impl TimeReader for TimeProvider { - fn from_bytes(buf: &[u8]) -> Result { + fn from_bytes_generic(buf: &[u8], _leap_seconds: Option) -> Result { Self::from_bytes_with_u24_days(buf) } } diff --git a/src/time/cuc.rs b/src/time/cuc.rs index 4b5d6e8..b8a6c4d 100644 --- a/src/time/cuc.rs +++ b/src/time/cuc.rs @@ -103,6 +103,7 @@ pub enum CucError { resolution: FractionalResolution, value: u64, }, + LeapSecondCorrectionError, } impl Display for CucError { @@ -120,6 +121,9 @@ impl Display for CucError { "invalid cuc fractional part {value} for resolution {resolution:?}" ) } + CucError::LeapSecondCorrectionError => { + write!(f, "error while correcting for leap seconds") + } } } } @@ -153,8 +157,10 @@ pub struct FractionalPart(FractionalResolution, u32); /// use spacepackets::time::cuc::{FractionalResolution, TimeProviderCcsdsEpoch}; /// use spacepackets::time::{TimeWriter, CcsdsTimeCodes, TimeReader, CcsdsTimeProvider}; /// +/// const LEAP_SECONDS: u32 = 37; /// // Highest fractional resolution -/// let timestamp_now = TimeProviderCcsdsEpoch::from_now(FractionalResolution::SixtyNs).expect("creating cuc stamp failed"); +/// let timestamp_now = TimeProviderCcsdsEpoch::from_now(FractionalResolution::SixtyNs, LEAP_SECONDS) +/// .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"); @@ -163,7 +169,7 @@ pub struct FractionalPart(FractionalResolution, u32); /// assert_eq!(written, 8); /// } /// { -/// let read_result = TimeProviderCcsdsEpoch::from_bytes(&raw_stamp); +/// let read_result = TimeProviderCcsdsEpoch::from_bytes_with_leap_seconds(&raw_stamp, LEAP_SECONDS); /// assert!(read_result.is_ok()); /// let stamp_deserialized = read_result.unwrap(); /// assert_eq!(stamp_deserialized, timestamp_now); @@ -175,6 +181,7 @@ pub struct TimeProviderCcsdsEpoch { pfield: u8, counter: WidthCounterPair, fractions: Option, + leap_seconds: u32, } #[inline] @@ -187,18 +194,26 @@ 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 { + pub fn new(counter: u32, leap_seconds: u32) -> Self { // These values are definitely valid, so it is okay to unwrap here. - Self::new_generic(WidthCounterPair(4, counter), None).unwrap() + Self::new_generic(WidthCounterPair(4, counter), None, leap_seconds).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)) + pub fn new_with_fractions( + counter: u32, + fractions: FractionalPart, + leap_seconds: u32, + ) -> Result { + Self::new_generic(WidthCounterPair(4, counter), Some(fractions), leap_seconds) } /// Fractions with a resolution of ~ 4 ms - pub fn new_with_coarse_fractions(counter: u32, subsec_fractions: u8) -> Self { + pub fn new_with_coarse_fractions( + counter: u32, + subsec_fractions: u8, + leap_seconds: u32, + ) -> Self { // These values are definitely valid, so it is okay to unwrap here. Self::new_generic( WidthCounterPair(4, counter), @@ -206,12 +221,17 @@ impl TimeProviderCcsdsEpoch { FractionalResolution::FourMs, subsec_fractions as u32, )), + leap_seconds, ) .unwrap() } /// Fractions with a resolution of ~ 16 us - pub fn new_with_medium_fractions(counter: u32, subsec_fractions: u16) -> Self { + pub fn new_with_medium_fractions( + counter: u32, + subsec_fractions: u16, + leap_seconds: u32, + ) -> Self { // These values are definitely valid, so it is okay to unwrap here. Self::new_generic( WidthCounterPair(4, counter), @@ -219,6 +239,7 @@ impl TimeProviderCcsdsEpoch { FractionalResolution::FifteenUs, subsec_fractions as u32, )), + leap_seconds, ) .unwrap() } @@ -226,40 +247,63 @@ impl TimeProviderCcsdsEpoch { /// 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 { + pub fn new_with_fine_fractions( + counter: u32, + subsec_fractions: u32, + leap_seconds: u32, + ) -> Result { Self::new_generic( WidthCounterPair(4, counter), Some(FractionalPart( FractionalResolution::SixtyNs, subsec_fractions, )), + leap_seconds, ) } /// 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. + /// + /// The CUC timestamp uses TAI as the reference time system. Therefore, leap second corrections + /// must be applied on top of the UTC based time retrieved from the system in addition to the + /// conversion to the CCSDS epoch. #[cfg(feature = "std")] #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] - pub fn from_now(fraction_resolution: FractionalResolution) -> Result { + pub fn from_now( + fraction_resolution: FractionalResolution, + leap_seconds: u32, + ) -> Result { let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; let ccsds_epoch = unix_epoch_to_ccsds_epoch(now.as_secs() as i64); + ccsds_epoch + .checked_add(i64::from(leap_seconds)) + .ok_or(TimestampError::Cuc(CucError::LeapSecondCorrectionError))?; if fraction_resolution == FractionalResolution::Seconds { - return Ok(Self::new(ccsds_epoch as u32)); + return Ok(Self::new(ccsds_epoch as u32, leap_seconds)); } let fractions = fractional_part_from_subsec_ns(fraction_resolution, now.subsec_nanos() as u64); - Self::new_with_fractions(ccsds_epoch as u32, fractions.unwrap()) + Self::new_with_fractions(ccsds_epoch as u32, fractions.unwrap(), leap_seconds) .map_err(|e| StdTimestampError::Timestamp(e.into())) } /// Updates the current time stamp from the current time. The fractional field width remains /// the same and will be updated accordingly. + /// + /// The CUC timestamp uses TAI as the reference time system. Therefore, leap second corrections + /// must be applied on top of the UTC based time retrieved from the system in addition to the + /// conversion to the CCSDS epoch. #[cfg(feature = "std")] #[cfg_attr(doc_cfg, doc(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 i64) as u32; + self.counter + .1 + .checked_add(self.leap_seconds) + .ok_or(TimestampError::Cuc(CucError::LeapSecondCorrectionError))?; if self.fractions.is_some() { self.fractions = fractional_part_from_subsec_ns( self.fractions.unwrap().0, @@ -272,14 +316,20 @@ impl TimeProviderCcsdsEpoch { pub fn from_date_time( dt: &DateTime, res: FractionalResolution, + leap_seconds: u32, ) -> Result { // Year before CCSDS epoch is invalid. if dt.year() < 1958 { return Err(TimestampError::DateBeforeCcsdsEpoch(*dt)); } + let counter = dt + .timestamp() + .checked_add(i64::from(leap_seconds)) + .ok_or(TimestampError::Cuc(CucError::LeapSecondCorrectionError))?; Self::new_generic( - WidthCounterPair(4, dt.timestamp() as u32), + WidthCounterPair(4, counter as u32), fractional_part_from_subsec_ns(res, dt.timestamp_subsec_nanos() as u64), + leap_seconds, ) .map_err(|e| e.into()) } @@ -289,6 +339,7 @@ impl TimeProviderCcsdsEpoch { pub fn from_unix_stamp( unix_stamp: &UnixTimestamp, res: FractionalResolution, + leap_seconds: u32, ) -> Result { let ccsds_epoch = unix_epoch_to_ccsds_epoch(unix_stamp.unix_seconds); // Negative CCSDS epoch is invalid. @@ -297,16 +348,24 @@ impl TimeProviderCcsdsEpoch { unix_stamp.as_date_time().unwrap(), )); } + ccsds_epoch + .checked_add(i64::from(leap_seconds)) + .ok_or(TimestampError::Cuc(CucError::LeapSecondCorrectionError))?; let fractions = fractional_part_from_subsec_ns( res, unix_stamp.subsecond_millis() as u64 * 10_u64.pow(6), ); - Self::new_generic(WidthCounterPair(4, ccsds_epoch as u32), fractions).map_err(|e| e.into()) + Self::new_generic( + WidthCounterPair(4, ccsds_epoch as u32), + fractions, + leap_seconds, + ) + .map_err(|e| e.into()) } - pub fn new_u16_counter(counter: u16) -> Self { + pub fn new_u16_counter(counter: u16, leap_seconds: u32) -> Self { // These values are definitely valid, so it is okay to unwrap here. - Self::new_generic(WidthCounterPair(2, counter as u32), None).unwrap() + Self::new_generic(WidthCounterPair(2, counter as u32), None, leap_seconds).unwrap() } pub fn width_counter_pair(&self) -> WidthCounterPair { @@ -352,6 +411,7 @@ impl TimeProviderCcsdsEpoch { pub fn new_generic( counter: WidthCounterPair, fractions: Option, + leap_seconds: u32, ) -> Result { Self::verify_counter_width(counter.0)?; if counter.1 > (2u64.pow(counter.0 as u32 * 8) - 1) as u32 { @@ -367,6 +427,7 @@ impl TimeProviderCcsdsEpoch { pfield: Self::build_p_field(counter.0, fractions.map(|v| v.0)), counter, fractions, + leap_seconds, }) } @@ -412,6 +473,8 @@ impl TimeProviderCcsdsEpoch { #[inline] fn unix_seconds(&self) -> i64 { ccsds_epoch_to_unix_epoch(self.counter.1 as i64) + .checked_sub(self.leap_seconds as i64) + .unwrap() } /// This returns the length of the individual components of the CUC timestamp in addition @@ -458,10 +521,7 @@ impl TimeProviderCcsdsEpoch { } impl TimeReader for TimeProviderCcsdsEpoch { - fn from_bytes(buf: &[u8]) -> Result - where - Self: Sized, - { + fn from_bytes_generic(buf: &[u8], leap_seconds: Option) -> Result { if buf.len() < MIN_CUC_LEN { return Err(TimestampError::ByteConversion( ByteConversionError::FromSliceTooSmall { @@ -470,6 +530,9 @@ impl TimeReader for TimeProviderCcsdsEpoch { }, )); } + if leap_seconds.is_none() { + return Err(TimestampError::Cuc(CucError::LeapSecondCorrectionError)); + } match ccsds_time_code_from_p_field(buf[0]) { Ok(code) => { if code != CcsdsTimeCodes::CucCcsdsEpoch { @@ -536,7 +599,11 @@ impl TimeReader for TimeProviderCcsdsEpoch { _ => panic!("unreachable match arm"), } } - let provider = Self::new_generic(WidthCounterPair(cntr_len, counter), fractions)?; + let provider = Self::new_generic( + WidthCounterPair(cntr_len, counter), + fractions, + leap_seconds.unwrap(), + )?; Ok(provider) } } @@ -704,9 +771,10 @@ impl Add for TimeProviderCcsdsEpoch { get_provider_values_after_duration_addition(&self, duration); if let Some(fractional_part) = new_fractional_part { // The generated fractional part should always be valid, so its okay to unwrap here. - return Self::new_with_fractions(new_counter, fractional_part).unwrap(); + return Self::new_with_fractions(new_counter, fractional_part, self.leap_seconds) + .unwrap(); } - Self::new(new_counter) + Self::new(new_counter, self.leap_seconds) } } @@ -718,9 +786,14 @@ impl Add for &TimeProviderCcsdsEpoch { get_provider_values_after_duration_addition(self, duration); if let Some(fractional_part) = new_fractional_part { // The generated fractional part should always be valid, so its okay to unwrap here. - return Self::Output::new_with_fractions(new_counter, fractional_part).unwrap(); + return Self::Output::new_with_fractions( + new_counter, + fractional_part, + self.leap_seconds, + ) + .unwrap(); } - Self::Output::new(new_counter) + Self::Output::new(new_counter, self.leap_seconds) } } @@ -732,9 +805,12 @@ mod tests { #[allow(unused_imports)] use std::println; + const LEAP_SECONDS: u32 = 37; + #[test] fn test_basic_zero_epoch() { - let zero_cuc = TimeProviderCcsdsEpoch::new(0); + // Include leap second correction for this. + let zero_cuc = TimeProviderCcsdsEpoch::new(0, LEAP_SECONDS); assert_eq!(zero_cuc.len_as_bytes(), 5); assert_eq!(zero_cuc.width(), zero_cuc.width_counter_pair().0); assert_eq!(zero_cuc.counter(), zero_cuc.width_counter_pair().1); @@ -747,18 +823,22 @@ mod tests { 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); + assert_eq!(dt.year(), 1957); + assert_eq!(dt.month(), 12); + assert_eq!(dt.day(), 31); + assert_eq!(dt.hour(), 23); + assert_eq!(dt.minute(), 59); + assert_eq!(dt.second(), 23); } #[test] fn test_write_no_fractions() { let mut buf: [u8; 16] = [0; 16]; - let zero_cuc = TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(4, 0x20102030), None); + let zero_cuc = TimeProviderCcsdsEpoch::new_generic( + WidthCounterPair(4, 0x20102030), + None, + LEAP_SECONDS, + ); assert!(zero_cuc.is_ok()); let zero_cuc = zero_cuc.unwrap(); let res = zero_cuc.write_to_bytes(&mut buf); @@ -782,7 +862,7 @@ mod tests { #[test] fn test_datetime_now() { let now = Utc::now(); - let cuc_now = TimeProviderCcsdsEpoch::from_now(FractionalResolution::SixtyNs); + let cuc_now = TimeProviderCcsdsEpoch::from_now(FractionalResolution::SixtyNs, LEAP_SECONDS); assert!(cuc_now.is_ok()); let cuc_now = cuc_now.unwrap(); let dt_opt = cuc_now.date_time(); @@ -797,11 +877,16 @@ mod tests { #[test] fn test_read_no_fractions() { let mut buf: [u8; 16] = [0; 16]; - let zero_cuc = - TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(4, 0x20102030), None).unwrap(); + let zero_cuc = TimeProviderCcsdsEpoch::new_generic( + WidthCounterPair(4, 0x20102030), + None, + LEAP_SECONDS, + ) + .unwrap(); zero_cuc.write_to_bytes(&mut buf).unwrap(); let cuc_read_back = - TimeProviderCcsdsEpoch::from_bytes(&buf).expect("reading cuc timestamp failed"); + TimeProviderCcsdsEpoch::from_bytes_with_leap_seconds(&buf, LEAP_SECONDS) + .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); @@ -811,7 +896,8 @@ mod tests { fn invalid_read_len() { let mut buf: [u8; 16] = [0; 16]; for i in 0..2 { - let res = TimeProviderCcsdsEpoch::from_bytes(&buf[0..i]); + let res = + TimeProviderCcsdsEpoch::from_bytes_with_leap_seconds(&buf[0..i], LEAP_SECONDS); assert!(res.is_err()); let err = res.unwrap_err(); if let TimestampError::ByteConversion(ByteConversionError::FromSliceTooSmall { @@ -823,10 +909,12 @@ mod tests { assert_eq!(expected, 2); } } - let large_stamp = TimeProviderCcsdsEpoch::new_with_fine_fractions(22, 300).unwrap(); + let large_stamp = + TimeProviderCcsdsEpoch::new_with_fine_fractions(22, 300, LEAP_SECONDS).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]); + let res = + TimeProviderCcsdsEpoch::from_bytes_with_leap_seconds(&buf[0..i], LEAP_SECONDS); assert!(res.is_err()); let err = res.unwrap_err(); if let TimestampError::ByteConversion(ByteConversionError::FromSliceTooSmall { @@ -843,7 +931,7 @@ mod tests { #[test] fn write_and_read_tiny_stamp() { let mut buf = [0; 2]; - let cuc = TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(1, 200), None); + let cuc = TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(1, 200), None, LEAP_SECONDS); assert!(cuc.is_ok()); let cuc = cuc.unwrap(); assert_eq!(cuc.len_as_bytes(), 2); @@ -852,7 +940,8 @@ mod tests { let written = res.unwrap(); assert_eq!(written, 2); assert_eq!(buf[1], 200); - let cuc_read_back = TimeProviderCcsdsEpoch::from_bytes(&buf); + let cuc_read_back = + TimeProviderCcsdsEpoch::from_bytes_with_leap_seconds(&buf, LEAP_SECONDS); assert!(cuc_read_back.is_ok()); let cuc_read_back = cuc_read_back.unwrap(); assert_eq!(cuc_read_back, cuc); @@ -861,7 +950,8 @@ mod tests { #[test] fn write_slightly_larger_stamp() { let mut buf = [0; 4]; - let cuc = TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(2, 40000), None); + let cuc = + TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(2, 40000), None, LEAP_SECONDS); assert!(cuc.is_ok()); let cuc = cuc.unwrap(); assert_eq!(cuc.len_as_bytes(), 3); @@ -870,7 +960,8 @@ mod tests { 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); + let cuc_read_back = + TimeProviderCcsdsEpoch::from_bytes_with_leap_seconds(&buf, LEAP_SECONDS); assert!(cuc_read_back.is_ok()); let cuc_read_back = cuc_read_back.unwrap(); assert_eq!(cuc_read_back, cuc); @@ -882,7 +973,11 @@ mod tests { #[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); + let cuc = TimeProviderCcsdsEpoch::new_generic( + WidthCounterPair(3, 2_u32.pow(24) - 2), + None, + LEAP_SECONDS, + ); assert!(cuc.is_ok()); let cuc = cuc.unwrap(); assert_eq!(cuc.len_as_bytes(), 4); @@ -893,7 +988,8 @@ mod tests { 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); + let cuc_read_back = + TimeProviderCcsdsEpoch::from_bytes_with_leap_seconds(&buf, LEAP_SECONDS); assert!(cuc_read_back.is_ok()); let cuc_read_back = cuc_read_back.unwrap(); assert_eq!(cuc_read_back, cuc); @@ -902,7 +998,7 @@ mod tests { #[test] fn test_write_invalid_buf() { let mut buf: [u8; 16] = [0; 16]; - let res = TimeProviderCcsdsEpoch::new_with_fine_fractions(0, 0); + let res = TimeProviderCcsdsEpoch::new_with_fine_fractions(0, 0, LEAP_SECONDS); let cuc = res.unwrap(); for i in 0..cuc.len_as_bytes() - 1 { let err = cuc.write_to_bytes(&mut buf[0..i]); @@ -924,7 +1020,7 @@ mod tests { 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); + let res = TimeProviderCcsdsEpoch::from_bytes_with_leap_seconds(&buf, LEAP_SECONDS); assert!(res.is_err()); let err = res.unwrap_err(); if let TimestampError::InvalidTimeCode { expected, found } = err { @@ -938,7 +1034,7 @@ mod tests { #[test] fn test_write_with_coarse_fractions() { let mut buf: [u8; 16] = [0; 16]; - let cuc = TimeProviderCcsdsEpoch::new_with_coarse_fractions(0x30201060, 120); + let cuc = TimeProviderCcsdsEpoch::new_with_coarse_fractions(0x30201060, 120, LEAP_SECONDS); assert!(cuc.fractions.is_some()); assert_eq!(cuc.fractions.unwrap().1, 120); assert_eq!(cuc.fractions.unwrap().0, FractionalResolution::FourMs); @@ -957,10 +1053,10 @@ mod tests { #[test] fn test_read_with_coarse_fractions() { let mut buf: [u8; 16] = [0; 16]; - let cuc = TimeProviderCcsdsEpoch::new_with_coarse_fractions(0x30201060, 120); + let cuc = TimeProviderCcsdsEpoch::new_with_coarse_fractions(0x30201060, 120, LEAP_SECONDS); let res = cuc.write_to_bytes(&mut buf); assert!(res.is_ok()); - let res = TimeProviderCcsdsEpoch::from_bytes(&buf); + let res = TimeProviderCcsdsEpoch::from_bytes_with_leap_seconds(&buf, LEAP_SECONDS); assert!(res.is_ok()); let read_back = res.unwrap(); assert_eq!(read_back, cuc); @@ -969,7 +1065,8 @@ mod tests { #[test] fn test_write_with_medium_fractions() { let mut buf: [u8; 16] = [0; 16]; - let cuc = TimeProviderCcsdsEpoch::new_with_medium_fractions(0x30303030, 30000); + let cuc = + TimeProviderCcsdsEpoch::new_with_medium_fractions(0x30303030, 30000, LEAP_SECONDS); let res = cuc.write_to_bytes(&mut buf); assert!(res.is_ok()); let written = res.unwrap(); @@ -981,10 +1078,11 @@ mod tests { #[test] fn test_read_with_medium_fractions() { let mut buf: [u8; 16] = [0; 16]; - let cuc = TimeProviderCcsdsEpoch::new_with_medium_fractions(0x30303030, 30000); + let cuc = + TimeProviderCcsdsEpoch::new_with_medium_fractions(0x30303030, 30000, LEAP_SECONDS); let res = cuc.write_to_bytes(&mut buf); assert!(res.is_ok()); - let res = TimeProviderCcsdsEpoch::from_bytes(&buf); + let res = TimeProviderCcsdsEpoch::from_bytes_with_leap_seconds(&buf, LEAP_SECONDS); assert!(res.is_ok()); let cuc_read_back = res.unwrap(); assert_eq!(cuc_read_back, cuc); @@ -993,8 +1091,11 @@ 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, + LEAP_SECONDS, + ); assert!(cuc.is_ok()); let cuc = cuc.unwrap(); let res = cuc.write_to_bytes(&mut buf); @@ -1009,13 +1110,16 @@ 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, + LEAP_SECONDS, + ); 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); + let res = TimeProviderCcsdsEpoch::from_bytes_with_leap_seconds(&buf, LEAP_SECONDS); assert!(res.is_ok()); let cuc_read_back = res.unwrap(); assert_eq!(cuc_read_back, cuc); @@ -1089,7 +1193,7 @@ mod tests { #[test] fn update_fractions() { - let mut stamp = TimeProviderCcsdsEpoch::new(2000); + let mut stamp = TimeProviderCcsdsEpoch::new(2000, LEAP_SECONDS); let res = stamp.set_fractions(FractionalPart(FractionalResolution::SixtyNs, 5000)); assert!(res.is_ok()); assert!(stamp.fractions.is_some()); @@ -1100,7 +1204,7 @@ mod tests { #[test] fn set_fract_resolution() { - let mut stamp = TimeProviderCcsdsEpoch::new(2000); + let mut stamp = TimeProviderCcsdsEpoch::new(2000, LEAP_SECONDS); stamp.set_fractional_resolution(FractionalResolution::SixtyNs); assert!(stamp.fractions.is_some()); let fractions = stamp.fractions.unwrap(); @@ -1135,7 +1239,7 @@ mod tests { #[test] fn add_duration_basic() { - let mut cuc_stamp = TimeProviderCcsdsEpoch::new(200); + let mut cuc_stamp = TimeProviderCcsdsEpoch::new(200, LEAP_SECONDS); cuc_stamp.set_fractional_resolution(FractionalResolution::FifteenUs); let duration = Duration::from_millis(2500); cuc_stamp += duration; @@ -1144,7 +1248,7 @@ mod tests { #[test] fn add_duration_basic_on_ref() { - let mut cuc_stamp = TimeProviderCcsdsEpoch::new(200); + let mut cuc_stamp = TimeProviderCcsdsEpoch::new(200, LEAP_SECONDS); cuc_stamp.set_fractional_resolution(FractionalResolution::FifteenUs); let duration = Duration::from_millis(2500); let new_stamp = cuc_stamp + duration; @@ -1153,7 +1257,7 @@ mod tests { #[test] fn add_duration_basic_no_fractions() { - let mut cuc_stamp = TimeProviderCcsdsEpoch::new(200); + let mut cuc_stamp = TimeProviderCcsdsEpoch::new(200, LEAP_SECONDS); let duration = Duration::from_millis(2000); cuc_stamp += duration; assert_eq!(cuc_stamp.counter(), 202); @@ -1162,7 +1266,7 @@ mod tests { #[test] fn add_duration_basic_on_ref_no_fractions() { - let cuc_stamp = TimeProviderCcsdsEpoch::new(200); + let cuc_stamp = TimeProviderCcsdsEpoch::new(200, LEAP_SECONDS); let duration = Duration::from_millis(2000); let new_stamp = cuc_stamp + duration; assert_eq!(new_stamp.counter(), 202); @@ -1171,7 +1275,8 @@ mod tests { #[test] fn add_duration_overflow() { let mut cuc_stamp = - TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(1, 255), None).unwrap(); + TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(1, 255), None, LEAP_SECONDS) + .unwrap(); let duration = Duration::from_secs(10); cuc_stamp += duration; assert_eq!(cuc_stamp.counter.1, 10); @@ -1179,7 +1284,7 @@ mod tests { #[test] fn test_invalid_width_param() { - let error = TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(8, 0), None); + let error = TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(8, 0), None, LEAP_SECONDS); assert!(error.is_err()); let error = error.unwrap_err(); if let CucError::InvalidCounterWidth(width) = error { @@ -1193,14 +1298,18 @@ mod tests { #[test] fn test_from_dt() { let dt = Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(); - let cuc = - TimeProviderCcsdsEpoch::from_date_time(&dt, FractionalResolution::Seconds).unwrap(); - assert_eq!(cuc.counter(), dt.timestamp() as u32); + let cuc = TimeProviderCcsdsEpoch::from_date_time( + &dt, + FractionalResolution::Seconds, + LEAP_SECONDS, + ) + .unwrap(); + assert_eq!(cuc.counter(), dt.timestamp() as u32 + LEAP_SECONDS); } #[test] fn test_new_u16_width() { - let cuc = TimeProviderCcsdsEpoch::new_u16_counter(0); + let cuc = TimeProviderCcsdsEpoch::new_u16_counter(0, LEAP_SECONDS); assert_eq!(cuc.width(), 2); assert_eq!(cuc.counter(), 0); } @@ -1208,9 +1317,12 @@ mod tests { #[test] fn from_unix_stamp() { let unix_stamp = UnixTimestamp::new(0, 0).unwrap(); - let cuc = - TimeProviderCcsdsEpoch::from_unix_stamp(&unix_stamp, FractionalResolution::Seconds) - .expect("failed to create cuc from unix stamp"); + let cuc = TimeProviderCcsdsEpoch::from_unix_stamp( + &unix_stamp, + FractionalResolution::Seconds, + LEAP_SECONDS, + ) + .expect("failed to create cuc from unix stamp"); assert_eq!( cuc.counter(), (-DAYS_CCSDS_TO_UNIX * SECONDS_PER_DAY as i32) as u32 @@ -1219,7 +1331,8 @@ mod tests { #[test] fn test_invalid_counter() { - let cuc_error = TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(1, 256), None); + let cuc_error = + TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(1, 256), None, LEAP_SECONDS); assert!(cuc_error.is_err()); let cuc_error = cuc_error.unwrap_err(); if let CucError::InvalidCounter { width, counter } = cuc_error { @@ -1233,7 +1346,7 @@ mod tests { #[test] fn test_stamp_to_vec() { - let stamp = TimeProviderCcsdsEpoch::new_u16_counter(100); + let stamp = TimeProviderCcsdsEpoch::new_u16_counter(100, LEAP_SECONDS); let stamp_vec = stamp.to_vec().unwrap(); let mut buf: [u8; 16] = [0; 16]; stamp.write_to_bytes(&mut buf).unwrap(); diff --git a/src/time/mod.rs b/src/time/mod.rs index c231404..5246d59 100644 --- a/src/time/mod.rs +++ b/src/time/mod.rs @@ -204,10 +204,16 @@ pub trait TimeWriter { } } -pub trait TimeReader { - fn from_bytes(buf: &[u8]) -> Result - where - Self: Sized; +pub trait TimeReader: Sized { + fn from_bytes(buf: &[u8]) -> Result { + Self::from_bytes_generic(buf, None) + } + + fn from_bytes_with_leap_seconds(buf: &[u8], leap_seconds: u32) -> Result { + Self::from_bytes_generic(buf, Some(leap_seconds)) + } + + fn from_bytes_generic(buf: &[u8], leap_seconds: Option) -> Result; } /// Trait for generic CCSDS time providers. -- 2.43.0 From aacdd9f364589885065d6678d76650b4195a9a34 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 13 Mar 2024 18:12:12 +0100 Subject: [PATCH 2/2] add support for the time library --- CHANGELOG.md | 2 ++ Cargo.toml | 8 ++++++ README.md | 2 ++ src/time/cds.rs | 68 ++++++++++++++++++++++++++++++++++--------------- src/time/cuc.rs | 45 ++++++++++++++++++++++---------- src/time/mod.rs | 8 +++++- 6 files changed, 99 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5d650b..cc87192 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## Added - `From<$EcssEnum$TY> from $TY` for the ECSS enum type definitions. +- Added basic support conversions to the `time` library. Introduce new `chrono` and `timelib` + feature gate # [v0.11.0-rc.0] 2024-03-04 diff --git a/Cargo.toml b/Cargo.toml index c1273d4..c0333c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,9 +34,15 @@ optional = true default-features = false features = ["derive"] +[dependencies.time] +version = "0.3" +default-features = false +optional = true + [dependencies.chrono] version = "0.4" default-features = false +optional = true [dependencies.num-traits] version = "0.2" @@ -50,6 +56,8 @@ default = ["std"] std = ["chrono/std", "chrono/clock", "alloc", "thiserror"] serde = ["dep:serde", "chrono/serde"] alloc = ["postcard/alloc", "chrono/alloc"] +chrono = ["dep:chrono"] +timelib = ["dep:time"] [package.metadata.docs.rs] all-features = true diff --git a/README.md b/README.md index e2551ca..c136b6a 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ deserializing them with an appropriate `serde` provider like ## Optional Features - [`serde`](https://serde.rs/): Adds `serde` support for most types by adding `Serialize` and `Deserialize` `derive`s + - [`chrono`](https://crates.io/crates/chrono): Add basic support for the `chrono` time library. + - [`timelib`](https://crates.io/crates/time): Add basic support for the `time` time library. # Examples diff --git a/src/time/cds.rs b/src/time/cds.rs index 96932aa..fcb3a72 100644 --- a/src/time/cds.rs +++ b/src/time/cds.rs @@ -8,6 +8,7 @@ use super::*; use crate::private::Sealed; #[cfg(feature = "alloc")] use alloc::boxed::Box; +#[cfg(feature = "chrono")] use chrono::Datelike; #[cfg(feature = "alloc")] use core::any::Any; @@ -588,6 +589,20 @@ impl TimeProvider { self.calc_unix_seconds(unix_days_seconds, ms_of_day); } + fn calc_ns_since_last_second(&self) -> u32 { + let mut ns_since_last_sec = (self.ms_of_day % 1000) * 10_u32.pow(6); + match self.submillis_precision() { + SubmillisPrecision::Microseconds => { + ns_since_last_sec += self.submillis() * 1000; + } + SubmillisPrecision::Picoseconds => { + ns_since_last_sec += self.submillis() / 1000; + } + _ => (), + } + ns_since_last_sec + } + #[inline] fn calc_unix_seconds(&mut self, mut unix_days_seconds: i64, ms_of_day: u32) { let seconds_of_day = (ms_of_day / 1000) as i64; @@ -599,7 +614,11 @@ impl TimeProvider { self.unix_stamp = UnixTimestamp::const_new(unix_days_seconds, (ms_of_day % 1000) as u16); } - fn calc_date_time(&self, ns_since_last_second: u32) -> Option> { + #[cfg(feature = "chrono")] + fn calc_chrono_date_time( + &self, + ns_since_last_second: u32, + ) -> Option> { assert!( ns_since_last_second < 10_u32.pow(9), "Invalid MS since last second" @@ -612,6 +631,17 @@ impl TimeProvider { None } + #[cfg(feature = "timelib")] + fn calc_timelib_date_time( + &self, + ns_since_last_second: u32, + ) -> Result { + Ok( + time::OffsetDateTime::from_unix_timestamp(self.unix_stamp.unix_seconds)? + + time::Duration::nanoseconds(ns_since_last_second.into()), + ) + } + fn length_check(&self, buf: &[u8], len_as_bytes: usize) -> Result<(), TimestampError> { if buf.len() < len_as_bytes { return Err(TimestampError::ByteConversion( @@ -1155,18 +1185,16 @@ impl CcsdsTimeProvider for TimeProvider Option> { - let mut ns_since_last_sec = (self.ms_of_day % 1000) * 10_u32.pow(6); - match self.submillis_precision() { - SubmillisPrecision::Microseconds => { - ns_since_last_sec += self.submillis() * 1000; - } - SubmillisPrecision::Picoseconds => { - ns_since_last_sec += self.submillis() / 1000; - } - _ => (), - } - self.calc_date_time(ns_since_last_sec) + #[cfg(feature = "chrono")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "chrono")))] + fn chrono_date_time(&self) -> Option> { + self.calc_chrono_date_time(self.calc_ns_since_last_second()) + } + + #[cfg(feature = "timelib")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "timelib")))] + fn timelib_date_time(&self) -> Result { + self.calc_timelib_date_time(self.calc_ns_since_last_second()) } } @@ -1329,7 +1357,7 @@ mod tests { time_stamper.p_field(), (1, [(CcsdsTimeCodes::Cds as u8) << 4, 0]) ); - let date_time = time_stamper.date_time().unwrap(); + let date_time = time_stamper.chrono_date_time().unwrap(); assert_eq!(date_time.year(), 1958); assert_eq!(date_time.month(), 1); assert_eq!(date_time.day(), 1); @@ -1346,7 +1374,7 @@ mod tests { time_stamper.submillis_precision(), SubmillisPrecision::Absent ); - let date_time = time_stamper.date_time().unwrap(); + let date_time = time_stamper.chrono_date_time().unwrap(); assert_eq!(date_time.year(), 1970); assert_eq!(date_time.month(), 1); assert_eq!(date_time.day(), 1); @@ -1539,7 +1567,7 @@ mod tests { timestamp_now: TimeProvider, compare_stamp: DateTime, ) { - let dt = timestamp_now.date_time().unwrap(); + let dt = timestamp_now.chrono_date_time().unwrap(); if compare_stamp.year() > dt.year() { assert_eq!(compare_stamp.year() - dt.year(), 1); } else { @@ -1736,7 +1764,7 @@ mod tests { time_provider.ms_of_day, 30 * 1000 + 49 * 60 * 1000 + 16 * 60 * 60 * 1000 + subsec_millis ); - assert_eq!(time_provider.date_time().unwrap(), datetime_utc); + assert_eq!(time_provider.chrono_date_time().unwrap(), datetime_utc); } #[test] @@ -1790,7 +1818,7 @@ mod tests { SubmillisPrecision::Microseconds ); assert_eq!(time_provider.submillis(), 500); - assert_eq!(time_provider.date_time().unwrap(), datetime_utc); + assert_eq!(time_provider.chrono_date_time().unwrap(), datetime_utc); } #[test] @@ -1844,7 +1872,7 @@ mod tests { SubmillisPrecision::Picoseconds ); assert_eq!(time_provider.submillis(), submilli_nanos * 1000); - assert_eq!(time_provider.date_time().unwrap(), datetime_utc); + assert_eq!(time_provider.chrono_date_time().unwrap(), datetime_utc); } #[test] @@ -1917,7 +1945,7 @@ mod tests { time_provider.ms_of_day, 30 * 1000 + 49 * 60 * 1000 + 16 * 60 * 60 * 1000 + subsec_millis ); - let dt_back = time_provider.date_time().unwrap(); + let dt_back = time_provider.chrono_date_time().unwrap(); assert_eq!(datetime_utc, dt_back); } diff --git a/src/time/cuc.rs b/src/time/cuc.rs index b8a6c4d..dc121ad 100644 --- a/src/time/cuc.rs +++ b/src/time/cuc.rs @@ -3,7 +3,10 @@ //! //! The core data structure to do this is the [TimeProviderCcsdsEpoch] struct. use super::*; + +#[cfg(feature = "chrono")] use chrono::Datelike; + use core::fmt::Debug; use core::ops::{Add, AddAssign}; use core::time::Duration; @@ -313,8 +316,10 @@ impl TimeProviderCcsdsEpoch { Ok(()) } - pub fn from_date_time( - dt: &DateTime, + #[cfg(feature = "chrono")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "chrono")))] + pub fn from_chrono_date_time( + dt: &chrono::DateTime, res: FractionalResolution, leap_seconds: u32, ) -> Result { @@ -688,18 +693,32 @@ impl CcsdsTimeProvider for TimeProviderCcsdsEpoch { 0 } - 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) { + #[cfg(feature = "chrono")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "chrono")))] + fn chrono_date_time(&self) -> Option> { + let mut ns = 0; + if let Some(fractional_part) = self.fractions { + ns = convert_fractional_part_to_ns(fractional_part); + } + if let LocalResult::Single(res) = chrono::Utc.timestamp_opt(self.unix_seconds(), ns as u32) + { return Some(res); } None } + + #[cfg(feature = "timelib")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "timelib")))] + fn timelib_date_time(&self) -> Result { + let mut ns = 0; + if let Some(fractional_part) = self.fractions { + ns = convert_fractional_part_to_ns(fractional_part); + } + Ok( + time::OffsetDateTime::from_unix_timestamp(self.unix_seconds())? + + time::Duration::nanoseconds(ns as i64), + ) + } } fn get_provider_values_after_duration_addition( @@ -820,7 +839,7 @@ mod tests { assert_eq!(counter.1, 0); let fractions = zero_cuc.width_fractions_pair(); assert!(fractions.is_none()); - let dt = zero_cuc.date_time(); + let dt = zero_cuc.chrono_date_time(); assert!(dt.is_some()); let dt = dt.unwrap(); assert_eq!(dt.year(), 1957); @@ -865,7 +884,7 @@ mod tests { let cuc_now = TimeProviderCcsdsEpoch::from_now(FractionalResolution::SixtyNs, LEAP_SECONDS); assert!(cuc_now.is_ok()); let cuc_now = cuc_now.unwrap(); - let dt_opt = cuc_now.date_time(); + let dt_opt = cuc_now.chrono_date_time(); assert!(dt_opt.is_some()); let dt = dt_opt.unwrap(); let diff = dt - now; @@ -1298,7 +1317,7 @@ mod tests { #[test] fn test_from_dt() { let dt = Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(); - let cuc = TimeProviderCcsdsEpoch::from_date_time( + let cuc = TimeProviderCcsdsEpoch::from_chrono_date_time( &dt, FractionalResolution::Seconds, LEAP_SECONDS, diff --git a/src/time/mod.rs b/src/time/mod.rs index 5246d59..c5d4734 100644 --- a/src/time/mod.rs +++ b/src/time/mod.rs @@ -236,7 +236,13 @@ pub trait CcsdsTimeProvider { UnixTimestamp::const_new(self.unix_seconds(), self.subsecond_millis()) } - fn date_time(&self) -> Option>; + #[cfg(feature = "chrono")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "chrono")))] + fn chrono_date_time(&self) -> Option>; + + #[cfg(feature = "timelib")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "timelib")))] + fn timelib_date_time(&self) -> Result; } /// UNIX timestamp: Elapsed seconds since 1970-01-01T00:00:00+00:00. -- 2.43.0