Skip to main content

leodos_protocols/misc/time/
cds.rs

1//! CCSDS Day Segmented (CDS) Time Format (CCSDS 301.0-B-4, §3.3)
2//!
3//! CDS encodes time as a day count since a reference epoch plus
4//! milliseconds within the day, with optional sub-millisecond
5//! resolution (microseconds or picoseconds).
6//!
7//! # P-Field
8//!
9//! ```text
10//! Byte 1:
11//!   [7]     Extension flag (0 = 1-byte P-field)
12//!   [6:4]   Time code ID: 100 = CDS
13//!   [3]     Epoch ID: 0 = CCSDS epoch, 1 = agency-defined
14//!   [2]     Day segment length: 0 = 16-bit, 1 = 24-bit
15//!   [1:0]   Sub-millisecond resolution:
16//!           00 = none, 01 = µs (16-bit), 10 = ps (32-bit)
17//! ```
18//!
19//! # T-Field
20//!
21//! ```text
22//! ┌──────────┬────────────┬──────────────────┐
23//! │ Day      │ ms of day  │ sub-ms (optional) │
24//! │ 16/24 b  │ 32 bits    │ 16 or 32 bits     │
25//! └──────────┴────────────┴──────────────────┘
26//! ```
27//!
28//! # CCSDS Epoch
29//!
30//! Same as CUC: **1958-01-01T00:00:00 TAI**.
31
32use crate::utils::get_bits_u8;
33use crate::utils::set_bits_u8;
34
35/// Milliseconds in one day.
36pub const MS_PER_DAY: u32 = 86_400_000;
37
38/// Time code ID for CDS in the P-field.
39const TIME_CODE_ID: u8 = 0b100;
40
41/// Bitmasks for CDS P-field byte fields.
42#[rustfmt::skip]
43mod bitmask {
44    /// Bitmask for the 3-bit time code ID field [6:4].
45    pub const TIME_CODE_ID_MASK: u8 = 0b_0111_0000;
46    /// Bitmask for the 1-bit epoch ID field [3].
47    pub const EPOCH_ID_MASK: u8 =     0b_0000_1000;
48    /// Bitmask for the 1-bit day segment length field [2].
49    pub const DAY_SEG_MASK: u8 =      0b_0000_0100;
50    /// Bitmask for the 2-bit sub-millisecond resolution field [1:0].
51    pub const SUB_MILLIS_MASK: u8 =   0b_0000_0011;
52}
53
54use bitmask::*;
55
56/// Sub-millisecond resolution options.
57#[derive(Debug, Copy, Clone, Eq, PartialEq)]
58pub enum SubMillis {
59    /// No sub-millisecond field.
60    None,
61    /// 16-bit microseconds within the millisecond (0..999).
62    Microseconds,
63    /// 32-bit picoseconds within the millisecond (0..999_999_999).
64    Picoseconds,
65}
66
67impl SubMillis {
68    /// Size of the sub-millisecond field in bytes.
69    pub const fn field_len(self) -> usize {
70        match self {
71            Self::None => 0,
72            Self::Microseconds => 2,
73            Self::Picoseconds => 4,
74        }
75    }
76
77    /// P-field code for this resolution.
78    const fn code(self) -> u8 {
79        match self {
80            Self::None => 0b00,
81            Self::Microseconds => 0b01,
82            Self::Picoseconds => 0b10,
83        }
84    }
85
86    /// Parses the sub-ms code from the P-field.
87    const fn from_code(code: u8) -> Result<Self, CdsError> {
88        match code {
89            0b00 => Ok(Self::None),
90            0b01 => Ok(Self::Microseconds),
91            0b10 => Ok(Self::Picoseconds),
92            _ => Err(CdsError::InvalidSubMillisCode(code)),
93        }
94    }
95}
96
97/// Epoch identifier.
98#[derive(Debug, Copy, Clone, Eq, PartialEq)]
99pub enum EpochId {
100    /// CCSDS epoch: 1958-01-01T00:00:00 TAI.
101    Ccsds,
102    /// Agency-defined epoch.
103    Agency,
104}
105
106/// CDS time code configuration.
107#[derive(Debug, Copy, Clone, Eq, PartialEq)]
108pub struct CdsConfig {
109    /// Epoch identifier.
110    pub epoch: EpochId,
111    /// Day segment uses 24 bits (true) or 16 bits (false).
112    pub day_24bit: bool,
113    /// Sub-millisecond resolution.
114    pub sub_millis: SubMillis,
115}
116
117impl CdsConfig {
118    /// Standard 16-bit day, no sub-ms, CCSDS epoch.
119    pub const CCSDS_16: Self = Self {
120        epoch: EpochId::Ccsds,
121        day_24bit: false,
122        sub_millis: SubMillis::None,
123    };
124
125    /// 16-bit day with microsecond resolution, CCSDS epoch.
126    pub const CCSDS_16_US: Self = Self {
127        epoch: EpochId::Ccsds,
128        day_24bit: false,
129        sub_millis: SubMillis::Microseconds,
130    };
131
132    /// Day segment size in bytes.
133    pub const fn day_len(&self) -> usize {
134        if self.day_24bit { 3 } else { 2 }
135    }
136
137    /// Total T-field size in bytes.
138    pub const fn t_field_len(&self) -> usize {
139        self.day_len() + 4 + self.sub_millis.field_len()
140    }
141
142    /// Total encoded size (P-field + T-field).
143    pub const fn encoded_len(&self) -> usize {
144        1 + self.t_field_len()
145    }
146
147    /// Encodes the P-field byte.
148    pub const fn p_field(&self) -> u8 {
149        let epoch_bit = match self.epoch {
150            EpochId::Ccsds => 0,
151            EpochId::Agency => 1,
152        };
153        let day_bit = if self.day_24bit { 1 } else { 0 };
154        let mut pf = 0u8;
155        set_bits_u8(&mut pf, TIME_CODE_ID_MASK, TIME_CODE_ID);
156        set_bits_u8(&mut pf, EPOCH_ID_MASK, epoch_bit);
157        set_bits_u8(&mut pf, DAY_SEG_MASK, day_bit);
158        set_bits_u8(&mut pf, SUB_MILLIS_MASK, self.sub_millis.code());
159        pf
160    }
161
162    /// Parses a P-field byte into a CDS configuration.
163    pub const fn from_p_field(pf: u8) -> Result<Self, CdsError> {
164        let id = get_bits_u8(pf, TIME_CODE_ID_MASK);
165        if id != TIME_CODE_ID {
166            return Err(CdsError::NotCds(id));
167        }
168        let epoch_bit = get_bits_u8(pf, EPOCH_ID_MASK);
169        let day_bit = get_bits_u8(pf, DAY_SEG_MASK);
170        let sub_code = get_bits_u8(pf, SUB_MILLIS_MASK);
171
172        let epoch = if epoch_bit == 0 {
173            EpochId::Ccsds
174        } else {
175            EpochId::Agency
176        };
177
178        let sub_millis = match SubMillis::from_code(sub_code) {
179            Ok(s) => s,
180            Err(e) => return Err(e),
181        };
182
183        Ok(Self {
184            epoch,
185            day_24bit: day_bit == 1,
186            sub_millis,
187        })
188    }
189}
190
191/// A CDS timestamp.
192#[derive(Debug, Copy, Clone, Eq, PartialEq)]
193pub struct CdsTime {
194    /// Encoding configuration.
195    pub config: CdsConfig,
196    /// Day count since epoch.
197    pub day: u32,
198    /// Milliseconds within the day (0..86_399_999).
199    pub ms_of_day: u32,
200    /// Sub-millisecond value (µs 0..999 or ps 0..999_999_999).
201    pub sub_ms: u32,
202}
203
204/// Errors from CDS time operations.
205#[derive(Debug, Copy, Clone, Eq, PartialEq)]
206pub enum CdsError {
207    /// P-field time code ID is not CDS (100).
208    NotCds(u8),
209    /// Invalid sub-millisecond code in P-field.
210    InvalidSubMillisCode(u8),
211    /// Buffer too short.
212    BufferTooShort {
213        /// Minimum bytes needed.
214        required: usize,
215        /// Bytes available.
216        provided: usize,
217    },
218    /// Milliseconds value exceeds 86_399_999.
219    MsOutOfRange(u32),
220}
221
222impl CdsTime {
223    /// Creates a new CDS timestamp.
224    pub const fn new(
225        config: CdsConfig,
226        day: u32,
227        ms_of_day: u32,
228        sub_ms: u32,
229    ) -> Self {
230        Self { config, day, ms_of_day, sub_ms }
231    }
232
233    /// Creates a CDS timestamp from total seconds since epoch.
234    pub fn from_seconds(config: CdsConfig, seconds: f64) -> Self {
235        let total_ms = (seconds * 1000.0) as u64;
236        let day = (total_ms / MS_PER_DAY as u64) as u32;
237        let ms_of_day = (total_ms % MS_PER_DAY as u64) as u32;
238
239        let frac_ms = seconds * 1000.0 - (total_ms as f64);
240        let sub_ms = match config.sub_millis {
241            SubMillis::None => 0,
242            SubMillis::Microseconds => {
243                (frac_ms * 1000.0) as u32
244            }
245            SubMillis::Picoseconds => {
246                (frac_ms * 1_000_000_000.0) as u32
247            }
248        };
249
250        Self { config, day, ms_of_day, sub_ms }
251    }
252
253    /// Converts to total seconds since epoch.
254    pub fn to_seconds(&self) -> f64 {
255        let day_secs = self.day as f64 * 86_400.0;
256        let ms_secs = self.ms_of_day as f64 / 1000.0;
257        let sub_secs = match self.config.sub_millis {
258            SubMillis::None => 0.0,
259            SubMillis::Microseconds => {
260                self.sub_ms as f64 / 1_000_000.0
261            }
262            SubMillis::Picoseconds => {
263                self.sub_ms as f64 / 1_000_000_000_000.0
264            }
265        };
266        day_secs + ms_secs + sub_secs
267    }
268
269    /// Encodes this timestamp (P-field + T-field).
270    pub fn encode(&self, buf: &mut [u8]) -> Result<usize, CdsError> {
271        let total = self.config.encoded_len();
272        if buf.len() < total {
273            return Err(CdsError::BufferTooShort {
274                required: total,
275                provided: buf.len(),
276            });
277        }
278
279        buf[0] = self.config.p_field();
280        self.write_t_field(&mut buf[1..])?;
281        Ok(total)
282    }
283
284    /// Encodes only the T-field (no P-field).
285    pub fn encode_t_field(
286        &self,
287        buf: &mut [u8],
288    ) -> Result<usize, CdsError> {
289        let t_len = self.config.t_field_len();
290        if buf.len() < t_len {
291            return Err(CdsError::BufferTooShort {
292                required: t_len,
293                provided: buf.len(),
294            });
295        }
296        self.write_t_field(buf)?;
297        Ok(t_len)
298    }
299
300    fn write_t_field(&self, buf: &mut [u8]) -> Result<(), CdsError> {
301        let mut pos = 0;
302
303        // Day segment
304        let day_bytes = self.day.to_be_bytes();
305        if self.config.day_24bit {
306            buf[pos..pos + 3].copy_from_slice(&day_bytes[1..4]);
307            pos += 3;
308        } else {
309            buf[pos..pos + 2].copy_from_slice(&day_bytes[2..4]);
310            pos += 2;
311        }
312
313        // Milliseconds of day
314        let ms_bytes = self.ms_of_day.to_be_bytes();
315        buf[pos..pos + 4].copy_from_slice(&ms_bytes);
316        pos += 4;
317
318        // Sub-millisecond
319        match self.config.sub_millis {
320            SubMillis::None => {}
321            SubMillis::Microseconds => {
322                let us_bytes = (self.sub_ms as u16).to_be_bytes();
323                buf[pos..pos + 2].copy_from_slice(&us_bytes);
324            }
325            SubMillis::Picoseconds => {
326                let ps_bytes = self.sub_ms.to_be_bytes();
327                buf[pos..pos + 4].copy_from_slice(&ps_bytes);
328            }
329        }
330
331        Ok(())
332    }
333
334    /// Decodes a CDS timestamp from bytes (P-field + T-field).
335    pub fn decode(buf: &[u8]) -> Result<Self, CdsError> {
336        if buf.is_empty() {
337            return Err(CdsError::BufferTooShort {
338                required: 1,
339                provided: 0,
340            });
341        }
342
343        let config = CdsConfig::from_p_field(buf[0])?;
344        let total = config.encoded_len();
345        if buf.len() < total {
346            return Err(CdsError::BufferTooShort {
347                required: total,
348                provided: buf.len(),
349            });
350        }
351
352        Self::decode_t_field(&config, &buf[1..])
353    }
354
355    /// Decodes from T-field bytes with an implicit configuration.
356    pub fn decode_t_field(
357        config: &CdsConfig,
358        buf: &[u8],
359    ) -> Result<Self, CdsError> {
360        let t_len = config.t_field_len();
361        if buf.len() < t_len {
362            return Err(CdsError::BufferTooShort {
363                required: t_len,
364                provided: buf.len(),
365            });
366        }
367
368        let mut pos = 0;
369
370        // Day
371        let day = if config.day_24bit {
372            let mut d = [0u8; 4];
373            d[1..4].copy_from_slice(&buf[pos..pos + 3]);
374            pos += 3;
375            u32::from_be_bytes(d)
376        } else {
377            let mut d = [0u8; 4];
378            d[2..4].copy_from_slice(&buf[pos..pos + 2]);
379            pos += 2;
380            u32::from_be_bytes(d)
381        };
382
383        // Milliseconds of day
384        let ms_of_day = u32::from_be_bytes([
385            buf[pos],
386            buf[pos + 1],
387            buf[pos + 2],
388            buf[pos + 3],
389        ]);
390        pos += 4;
391
392        // Sub-millisecond
393        let sub_ms = match config.sub_millis {
394            SubMillis::None => 0,
395            SubMillis::Microseconds => {
396                let v = u16::from_be_bytes([buf[pos], buf[pos + 1]]);
397                v as u32
398            }
399            SubMillis::Picoseconds => {
400                u32::from_be_bytes([
401                    buf[pos],
402                    buf[pos + 1],
403                    buf[pos + 2],
404                    buf[pos + 3],
405                ])
406            }
407        };
408
409        Ok(Self {
410            config: *config,
411            day,
412            ms_of_day,
413            sub_ms,
414        })
415    }
416}
417
418impl core::fmt::Display for CdsTime {
419    fn fmt(
420        &self,
421        f: &mut core::fmt::Formatter<'_>,
422    ) -> core::fmt::Result {
423        let h = self.ms_of_day / 3_600_000;
424        let m = (self.ms_of_day % 3_600_000) / 60_000;
425        let s = (self.ms_of_day % 60_000) / 1000;
426        let ms = self.ms_of_day % 1000;
427        write!(
428            f,
429            "CDS(day={}, {:02}:{:02}:{:02}.{:03}",
430            self.day, h, m, s, ms
431        )?;
432        match self.config.sub_millis {
433            SubMillis::None => {}
434            SubMillis::Microseconds => {
435                write!(f, ".{:03}µs", self.sub_ms)?;
436            }
437            SubMillis::Picoseconds => {
438                write!(f, ".{:09}ps", self.sub_ms)?;
439            }
440        }
441        write!(f, ")")
442    }
443}
444
445#[cfg(test)]
446mod tests {
447    use super::*;
448
449    #[test]
450    fn p_field_roundtrip() {
451        let config = CdsConfig::CCSDS_16;
452        let pf = config.p_field();
453        let parsed = CdsConfig::from_p_field(pf).unwrap();
454        assert_eq!(parsed, config);
455    }
456
457    #[test]
458    fn p_field_with_us() {
459        let config = CdsConfig::CCSDS_16_US;
460        let pf = config.p_field();
461        let parsed = CdsConfig::from_p_field(pf).unwrap();
462        assert_eq!(parsed, config);
463        assert_eq!(parsed.sub_millis, SubMillis::Microseconds);
464    }
465
466    #[test]
467    fn p_field_24bit_day() {
468        let config = CdsConfig {
469            epoch: EpochId::Ccsds,
470            day_24bit: true,
471            sub_millis: SubMillis::None,
472        };
473        let pf = config.p_field();
474        let parsed = CdsConfig::from_p_field(pf).unwrap();
475        assert!(parsed.day_24bit);
476    }
477
478    #[test]
479    fn p_field_agency_epoch() {
480        let config = CdsConfig {
481            epoch: EpochId::Agency,
482            day_24bit: false,
483            sub_millis: SubMillis::None,
484        };
485        let pf = config.p_field();
486        assert_eq!(get_bits_u8(pf, EPOCH_ID_MASK), 1);
487        let parsed = CdsConfig::from_p_field(pf).unwrap();
488        assert_eq!(parsed.epoch, EpochId::Agency);
489    }
490
491    #[test]
492    fn p_field_value() {
493        // CDS (100), CCSDS epoch (0), 16-bit day (0), no sub-ms (00)
494        // = 0b0_100_0_0_00 = 0x40
495        assert_eq!(CdsConfig::CCSDS_16.p_field(), 0x40);
496        // CDS (100), CCSDS epoch (0), 16-bit day (0), µs (01)
497        // = 0b0_100_0_0_01 = 0x41
498        assert_eq!(CdsConfig::CCSDS_16_US.p_field(), 0x41);
499    }
500
501    #[test]
502    fn encode_decode_16bit_no_subms() {
503        let t = CdsTime::new(CdsConfig::CCSDS_16, 1000, 43_200_000, 0);
504        let mut buf = [0u8; 16];
505        let len = t.encode(&mut buf).unwrap();
506        // 1 P + 2 day + 4 ms = 7
507        assert_eq!(len, 7);
508
509        let decoded = CdsTime::decode(&buf[..len]).unwrap();
510        assert_eq!(decoded.day, 1000);
511        assert_eq!(decoded.ms_of_day, 43_200_000);
512        assert_eq!(decoded.sub_ms, 0);
513    }
514
515    #[test]
516    fn encode_decode_16bit_us() {
517        let t = CdsTime::new(CdsConfig::CCSDS_16_US, 500, 1000, 750);
518        let mut buf = [0u8; 16];
519        let len = t.encode(&mut buf).unwrap();
520        // 1 P + 2 day + 4 ms + 2 µs = 9
521        assert_eq!(len, 9);
522
523        let decoded = CdsTime::decode(&buf[..len]).unwrap();
524        assert_eq!(decoded.day, 500);
525        assert_eq!(decoded.ms_of_day, 1000);
526        assert_eq!(decoded.sub_ms, 750);
527    }
528
529    #[test]
530    fn encode_decode_24bit_ps() {
531        let config = CdsConfig {
532            epoch: EpochId::Ccsds,
533            day_24bit: true,
534            sub_millis: SubMillis::Picoseconds,
535        };
536        let t = CdsTime::new(config, 100_000, 50_000_000, 123_456_789);
537        let mut buf = [0u8; 16];
538        let len = t.encode(&mut buf).unwrap();
539        // 1 P + 3 day + 4 ms + 4 ps = 12
540        assert_eq!(len, 12);
541
542        let decoded = CdsTime::decode(&buf[..len]).unwrap();
543        assert_eq!(decoded.day, 100_000);
544        assert_eq!(decoded.ms_of_day, 50_000_000);
545        assert_eq!(decoded.sub_ms, 123_456_789);
546    }
547
548    #[test]
549    fn t_field_only() {
550        let config = CdsConfig::CCSDS_16;
551        let t = CdsTime::new(config, 365, 72_000_000, 0);
552        let mut buf = [0u8; 8];
553        let len = t.encode_t_field(&mut buf).unwrap();
554        assert_eq!(len, 6); // 2 day + 4 ms
555
556        let decoded =
557            CdsTime::decode_t_field(&config, &buf[..len]).unwrap();
558        assert_eq!(decoded.day, 365);
559        assert_eq!(decoded.ms_of_day, 72_000_000);
560    }
561
562    #[test]
563    fn from_seconds_and_back() {
564        let config = CdsConfig::CCSDS_16;
565        // 1.5 days = 129600 seconds
566        let t = CdsTime::from_seconds(config, 129_600.0);
567        assert_eq!(t.day, 1);
568        assert_eq!(t.ms_of_day, 43_200_000); // 12 hours in ms
569
570        let secs = t.to_seconds();
571        assert!((secs - 129_600.0).abs() < 0.001);
572    }
573
574    #[test]
575    fn from_seconds_with_us() {
576        let config = CdsConfig::CCSDS_16_US;
577        // 0.0015005 seconds = 1 ms + 500 µs + 0.5 µs
578        let t = CdsTime::from_seconds(config, 0.001_500_5);
579        assert_eq!(t.day, 0);
580        assert_eq!(t.ms_of_day, 1);
581        assert_eq!(t.sub_ms, 500);
582    }
583
584    #[test]
585    fn buffer_too_short() {
586        let t = CdsTime::new(CdsConfig::CCSDS_16, 0, 0, 0);
587        let mut buf = [0u8; 3];
588        assert!(matches!(
589            t.encode(&mut buf),
590            Err(CdsError::BufferTooShort { required: 7, .. })
591        ));
592    }
593
594    #[test]
595    fn not_cds_p_field() {
596        // CUC P-field (time code ID = 010)
597        let err = CdsConfig::from_p_field(0x2E);
598        assert!(matches!(err, Err(CdsError::NotCds(0b010))));
599    }
600
601    #[test]
602    fn max_16bit_day() {
603        let t = CdsTime::new(CdsConfig::CCSDS_16, 65535, 0, 0);
604        let mut buf = [0u8; 8];
605        let len = t.encode(&mut buf).unwrap();
606        let decoded = CdsTime::decode(&buf[..len]).unwrap();
607        assert_eq!(decoded.day, 65535);
608    }
609
610    #[test]
611    fn max_24bit_day() {
612        let config = CdsConfig {
613            epoch: EpochId::Ccsds,
614            day_24bit: true,
615            sub_millis: SubMillis::None,
616        };
617        let t = CdsTime::new(config, 0xFF_FFFF, 0, 0);
618        let mut buf = [0u8; 12];
619        let len = t.encode(&mut buf).unwrap();
620        let decoded = CdsTime::decode(&buf[..len]).unwrap();
621        assert_eq!(decoded.day, 0xFF_FFFF);
622    }
623
624    #[test]
625    fn midnight_and_end_of_day() {
626        let config = CdsConfig::CCSDS_16;
627
628        let midnight = CdsTime::new(config, 0, 0, 0);
629        assert_eq!(midnight.ms_of_day, 0);
630
631        let end = CdsTime::new(config, 0, MS_PER_DAY - 1, 0);
632        assert_eq!(end.ms_of_day, 86_399_999);
633
634        let mut buf = [0u8; 8];
635        end.encode(&mut buf).unwrap();
636        let decoded = CdsTime::decode(&buf).unwrap();
637        assert_eq!(decoded.ms_of_day, 86_399_999);
638    }
639
640    #[test]
641    fn encoded_len_values() {
642        assert_eq!(CdsConfig::CCSDS_16.encoded_len(), 7);
643        assert_eq!(CdsConfig::CCSDS_16_US.encoded_len(), 9);
644        let ps_24 = CdsConfig {
645            epoch: EpochId::Ccsds,
646            day_24bit: true,
647            sub_millis: SubMillis::Picoseconds,
648        };
649        assert_eq!(ps_24.encoded_len(), 12);
650    }
651}