12 Commits

Author SHA1 Message Date
256407432d create release checklist
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-01-22 13:06:22 +01:00
c59b015a20 added additional tests
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-01-22 12:53:39 +01:00
e081504b33 Merge branch 'main' of https://egit.irs.uni-stuttgart.de/rust/spacepackets
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-01-22 12:51:23 +01:00
6eb1b1efbc bugfix, additional test and CHANGELOG bump 2023-01-22 12:50:49 +01:00
51d0a08e7b better document panics and tweak Add/AddAssign impl
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
for UnixTimestamp
2023-01-22 01:54:41 +01:00
6e557c2568 add Add impls for &
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-01-21 14:50:35 +01:00
fc76a975c1 add Add<Duration> for &UnixTimestamp
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-01-21 14:28:29 +01:00
74e489bd07 add basic tests and update changelog
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-01-21 13:17:38 +01:00
1e90793072 cargo fmt
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-01-21 11:37:57 +01:00
ef55bc4a6e Improve time module
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-01-21 01:25:05 +01:00
55862a2433 docs typo
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-01-20 20:01:27 +01:00
9afc3fc8de Update changelog
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-01-20 19:48:20 +01:00
8 changed files with 434 additions and 11 deletions

View File

@ -8,6 +8,27 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
# [unreleased]
# [v0.5.1] 2023-01-22
## Added
- `time::cds::TimeProvider`
- Add `Ord` and `PartialOrd`, use custom `PartialEq` impl to account for precision correctly.
- Add `precision_as_ns` function which converts microsecond and picosecond precision values
into nanoseconds.
- Add conversion trait to convert `cds::TimeProvider<DaysLen16Bits>` into
`cds::TimeProvider<DaysLen24Bits>` and vice-versa.
- `time::UnixTimestamp`
- Add `Ord` and `PartialOrd` implementations.
- Add `Add<Duration>` and `AddAssign<Duration>` implementations.
## Fixed
- `time::cds::TimeProvider`: Fixed a big where subsecond milliseconds were not accounted for
when the provider has no submillisecond precision.
# [v0.5.0] 2023-01-20
The timestamp of `PusTm` is now optional. See Added and Changed section for details.
## Added

View File

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

View File

