//! CCSDS Time Code Formats according to [CCSDS 301.0-B-4](https://public.ccsds.org/Pubs/301x0b4e1.pdf) use crate::{ByteConversionError, SizeMissmatch}; use chrono::{DateTime, LocalResult, TimeZone, Utc}; use core::fmt::{Display, Formatter}; #[allow(unused_imports)] #[cfg(not(feature = "std"))] use num_traits::float::FloatCore; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; #[cfg(feature = "std")] use std::error::Error; #[cfg(feature = "std")] use std::time::{SystemTime, SystemTimeError}; pub mod ascii; pub mod cds; pub mod cuc; pub const DAYS_CCSDS_TO_UNIX: i32 = -4383; pub const SECONDS_PER_DAY: u32 = 86400; pub const MS_PER_DAY: u32 = SECONDS_PER_DAY * 1000; #[derive(Debug, PartialEq, Eq, Copy, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum CcsdsTimeCodes { CucCcsdsEpoch = 0b001, CucAgencyEpoch = 0b010, Cds = 0b100, Ccs = 0b101, AgencyDefined = 0b110, } impl TryFrom for CcsdsTimeCodes { type Error = (); fn try_from(value: u8) -> Result { match value { x if x == CcsdsTimeCodes::CucCcsdsEpoch as u8 => Ok(CcsdsTimeCodes::CucCcsdsEpoch), x if x == CcsdsTimeCodes::CucAgencyEpoch as u8 => Ok(CcsdsTimeCodes::CucAgencyEpoch), x if x == CcsdsTimeCodes::Cds as u8 => Ok(CcsdsTimeCodes::Cds), x if x == CcsdsTimeCodes::Ccs as u8 => Ok(CcsdsTimeCodes::Ccs), x if x == CcsdsTimeCodes::AgencyDefined as u8 => Ok(CcsdsTimeCodes::AgencyDefined), _ => Err(()), } } } /// Retrieve the CCSDS time code from the p-field. If no valid time code identifier is found, the /// value of the raw time code identification field is returned. pub fn ccsds_time_code_from_p_field(pfield: u8) -> Result { let raw_bits = (pfield >> 4) & 0b111; CcsdsTimeCodes::try_from(raw_bits).map_err(|_| raw_bits) } #[derive(Debug, PartialEq, Eq, Copy, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[non_exhaustive] pub enum TimestampError { /// Contains tuple where first value is the expected time code and the second /// value is the found raw value InvalidTimeCode(CcsdsTimeCodes, u8), ByteConversionError(ByteConversionError), CdsError(cds::CdsError), CucError(cuc::CucError), DateBeforeCcsdsEpoch(DateTime), CustomEpochNotSupported, } impl From for TimestampError { fn from(e: cds::CdsError) -> Self { TimestampError::CdsError(e) } } impl From for TimestampError { fn from(e: cuc::CucError) -> Self { TimestampError::CucError(e) } } #[cfg(feature = "std")] #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] #[derive(Debug, Clone)] pub enum StdTimestampError { SystemTimeError(SystemTimeError), TimestampError(TimestampError), } #[cfg(feature = "std")] impl From for StdTimestampError { fn from(v: TimestampError) -> Self { Self::TimestampError(v) } } #[cfg(feature = "std")] impl From for StdTimestampError { fn from(v: SystemTimeError) -> Self { Self::SystemTimeError(v) } } impl Display for TimestampError { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { match self { TimestampError::InvalidTimeCode(time_code, raw_val) => { write!( f, "invalid raw time code value {} for time code {:?}", raw_val, time_code ) } TimestampError::CdsError(e) => { write!(f, "cds error {}", e) } TimestampError::CucError(e) => { write!(f, "cuc error {}", e) } TimestampError::ByteConversionError(e) => { write!(f, "byte conversion error {}", e) } TimestampError::DateBeforeCcsdsEpoch(e) => { write!(f, "datetime with date before ccsds epoch: {}", e) } TimestampError::CustomEpochNotSupported => { write!(f, "custom epochs are not supported") } } } } #[cfg(feature = "std")] impl Error for TimestampError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { TimestampError::ByteConversionError(e) => Some(e), TimestampError::CdsError(e) => Some(e), TimestampError::CucError(e) => Some(e), _ => None, } } } #[cfg(feature = "std")] #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] pub fn seconds_since_epoch() -> f64 { SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .expect("System time generation failed") .as_secs_f64() } /// Convert UNIX days to CCSDS days /// /// - CCSDS epoch: 1958 January 1 /// - UNIX Epoch: 1970 January 1 pub const fn unix_to_ccsds_days(unix_days: i64) -> i64 { unix_days - DAYS_CCSDS_TO_UNIX as i64 } /// Convert CCSDS days to UNIX days /// /// - CCSDS epoch: 1958 January 1 /// - UNIX Epoch: 1970 January 1 pub const fn ccsds_to_unix_days(ccsds_days: i64) -> i64 { ccsds_days + DAYS_CCSDS_TO_UNIX as i64 } /// Similar to [unix_to_ccsds_days] but converts the epoch instead, which is the number of elpased /// seconds since the CCSDS and UNIX epoch times. pub const fn unix_epoch_to_ccsds_epoch(unix_epoch: i64) -> i64 { unix_epoch - (DAYS_CCSDS_TO_UNIX as i64 * SECONDS_PER_DAY as i64) } pub const fn ccsds_epoch_to_unix_epoch(ccsds_epoch: i64) -> i64 { ccsds_epoch + (DAYS_CCSDS_TO_UNIX as i64 * SECONDS_PER_DAY as i64) } #[cfg(feature = "std")] #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] pub fn ms_of_day_using_sysclock() -> u32 { ms_of_day(seconds_since_epoch()) } pub fn ms_of_day(seconds_since_epoch: f64) -> u32 { let fraction_ms = seconds_since_epoch - seconds_since_epoch.floor(); let ms_of_day: u32 = (((seconds_since_epoch.floor() as u32 % SECONDS_PER_DAY) * 1000) as f64 + fraction_ms) .floor() as u32; ms_of_day } pub trait TimeWriter { /// Generic function to convert write a timestamp into a raw buffer. /// Returns the number of written bytes on success. fn write_to_bytes(&self, bytes: &mut [u8]) -> Result; } pub trait TimeReader { fn from_bytes(buf: &[u8]) -> Result where Self: Sized; } /// Trait for generic CCSDS time providers. /// /// 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; /// Returns the pfield of the time provider. The pfield can have one or two bytes depending /// on the extension bit (first bit). The time provider should returns a tuple where the first /// entry denotes the length of the pfield and the second entry is the value of the pfield /// in big endian format. fn p_field(&self) -> (usize, [u8; 2]); fn ccdsd_time_code(&self) -> CcsdsTimeCodes; fn unix_seconds(&self) -> i64; fn 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::*; #[test] fn test_days_conversion() { assert_eq!(unix_to_ccsds_days(DAYS_CCSDS_TO_UNIX.into()), 0); assert_eq!(ccsds_to_unix_days(0), DAYS_CCSDS_TO_UNIX.into()); } #[test] fn test_get_current_time() { let sec_floats = seconds_since_epoch(); assert!(sec_floats > 0.0); } #[test] fn test_ccsds_epoch() { let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap(); let unix_epoch = now.as_secs(); let ccsds_epoch = unix_epoch_to_ccsds_epoch(now.as_secs() as i64) as u64; assert!(ccsds_epoch > unix_epoch); assert_eq!((ccsds_epoch - unix_epoch) % SECONDS_PER_DAY as u64, 0); let days_diff = (ccsds_epoch - unix_epoch) / SECONDS_PER_DAY as u64; assert_eq!(days_diff, -DAYS_CCSDS_TO_UNIX as u64); } #[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); } }