Skip to main content

leodos_protocols/misc/time/
cuc.rs

1//! CCSDS Unsegmented Code (CUC) Time Format (CCSDS 301.0-B-4, §3.2)
2//!
3//! CUC encodes time as a binary count of whole seconds (coarse) and
4//! fractional seconds (fine) since a reference epoch. The number of
5//! coarse and fine bytes is configurable (1-4 each), making it
6//! flexible for different precision requirements.
7//!
8//! # P-Field (Preamble)
9//!
10//! The P-field is a 1-byte or 2-byte preamble that describes the
11//! time code format. When the P-field is implicit (agreed upon by
12//! sender and receiver), it may be omitted from the encoded bytes.
13//!
14//! ```text
15//! Byte 1:
16//!   [7]     Extension flag (0 = 1-byte P-field, 1 = 2-byte)
17//!   [6:4]   Time code ID: 001 = CUC with agency-defined epoch
18//!                          010 = CUC with CCSDS epoch (TAI)
19//!   [3:2]   Number of coarse time octets - 1 (0-3 → 1-4 bytes)
20//!   [1:0]   Number of fine time octets (0-3 bytes)
21//!
22//! Byte 2 (if extension flag = 1):
23//!   [7]     Must be 0
24//!   [6:5]   Additional coarse octets (0-3)
25//!   [4:3]   Additional fine octets (0-3)
26//!   [2:0]   Reserved
27//! ```
28//!
29//! # CCSDS Epoch
30//!
31//! The standard CCSDS epoch is **1958-01-01T00:00:00 TAI**.
32
33use crate::utils::get_bits_u8;
34use crate::utils::set_bits_u8;
35
36/// Maximum number of coarse (whole-second) bytes.
37pub const MAX_COARSE_BYTES: u8 = 4;
38
39/// Maximum number of fine (fractional-second) bytes.
40pub const MAX_FINE_BYTES: u8 = 3;
41
42/// The CCSDS epoch: 1958-01-01T00:00:00 TAI.
43/// Offset from the Unix epoch (1970-01-01) in seconds.
44/// Unix epoch = CCSDS epoch + 378_691_200 seconds.
45pub const CCSDS_EPOCH_UNIX_OFFSET: i64 = -378_691_200;
46
47/// Time code IDs for the P-field.
48#[derive(Debug, Copy, Clone, Eq, PartialEq)]
49#[repr(u8)]
50pub enum TimeCodeId {
51    /// CUC with agency-defined epoch (ID = 0b001).
52    AgencyEpoch = 0b001,
53    /// CUC with CCSDS epoch (1958-01-01 TAI) (ID = 0b010).
54    CcsdsEpoch = 0b010,
55}
56
57/// Bitmasks for CUC P-field byte fields.
58#[rustfmt::skip]
59mod bitmask {
60    /// Bitmask for the 3-bit time code ID field [6:4].
61    pub const TIME_CODE_ID_MASK: u8 = 0b_0111_0000;
62    /// Bitmask for the 2-bit coarse length code field [3:2].
63    pub const COARSE_LEN_MASK: u8 =   0b_0000_1100;
64    /// Bitmask for the 2-bit fine length code field [1:0].
65    pub const FINE_LEN_MASK: u8 =      0b_0000_0011;
66}
67
68use bitmask::*;
69
70/// Configuration for a CUC time code.
71#[derive(Debug, Copy, Clone, Eq, PartialEq)]
72pub struct CucConfig {
73    /// Time code identifier.
74    pub time_code_id: TimeCodeId,
75    /// Number of coarse (whole-second) bytes (1-4).
76    pub coarse_len: u8,
77    /// Number of fine (fractional-second) bytes (0-3).
78    pub fine_len: u8,
79}
80
81impl CucConfig {
82    /// Creates a new CUC configuration.
83    ///
84    /// Panics if `coarse_len` is 0 or > 4, or `fine_len` > 3.
85    pub const fn new(time_code_id: TimeCodeId, coarse_len: u8, fine_len: u8) -> Self {
86        assert!(coarse_len >= 1 && coarse_len <= MAX_COARSE_BYTES);
87        assert!(fine_len <= MAX_FINE_BYTES);
88        Self {
89            time_code_id,
90            coarse_len,
91            fine_len,
92        }
93    }
94
95    /// Standard 4+2 configuration: 4 coarse bytes (seconds since
96    /// CCSDS epoch) + 2 fine bytes (~15 µs resolution).
97    pub const CCSDS_4_2: Self = Self::new(TimeCodeId::CcsdsEpoch, 4, 2);
98
99    /// Standard 4+0 configuration: 4 coarse bytes, no fractional.
100    pub const CCSDS_4_0: Self = Self::new(TimeCodeId::CcsdsEpoch, 4, 0);
101
102    /// Returns the P-field byte for this configuration.
103    ///
104    /// This encodes the time code ID, coarse length, and fine
105    /// length into a single byte. The extension flag is 0.
106    pub const fn p_field(&self) -> u8 {
107        let id = self.time_code_id as u8;
108        let coarse_code = self.coarse_len - 1;
109        let fine_code = self.fine_len;
110        let mut pf = 0u8;
111        set_bits_u8(&mut pf, TIME_CODE_ID_MASK, id);
112        set_bits_u8(&mut pf, COARSE_LEN_MASK, coarse_code);
113        set_bits_u8(&mut pf, FINE_LEN_MASK, fine_code);
114        pf
115    }
116
117    /// Parses a P-field byte into a CUC configuration.
118    pub const fn from_p_field(pf: u8) -> Result<Self, Error> {
119        let id_bits = get_bits_u8(pf, TIME_CODE_ID_MASK);
120        let coarse_code = get_bits_u8(pf, COARSE_LEN_MASK);
121        let fine_code = get_bits_u8(pf, FINE_LEN_MASK);
122
123        let time_code_id = match id_bits {
124            0b001 => TimeCodeId::AgencyEpoch,
125            0b010 => TimeCodeId::CcsdsEpoch,
126            _ => return Err(Error::InvalidTimeCodeId(id_bits)),
127        };
128
129        Ok(Self {
130            time_code_id,
131            coarse_len: coarse_code + 1,
132            fine_len: fine_code,
133        })
134    }
135
136    /// Total T-field size in bytes (coarse + fine).
137    pub const fn t_field_len(&self) -> usize {
138        self.coarse_len as usize + self.fine_len as usize
139    }
140
141    /// Total encoded size including the P-field (1 + T-field).
142    pub const fn encoded_len(&self) -> usize {
143        1 + self.t_field_len()
144    }
145}
146
147/// A CUC timestamp value.
148#[derive(Debug, Copy, Clone, Eq, PartialEq)]
149pub struct CucTime {
150    /// Configuration describing the encoding format.
151    pub config: CucConfig,
152    /// Coarse time: whole seconds since epoch.
153    pub coarse: u32,
154    /// Fine time: fractional seconds (interpretation depends on
155    /// `config.fine_len`).
156    pub fine: u32,
157}
158
159/// Errors for CUC time operations.
160#[derive(Debug, Copy, Clone, Eq, PartialEq, thiserror::Error)]
161pub enum Error {
162    /// The P-field contains an unrecognized time code ID.
163    #[error("Invalid time code ID in P-field: {0:#03b}")]
164    InvalidTimeCodeId(u8),
165    /// The buffer is too short for the expected encoding.
166    #[error("Buffer too short: required {required} bytes, but provided {provided} bytes")]
167    BufferTooShort {
168        /// Minimum bytes needed.
169        required: usize,
170        /// Actual bytes available.
171        provided: usize,
172    },
173}
174
175impl CucTime {
176    /// Creates a new CUC timestamp from coarse and fine values.
177    pub const fn new(config: CucConfig, coarse: u32, fine: u32) -> Self {
178        Self {
179            config,
180            coarse,
181            fine,
182        }
183    }
184
185    /// Creates a CUC timestamp from a floating-point seconds value.
186    ///
187    /// The integer part becomes the coarse time; the fractional part
188    /// is quantized into `config.fine_len` bytes of resolution.
189    pub fn from_seconds(config: CucConfig, seconds: f64) -> Self {
190        let coarse = seconds as u32;
191        let frac = seconds - coarse as f64;
192        let fine_bits = config.fine_len as u32 * 8;
193        let fine = if fine_bits > 0 {
194            (frac * (1u64 << fine_bits) as f64) as u32
195        } else {
196            0
197        };
198        Self {
199            config,
200            coarse,
201            fine,
202        }
203    }
204
205    /// Converts this timestamp to a floating-point seconds value.
206    pub fn to_seconds(&self) -> f64 {
207        let fine_bits = self.config.fine_len as u32 * 8;
208        let frac = if fine_bits > 0 {
209            self.fine as f64 / (1u64 << fine_bits) as f64
210        } else {
211            0.0
212        };
213        self.coarse as f64 + frac
214    }
215
216    /// Returns the fractional-second resolution in seconds.
217    pub fn resolution(&self) -> f64 {
218        let fine_bits = self.config.fine_len as u32 * 8;
219        if fine_bits > 0 {
220            1.0 / (1u64 << fine_bits) as f64
221        } else {
222            1.0
223        }
224    }
225
226    /// Encodes this timestamp into a byte buffer (P-field + T-field).
227    ///
228    /// Returns the number of bytes written.
229    pub fn encode(&self, buf: &mut [u8]) -> Result<usize, Error> {
230        let total = self.config.encoded_len();
231        if buf.len() < total {
232            return Err(Error::BufferTooShort {
233                required: total,
234                provided: buf.len(),
235            });
236        }
237
238        buf[0] = self.config.p_field();
239        let mut pos = 1;
240
241        // Write coarse bytes (big-endian, only the low N bytes)
242        let coarse_bytes = self.coarse.to_be_bytes();
243        let coarse_len = self.config.coarse_len as usize;
244        let coarse_start = 4 - coarse_len;
245        buf[pos..pos + coarse_len].copy_from_slice(&coarse_bytes[coarse_start..]);
246        pos += coarse_len;
247
248        // Write fine bytes (big-endian, only the high N bytes)
249        let fine_len = self.config.fine_len as usize;
250        if fine_len > 0 {
251            let fine_bytes = self.fine.to_be_bytes();
252            buf[pos..pos + fine_len].copy_from_slice(&fine_bytes[..fine_len]);
253            pos += fine_len;
254        }
255
256        Ok(pos)
257    }
258
259    /// Encodes only the T-field (no P-field) into a buffer.
260    ///
261    /// Useful when the P-field is implicit.
262    pub fn encode_t_field(&self, buf: &mut [u8]) -> Result<usize, Error> {
263        let t_len = self.config.t_field_len();
264        if buf.len() < t_len {
265            return Err(Error::BufferTooShort {
266                required: t_len,
267                provided: buf.len(),
268            });
269        }
270
271        let mut pos = 0;
272
273        let coarse_bytes = self.coarse.to_be_bytes();
274        let coarse_len = self.config.coarse_len as usize;
275        let coarse_start = 4 - coarse_len;
276        buf[pos..pos + coarse_len].copy_from_slice(&coarse_bytes[coarse_start..]);
277        pos += coarse_len;
278
279        let fine_len = self.config.fine_len as usize;
280        if fine_len > 0 {
281            let fine_bytes = self.fine.to_be_bytes();
282            buf[pos..pos + fine_len].copy_from_slice(&fine_bytes[..fine_len]);
283            pos += fine_len;
284        }
285
286        Ok(pos)
287    }
288
289    /// Decodes a CUC timestamp from bytes (P-field + T-field).
290    pub fn decode(buf: &[u8]) -> Result<Self, Error> {
291        if buf.is_empty() {
292            return Err(Error::BufferTooShort {
293                required: 1,
294                provided: 0,
295            });
296        }
297
298        let config = CucConfig::from_p_field(buf[0])?;
299        let total = config.encoded_len();
300        if buf.len() < total {
301            return Err(Error::BufferTooShort {
302                required: total,
303                provided: buf.len(),
304            });
305        }
306
307        Self::decode_t_field(&config, &buf[1..])
308    }
309
310    /// Decodes a CUC timestamp from T-field bytes only.
311    ///
312    /// The caller provides the configuration (implicit P-field).
313    pub fn decode_t_field(config: &CucConfig, buf: &[u8]) -> Result<Self, Error> {
314        let t_len = config.t_field_len();
315        if buf.len() < t_len {
316            return Err(Error::BufferTooShort {
317                required: t_len,
318                provided: buf.len(),
319            });
320        }
321
322        let mut pos = 0;
323        let coarse_len = config.coarse_len as usize;
324        let mut coarse_buf = [0u8; 4];
325        let coarse_start = 4 - coarse_len;
326        coarse_buf[coarse_start..].copy_from_slice(&buf[pos..pos + coarse_len]);
327        let coarse = u32::from_be_bytes(coarse_buf);
328        pos += coarse_len;
329
330        let fine_len = config.fine_len as usize;
331        let fine = if fine_len > 0 {
332            let mut fine_buf = [0u8; 4];
333            fine_buf[..fine_len].copy_from_slice(&buf[pos..pos + fine_len]);
334            u32::from_be_bytes(fine_buf)
335        } else {
336            0
337        };
338
339        Ok(Self {
340            config: *config,
341            coarse,
342            fine,
343        })
344    }
345}
346
347impl core::fmt::Display for CucTime {
348    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
349        write!(f, "CUC({:.6}s)", self.to_seconds())
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    #[test]
358    fn p_field_roundtrip() {
359        let config = CucConfig::CCSDS_4_2;
360        let pf = config.p_field();
361        let parsed = CucConfig::from_p_field(pf).unwrap();
362        assert_eq!(parsed, config);
363    }
364
365    #[test]
366    fn p_field_values() {
367        // CcsdsEpoch (0b010), coarse=4 (code=3), fine=2
368        // = 0b0_010_11_10 = 0x2E
369        let config = CucConfig::CCSDS_4_2;
370        assert_eq!(config.p_field(), 0x2E);
371
372        // CcsdsEpoch (0b010), coarse=4 (code=3), fine=0
373        // = 0b0_010_11_00 = 0x2C
374        let config = CucConfig::CCSDS_4_0;
375        assert_eq!(config.p_field(), 0x2C);
376    }
377
378    #[test]
379    fn encode_decode_roundtrip_4_2() {
380        let config = CucConfig::CCSDS_4_2;
381        let t = CucTime::new(config, 1_000_000, 0x8000_0000);
382
383        let mut buf = [0u8; 16];
384        let len = t.encode(&mut buf).unwrap();
385        assert_eq!(len, 7); // 1 P-field + 4 coarse + 2 fine
386
387        let decoded = CucTime::decode(&buf[..len]).unwrap();
388        assert_eq!(decoded.coarse, 1_000_000);
389        // Fine: we wrote 0x8000_0000, but only 2 bytes → 0x8000
390        // decoded back into u32: 0x8000_0000
391        assert_eq!(decoded.fine, 0x8000_0000);
392    }
393
394    #[test]
395    fn encode_decode_roundtrip_4_0() {
396        let config = CucConfig::CCSDS_4_0;
397        let t = CucTime::new(config, 42, 0);
398
399        let mut buf = [0u8; 8];
400        let len = t.encode(&mut buf).unwrap();
401        assert_eq!(len, 5); // 1 + 4
402
403        let decoded = CucTime::decode(&buf[..len]).unwrap();
404        assert_eq!(decoded.coarse, 42);
405        assert_eq!(decoded.fine, 0);
406    }
407
408    #[test]
409    fn t_field_only() {
410        let config = CucConfig::CCSDS_4_2;
411        let t = CucTime::new(config, 12345, 0xABCD_0000);
412
413        let mut buf = [0u8; 8];
414        let len = t.encode_t_field(&mut buf).unwrap();
415        assert_eq!(len, 6); // 4 coarse + 2 fine
416
417        let decoded = CucTime::decode_t_field(&config, &buf[..len]).unwrap();
418        assert_eq!(decoded.coarse, 12345);
419        assert_eq!(decoded.fine, 0xABCD_0000);
420    }
421
422    #[test]
423    fn from_seconds_and_back() {
424        let config = CucConfig::CCSDS_4_2;
425        let t = CucTime::from_seconds(config, 100.5);
426
427        assert_eq!(t.coarse, 100);
428        // 0.5 * 2^16 = 32768 = 0x8000 → stored as 0x8000_0000
429        assert_eq!(t.fine, 0x8000);
430
431        let secs = t.to_seconds();
432        let diff = (secs - 100.5).abs();
433        assert!(diff < 0.001);
434    }
435
436    #[test]
437    fn resolution_values() {
438        let c0 = CucConfig::new(TimeCodeId::CcsdsEpoch, 4, 0);
439        assert_eq!(c0.fine_len, 0);
440        assert_eq!(CucTime::new(c0, 0, 0).resolution(), 1.0);
441
442        let c1 = CucConfig::new(TimeCodeId::CcsdsEpoch, 4, 1);
443        let r1 = CucTime::new(c1, 0, 0).resolution();
444        let diff1 = (r1 - 1.0 / 256.0).abs();
445        assert!(diff1 < 1e-10);
446
447        let c2 = CucConfig::CCSDS_4_2;
448        let r2 = CucTime::new(c2, 0, 0).resolution();
449        let diff2 = (r2 - 1.0 / 65536.0).abs();
450        assert!(diff2 < 1e-12);
451
452        let c3 = CucConfig::new(TimeCodeId::CcsdsEpoch, 4, 3);
453        let r3 = CucTime::new(c3, 0, 0).resolution();
454        let diff3 = (r3 - 1.0 / 16777216.0).abs();
455        assert!(diff3 < 1e-15);
456    }
457
458    #[test]
459    fn agency_epoch() {
460        let config = CucConfig::new(TimeCodeId::AgencyEpoch, 2, 1);
461        let t = CucTime::new(config, 300, 0x8000_0000);
462
463        let mut buf = [0u8; 8];
464        let len = t.encode(&mut buf).unwrap();
465        assert_eq!(len, 4); // 1 P-field + 2 coarse + 1 fine
466
467        let decoded = CucTime::decode(&buf[..len]).unwrap();
468        assert_eq!(decoded.config.time_code_id, TimeCodeId::AgencyEpoch);
469        assert_eq!(decoded.coarse, 300);
470        assert_eq!(decoded.fine, 0x8000_0000);
471    }
472
473    #[test]
474    fn buffer_too_short() {
475        let t = CucTime::new(CucConfig::CCSDS_4_2, 0, 0);
476        let mut buf = [0u8; 3]; // need 7
477        let err = t.encode(&mut buf);
478        assert!(matches!(
479            err,
480            Err(Error::BufferTooShort {
481                required: 7,
482                provided: 3,
483            })
484        ));
485    }
486
487    #[test]
488    fn invalid_time_code_id() {
489        let err = CucConfig::from_p_field(0x00); // id = 0b000
490        assert!(matches!(err, Err(Error::InvalidTimeCodeId(0))));
491    }
492
493    #[test]
494    fn small_coarse_field() {
495        // 1-byte coarse, 0 fine — can represent 0..255 seconds
496        let config = CucConfig::new(TimeCodeId::CcsdsEpoch, 1, 0);
497        let t = CucTime::new(config, 200, 0);
498
499        let mut buf = [0u8; 4];
500        let len = t.encode(&mut buf).unwrap();
501        assert_eq!(len, 2); // 1 P-field + 1 coarse
502
503        let decoded = CucTime::decode(&buf[..len]).unwrap();
504        assert_eq!(decoded.coarse, 200);
505    }
506}