@ -19,7 +19,7 @@ Currently, this includes the following components:
[CCSDS 301.0-B-4 3.2](https://public.ccsds.org/Pubs/301x0b4e1.pdf)
- CDS (CCSDS Day Segmented Time Code) implementation according to
[CCSDS 301.0-B-4 3.3](https://public.ccsds.org/Pubs/301x0b4e1.pdf)
- Some helper types to support ASCII timecodes ad specified in
- Some helper types to support ASCII timecodes as specified in
[CCSDS 301.0-B-4 3.5](https://public.ccsds.org/Pubs/301x0b4e1.pdf)
# Features

18
release_checklist.md Normal file
View File

@ -0,0 +1,18 @@
Checklist for new releases
=======
# Pre-Release
1. Make sure any new modules are documented sufficiently enough and check docs with
`cargo doc --all-features --open`.
2. Bump version specifier in `Cargo.toml`.
3. Update `CHANGELOG.md`: Convert `unreleased` section into version section with date and add new
`unreleased` section.
4. Run `cargo test --all-features`.
5. Run `cargo fmt` and `cargo clippy`. Check `cargo msrv` against MSRV in `Cargo.toml`.
6. Wait for CI/CD results for EGit and Github. These also check cross-compilation for bare-metal
targets.
# Post-Release
1. Create a new release on `EGit` based on the release branch.

View File

@ -13,7 +13,7 @@
//! [CCSDS 301.0-B-4 3.2](https://public.ccsds.org/Pubs/301x0b4e1.pdf)
//! - CDS (CCSDS Day Segmented Time Code) implementation according to
//! [CCSDS 301.0-B-4 3.3](https://public.ccsds.org/Pubs/301x0b4e1.pdf)
//! - Some helper types to support ASCII timecodes ad specified in
//! - Some helper types to support ASCII timecodes as specified in
//! [CCSDS 301.0-B-4 3.5](https://public.ccsds.org/Pubs/301x0b4e1.pdf)
//!
//! ## Features

View File

@ -11,6 +11,7 @@ use alloc::boxed::Box;
use chrono::Datelike;
#[cfg(feature = "alloc")]
use core::any::Any;
use core::cmp::Ordering;
use core::fmt::Debug;
use core::ops::{Add, AddAssign};
use core::time::Duration;
@ -158,7 +159,7 @@ pub fn precision_from_pfield(pfield: u8) -> SubmillisPrecision {
/// 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)]
#[derive(Debug, Copy, Clone, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct TimeProvider<DaysLen: ProvidesDaysLength = DaysLen16Bits> {
pfield: u8,
@ -459,6 +460,7 @@ impl<ProvidesDaysLen: ProvidesDaysLength> CdsCommon for TimeProvider<ProvidesDay
}
impl<ProvidesDaysLen: ProvidesDaysLength> TimeProvider<ProvidesDaysLen> {
/// Please note that a precision value of 0 will be converted to [None] (no precision).
pub fn set_submillis_precision(&mut self, prec: SubmillisPrecision) {
self.pfield &= !(0b11);
if let SubmillisPrecision::Absent = prec {
@ -486,6 +488,25 @@ impl<ProvidesDaysLen: ProvidesDaysLength> TimeProvider<ProvidesDaysLen> {
self.ccsds_days
}
/// Maps the submillisecond precision to a nanosecond value. This will reduce precision when
/// using picosecond resolution, but significantly simplifies comparison of timestamps.
pub fn precision_as_ns(&self) -> Option<u32> {
if let Some(prec) = self.submillis_precision {
match prec {
SubmillisPrecision::Microseconds(us) => {
return Some(us as u32 * 1000);
}
SubmillisPrecision::Picoseconds(ps) => {
return Some(ps / 1000);
}
_ => {
return None;
}
}
}
None
}
fn generic_raw_read_checks(
buf: &[u8],
days_len: LengthOfDaySegment,
@ -975,8 +996,14 @@ fn add_for_max_ccsds_days_val<T: ProvidesDaysLength>(
_ => None,
}
} else {
increment_ms_of_day(
&mut next_ms_of_day,
duration.subsec_millis(),
&mut next_ccsds_days,
);
None
};
// The subsecond millisecond were already handled.
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;
@ -1017,6 +1044,20 @@ impl Add<Duration> for TimeProvider<DaysLen16Bits> {
}
}
impl Add<Duration> for &TimeProvider<DaysLen16Bits> {
type Output = TimeProvider<DaysLen16Bits>;
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::Output::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.
@ -1034,6 +1075,20 @@ impl Add<Duration> for TimeProvider<DaysLen24Bits> {
}
}
impl Add<Duration> for &TimeProvider<DaysLen24Bits> {
type Output = TimeProvider<DaysLen24Bits>;
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::Output::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.
@ -1177,6 +1232,75 @@ impl TimeWriter for TimeProvider<DaysLen24Bits> {
}
}
impl<DaysLenProvider: ProvidesDaysLength> PartialEq for TimeProvider<DaysLenProvider> {
fn eq(&self, other: &Self) -> bool {
if self.ccsds_days == other.ccsds_days
&& self.ms_of_day == other.ms_of_day
&& self.precision_as_ns().unwrap_or(0) == other.precision_as_ns().unwrap_or(0)
{
return true;
}
false
}
}
impl<DaysLenProvider: ProvidesDaysLength> PartialOrd for TimeProvider<DaysLenProvider> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if self == other {
return Some(Ordering::Equal);
}
match self.ccsds_days_as_u32().cmp(&other.ccsds_days_as_u32()) {
Ordering::Less => return Some(Ordering::Less),
Ordering::Greater => return Some(Ordering::Greater),
_ => (),
}
match self.ms_of_day().cmp(&other.ms_of_day()) {
Ordering::Less => return Some(Ordering::Less),
Ordering::Greater => return Some(Ordering::Greater),
_ => (),
}
match self
.precision_as_ns()
.unwrap_or(0)
.cmp(&other.precision_as_ns().unwrap_or(0))
{
Ordering::Less => return Some(Ordering::Less),
Ordering::Greater => return Some(Ordering::Greater),
_ => (),
}
Some(Ordering::Equal)
}
}
impl<DaysLenProvider: ProvidesDaysLength + Eq> Ord for TimeProvider<DaysLenProvider> {
fn cmp(&self, other: &Self) -> Ordering {
PartialOrd::partial_cmp(self, other).unwrap()
}
}
impl From<TimeProvider<DaysLen16Bits>> for TimeProvider<DaysLen24Bits> {
fn from(value: TimeProvider<DaysLen16Bits>) -> Self {
// This function only fails if the days value exceeds 24 bits, which is not possible here,
// so it is okay to unwrap.
Self::new_with_u24_days(value.ccsds_days_as_u32(), value.ms_of_day()).unwrap()
}
}
/// This conversion can fail if the days value exceeds 16 bits.
impl TryFrom<TimeProvider<DaysLen24Bits>> for TimeProvider<DaysLen16Bits> {
type Error = CdsError;
fn try_from(value: TimeProvider<DaysLen24Bits>) -> Result<Self, CdsError> {
let ccsds_days = value.ccsds_days_as_u32();
if ccsds_days > u16::MAX as u32 {
return Err(CdsError::InvalidCcsdsDays(ccsds_days as i64));
}
Ok(Self::new_with_u16_days(
ccsds_days as u16,
value.ms_of_day(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -1984,6 +2108,16 @@ mod tests {
}
}
#[test]
fn test_addition_on_ref() {
// This test case also tests the case where there is no submillis precision but subsecond
// milliseconds.
let provider_ref = &TimeProvider::new_with_u16_days(2, 500);
let new_stamp = provider_ref + Duration::from_millis(2 * 24 * 60 * 60 * 1000 + 500);
assert_eq!(new_stamp.ccsds_days_as_u32(), 4);
assert_eq!(new_stamp.ms_of_day, 1000);
}
fn check_ps_and_carryover(prec: SubmillisPrecision, ms_of_day: u32, val: u32) {
if let SubmillisPrecision::Picoseconds(ps) = prec {
assert_eq!(ps, val);
@ -2066,6 +2200,43 @@ mod tests {
}
}
#[test]
fn test_eq() {
let stamp0 = TimeProvider::new_with_u16_days(0, 0);
let mut buf: [u8; 7] = [0; 7];
stamp0.write_to_bytes(&mut buf).unwrap();
let stamp1 = TimeProvider::from_bytes_with_u16_days(&buf).unwrap();
assert_eq!(stamp0, stamp1);
assert!(!(stamp0 < stamp1));
assert!(!(stamp1 > stamp0));
}
#[test]
fn test_ord() {
let stamp0 = TimeProvider::new_with_u24_days(0, 0).unwrap();
let stamp1 = TimeProvider::new_with_u24_days(0, 50000).unwrap();
let mut stamp2 = TimeProvider::new_with_u24_days(0, 50000).unwrap();
stamp2.set_submillis_precision(SubmillisPrecision::Microseconds(500));
let stamp3 = TimeProvider::new_with_u24_days(1, 0).unwrap();
assert!(stamp1 > stamp0);
assert!(stamp2 > stamp0);
assert!(stamp2 > stamp1);
assert!(stamp3 > stamp0);
assert!(stamp3 > stamp1);
assert!(stamp3 > stamp2);
}
#[test]
fn test_conversion() {
let mut stamp_small = TimeProvider::new_with_u16_days(u16::MAX, 500);
let stamp_larger: TimeProvider<DaysLen24Bits> = stamp_small.into();
assert_eq!(stamp_larger.ccsds_days_as_u32(), u16::MAX as u32);
assert_eq!(stamp_larger.ms_of_day(), 500);
stamp_small = stamp_larger.try_into().unwrap();
assert_eq!(stamp_small.ccsds_days_as_u32(), u16::MAX as u32);
assert_eq!(stamp_small.ms_of_day(), 500);
}
#[test]
#[cfg(feature = "serde")]
fn test_serialization() {

View File

@ -696,6 +696,20 @@ impl Add<Duration> for TimeProviderCcsdsEpoch {
}
}
impl Add<Duration> for &TimeProviderCcsdsEpoch {
type Output = TimeProviderCcsdsEpoch;
fn add(self, duration: Duration) -> Self::Output {
let (new_counter, new_fractional_part) =
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();
}
Self::Output::new(new_counter)
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -1,7 +1,10 @@
//! 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::cmp::Ordering;
use core::fmt::{Display, Formatter};
use core::ops::{Add, AddAssign};
use core::time::Duration;
#[allow(unused_imports)]
#[cfg(not(feature = "std"))]
@ -232,7 +235,8 @@ pub trait CcsdsTimeProvider {
/// UNIX timestamp: Elapsed seconds since 01-01-1970 00:00:00.
///
/// Also can optionally include subsecond millisecond for greater accuracy.
/// Also can optionally include subsecond millisecond for greater accuracy. Please note that a
/// subsecond millisecond value of 0 gets converted to [None].
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct UnixTimestamp {
@ -241,24 +245,28 @@ pub struct UnixTimestamp {
}
impl UnixTimestamp {
/// Returns none if the subsecond millisecond value is larger than 999.
/// Returns none if the subsecond millisecond value is larger than 999. 0 is converted to
/// a [None] value.
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),
})
Some(Self::const_new(unix_seconds, subsec_millis))
}
/// Like [Self::new] but const. Panics if the subsecond value is larger than 999.
pub const fn const_new(unix_seconds: i64, subsec_millis: u16) -> Self {
if subsec_millis > 999 {
panic!("subsec milliseconds exceeds 999");
}
let subsecond_millis = if subsec_millis == 0 {
None
} else {
Some(subsec_millis)
};
Self {
unix_seconds,
subsecond_millis: Some(subsec_millis),
subsecond_millis,
}
}
@ -310,6 +318,111 @@ impl From<DateTime<Utc>> for UnixTimestamp {
}
}
impl PartialOrd for UnixTimestamp {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if self == other {
return Some(Ordering::Equal);
}
match self.unix_seconds.cmp(&other.unix_seconds) {
Ordering::Less => return Some(Ordering::Less),
Ordering::Greater => return Some(Ordering::Greater),
_ => (),
}
match self
.subsecond_millis()
.unwrap_or(0)
.cmp(&other.subsecond_millis().unwrap_or(0))
{
Ordering::Less => {
return if self.unix_seconds < 0 {
Some(Ordering::Greater)
} else {
Some(Ordering::Less)
}
}
Ordering::Greater => {
return if self.unix_seconds < 0 {
Some(Ordering::Less)
} else {
Some(Ordering::Greater)
}
}
Ordering::Equal => (),
}
Some(Ordering::Equal)
}
}
impl Ord for UnixTimestamp {
fn cmp(&self, other: &Self) -> Ordering {
PartialOrd::partial_cmp(self, other).unwrap()
}
}
fn get_new_stamp_after_addition(
current_stamp: &UnixTimestamp,
duration: Duration,
) -> UnixTimestamp {
let mut new_subsec_millis =
current_stamp.subsecond_millis().unwrap_or(0) + duration.subsec_millis() as u16;
let mut new_unix_seconds = current_stamp.unix_seconds;
let mut increment_seconds = |value: u32| {
if new_unix_seconds < 0 {
new_unix_seconds = new_unix_seconds
.checked_sub(value.into())
.expect("new unix seconds would exceed i64::MIN");
} else {
new_unix_seconds = new_unix_seconds
.checked_add(value.into())
.expect("new unix seconds would exceed i64::MAX");
}
};
if new_subsec_millis >= 1000 {
new_subsec_millis -= 1000;
increment_seconds(1);
}
increment_seconds(
duration
.as_secs()
.try_into()
.expect("duration seconds exceeds u32::MAX"),
);
UnixTimestamp::const_new(new_unix_seconds, new_subsec_millis)
}
/// Please note that this operation will panic on the following conditions:
///
/// - Unix seconds after subtraction for stamps before the unix epoch exceeds [i64::MIN].
/// - Unix seconds after addition exceeds [i64::MAX].
/// - Seconds from duration to add exceeds [u32::MAX].
impl AddAssign<Duration> for UnixTimestamp {
fn add_assign(&mut self, duration: Duration) {
*self = get_new_stamp_after_addition(self, duration);
}
}
/// Please note that this operation will panic for the following conditions:
///
/// - Unix seconds after subtraction for stamps before the unix epoch exceeds [i64::MIN].
/// - Unix seconds after addition exceeds [i64::MAX].
/// - Unix seconds exceeds [u32::MAX].
impl Add<Duration> for UnixTimestamp {
type Output = Self;
fn add(self, duration: Duration) -> Self::Output {
get_new_stamp_after_addition(&self, duration)
}
}
impl Add<Duration> for &UnixTimestamp {
type Output = UnixTimestamp;
fn add(self, duration: Duration) -> Self::Output {
get_new_stamp_after_addition(self, duration)
}
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
@ -358,4 +471,90 @@ mod tests {
assert_eq!(subsec_millis, 600);
assert!((500.6 - stamp.unix_seconds_f64()).abs() < 0.0001);
}
#[test]
fn test_ord_larger() {
let stamp0 = UnixTimestamp::new_only_seconds(5);
let stamp1 = UnixTimestamp::new(5, 500).unwrap();
let stamp2 = UnixTimestamp::new_only_seconds(6);
assert!(stamp1 > stamp0);
assert!(stamp2 > stamp0);
assert!(stamp2 > stamp1);
}
#[test]
fn test_ord_smaller() {
let stamp0 = UnixTimestamp::new_only_seconds(5);
let stamp1 = UnixTimestamp::new(5, 500).unwrap();
let stamp2 = UnixTimestamp::new_only_seconds(6);
assert!(stamp0 < stamp1);
assert!(stamp0 < stamp2);
assert!(stamp1 < stamp2);
}
#[test]
fn test_ord_larger_neg_numbers() {
let stamp0 = UnixTimestamp::new_only_seconds(-5);
let stamp1 = UnixTimestamp::new(-5, 500).unwrap();
let stamp2 = UnixTimestamp::new_only_seconds(-6);
assert!(stamp0 > stamp1);
assert!(stamp0 > stamp2);
assert!(stamp1 > stamp2);
assert!(stamp1 >= stamp2);
assert!(stamp0 >= stamp1);
}
#[test]
fn test_ord_smaller_neg_numbers() {
let stamp0 = UnixTimestamp::new_only_seconds(-5);
let stamp1 = UnixTimestamp::new(-5, 500).unwrap();
let stamp2 = UnixTimestamp::new_only_seconds(-6);
assert!(stamp2 < stamp1);
assert!(stamp2 < stamp0);
assert!(stamp1 < stamp0);
assert!(stamp1 <= stamp0);
assert!(stamp2 <= stamp1);
}
#[test]
fn test_eq() {
let stamp0 = UnixTimestamp::new(5, 0).unwrap();
let stamp1 = UnixTimestamp::new_only_seconds(5);
assert_eq!(stamp0, stamp1);
assert!(stamp0 <= stamp1);
assert!(stamp0 >= stamp1);
assert!(!(stamp0 < stamp1));
assert!(!(stamp0 > stamp1));
}
#[test]
fn test_addition() {
let mut stamp0 = UnixTimestamp::new_only_seconds(1);
stamp0 += Duration::from_secs(5);
assert_eq!(stamp0.unix_seconds, 6);
assert!(stamp0.subsecond_millis().is_none());
let stamp1 = stamp0 + Duration::from_millis(500);
assert_eq!(stamp1.unix_seconds, 6);
assert!(stamp1.subsecond_millis().is_some());
assert_eq!(stamp1.subsecond_millis().unwrap(), 500);
}
#[test]
fn test_addition_on_ref() {
let stamp0 = &UnixTimestamp::new(20, 500).unwrap();
let stamp1 = stamp0 + Duration::from_millis(2500);
assert_eq!(stamp1.unix_seconds, 23);
assert!(stamp1.subsecond_millis().is_none());
}
#[test]
fn test_addition_spillover() {
let mut stamp0 = UnixTimestamp::new(1, 900).unwrap();
stamp0 += Duration::from_millis(100);
assert_eq!(stamp0.unix_seconds, 2);
assert!(stamp0.subsecond_millis().is_none());
stamp0 += Duration::from_millis(1100);
assert_eq!(stamp0.unix_seconds, 3);
assert_eq!(stamp0.subsecond_millis().unwrap(), 100);
}
}