#![allow(clippy::all, clippy::pedantic, clippy::restriction, warnings)]
use deep_time::civil_parts::Parts;
use deep_time::{Dt, Scale};
use spacepackets::time::cds::{CdsBase, CdsTime, SubmillisPrecision};
use spacepackets::time::cuc::{
CucTime, FractionalPart, FractionalResolution, WidthCounterPair, fractional_part_from_subsec_ns,
};
use spacepackets::time::{TimeReader, TimeWriter, UnixTime};
fn pack_sp_cds(days: u16, ms_of_day: u32, prec: SubmillisPrecision, submillis: u32) -> Vec<u8> {
let mut stamp = CdsTime::new_with_u16_days(days, ms_of_day);
assert!(
stamp.set_submillis(prec, submillis),
"spacepackets set_submillis failed (value out of width?)"
);
let mut buf = [0u8; 16];
let n = stamp
.write_to_bytes(&mut buf)
.expect("spacepackets CDS write");
buf[..n].to_vec()
}
fn days_since_1958(y: i64, mo: u8, d: u8) -> u16 {
let days = Dt::ymd_to_days_since_1958(y, mo, d);
assert!(days >= 0, "before CDS epoch");
u16::try_from(days).expect("day count exceeds 16-bit CDS field")
}
#[test]
fn cds_wire_identity_from_shared_segments() {
let cases: &[(u16, u32, SubmillisPrecision, u32)] = &[
(0, 0, SubmillisPrecision::Absent, 0),
(4383, 0, SubmillisPrecision::Absent, 0), (15340, 0, SubmillisPrecision::Absent, 0), (15340, 43_200_000, SubmillisPrecision::Absent, 0), (24_578, 52_245_000, SubmillisPrecision::Absent, 0), (24_578, 52_245_123, SubmillisPrecision::Microseconds, 456),
(
24_578,
52_245_123,
SubmillisPrecision::Picoseconds,
456_789_012,
),
(0, 1, SubmillisPrecision::Microseconds, 500), (0, 86_399_999, SubmillisPrecision::Microseconds, 999),
];
for &(days, ms, prec, sub) in cases {
let sp = pack_sp_cds(days, ms, prec, sub);
let (y, mo, d) = Dt::days_since_1958_to_ymd(days as i64);
let total_ms = ms as u64;
let is_leap = total_ms / 1000 == 86_400;
let sec_of_day = if is_leap { 86_399 } else { total_ms / 1000 };
let rem_ms = total_ms % 1000;
let hr = (sec_of_day / 3600) as u8;
let min = ((sec_of_day % 3600) / 60) as u8;
let mut sec = (sec_of_day % 60) as u8;
if is_leap {
sec = 60;
}
let attos_in_sec = match prec {
SubmillisPrecision::Absent => rem_ms * 1_000_000_000_000_000,
SubmillisPrecision::Microseconds => {
rem_ms * 1_000_000_000_000_000 + (sub as u64) * 1_000_000_000_000
}
SubmillisPrecision::Picoseconds => {
rem_ms * 1_000_000_000_000_000 + (sub as u64) * 1_000_000
}
SubmillisPrecision::Reserved => unreachable!(),
};
let sub_ms_code = match prec {
SubmillisPrecision::Absent => 0,
SubmillisPrecision::Microseconds => 1,
SubmillisPrecision::Picoseconds => 2,
SubmillisPrecision::Reserved => unreachable!(),
};
let dt = Dt::from_ymd(y, mo, d, Scale::UTC, hr, min, sec, attos_in_sec);
let (buf, len) = dt
.to_ccsds_cds(2, sub_ms_code, false)
.expect("deep-time CDS encode");
assert_eq!(
&buf[..len],
sp.as_slice(),
"wire mismatch days={days} ms={ms} prec={prec:?} sub={sub}\n\
deep-time={:02x?}\nspacepackets={:02x?}",
&buf[..len],
&sp
);
}
}
#[test]
fn cds_deep_time_encode_spacepackets_decode() {
let cases: &[(i64, u8, u8, u8, u8, u8, u64, u8)] = &[
(1958, 1, 1, 0, 0, 0, 0, 0),
(1970, 1, 1, 0, 0, 0, 0, 0),
(2000, 1, 1, 12, 0, 0, 0, 0),
(2025, 4, 17, 14, 30, 45, 0, 0),
(2025, 4, 17, 14, 30, 45, 123_456_789_000_000_000, 1),
(2025, 4, 17, 14, 30, 45, 123_456_789_012_345_678, 2),
(1958, 1, 1, 0, 0, 0, 1_500_000_000_000_000, 1),
];
for &(y, mo, d, h, mi, s, attos, sub_code) in cases {
let dt = Dt::from_ymd(y, mo, d, Scale::UTC, h, mi, s, attos);
let (buf, len) = dt.to_ccsds_cds(2, sub_code, false).unwrap();
let sp = CdsTime::from_bytes_with_u16_days(&buf[..len])
.unwrap_or_else(|e| panic!("spacepackets decode failed for {y}-{mo}-{d}: {e:?}"));
let expected_days = days_since_1958(y, mo, d);
assert_eq!(sp.ccsds_days(), expected_days, "days {y}-{mo}-{d}");
let sec_of_day = u32::from(h) * 3600 + u32::from(mi) * 60 + u32::from(s);
let ms_in_sec = (attos / 1_000_000_000_000_000) as u32;
let expected_ms = sec_of_day * 1000 + ms_in_sec;
assert_eq!(sp.ms_of_day(), expected_ms, "ms {y}-{mo}-{d}");
let attos_in_ms = attos % 1_000_000_000_000_000;
match sub_code {
0 => assert_eq!(sp.submillis_precision(), SubmillisPrecision::Absent),
1 => {
assert_eq!(sp.submillis_precision(), SubmillisPrecision::Microseconds);
let us = (attos_in_ms / 1_000_000_000_000) as u32;
assert_eq!(sp.submillis(), us, "µs {y}-{mo}-{d}");
}
2 => {
assert_eq!(sp.submillis_precision(), SubmillisPrecision::Picoseconds);
let ps = (attos_in_ms / 1_000_000) as u32;
assert_eq!(sp.submillis(), ps, "ps {y}-{mo}-{d}");
}
_ => unreachable!(),
}
}
}
#[test]
fn cds_spacepackets_encode_deep_time_decode() {
let cases: &[(u16, u32, SubmillisPrecision, u32)] = &[
(0, 0, SubmillisPrecision::Absent, 0),
(15_340, 43_200_000, SubmillisPrecision::Absent, 0),
(24_578, 52_245_123, SubmillisPrecision::Microseconds, 456),
(
24_578,
52_245_123,
SubmillisPrecision::Picoseconds,
456_000_000,
),
(0, 1, SubmillisPrecision::Microseconds, 500),
];
for &(days, ms, prec, sub) in cases {
let sp_bytes = pack_sp_cds(days, ms, prec, sub);
let parsed = Parts::from_ccsds_cds(&sp_bytes)
.unwrap_or_else(|e| panic!("deep-time decode failed: {e:?}"));
let (y, mo, d) = Dt::days_since_1958_to_ymd(days as i64);
assert_eq!(parsed.yr, Some(y));
assert_eq!(parsed.mo, Some(mo));
assert_eq!(parsed.day, Some(d));
let rem_ms = (ms % 1000) as u64;
let expected_attos = match prec {
SubmillisPrecision::Absent => rem_ms * 1_000_000_000_000_000,
SubmillisPrecision::Microseconds => {
rem_ms * 1_000_000_000_000_000 + (sub as u64) * 1_000_000_000_000
}
SubmillisPrecision::Picoseconds => {
rem_ms * 1_000_000_000_000_000 + (sub as u64) * 1_000_000
}
SubmillisPrecision::Reserved => unreachable!(),
};
assert_eq!(parsed.attos, expected_attos, "attos days={days} ms={ms}");
}
}
#[test]
fn cds_from_unix_matches_deep_time_for_non_leap_instants() {
let cases: &[(i64, u32, SubmillisPrecision)] = &[
(0, 0, SubmillisPrecision::Absent), (946_684_800, 0, SubmillisPrecision::Absent), (946_728_000, 0, SubmillisPrecision::Absent), (1_744_900_245, 0, SubmillisPrecision::Absent), (1_744_900_245, 123_456_789, SubmillisPrecision::Microseconds),
(1_744_900_245, 123_456_789, SubmillisPrecision::Picoseconds),
(1_744_900_245, 123_000_000, SubmillisPrecision::Absent),
];
for &(unix_secs, sub_ns, prec) in cases {
let unix = UnixTime::new_checked(unix_secs, sub_ns).expect("unix");
let sp = CdsTime::from_unix_time_with_u16_days(&unix, prec)
.unwrap_or_else(|e| panic!("from_unix failed secs={unix_secs}: {e:?}"));
let mut sp_buf = [0u8; 16];
let sp_len = sp.write_to_bytes(&mut sp_buf).expect("sp write");
let unix_dt = Dt::from_sec(unix_secs as i128, Scale::UTC, Scale::UTC)
.add_attos((sub_ns as i128) * 1_000_000_000); let dt = Dt::from_unix(unix_dt).target(Scale::UTC);
let sub_code = match prec {
SubmillisPrecision::Absent => 0,
SubmillisPrecision::Microseconds => 1,
SubmillisPrecision::Picoseconds => 2,
SubmillisPrecision::Reserved => unreachable!(),
};
let (dt_buf, dt_len) = dt.to_ccsds_cds(2, sub_code, false).unwrap();
assert_eq!(
&dt_buf[..dt_len],
&sp_buf[..sp_len],
"unix={unix_secs}.{sub_ns:09} prec={prec:?}\n\
deep-time={:02x?}\nspacepackets={:02x?}",
&dt_buf[..dt_len],
&sp_buf[..sp_len]
);
}
}
#[test]
fn cuc_wire_identity_coarse_and_fine() {
for secs in [0u32, 1, 86_400, 1_000_000, 0x0102_0304] {
let sp = CucTime::new(secs);
let mut sp_buf = [0u8; 16];
let sp_len = sp.write_to_bytes(&mut sp_buf).unwrap();
let dt = Dt::CCSDS_EPOCH.add_sec(secs as i128);
let (dt_buf, dt_len) = dt.to_ccsds_cuc(4, 0, false).unwrap();
assert_eq!(&dt_buf[..dt_len], &sp_buf[..sp_len], "CUC secs={secs}");
}
{
let sp = CucTime::new_with_coarse_fractions(0, 0x80);
let mut sp_buf = [0u8; 16];
let sp_len = sp.write_to_bytes(&mut sp_buf).unwrap();
let dt = Dt::CCSDS_EPOCH.add_attos(500_000_000_000_000_000);
let (dt_buf, dt_len) = dt.to_ccsds_cuc(4, 1, false).unwrap();
assert_eq!(&dt_buf[..dt_len], &sp_buf[..sp_len], "CUC 0.5s 1-frac");
}
let ns_samples = [
0u64,
1,
500_000_000,
123_456_789,
999_999_999,
1_000_000,
15_625_000, ];
for ns in ns_samples {
for (res, n_frac) in [
(FractionalResolution::FourMs, 1u8),
(FractionalResolution::FifteenUs, 2),
(FractionalResolution::SixtyNs, 3),
] {
let frac = fractional_part_from_subsec_ns(res, ns);
let sp = CucTime::new_with_fractions(42, frac).expect("cuc fractions");
let mut sp_buf = [0u8; 16];
let sp_len = sp.write_to_bytes(&mut sp_buf).unwrap();
let dt = Dt::CCSDS_EPOCH
.add_sec(42)
.add_attos((ns as i128) * 1_000_000_000);
let (dt_buf, dt_len) = dt.to_ccsds_cuc(4, n_frac, false).unwrap();
assert_eq!(
&dt_buf[..dt_len],
&sp_buf[..sp_len],
"CUC ns={ns} n_frac={n_frac} res={res:?}\n\
deep-time={:02x?}\nspacepackets={:02x?}",
&dt_buf[..dt_len],
&sp_buf[..sp_len]
);
}
}
}
#[test]
fn cuc_deep_time_encode_spacepackets_decode() {
let dt = Dt::CCSDS_EPOCH
.add_sec(1_000_000)
.add_attos(500_000_000_000_000_000);
let (buf, len) = dt.to_ccsds_cuc(4, 1, false).unwrap();
let sp = CucTime::from_bytes(&buf[..len]).expect("spacepackets CUC decode");
assert_eq!(sp.counter(), 1_000_000);
assert_eq!(sp.counter_width(), 4);
assert_eq!(sp.fractions().resolution(), FractionalResolution::FourMs);
assert_eq!(sp.fractions().counter(), 0x80);
}
#[test]
fn cuc_spacepackets_encode_deep_time_decode() {
let frac = FractionalPart::new_checked(FractionalResolution::FifteenUs, 0x1234).expect("frac");
let sp = CucTime::new_generic(WidthCounterPair(4, 0x00AA_BBCC), frac).expect("cuc");
let mut sp_buf = [0u8; 16];
let sp_len = sp.write_to_bytes(&mut sp_buf).unwrap();
let parsed = Parts::from_ccsds_cuc(&sp_buf[..sp_len]).expect("deep-time CUC decode");
let recovered = parsed.to_dt().unwrap();
let since = recovered
.target(Scale::TAI)
.to_scale_and_diff(Dt::CCSDS_EPOCH, false);
assert_eq!(since.to_sec64_floor(), 0x00AA_BBCC);
let expected_attos = (0x1234u128 * 1_000_000_000_000_000_000u128) / (1u128 << 16);
assert_eq!(parsed.attos as u128, expected_attos);
}
#[test]
fn cds_24bit_day_wire_identity() {
let days: u32 = 100_000;
let ms: u32 = 12_345_678;
let stamp = CdsTime::new_with_u24_days(days, ms).expect("u24 days");
let mut sp_buf = [0u8; 16];
let sp_len = stamp.write_to_bytes(&mut sp_buf).expect("write");
let (y, mo, d) = Dt::days_since_1958_to_ymd(days as i64);
let sec_of_day = (ms / 1000) as u64;
let rem_ms = (ms % 1000) as u64;
let hr = (sec_of_day / 3600) as u8;
let min = ((sec_of_day % 3600) / 60) as u8;
let sec = (sec_of_day % 60) as u8;
let attos = rem_ms * 1_000_000_000_000_000;
let dt = Dt::from_ymd(y, mo, d, Scale::UTC, hr, min, sec, attos);
let (dt_buf, dt_len) = dt.to_ccsds_cds(3, 0, false).unwrap();
assert_eq!(
&dt_buf[..dt_len],
&sp_buf[..sp_len],
"24-bit day CDS wire mismatch"
);
}