diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eff220..f0e4913 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,49 @@ and this project adheres to [Semantic Versioning](http://semver.org/). # [unreleased] +## Added + +- New `UnixTimestamp` abstraction which contains the unix seconds as an `i64` + and an optional subsecond millisecond counter (`u16`) +- `MS_PER_DAY` constant. + +### CDS time module + +- Implement `Add` and `AddAssign` for time providers, which allows + easily adding offsets to the providers. +- Implement `TryFrom>` for time providers. +- `get_dyn_time_provider_from_bytes`: Requires `alloc` support and returns + the correct `TimeProvider` instance wrapped as a boxed trait object + `Box` by checking the length of days field. +- Added constructor function to create the time provider + from `chrono::DateTime` and a generic UNIX timestamp (`i64` seconds + and subsecond milliseconds). +- `MAX_DAYS_24_BITS` which contains maximum value which can be supplied + to the days field of a CDS time provider with 24 bits days field width. +- New `CdsTimestamp` trait which encapsulates common fields for all CDS time providers. +- `from_unix_secs_with_u24_days` and `from_unix_secs_with_u16_days` which create + the time provider from a `UnixTimestamp` reference. +- `from_dt_with_u16_days`, `from_dt_with_u24_days` and their `..._us_precision` and + `..._ps_precision` variants which allow to create time providers from + a `chrono::DateTime`. +- Add `from_bytes_with_u24_days` and `from_bytes_with_u16_days` associated methods + +## Changed + +- (breaking): Renamed `from_now_with_u24_days_and_us_prec` to `from_now_with_u24_days_us_precision`. + Also did the same for the `u16` variant. +- (breaking): Renamed `from_now_with_u24_days_and_ps_prec` to `from_now_with_u24_days_ps_precision`. + Also did the same for the `u16` variant. +- `CcsdsTimeProvider` trait (breaking): + - Add new `unix_stamp` method returning the new `UnixTimeStamp` struct. + - Add new `subsecond_millis` method returning counter `Option`. + - Default impl for `unix_stamp` which re-uses `subsecond_millis` and + existing `unix_seconds` method. +- `TimestampError` (breaking): Add `DateBeforeCcsdsEpoch` error type + because new CDS API allow supplying invalid date times before CCSDS epoch. + Make `TimestampError` with `#[non_exhaustive]` attribute to prevent + future breakages if new error variants are added. + # [v0.4.2] 14.01.2023 ## Fixed diff --git a/Cargo.toml b/Cargo.toml index 615cc77..8bc5290 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spacepackets" -version = "0.4.2" +version = "0.5.0-rc.0" edition = "2021" rust-version = "1.60" authors = ["Robin Mueller "] diff --git a/src/time/cds.rs b/src/time/cds.rs index e600fbb..a8d21f2 100644 --- a/src/time/cds.rs +++ b/src/time/cds.rs @@ -1,19 +1,39 @@ //! 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. +//! The core data structure to do this is the [TimeProvider] struct and the +//! [get_dyn_time_provider_from_bytes] function to retrieve correct instances of the +//! struct from a bytestream. use super::*; use crate::private::Sealed; +#[cfg(feature = "alloc")] +use alloc::boxed::Box; +use chrono::Datelike; +#[cfg(feature = "alloc")] +use core::any::Any; use core::fmt::Debug; +use core::ops::{Add, AddAssign}; +use core::time::Duration; +use delegate::delegate; /// 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; +pub const MAX_DAYS_24_BITS: u32 = 2_u32.pow(24) - 1; /// 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 FieldType: Debug + + Copy + + Clone + + PartialEq + + Eq + + TryFrom + + TryFrom + + From + + Into + + Into; } /// Type level token to be used as a generic parameter to [TimeProvider]. @@ -84,6 +104,7 @@ pub fn length_of_day_segment_from_pfield(pfield: u8) -> LengthOfDaySegment { } LengthOfDaySegment::Short16Bits } + pub fn precision_from_pfield(pfield: u8) -> SubmillisPrecision { match pfield & 0b11 { 0b01 => SubmillisPrecision::Microseconds(0), @@ -100,6 +121,11 @@ pub fn precision_from_pfield(pfield: u8) -> SubmillisPrecision { /// 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]. /// +/// If you do not want to perform a forward check of the days length field with +/// [length_of_day_segment_from_pfield] and you have [alloc] support, you can also +/// use [get_dyn_time_provider_from_bytes] to retrieve the correct instance as a [DynCdsTimeProvider] +/// trait object. +/// /// 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 @@ -108,8 +134,9 @@ pub fn precision_from_pfield(pfield: u8) -> SubmillisPrecision { /// # Example /// /// ``` -/// use spacepackets::time::cds::{TimeProvider, DaysLen16Bits}; -/// use spacepackets::time::{TimeWriter, CcsdsTimeCodes, TimeReader, CcsdsTimeProvider}; +/// use core::time::Duration; +/// use spacepackets::time::cds::{TimeProvider, length_of_day_segment_from_pfield, LengthOfDaySegment}; +/// use spacepackets::time::{TimeWriter, CcsdsTimeCodes, CcsdsTimeProvider}; /// /// let timestamp_now = TimeProvider::from_now_with_u16_days().unwrap(); /// let mut raw_stamp = [0; 7]; @@ -119,11 +146,17 @@ pub fn precision_from_pfield(pfield: u8) -> SubmillisPrecision { /// assert_eq!(written, 7); /// } /// { -/// let read_result = TimeProvider::::from_bytes(&raw_stamp); +/// assert_eq!(length_of_day_segment_from_pfield(raw_stamp[0]), LengthOfDaySegment::Short16Bits); +/// let read_result = TimeProvider::from_bytes_with_u16_days(&raw_stamp); /// assert!(read_result.is_ok()); /// let stamp_deserialized = read_result.unwrap(); /// assert_eq!(stamp_deserialized.len_as_bytes(), 7); /// } +/// // It is possible to add a Duration offset to a timestamp provider. Add 5 minutes offset here +/// let offset = Duration::from_secs(60 * 5); +/// let former_unix_seconds = timestamp_now.unix_seconds(); +/// let timestamp_in_5_minutes = timestamp_now + offset; +/// assert_eq!(timestamp_in_5_minutes.unix_seconds(), former_unix_seconds + 5 * 60); /// ``` #[derive(Debug, Copy, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -132,14 +165,164 @@ pub struct TimeProvider { ccsds_days: DaysLen::FieldType, ms_of_day: u32, submillis_precision: Option, - unix_seconds: i64, + /// This is not strictly necessary but still cached because it significantly simplifies the + /// calculation of [`DateTime`]. + unix_stamp: UnixTimestamp, +} + +/// Common properties for all CDS time providers. +/// +/// Also exists to encapsulate properties used by private converters. +pub trait CdsCommon { + fn submillis_precision(&self) -> Option; + fn ms_of_day(&self) -> u32; + fn ccsds_days_as_u32(&self) -> u32; +} + +/// Generic properties for all CDS time providers. +pub trait CdsTimestamp: CdsCommon { + fn len_of_day_seg(&self) -> LengthOfDaySegment; +} + +/// Private trait which serves as an abstraction for different converters. +trait CdsConverter: CdsCommon { + fn unix_days_seconds(&self) -> i64; +} + +struct ConversionFromUnix { + ccsds_days: u32, + ms_of_day: u32, + /// This is a side-product of the calculation of the CCSDS days. It is useful for + /// re-calculating the datetime at a later point and therefore supplied as well. + unix_days_seconds: i64, +} + +impl ConversionFromUnix { + fn new(unix_seconds: i64, subsec_millis: u32) -> Result { + let (unix_days, secs_of_day) = calc_unix_days_and_secs_of_day(unix_seconds); + let ccsds_days = unix_to_ccsds_days(unix_days); + if ccsds_days == 0 && (secs_of_day > 0 || subsec_millis > 0) || ccsds_days < 0 { + let millis = if unix_seconds < 0 { + unix_seconds * 1000 - subsec_millis as i64 + } else { + unix_seconds * 1000 + subsec_millis as i64 + }; + return Err(TimestampError::DateBeforeCcsdsEpoch( + Utc.timestamp_millis_opt(millis).unwrap(), + )); + } + Ok(Self { + ccsds_days: unix_to_ccsds_days(unix_days) as u32, + ms_of_day: secs_of_day * 1000 + subsec_millis, + unix_days_seconds: unix_days * SECONDS_PER_DAY as i64, + }) + } +} + +impl CdsCommon for ConversionFromUnix { + fn submillis_precision(&self) -> Option { + None + } + + fn ms_of_day(&self) -> u32 { + self.ms_of_day + } + + fn ccsds_days_as_u32(&self) -> u32 { + self.ccsds_days + } +} + +impl CdsConverter for ConversionFromUnix { + fn unix_days_seconds(&self) -> i64 { + self.unix_days_seconds + } +} +/// Helper struct which generates fields for the CDS time provider from a datetime. +struct ConversionFromDatetime { + unix_conversion: ConversionFromUnix, + submillis_prec: Option, +} + +impl CdsCommon for ConversionFromDatetime { + fn submillis_precision(&self) -> Option { + self.submillis_prec + } + + delegate! { + to self.unix_conversion { + fn ms_of_day(&self) -> u32; + fn ccsds_days_as_u32(&self) -> u32; + } + } +} + +impl CdsConverter for ConversionFromDatetime { + delegate! {to self.unix_conversion { fn unix_days_seconds(&self) -> i64; }} +} + +#[inline] +fn calc_unix_days_and_secs_of_day(unix_seconds: i64) -> (i64, u32) { + let mut secs_of_day = unix_seconds % SECONDS_PER_DAY as i64; + let mut unix_days = (unix_seconds - secs_of_day) / SECONDS_PER_DAY as i64; + // Imagine the CCSDS epoch time minus 5 seconds: We now have the last day in the year + // 1969 (-1 unix days) shortly before midnight (SECONDS_PER_DAY - 5). + if secs_of_day < 0 { + unix_days -= 1; + secs_of_day += SECONDS_PER_DAY as i64 + } + (unix_days, secs_of_day as u32) +} + +impl ConversionFromDatetime { + fn new(dt: &DateTime) -> Result { + Self::new_generic(dt, None) + } + + fn new_with_submillis_us_prec(dt: &DateTime) -> Result { + Self::new_generic(dt, Some(SubmillisPrecision::Microseconds(0))) + } + + fn new_with_submillis_ps_prec(dt: &DateTime) -> Result { + Self::new_generic(dt, Some(SubmillisPrecision::Picoseconds(0))) + } + + fn new_generic( + dt: &DateTime, + mut prec: Option, + ) -> Result { + // The CDS timestamp does not support timestamps before the CCSDS epoch. + if dt.year() < 1958 { + return Err(TimestampError::DateBeforeCcsdsEpoch(*dt)); + } + // The contained values in the conversion should be all positive now + let unix_conversion = + ConversionFromUnix::new(dt.timestamp(), dt.timestamp_subsec_millis())?; + if let Some(submilli_prec) = prec { + match submilli_prec { + SubmillisPrecision::Microseconds(_) => { + prec = Some(SubmillisPrecision::Microseconds( + (dt.timestamp_subsec_micros() % 1000) as u16, + )); + } + SubmillisPrecision::Picoseconds(_) => { + prec = Some(SubmillisPrecision::Picoseconds( + (dt.timestamp_subsec_nanos() % 10_u32.pow(6)) * 1000, + )); + } + _ => (), + } + } + Ok(Self { + unix_conversion, + submillis_prec: prec, + }) + } } #[cfg(feature = "std")] struct ConversionFromNow { - ccsds_days: i32, - ms_of_day: u64, - unix_days_seconds: u64, + unix_conversion: ConversionFromUnix, submillis_prec: Option, } @@ -160,8 +343,10 @@ impl ConversionFromNow { 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; + // This should always return a value with valid (non-negative) CCSDS days, + // so it is okay to unwrap + let unix_conversion = ConversionFromUnix::new(epoch as i64, now.subsec_millis()).unwrap(); + // Both values should now be positive if let Some(submilli_prec) = prec { match submilli_prec { SubmillisPrecision::Microseconds(_) => { @@ -178,15 +363,97 @@ impl ConversionFromNow { } } 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, + unix_conversion, submillis_prec: prec, }) } } +#[cfg(feature = "std")] +impl CdsCommon for ConversionFromNow { + fn submillis_precision(&self) -> Option { + self.submillis_prec + } + delegate! { + to self.unix_conversion { + fn ms_of_day(&self) -> u32; + fn ccsds_days_as_u32(&self) -> u32; + } + } +} + +#[cfg(feature = "std")] +impl CdsConverter for ConversionFromNow { + delegate! {to self.unix_conversion { fn unix_days_seconds(&self) -> i64; }} +} + +#[cfg(feature = "alloc")] +pub trait DynCdsTimeProvider: CcsdsTimeProvider + CdsTimestamp + TimeWriter + Any {} +#[cfg(feature = "alloc")] +impl DynCdsTimeProvider for TimeProvider {} +#[cfg(feature = "alloc")] +impl DynCdsTimeProvider for TimeProvider {} + +/// This function returns the correct [TimeProvider] instance from a raw byte array +/// by checking the length of days field. It also checks the CCSDS time code for correctness. +/// +/// # Example +/// +/// ``` +/// use spacepackets::time::cds::{TimeProvider, LengthOfDaySegment, get_dyn_time_provider_from_bytes}; +/// use spacepackets::time::{TimeWriter, CcsdsTimeCodes, CcsdsTimeProvider}; +/// +/// let timestamp_now = TimeProvider::new_with_u16_days(24, 24); +/// 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 dyn_provider = get_dyn_time_provider_from_bytes(&raw_stamp).unwrap(); +/// assert_eq!(dyn_provider.len_of_day_seg(), LengthOfDaySegment::Short16Bits); +/// assert_eq!(dyn_provider.ccsds_days_as_u32(), 24); +/// assert_eq!(dyn_provider.ms_of_day(), 24); +/// assert_eq!(dyn_provider.submillis_precision(), None); +/// } +/// ``` +#[cfg(feature = "alloc")] +pub fn get_dyn_time_provider_from_bytes( + buf: &[u8], +) -> Result, TimestampError> { + let time_code = ccsds_time_code_from_p_field(buf[0]); + if let Err(e) = time_code { + return Err(TimestampError::InvalidTimeCode(CcsdsTimeCodes::Cds, e)); + } + let time_code = time_code.unwrap(); + if time_code != CcsdsTimeCodes::Cds { + return Err(TimestampError::InvalidTimeCode( + CcsdsTimeCodes::Cds, + time_code as u8, + )); + } + if length_of_day_segment_from_pfield(buf[0]) == LengthOfDaySegment::Short16Bits { + Ok(Box::new(TimeProvider::from_bytes_with_u16_days(buf)?)) + } else { + Ok(Box::new(TimeProvider::from_bytes_with_u24_days(buf)?)) + } +} + +impl CdsCommon for TimeProvider { + fn submillis_precision(&self) -> Option { + self.submillis_precision + } + + fn ms_of_day(&self) -> u32 { + self.ms_of_day + } + + fn ccsds_days_as_u32(&self) -> u32 { + self.ccsds_days.into() + } +} + impl TimeProvider { pub fn set_submillis_precision(&mut self, prec: SubmillisPrecision) { self.pfield &= !(0b11); @@ -215,14 +482,6 @@ impl TimeProvider { 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, @@ -289,24 +548,30 @@ impl TimeProvider { init_len } - fn setup(&mut self, unix_days_seconds: i64, ms_of_day: u64) { + fn setup(&mut self, unix_days_seconds: i64, ms_of_day: u32) { 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; + #[inline] + fn calc_unix_seconds(&mut self, unix_days_seconds: i64, ms_of_day: u32) { + self.unix_stamp.unix_seconds = unix_days_seconds; + self.unix_stamp.subsecond_millis = Some((ms_of_day % 1000) as u16); let seconds_of_day = (ms_of_day / 1000) as i64; - if self.unix_seconds < 0 { - self.unix_seconds -= seconds_of_day; + if self.unix_stamp.unix_seconds < 0 { + self.unix_stamp.unix_seconds -= seconds_of_day; } else { - self.unix_seconds += seconds_of_day; + self.unix_stamp.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) { + fn calc_date_time(&self, ns_since_last_second: u32) -> Option> { + assert!( + ns_since_last_second < 10_u32.pow(9), + "Invalid MS since last second" + ); + if let LocalResult::Single(val) = + Utc.timestamp_opt(self.unix_stamp.unix_seconds, ns_since_last_second) + { return Some(val); } None @@ -336,39 +601,88 @@ impl TimeProvider { pfield: Self::generate_p_field(days_len, None), ccsds_days, ms_of_day, - unix_seconds: 0, + unix_stamp: Default::default(), submillis_precision: None, }; - 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()); + let unix_days_seconds = ccsds_to_unix_days(i64::from(ccsds_days)) * SECONDS_PER_DAY as i64; + provider.setup(unix_days_seconds, ms_of_day); Ok(provider) } - #[cfg(feature = "std")] - fn generic_from_now( + fn from_dt_generic( + dt: &DateTime, days_len: LengthOfDaySegment, - conversion_from_now: ConversionFromNow, - ) -> Result - where - >::Error: Debug, - { + ) -> Result { + let conv_from_dt = ConversionFromDatetime::new(dt)?; + Self::generic_from_conversion(days_len, conv_from_dt) + } + + fn from_dt_generic_us_prec( + dt: &DateTime, + days_len: LengthOfDaySegment, + ) -> Result { + let conv_from_dt = ConversionFromDatetime::new_with_submillis_us_prec(dt)?; + Self::generic_from_conversion(days_len, conv_from_dt) + } + + fn from_dt_generic_ps_prec( + dt: &DateTime, + days_len: LengthOfDaySegment, + ) -> Result { + let conv_from_dt = ConversionFromDatetime::new_with_submillis_ps_prec(dt)?; + Self::generic_from_conversion(days_len, conv_from_dt) + } + + fn from_unix_generic( + unix_stamp: &UnixTimestamp, + days_len: LengthOfDaySegment, + ) -> Result { + let conv_from_dt = ConversionFromUnix::new( + unix_stamp.unix_seconds, + unix_stamp.subsecond_millis.unwrap_or(0) as u32, + )?; + Self::generic_from_conversion(days_len, conv_from_dt) + } + + #[cfg(feature = "std")] + fn from_now_generic(days_len: LengthOfDaySegment) -> Result { + let conversion_from_now = ConversionFromNow::new()?; + Self::generic_from_conversion(days_len, conversion_from_now) + .map_err(StdTimestampError::TimestampError) + } + + #[cfg(feature = "std")] + fn from_now_generic_us_prec(days_len: LengthOfDaySegment) -> Result { + let conversion_from_now = ConversionFromNow::new_with_submillis_us_prec()?; + Self::generic_from_conversion(days_len, conversion_from_now) + .map_err(StdTimestampError::TimestampError) + } + + #[cfg(feature = "std")] + fn from_now_generic_ps_prec(days_len: LengthOfDaySegment) -> Result { + let conversion_from_now = ConversionFromNow::new_with_submillis_ps_prec()?; + Self::generic_from_conversion(days_len, conversion_from_now) + .map_err(StdTimestampError::TimestampError) + } + + fn generic_from_conversion( + days_len: LengthOfDaySegment, + converter: C, + ) -> Result { 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(), - ) + converter.ccsds_days_as_u32().try_into().map_err(|_| { + TimestampError::CdsError(CdsError::InvalidCcsdsDays( + converter.ccsds_days_as_u32().into(), + )) })?; let mut provider = Self { - pfield: Self::generate_p_field(days_len, conversion_from_now.submillis_prec), + pfield: Self::generate_p_field(days_len, converter.submillis_precision()), ccsds_days, - ms_of_day: conversion_from_now.ms_of_day as u32, - unix_seconds: 0, - submillis_precision: conversion_from_now.submillis_prec, + ms_of_day: converter.ms_of_day(), + unix_stamp: Default::default(), + submillis_precision: converter.submillis_precision(), }; - provider.setup( - conversion_from_now.unix_days_seconds as i64, - conversion_from_now.ms_of_day, - ); + provider.setup(converter.unix_days_seconds(), converter.ms_of_day()); Ok(provider) } @@ -406,22 +720,25 @@ impl TimeProvider { #[cfg(feature = "std")] #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] - pub fn update_from_now(&mut self) -> Result<(), StdTimestampError> - where - >::Error: Debug, - { + pub fn update_from_now(&mut self) -> Result<(), StdTimestampError> { let conversion_from_now = self.generic_conversion_from_now()?; - let ccsds_days: ProvidesDaysLen::FieldType = - conversion_from_now.ccsds_days.try_into().map_err(|_| { + let ccsds_days: ProvidesDaysLen::FieldType = conversion_from_now + .unix_conversion + .ccsds_days + .try_into() + .map_err(|_| { StdTimestampError::TimestampError( - CdsError::InvalidCcsdsDays(conversion_from_now.ccsds_days as i64).into(), + CdsError::InvalidCcsdsDays( + conversion_from_now.unix_conversion.ccsds_days as i64, + ) + .into(), ) })?; self.ccsds_days = ccsds_days; - self.ms_of_day = conversion_from_now.ms_of_day as u32; + self.ms_of_day = conversion_from_now.unix_conversion.ms_of_day; self.setup( - conversion_from_now.unix_days_seconds as i64, - conversion_from_now.ms_of_day, + conversion_from_now.unix_conversion.unix_days_seconds, + conversion_from_now.unix_conversion.ms_of_day, ); Ok(()) } @@ -430,37 +747,68 @@ impl TimeProvider { 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) { + if ccsds_days > MAX_DAYS_24_BITS { return Err(CdsError::InvalidCcsdsDays(ccsds_days.into())); } Self::generic_new(LengthOfDaySegment::Long24Bits, ccsds_days, ms_of_day) } + /// Create a provider from a generic UNIX timestamp (seconds since 01-01-1970 00:00:00). + /// + /// ## Errors + /// + /// This function will return [TimestampError::DateBeforeCcsdsEpoch] or + /// [TimestampError::CdsError] if the time is before the CCSDS epoch (01-01-1958 00:00:00) or + /// the CCSDS days value exceeds the allowed bit width (24 bits). + pub fn from_unix_secs_with_u24_days( + unix_stamp: &UnixTimestamp, + ) -> Result { + Self::from_unix_generic(unix_stamp, LengthOfDaySegment::Long24Bits) + } + + /// Create a provider from a [`DateTime`] struct. + /// + /// ## Errors + /// + /// This function will return [TimestampError::DateBeforeCcsdsEpoch] or + /// [TimestampError::CdsError] if the time is before the CCSDS epoch (01-01-1958 00:00:00) or + /// the CCSDS days value exceeds the allowed bit width (24 bits). + pub fn from_dt_with_u24_days(dt: &DateTime) -> Result { + Self::from_dt_generic(dt, LengthOfDaySegment::Long24Bits) + } + + /// Like [Self::from_dt_with_u24_days] but with microsecond sub-millisecond precision. + pub fn from_dt_with_u24_days_us_precision(dt: &DateTime) -> Result { + Self::from_dt_generic_us_prec(dt, LengthOfDaySegment::Long24Bits) + } + + /// Like [Self::from_dt_with_u24_days] but with picoseconds sub-millisecond precision. + pub fn from_dt_with_u24_days_ps_precision(dt: &DateTime) -> Result { + Self::from_dt_generic_ps_prec(dt, LengthOfDaySegment::Long24Bits) + } + /// 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) + Self::from_now_generic(LengthOfDaySegment::Long24Bits) } /// 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) + pub fn from_now_with_u24_days_us_precision() -> Result { + Self::from_now_generic_us_prec(LengthOfDaySegment::Long24Bits) } /// 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) + pub fn from_now_with_u24_days_ps_precision() -> Result { + Self::from_now_generic_us_prec(LengthOfDaySegment::Long24Bits) } - fn from_bytes_24_bit_days(buf: &[u8]) -> Result { + pub fn from_bytes_with_u24_days(buf: &[u8]) -> Result { let submillis_precision = Self::generic_raw_read_checks(buf, LengthOfDaySegment::Long24Bits)?; let mut temp_buf: [u8; 4] = [0; 4]; @@ -490,31 +838,60 @@ impl TimeProvider { Self::generic_new(LengthOfDaySegment::Short16Bits, ccsds_days, ms_of_day).unwrap() } + /// Create a provider from a generic UNIX timestamp (seconds since 01-01-1970 00:00:00). + /// + /// ## Errors + /// + /// This function will return [TimestampError::DateBeforeCcsdsEpoch] or + /// [TimestampError::CdsError] if the time is before the CCSDS epoch (01-01-1958 00:00:00) or + /// the CCSDS days value exceeds the allowed bit width (24 bits). + pub fn from_unix_secs_with_u16_days( + unix_stamp: &UnixTimestamp, + ) -> Result { + Self::from_unix_generic(unix_stamp, LengthOfDaySegment::Short16Bits) + } + + /// Create a provider from a [`DateTime`] struct. + /// + /// This function will return a [TimestampError::DateBeforeCcsdsEpoch] or a + /// [TimestampError::CdsError] if the time is before the CCSDS epoch (01-01-1958 00:00:00) or + /// the CCSDS days value exceeds the allowed bit width (16 bits). + pub fn from_dt_with_u16_days(dt: &DateTime) -> Result { + Self::from_dt_generic(dt, LengthOfDaySegment::Short16Bits) + } + + /// Like [Self::from_dt_with_u16_days] but with microsecond sub-millisecond precision. + pub fn from_dt_with_u16_days_us_precision(dt: &DateTime) -> Result { + Self::from_dt_generic_us_prec(dt, LengthOfDaySegment::Short16Bits) + } + + /// Like [Self::from_dt_with_u16_days] but with picoseconds sub-millisecond precision. + pub fn from_dt_with_u16_days_ps_precision(dt: &DateTime) -> Result { + Self::from_dt_generic_ps_prec(dt, LengthOfDaySegment::Short16Bits) + } + /// 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) + Self::from_now_generic(LengthOfDaySegment::Short16Bits) } /// 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) + pub fn from_now_with_u16_days_us_precision() -> Result { + Self::from_now_generic_us_prec(LengthOfDaySegment::Short16Bits) } /// 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) + pub fn from_now_with_u16_days_ps_precision() -> Result { + Self::from_now_generic_ps_prec(LengthOfDaySegment::Short16Bits) } - fn from_bytes_16_bit_days(buf: &[u8]) -> Result { + pub fn from_bytes_with_u16_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()); @@ -534,6 +911,164 @@ impl TimeProvider { } } +fn add_for_max_ccsds_days_val( + time_provider: &TimeProvider, + max_days_val: u32, + duration: Duration, +) -> (u32, u32, Option) { + let mut next_ccsds_days = time_provider.ccsds_days_as_u32(); + let mut next_ms_of_day = time_provider.ms_of_day; + // Increment CCSDS days by a certain amount while also accounting for overflow. + let increment_days = |ccsds_days: &mut u32, days_inc: u32| { + let days_addition: u64 = *ccsds_days as u64 + days_inc as u64; + if days_addition > max_days_val as u64 { + *ccsds_days = (days_addition - max_days_val as u64) as u32; + } else { + *ccsds_days += days_inc; + } + }; + // Increment MS of day by a certain amount while also accounting for overflow, where + // the new value exceeds the MS of a day. + let increment_ms_of_day = |ms_of_day: &mut u32, ms_inc: u32, ccsds_days: &mut u32| { + *ms_of_day += ms_inc; + if *ms_of_day >= MS_PER_DAY { + *ms_of_day -= MS_PER_DAY; + // Re-use existing closure to always amount for overflow. + increment_days(ccsds_days, 1); + } + }; + let precision = if let Some(submillis_prec) = time_provider.submillis_precision { + match submillis_prec { + SubmillisPrecision::Microseconds(mut us) => { + let micros = duration.subsec_micros(); + let submilli_micros = (micros % 1000) as u16; + us += submilli_micros; + if us >= 1000 { + let carryover_us = us - 1000; + increment_ms_of_day(&mut next_ms_of_day, 1, &mut next_ccsds_days); + us = carryover_us; + } + Some(SubmillisPrecision::Microseconds(us)) + } + SubmillisPrecision::Picoseconds(mut ps) => { + // 1 ms as ns is 1e6. + let submilli_nanos = duration.subsec_nanos() % 10_u32.pow(6); + // No overflow risk: The maximum value of an u32 is ~4.294e9, and one ms as ps + // is 1e9. The amount ps can now have is always less than 2e9. + ps += submilli_nanos * 1000; + if ps >= 10_u32.pow(9) { + let carry_over_ps = ps - 10_u32.pow(6); + increment_ms_of_day(&mut next_ms_of_day, 1, &mut next_ccsds_days); + ps = carry_over_ps; + } + Some(SubmillisPrecision::Picoseconds(ps)) + } + _ => None, + } + } else { + None + }; + let full_seconds = duration.as_secs(); + let secs_of_day = (full_seconds % SECONDS_PER_DAY as u64) as u32; + let ms_of_day = secs_of_day * 1000; + increment_ms_of_day(&mut next_ms_of_day, ms_of_day, &mut next_ccsds_days); + increment_days( + &mut next_ccsds_days, + (full_seconds as u32 - secs_of_day) / SECONDS_PER_DAY, + ); + (next_ccsds_days, next_ms_of_day, precision) +} + +impl CdsTimestamp for TimeProvider { + fn len_of_day_seg(&self) -> LengthOfDaySegment { + LengthOfDaySegment::Short16Bits + } +} + +impl CdsTimestamp for TimeProvider { + fn len_of_day_seg(&self) -> LengthOfDaySegment { + LengthOfDaySegment::Long24Bits + } +} + +/// Allows adding an duration in form of an offset. Please note that the CCSDS days will rollover +/// when they overflow, because addition needs to be infallible. The user needs to check for a +/// days overflow when this is a possibility and might be a problem. +impl Add for TimeProvider { + type Output = Self; + + fn add(self, duration: Duration) -> Self::Output { + let (next_ccsds_days, next_ms_of_day, precision) = + add_for_max_ccsds_days_val(&self, u16::MAX as u32, duration); + let mut provider = Self::new_with_u16_days(next_ccsds_days as u16, next_ms_of_day); + if let Some(prec) = precision { + provider.set_submillis_precision(prec); + } + provider + } +} + +/// Allows adding an duration in form of an offset. Please note that the CCSDS days will rollover +/// when they overflow, because addition needs to be infallible. The user needs to check for a +/// days overflow when this is a possibility and might be a problem. +impl Add for TimeProvider { + type Output = Self; + + fn add(self, duration: Duration) -> Self::Output { + let (next_ccsds_days, next_ms_of_day, precision) = + add_for_max_ccsds_days_val(&self, MAX_DAYS_24_BITS, duration); + let mut provider = Self::new_with_u24_days(next_ccsds_days, next_ms_of_day).unwrap(); + if let Some(prec) = precision { + provider.set_submillis_precision(prec); + } + provider + } +} + +/// Allows adding an duration in form of an offset. Please note that the CCSDS days will rollover +/// when they overflow, because addition needs to be infallible. The user needs to check for a +/// days overflow when this is a possibility and might be a problem. +impl AddAssign for TimeProvider { + fn add_assign(&mut self, duration: Duration) { + let (next_ccsds_days, next_ms_of_day, precision) = + add_for_max_ccsds_days_val(self, u16::MAX as u32, duration); + self.ccsds_days = next_ccsds_days as u16; + self.ms_of_day = next_ms_of_day; + self.submillis_precision = precision; + } +} + +/// Allows adding an duration in form of an offset. Please note that the CCSDS days will rollover +/// when they overflow, because addition needs to be infallible. The user needs to check for a +/// days overflow when this is a possibility and might be a problem. +impl AddAssign for TimeProvider { + fn add_assign(&mut self, duration: Duration) { + let (next_ccsds_days, next_ms_of_day, precision) = + add_for_max_ccsds_days_val(self, MAX_DAYS_24_BITS, duration); + self.ccsds_days = next_ccsds_days; + self.ms_of_day = next_ms_of_day; + self.submillis_precision = precision; + } +} + +impl TryFrom> for TimeProvider { + type Error = TimestampError; + + fn try_from(dt: DateTime) -> Result { + let conversion = ConversionFromDatetime::new(&dt)?; + Self::generic_from_conversion(LengthOfDaySegment::Short16Bits, conversion) + } +} + +impl TryFrom> for TimeProvider { + type Error = TimestampError; + + fn try_from(dt: DateTime) -> Result { + let conversion = ConversionFromDatetime::new(&dt)?; + Self::generic_from_conversion(LengthOfDaySegment::Long24Bits, conversion) + } +} + impl CcsdsTimeProvider for TimeProvider { fn len_as_bytes(&self) -> usize { Self::calc_stamp_len(self.pfield) @@ -547,24 +1082,46 @@ impl CcsdsTimeProvider for TimeProvider i64 { - self.unix_seconds + self.unix_stamp.unix_seconds + } + #[inline] + fn subsecond_millis(&self) -> Option { + self.unix_stamp.subsecond_millis + } + + #[inline] + fn unix_stamp(&self) -> UnixTimestamp { + self.unix_stamp } fn date_time(&self) -> Option> { - self.calc_date_time(self.ms_of_day % 1000) + let mut ns_since_last_sec = (self.ms_of_day % 1000) * 10_u32.pow(6); + if let Some(precision) = self.submillis_precision { + match precision { + SubmillisPrecision::Microseconds(us) => { + ns_since_last_sec += us as u32 * 1000; + } + SubmillisPrecision::Picoseconds(ps) => { + ns_since_last_sec += ps / 1000; + } + _ => (), + } + } + self.calc_date_time(ns_since_last_sec) } } impl TimeReader for TimeProvider { fn from_bytes(buf: &[u8]) -> Result { - Self::from_bytes_16_bit_days(buf) + Self::from_bytes_with_u16_days(buf) } } impl TimeReader for TimeProvider { fn from_bytes(buf: &[u8]) -> Result { - Self::from_bytes_24_bit_days(buf) + Self::from_bytes_with_u24_days(buf) } } @@ -616,7 +1173,7 @@ mod tests { use super::*; use crate::time::TimestampError::{ByteConversionError, InvalidTimeCode}; use crate::ByteConversionError::{FromSliceTooSmall, ToSliceTooSmall}; - use chrono::{Datelike, Timelike}; + use chrono::{Datelike, NaiveDate, Timelike}; #[cfg(feature = "serde")] use postcard::{from_bytes, to_allocvec}; use std::format; @@ -624,11 +1181,18 @@ mod tests { #[test] fn test_time_stamp_zero_args() { let time_stamper = TimeProvider::new_with_u16_days(0, 0); + let unix_stamp = time_stamper.unix_stamp(); assert_eq!( - time_stamper.unix_seconds(), + unix_stamp.unix_seconds, (DAYS_CCSDS_TO_UNIX * SECONDS_PER_DAY as i32) as i64 ); + let subsecond_millis = unix_stamp.subsecond_millis; + assert!(subsecond_millis.is_some()); + + assert_eq!(subsecond_millis.unwrap(), 0); assert_eq!(time_stamper.submillis_precision(), None); + assert!(time_stamper.subsecond_millis().is_some()); + assert_eq!(time_stamper.subsecond_millis().unwrap(), 0); assert_eq!(time_stamper.ccdsd_time_code(), CcsdsTimeCodes::Cds); assert_eq!( time_stamper.p_field(), @@ -646,7 +1210,7 @@ mod tests { #[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.unix_stamp().unix_seconds, 0); assert_eq!(time_stamper.submillis_precision(), None); let date_time = time_stamper.date_time().unwrap(); assert_eq!(date_time.year(), 1970); @@ -655,11 +1219,17 @@ mod tests { assert_eq!(date_time.hour(), 0); assert_eq!(date_time.minute(), 0); assert_eq!(date_time.second(), 0); + let time_stamper = TimeProvider::new_with_u16_days((-DAYS_CCSDS_TO_UNIX) as u16, 40); + assert!(time_stamper.subsecond_millis().is_some()); + assert_eq!(time_stamper.subsecond_millis().unwrap(), 40); + let time_stamper = TimeProvider::new_with_u16_days((-DAYS_CCSDS_TO_UNIX) as u16, 1040); + assert!(time_stamper.subsecond_millis().is_some()); + assert_eq!(time_stamper.subsecond_millis().unwrap(), 40); } #[test] fn test_large_days_field_write() { - let time_stamper = TimeProvider::new_with_u24_days(0x108020, 0); + let time_stamper = TimeProvider::new_with_u24_days(0x108020_u32, 0x10203040); assert!(time_stamper.is_ok()); let time_stamper = time_stamper.unwrap(); assert_eq!(time_stamper.len_as_bytes(), 8); @@ -672,13 +1242,13 @@ mod tests { 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!(ms, 0x10203040); assert_eq!((buf[0] >> 2) & 0b1, 1); } #[test] fn test_large_days_field_read() { - let time_stamper = TimeProvider::new_with_u24_days(0x108020, 0); + let time_stamper = TimeProvider::new_with_u24_days(0x108020_u32, 0); assert!(time_stamper.is_ok()); let time_stamper = time_stamper.unwrap(); let mut buf = [0; 16]; @@ -702,9 +1272,8 @@ mod tests { 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 + if let TimestampError::CdsError(CdsError::InvalidCtorForDaysOfLenInPreamble(len_of_day)) = + error { assert_eq!(len_of_day, LengthOfDaySegment::Long24Bits); } else { @@ -824,10 +1393,10 @@ mod tests { 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(); + fn generic_now_test( + timestamp_now: TimeProvider, + compare_stamp: DateTime, + ) { let dt = timestamp_now.date_time().unwrap(); if compare_stamp.year() > dt.year() { assert_eq!(compare_stamp.year() - dt.year(), 1); @@ -849,6 +1418,41 @@ mod tests { generic_dt_property_equality_check(dt.minute(), compare_stamp.minute(), 0, 59); } + #[test] + fn test_time_now() { + let timestamp_now = TimeProvider::from_now_with_u16_days().unwrap(); + let compare_stamp = Utc::now(); + generic_now_test(timestamp_now, compare_stamp); + } + + #[test] + fn test_time_now_us_prec() { + let timestamp_now = TimeProvider::from_now_with_u16_days_us_precision().unwrap(); + let compare_stamp = Utc::now(); + generic_now_test(timestamp_now, compare_stamp); + } + + #[test] + fn test_time_now_ps_prec() { + let timestamp_now = TimeProvider::from_now_with_u16_days_ps_precision().unwrap(); + let compare_stamp = Utc::now(); + generic_now_test(timestamp_now, compare_stamp); + } + + #[test] + fn test_time_now_ps_prec_u16_days() { + let timestamp_now = TimeProvider::from_now_with_u16_days_ps_precision().unwrap(); + let compare_stamp = Utc::now(); + generic_now_test(timestamp_now, compare_stamp); + } + + #[test] + fn test_time_now_ps_prec_u24_days() { + let timestamp_now = TimeProvider::from_now_with_u24_days_ps_precision().unwrap(); + let compare_stamp = Utc::now(); + generic_now_test(timestamp_now, compare_stamp); + } + #[test] fn test_submillis_precision_micros() { let mut time_stamper = TimeProvider::new_with_u16_days(0, 0); @@ -933,6 +1537,461 @@ mod tests { } } + #[test] + fn read_u24_stamp_with_us_submillis_precision() { + let mut time_stamper = TimeProvider::new_with_u24_days(u16::MAX as u32 + 1, 0).unwrap(); + 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"); + // 1 byte pfield + 3 bytes days + 4 bytes ms of day + 2 bytes us precision + assert_eq!(written, 10); + let stamp_deserialized = TimeProvider::from_bytes_with_u24_days(&write_buf); + assert!(stamp_deserialized.is_ok()); + let stamp_deserialized = stamp_deserialized.unwrap(); + assert_eq!(stamp_deserialized.len_as_bytes(), 10); + assert_eq!(stamp_deserialized.ccsds_days(), u16::MAX as u32 + 1); + 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] + fn read_u24_stamp_with_ps_submillis_precision() { + let mut time_stamper = TimeProvider::new_with_u24_days(u16::MAX as u32 + 1, 0).unwrap(); + 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"); + // 1 byte pfield + 3 bytes days + 4 bytes ms of day + 4 bytes us precision + assert_eq!(written, 12); + let stamp_deserialized = TimeProvider::from_bytes_with_u24_days(&write_buf); + assert!(stamp_deserialized.is_ok()); + let stamp_deserialized = stamp_deserialized.unwrap(); + assert_eq!(stamp_deserialized.len_as_bytes(), 12); + assert_eq!(stamp_deserialized.ccsds_days(), u16::MAX as u32 + 1); + 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"); + } + } + + fn generic_dt_case_0_no_prec(subsec_millis: u32) -> DateTime { + let naivedatetime_utc = NaiveDate::from_ymd_opt(2023, 01, 14) + .unwrap() + .and_hms_milli_opt(16, 49, 30, subsec_millis) + .unwrap(); + DateTime::::from_utc(naivedatetime_utc, Utc) + } + + fn generic_check_dt_case_0( + time_provider: &TimeProvider, + subsec_millis: u32, + datetime_utc: DateTime, + ) { + // https://www.timeanddate.com/date/durationresult.html?d1=01&m1=01&y1=1958&d2=14&m2=01&y2=2023 + // Leap years need to be accounted for as well. + assert_eq!(time_provider.ccsds_days, 23754.into()); + assert_eq!( + 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); + } + + #[test] + fn test_creation_from_dt_u16_days() { + let subsec_millis = 250; + let datetime_utc = generic_dt_case_0_no_prec(subsec_millis); + let time_provider = TimeProvider::from_dt_with_u16_days(&datetime_utc).unwrap(); + generic_check_dt_case_0(&time_provider, subsec_millis, datetime_utc); + let time_provider_2: TimeProvider = + datetime_utc.try_into().expect("conversion failed"); + // Test the TryInto trait impl + assert_eq!(time_provider, time_provider_2); + } + #[test] + fn test_creation_from_dt_u24_days() { + let subsec_millis = 250; + let datetime_utc = generic_dt_case_0_no_prec(subsec_millis); + let time_provider = TimeProvider::from_dt_with_u24_days(&datetime_utc).unwrap(); + generic_check_dt_case_0(&time_provider, subsec_millis, datetime_utc); + let time_provider_2: TimeProvider = + datetime_utc.try_into().expect("conversion failed"); + // Test the TryInto trait impl + assert_eq!(time_provider, time_provider_2); + } + + fn generic_dt_case_1_us_prec(subsec_millis: u32) -> DateTime { + // 250 ms + 500 us + let subsec_micros = subsec_millis * 1000 + 500; + let naivedatetime_utc = NaiveDate::from_ymd_opt(2023, 01, 14) + .unwrap() + .and_hms_micro_opt(16, 49, 30, subsec_micros) + .unwrap(); + DateTime::::from_utc(naivedatetime_utc, Utc) + } + + fn generic_check_dt_case_1_us_prec( + time_provider: &TimeProvider, + subsec_millis: u32, + datetime_utc: DateTime, + ) { + // https://www.timeanddate.com/date/durationresult.html?d1=01&m1=01&y1=1958&d2=14&m2=01&y2=2023 + // Leap years need to be accounted for as well. + assert_eq!(time_provider.ccsds_days, 23754.into()); + assert_eq!( + time_provider.ms_of_day, + 30 * 1000 + 49 * 60 * 1000 + 16 * 60 * 60 * 1000 + subsec_millis + ); + assert!(time_provider.submillis_precision.is_some()); + match time_provider.submillis_precision.unwrap() { + SubmillisPrecision::Microseconds(us) => { + assert_eq!(us, 500); + } + _ => panic!("unexpected precision field"), + } + assert_eq!(time_provider.date_time().unwrap(), datetime_utc); + } + + #[test] + fn test_creation_from_dt_u16_days_us_prec() { + let subsec_millis = 250; + let datetime_utc = generic_dt_case_1_us_prec(subsec_millis); + let time_provider = + TimeProvider::from_dt_with_u16_days_us_precision(&datetime_utc).unwrap(); + generic_check_dt_case_1_us_prec(&time_provider, subsec_millis, datetime_utc); + } + + #[test] + fn test_creation_from_dt_u24_days_us_prec() { + let subsec_millis = 250; + let datetime_utc = generic_dt_case_1_us_prec(subsec_millis); + let time_provider = + TimeProvider::from_dt_with_u24_days_us_precision(&datetime_utc).unwrap(); + generic_check_dt_case_1_us_prec(&time_provider, subsec_millis, datetime_utc); + } + + fn generic_dt_case_2_ps_prec(subsec_millis: u32) -> (DateTime, u32) { + // 250 ms + 500 us + let subsec_nanos = subsec_millis * 1000 * 1000 + 500 * 1000; + let submilli_nanos = subsec_nanos % 10_u32.pow(6); + let naivedatetime_utc = NaiveDate::from_ymd_opt(2023, 01, 14) + .unwrap() + .and_hms_nano_opt(16, 49, 30, subsec_nanos) + .unwrap(); + ( + DateTime::::from_utc(naivedatetime_utc, Utc), + submilli_nanos, + ) + } + + fn generic_check_dt_case_2_ps_prec( + time_provider: &TimeProvider, + subsec_millis: u32, + submilli_nanos: u32, + datetime_utc: DateTime, + ) { + // https://www.timeanddate.com/date/durationresult.html?d1=01&m1=01&y1=1958&d2=14&m2=01&y2=2023 + // Leap years need to be accounted for as well. + assert_eq!(time_provider.ccsds_days, 23754.into()); + assert_eq!( + time_provider.ms_of_day, + 30 * 1000 + 49 * 60 * 1000 + 16 * 60 * 60 * 1000 + subsec_millis + ); + assert!(time_provider.submillis_precision.is_some()); + match time_provider.submillis_precision.unwrap() { + SubmillisPrecision::Picoseconds(ps) => { + assert_eq!(ps, submilli_nanos * 1000); + } + _ => panic!("unexpected precision field"), + } + assert_eq!(time_provider.date_time().unwrap(), datetime_utc); + } + + #[test] + fn test_creation_from_dt_u16_days_ps_prec() { + let subsec_millis = 250; + let (datetime_utc, submilli_nanos) = generic_dt_case_2_ps_prec(subsec_millis); + let time_provider = + TimeProvider::from_dt_with_u16_days_ps_precision(&datetime_utc).unwrap(); + generic_check_dt_case_2_ps_prec( + &time_provider, + subsec_millis, + submilli_nanos, + datetime_utc, + ); + } + + #[test] + fn test_creation_from_dt_u24_days_ps_prec() { + let subsec_millis = 250; + let (datetime_utc, submilli_nanos) = generic_dt_case_2_ps_prec(subsec_millis); + let time_provider = + TimeProvider::from_dt_with_u24_days_ps_precision(&datetime_utc).unwrap(); + generic_check_dt_case_2_ps_prec( + &time_provider, + subsec_millis, + submilli_nanos, + datetime_utc, + ); + } + + #[test] + fn test_creation_from_unix_stamp_0_u16_days() { + let unix_secs = 0; + let subsec_millis = 0; + let time_provider = TimeProvider::from_unix_secs_with_u16_days(&UnixTimestamp::const_new( + unix_secs, + subsec_millis, + )) + .expect("creating provider from unix stamp failed"); + assert_eq!(time_provider.ccsds_days, -DAYS_CCSDS_TO_UNIX as u16) + } + + #[test] + fn test_creation_from_unix_stamp_0_u24_days() { + let unix_secs = 0; + let subsec_millis = 0; + let time_provider = TimeProvider::from_unix_secs_with_u24_days(&UnixTimestamp::const_new( + unix_secs, + subsec_millis, + )) + .expect("creating provider from unix stamp failed"); + assert_eq!(time_provider.ccsds_days, (-DAYS_CCSDS_TO_UNIX) as u32) + } + + #[test] + fn test_creation_from_unix_stamp_1() { + let subsec_millis = 250; + let naivedatetime_utc = NaiveDate::from_ymd_opt(2023, 01, 14) + .unwrap() + .and_hms_milli_opt(16, 49, 30, subsec_millis) + .unwrap(); + let datetime_utc = DateTime::::from_utc(naivedatetime_utc, Utc); + let time_provider = TimeProvider::from_unix_secs_with_u16_days(&datetime_utc.into()) + .expect("creating provider from unix stamp failed"); + // https://www.timeanddate.com/date/durationresult.html?d1=01&m1=01&y1=1958&d2=14&m2=01&y2=2023 + // Leap years need to be accounted for as well. + assert_eq!(time_provider.ccsds_days, 23754); + assert_eq!( + time_provider.ms_of_day, + 30 * 1000 + 49 * 60 * 1000 + 16 * 60 * 60 * 1000 + subsec_millis + ); + let dt_back = time_provider.date_time().unwrap(); + assert_eq!(datetime_utc, dt_back); + } + + #[test] + fn test_creation_0_ccsds_days() { + let unix_secs = DAYS_CCSDS_TO_UNIX as i64 * SECONDS_PER_DAY as i64; + let subsec_millis = 0; + let time_provider = TimeProvider::from_unix_secs_with_u16_days(&UnixTimestamp::const_new( + unix_secs, + subsec_millis, + )) + .expect("creating provider from unix stamp failed"); + assert_eq!(time_provider.ccsds_days, 0) + } + + #[test] + fn test_invalid_creation_from_unix_stamp_days_too_large() { + let invalid_unix_secs: i64 = (u16::MAX as i64 + 1) * SECONDS_PER_DAY as i64; + let subsec_millis = 0; + match TimeProvider::from_unix_secs_with_u16_days(&UnixTimestamp::const_new( + invalid_unix_secs as i64, + subsec_millis, + )) { + Ok(_) => { + panic!("creation should not succeed") + } + Err(e) => { + if let TimestampError::CdsError(CdsError::InvalidCcsdsDays(days)) = e { + assert_eq!( + days, + unix_to_ccsds_days(invalid_unix_secs / SECONDS_PER_DAY as i64) + ); + } else { + panic!("unexpected error {}", e) + } + } + } + } + + #[test] + fn test_invalid_creation_from_unix_stamp_before_ccsds_epoch() { + // This is a unix stamp before the CCSDS epoch (01-01-1958 00:00:00), this should be + // precisely 31-12-1957 23:59:55 + let unix_secs = DAYS_CCSDS_TO_UNIX * SECONDS_PER_DAY as i32 - 5; + let subsec_millis = 0; + match TimeProvider::from_unix_secs_with_u16_days(&UnixTimestamp::const_new( + unix_secs as i64, + subsec_millis, + )) { + Ok(_) => { + panic!("creation should not succeed") + } + Err(e) => { + if let TimestampError::DateBeforeCcsdsEpoch(dt) = e { + 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(), 55); + } else { + panic!("unexpected error {}", e) + } + } + } + } + + #[test] + fn test_addition_u16_days_day_increment() { + let mut provider = TimeProvider::new_with_u16_days(0, MS_PER_DAY - 5 * 1000); + let seconds_offset = Duration::from_secs(10); + assert_eq!(provider.ccsds_days, 0); + assert_eq!(provider.ms_of_day, MS_PER_DAY - 5 * 1000); + provider += seconds_offset; + assert_eq!(provider.ccsds_days, 1); + assert_eq!(provider.ms_of_day, 5000); + } + + #[test] + fn test_addition_u16_days() { + let mut provider = TimeProvider::new_with_u16_days(0, 0); + let seconds_offset = Duration::from_secs(5); + assert_eq!(provider.ccsds_days, 0); + assert_eq!(provider.ms_of_day, 0); + provider += seconds_offset; + assert_eq!(provider.ms_of_day, 5000); + // Add one day and test Add operator + let provider2 = provider + Duration::from_secs(60 * 60 * 24); + assert_eq!(provider2.ccsds_days, 1); + assert_eq!(provider2.ms_of_day, 5000); + } + + #[test] + fn test_addition_u24_days() { + let mut provider = TimeProvider::new_with_u24_days(u16::MAX as u32, 0).unwrap(); + let seconds_offset = Duration::from_secs(5); + assert_eq!(provider.ccsds_days, u16::MAX as u32); + assert_eq!(provider.ms_of_day, 0); + provider += seconds_offset; + assert_eq!(provider.ms_of_day, 5000); + // Add one day and test Add operator + let provider2 = provider + Duration::from_secs(60 * 60 * 24); + assert_eq!(provider2.ccsds_days, u16::MAX as u32 + 1); + assert_eq!(provider2.ms_of_day, 5000); + } + + #[test] + fn test_dyn_creation_u24_days() { + let stamp = TimeProvider::new_with_u24_days(u16::MAX as u32 + 1, 24).unwrap(); + let mut buf: [u8; 32] = [0; 32]; + stamp.write_to_bytes(&mut buf).unwrap(); + let dyn_provider = get_dyn_time_provider_from_bytes(&buf); + assert!(dyn_provider.is_ok()); + let dyn_provider = dyn_provider.unwrap(); + assert_eq!(dyn_provider.ccdsd_time_code(), CcsdsTimeCodes::Cds); + assert_eq!(dyn_provider.ccsds_days_as_u32(), u16::MAX as u32 + 1); + assert_eq!(dyn_provider.ms_of_day(), 24); + assert_eq!(dyn_provider.submillis_precision(), None); + assert_eq!( + dyn_provider.len_of_day_seg(), + LengthOfDaySegment::Long24Bits + ); + } + + #[test] + fn test_addition_with_us_precision_u16_days() { + let mut provider = TimeProvider::new_with_u16_days(0, 0); + provider.set_submillis_precision(SubmillisPrecision::Microseconds(0)); + let duration = Duration::from_micros(500); + provider += duration; + assert!(provider.submillis_precision().is_some()); + let prec = provider.submillis_precision().unwrap(); + if let SubmillisPrecision::Microseconds(us) = prec { + assert_eq!(us, 500); + } else { + panic!("invalid precision {:?}", prec) + } + } + + #[test] + fn test_addition_with_ps_precision_u16_days() { + let mut provider = TimeProvider::new_with_u16_days(0, 0); + provider.set_submillis_precision(SubmillisPrecision::Picoseconds(0)); + // 500 us as ns + let duration = Duration::from_nanos(500 * 10u32.pow(3) as u64); + provider += duration; + assert!(provider.submillis_precision().is_some()); + let prec = provider.submillis_precision().unwrap(); + if let SubmillisPrecision::Picoseconds(ps) = prec { + assert_eq!(ps, 500 * 10_u32.pow(6)); + } else { + panic!("invalid precision {:?}", prec) + } + } + + #[test] + fn test_dyn_creation_u16_days_with_precision() { + let mut stamp = TimeProvider::new_with_u16_days(24, 24); + stamp.set_submillis_precision(SubmillisPrecision::Microseconds(666)); + let mut buf: [u8; 32] = [0; 32]; + stamp.write_to_bytes(&mut buf).unwrap(); + let dyn_provider = get_dyn_time_provider_from_bytes(&buf); + assert!(dyn_provider.is_ok()); + let dyn_provider = dyn_provider.unwrap(); + assert_eq!(dyn_provider.ccdsd_time_code(), CcsdsTimeCodes::Cds); + assert_eq!(dyn_provider.ccsds_days_as_u32(), 24); + assert_eq!(dyn_provider.ms_of_day(), 24); + assert_eq!( + dyn_provider.len_of_day_seg(), + LengthOfDaySegment::Short16Bits + ); + assert!(dyn_provider.submillis_precision().is_some()); + if let SubmillisPrecision::Microseconds(us) = dyn_provider.submillis_precision().unwrap() { + assert_eq!(us, 666); + } + } + + #[test] + fn test_new_u24_days_too_large() { + let time_provider = TimeProvider::new_with_u24_days(2_u32.pow(24), 0); + assert!(time_provider.is_err()); + let e = time_provider.unwrap_err(); + if let CdsError::InvalidCcsdsDays(days) = e { + assert_eq!(days, 2_u32.pow(24) as i64); + } else { + panic!("unexpected error {}", e) + } + } + + #[test] + fn test_from_dt_invalid_time() { + // Date before CCSDS epoch + let naivedatetime_utc = NaiveDate::from_ymd_opt(1957, 12, 31) + .unwrap() + .and_hms_milli_opt(23, 59, 59, 999) + .unwrap(); + let datetime_utc = DateTime::::from_utc(naivedatetime_utc, Utc); + let time_provider = TimeProvider::from_dt_with_u24_days(&datetime_utc); + assert!(time_provider.is_err()); + if let TimestampError::DateBeforeCcsdsEpoch(dt) = time_provider.unwrap_err() { + assert_eq!(dt, datetime_utc); + } + } + #[test] #[cfg(feature = "serde")] fn test_serialization() { diff --git a/src/time/cuc.rs b/src/time/cuc.rs index 029a241..21b24a7 100644 --- a/src/time/cuc.rs +++ b/src/time/cuc.rs @@ -42,12 +42,14 @@ impl TryFrom for FractionalResolution { /// 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). +#[inline] 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 } +#[inline(always)] pub const fn fractional_res_to_div(res: FractionalResolution) -> u32 { 2_u32.pow(8 * res as u32) - 1 } @@ -351,6 +353,11 @@ impl TimeProviderCcsdsEpoch { pfield & 0b11 } + #[inline] + fn unix_seconds(&self) -> i64 { + ccsds_epoch_to_unix_epoch(self.counter.1 as u64) as i64 + } + /// This returns the length of the individual components of the CUC timestamp in addition /// to the total size. /// @@ -543,10 +550,19 @@ impl CcsdsTimeProvider for TimeProviderCcsdsEpoch { 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 + self.unix_seconds() + } + + fn subsecond_millis(&self) -> Option { + if let Some(fractions) = self.fractions { + if fractions.0 == FractionalResolution::Seconds { + return None; + } + // Rounding down here is the correct approach. + return Some((convert_fractional_part_to_ns(fractions) / 10_u32.pow(6) as u64) as u16); + } + None } fn date_time(&self) -> Option> { diff --git a/src/time/mod.rs b/src/time/mod.rs index 26d74b2..b5dd984 100644 --- a/src/time/mod.rs +++ b/src/time/mod.rs @@ -20,6 +20,7 @@ pub mod cuc; pub const DAYS_CCSDS_TO_UNIX: i32 = -4383; pub const SECONDS_PER_DAY: u32 = 86400; +pub const MS_PER_DAY: u32 = SECONDS_PER_DAY * 1000; #[derive(Debug, PartialEq, Eq, Copy, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -55,6 +56,7 @@ pub fn ccsds_time_code_from_p_field(pfield: u8) -> Result { #[derive(Debug, PartialEq, Eq, Copy, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] pub enum TimestampError { /// Contains tuple where first value is the expected time code and the second /// value is the found raw value @@ -62,6 +64,7 @@ pub enum TimestampError { ByteConversionError(ByteConversionError), CdsError(cds::CdsError), CucError(cuc::CucError), + DateBeforeCcsdsEpoch(DateTime), CustomEpochNotSupported, } @@ -118,6 +121,9 @@ impl Display for TimestampError { TimestampError::ByteConversionError(e) => { write!(f, "byte conversion error {}", e) } + TimestampError::DateBeforeCcsdsEpoch(e) => { + write!(f, "datetime with date before ccsds epoch: {}", e) + } TimestampError::CustomEpochNotSupported => { write!(f, "custom epochs are not supported") } @@ -199,6 +205,9 @@ pub trait TimeReader { } /// Trait for generic CCSDS time providers. +/// +/// The UNIX helper methods and the [date_time] method are not strictly necessary but extremely +/// practical because they are a very common and simple exchange format for time information. pub trait CcsdsTimeProvider { fn len_as_bytes(&self) -> usize; @@ -208,10 +217,99 @@ pub trait CcsdsTimeProvider { /// in big endian format. fn p_field(&self) -> (usize, [u8; 2]); fn ccdsd_time_code(&self) -> CcsdsTimeCodes; + fn unix_seconds(&self) -> i64; + fn subsecond_millis(&self) -> Option; + fn unix_stamp(&self) -> UnixTimestamp { + UnixTimestamp { + unix_seconds: self.unix_seconds(), + subsecond_millis: self.subsecond_millis(), + } + } + fn date_time(&self) -> Option>; } +/// UNIX timestamp: Elapsed seconds since 01-01-1970 00:00:00. +/// +/// Also can optionally include subsecond millisecond for greater accuracy. +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct UnixTimestamp { + pub unix_seconds: i64, + subsecond_millis: Option, +} + +impl UnixTimestamp { + /// Returns none if the subsecond millisecond value is larger than 999. + pub fn new(unix_seconds: i64, subsec_millis: u16) -> Option { + if subsec_millis > 999 { + return None; + } + Some(Self { + unix_seconds, + subsecond_millis: Some(subsec_millis), + }) + } + + pub const fn const_new(unix_seconds: i64, subsec_millis: u16) -> Self { + if subsec_millis > 999 { + panic!("subsec milliseconds exceeds 999"); + } + Self { + unix_seconds, + subsecond_millis: Some(subsec_millis), + } + } + + pub fn new_only_seconds(unix_seconds: i64) -> Self { + Self { + unix_seconds, + subsecond_millis: None, + } + } + + pub fn subsecond_millis(&self) -> Option { + self.subsecond_millis + } + + #[cfg(feature = "std")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] + pub fn from_now() -> Result { + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; + let epoch = now.as_secs(); + Ok(UnixTimestamp { + unix_seconds: epoch as i64, + subsecond_millis: Some(now.subsec_millis() as u16), + }) + } + + #[inline] + pub fn unix_seconds_f64(&self) -> f64 { + let mut secs = self.unix_seconds as f64; + if let Some(subsec_millis) = self.subsecond_millis { + secs += subsec_millis as f64 / 1000.0; + } + secs + } + + pub fn as_date_time(&self) -> LocalResult> { + Utc.timestamp_opt( + self.unix_seconds, + self.subsecond_millis.unwrap_or(0) as u32 * 10_u32.pow(6), + ) + } +} + +impl From> for UnixTimestamp { + fn from(value: DateTime) -> Self { + Self { + unix_seconds: value.timestamp(), + subsecond_millis: Some(value.timestamp_subsec_millis() as u16), + } + } +} + #[cfg(all(test, feature = "std"))] mod tests { use super::*; @@ -240,4 +338,24 @@ mod tests { let days_diff = (ccsds_epoch - unix_epoch) / SECONDS_PER_DAY as u64; assert_eq!(days_diff, -DAYS_CCSDS_TO_UNIX as u64); } + + #[test] + fn basic_unix_stamp_test() { + let stamp = UnixTimestamp::new_only_seconds(-200); + assert_eq!(stamp.unix_seconds, -200); + assert!(stamp.subsecond_millis().is_none()); + let stamp = UnixTimestamp::new_only_seconds(250); + assert_eq!(stamp.unix_seconds, 250); + assert!(stamp.subsecond_millis().is_none()); + } + + #[test] + fn basic_float_unix_stamp_test() { + let stamp = UnixTimestamp::new(500, 600).unwrap(); + assert!(stamp.subsecond_millis.is_some()); + assert_eq!(stamp.unix_seconds, 500); + let subsec_millis = stamp.subsecond_millis().unwrap(); + assert_eq!(subsec_millis, 600); + assert!((500.6 - stamp.unix_seconds_f64()).abs() < 0.0001); + } }