Time module extensions #9

Merged
muellerr merged 34 commits from time_extensions into main 2023-01-16 17:51:28 +01:00
5 changed files with 1341 additions and 105 deletions

View File

@ -8,6 +8,49 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
# [unreleased] # [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<Duration>` and `AddAssign<Duration>` for time providers, which allows
easily adding offsets to the providers.
- Implement `TryFrom<DateTime<Utc>>` 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<DynCdsTimeProvider>` by checking the length of days field.
- Added constructor function to create the time provider
from `chrono::DateTime<Utc>` 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<Utc>`.
- 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<u16>`.
- 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 # [v0.4.2] 14.01.2023
## Fixed ## Fixed

View File

@ -1,6 +1,6 @@
[package] [package]
name = "spacepackets" name = "spacepackets"
version = "0.4.2" version = "0.5.0-rc.0"
edition = "2021" edition = "2021"
rust-version = "1.60" rust-version = "1.60"
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"] authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]

File diff suppressed because it is too large Load Diff

View File

@ -42,12 +42,14 @@ impl TryFrom<u8> for FractionalResolution {
/// Please note that this function will panic if the fractional value is not smaller than /// 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. /// the maximum number of fractions allowed for the particular resolution.
/// (e.g. passing 270 when the resolution only allows 255 values). /// (e.g. passing 270 when the resolution only allows 255 values).
#[inline]
pub fn convert_fractional_part_to_ns(fractional_part: FractionalPart) -> u64 { pub fn convert_fractional_part_to_ns(fractional_part: FractionalPart) -> u64 {
let div = fractional_res_to_div(fractional_part.0); let div = fractional_res_to_div(fractional_part.0);
assert!(fractional_part.1 < div); assert!(fractional_part.1 < div);
10_u64.pow(9) * fractional_part.1 as u64 / div as u64 10_u64.pow(9) * fractional_part.1 as u64 / div as u64
} }
#[inline(always)]
pub const fn fractional_res_to_div(res: FractionalResolution) -> u32 { pub const fn fractional_res_to_div(res: FractionalResolution) -> u32 {
2_u32.pow(8 * res as u32) - 1 2_u32.pow(8 * res as u32) - 1
} }
@ -351,6 +353,11 @@ impl TimeProviderCcsdsEpoch {
pfield & 0b11 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 /// This returns the length of the individual components of the CUC timestamp in addition
/// to the total size. /// to the total size.
/// ///
@ -543,10 +550,19 @@ impl CcsdsTimeProvider for TimeProviderCcsdsEpoch {
CcsdsTimeCodes::CucCcsdsEpoch CcsdsTimeCodes::CucCcsdsEpoch
} }
/// Please note that this function only works as intended if the time counter resolution
/// is one second.
fn unix_seconds(&self) -> i64 { 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<u16> {
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<DateTime<Utc>> { fn date_time(&self) -> Option<DateTime<Utc>> {

View File

@ -20,6 +20,7 @@ pub mod cuc;
pub const DAYS_CCSDS_TO_UNIX: i32 = -4383; pub const DAYS_CCSDS_TO_UNIX: i32 = -4383;
pub const SECONDS_PER_DAY: u32 = 86400; pub const SECONDS_PER_DAY: u32 = 86400;
pub const MS_PER_DAY: u32 = SECONDS_PER_DAY * 1000;
#[derive(Debug, PartialEq, Eq, Copy, Clone)] #[derive(Debug, PartialEq, Eq, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
@ -55,6 +56,7 @@ pub fn ccsds_time_code_from_p_field(pfield: u8) -> Result<CcsdsTimeCodes, u8> {
#[derive(Debug, PartialEq, Eq, Copy, Clone)] #[derive(Debug, PartialEq, Eq, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[non_exhaustive]
pub enum TimestampError { pub enum TimestampError {
/// Contains tuple where first value is the expected time code and the second /// Contains tuple where first value is the expected time code and the second
/// value is the found raw value /// value is the found raw value
@ -62,6 +64,7 @@ pub enum TimestampError {
ByteConversionError(ByteConversionError), ByteConversionError(ByteConversionError),
CdsError(cds::CdsError), CdsError(cds::CdsError),
CucError(cuc::CucError), CucError(cuc::CucError),
DateBeforeCcsdsEpoch(DateTime<Utc>),
CustomEpochNotSupported, CustomEpochNotSupported,
} }
@ -118,6 +121,9 @@ impl Display for TimestampError {
TimestampError::ByteConversionError(e) => { TimestampError::ByteConversionError(e) => {
write!(f, "byte conversion error {}", e) write!(f, "byte conversion error {}", e)
} }
TimestampError::DateBeforeCcsdsEpoch(e) => {
write!(f, "datetime with date before ccsds epoch: {}", e)
}
TimestampError::CustomEpochNotSupported => { TimestampError::CustomEpochNotSupported => {
write!(f, "custom epochs are not supported") write!(f, "custom epochs are not supported")
} }
@ -199,6 +205,9 @@ pub trait TimeReader {
} }
/// Trait for generic CCSDS time providers. /// 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 { pub trait CcsdsTimeProvider {
fn len_as_bytes(&self) -> usize; fn len_as_bytes(&self) -> usize;
@ -208,10 +217,99 @@ pub trait CcsdsTimeProvider {
/// in big endian format. /// in big endian format.
fn p_field(&self) -> (usize, [u8; 2]); fn p_field(&self) -> (usize, [u8; 2]);
fn ccdsd_time_code(&self) -> CcsdsTimeCodes; fn ccdsd_time_code(&self) -> CcsdsTimeCodes;
fn unix_seconds(&self) -> i64; fn unix_seconds(&self) -> i64;
fn subsecond_millis(&self) -> Option<u16>;
fn unix_stamp(&self) -> UnixTimestamp {
UnixTimestamp {
unix_seconds: self.unix_seconds(),
subsecond_millis: self.subsecond_millis(),
}
}
fn date_time(&self) -> Option<DateTime<Utc>>; fn date_time(&self) -> Option<DateTime<Utc>>;
} }
/// 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<u16>,
}
impl UnixTimestamp {
/// Returns none if the subsecond millisecond value is larger than 999.
pub fn new(unix_seconds: i64, subsec_millis: u16) -> Option<Self> {
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<u16> {
self.subsecond_millis
}
#[cfg(feature = "std")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))]
pub fn from_now() -> Result<Self, SystemTimeError> {
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<DateTime<Utc>> {
Utc.timestamp_opt(
self.unix_seconds,
self.subsecond_millis.unwrap_or(0) as u32 * 10_u32.pow(6),
)
}
}
impl From<DateTime<Utc>> for UnixTimestamp {
fn from(value: DateTime<Utc>) -> Self {
Self {
unix_seconds: value.timestamp(),
subsecond_millis: Some(value.timestamp_subsec_millis() as u16),
}
}
}
#[cfg(all(test, feature = "std"))] #[cfg(all(test, feature = "std"))]
mod tests { mod tests {
use super::*; use super::*;
@ -240,4 +338,24 @@ mod tests {
let days_diff = (ccsds_epoch - unix_epoch) / SECONDS_PER_DAY as u64; let days_diff = (ccsds_epoch - unix_epoch) / SECONDS_PER_DAY as u64;
assert_eq!(days_diff, -DAYS_CCSDS_TO_UNIX 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);
}
} }