Skip to main content

deep_time/dt/
to_bin_ccsds.rs

1use crate::{
2    ATTOS_PER_MS_I128, ATTOS_PER_PS_I128, ATTOS_PER_SEC_U128, ATTOS_PER_US_I128, Dt, DtErr,
3    DtErrKind, Scale, an_err,
4};
5
6impl Dt {
7    /// Maximum size needed for a CCSDS **CUC or CDS** binary packet (with extended P-field).
8    ///
9    /// Sized for the largest supported CUC layout (2-byte P-field + 7 coarse + 10 fine = 19 octets)
10    /// with headroom; also sufficient for CDS (≤ 2 + 3 day + 4 ms + 4 sub-ms = 13 octets).
11    pub const CCSDS_C_AND_D_MAX_SIZE: usize = 32;
12
13    /// Formats this [`Dt`] as a **CCSDS C (CUC – Unsegmented Time Code)** binary packet.
14    ///
15    /// Fully configurable for round-tripping with
16    /// [`Dt::from_ccsds_cuc`](../struct.Dt.html#method.from_ccsds_cuc).
17    /// Conforms to **CCSDS 301.0-B-4 §3.2 (Level 1)**, including full support for the
18    /// extended 2-byte P-field.
19    ///
20    /// - The time is always encoded on the **TAI** timescale (Code ID `001`).
21    /// - The `target` field of this [`Dt`] is ignored.
22    ///
23    /// ## Parameters
24    ///
25    /// - `n_coarse`: Number of bytes used for the coarse (integer) seconds since the
26    ///   1958 epoch. Must be in `1..=7`. The value must be large enough to hold the
27    ///   full second count; if the instant does not fit, encoding returns
28    ///   [`DtErrKind::OutOfRange`] (no silent truncation). For example, a date in
29    ///   2025 requires at least 4 bytes (`n_coarse >= 4`); 3 bytes only reach
30    ///   roughly mid-1968.
31    /// - `n_frac`: Number of bytes used for the fractional seconds. Must be in `0..=10`.
32    ///   The fine field is the **binary fraction** of a second
33    ///   (`floor(frac × 2^(8·n_frac))`), matching a free-running binary counter
34    ///   (CCSDS 301.0-B-4 §3.2). Values are **truncated**, never rounded.
35    /// - `extension`: If `true`, forces inclusion of the second P-field octet even
36    ///   when it is not strictly required by the field sizes.
37    ///
38    /// ## Epoch
39    ///
40    /// Seconds are counted since **1958-01-01 00:00:00 TAI**.
41    ///
42    /// ## Returns
43    ///
44    /// `(buffer, len)` where `buffer` is a fixed-size array of length
45    /// [`Dt::CCSDS_C_AND_D_MAX_SIZE`](../struct.Dt.html#associatedconstant.CCSDS_C_AND_D_MAX_SIZE)
46    /// and `len` is the number of bytes written.
47    ///
48    /// ## Errors
49    ///
50    /// - [`DtErrKind::OutOfRange`] if `n_coarse` is not in `1..=7`, or if the
51    ///   coarse second count does not fit in `n_coarse` octets.
52    /// - [`DtErrKind::FracOutOfRange`] if `n_frac > 10`.
53    /// - [`DtErrKind::YearOutOfRange`] if this instant is before
54    ///   **1958-01-01 00:00:00 TAI** (the CUC epoch).
55    ///
56    /// ## See also
57    ///
58    /// - [`Dt::from_ccsds_cuc`](../struct.Dt.html#method.from_ccsds_cuc)
59    pub fn to_ccsds_cuc(
60        &self,
61        n_coarse: u8,
62        n_frac: u8,
63        extension: bool,
64    ) -> Result<([u8; Self::CCSDS_C_AND_D_MAX_SIZE], usize), DtErr> {
65        if !(1..=7).contains(&n_coarse) {
66            return Err(an_err!(DtErrKind::OutOfRange));
67        } else if n_frac > 10 {
68            return Err(an_err!(DtErrKind::FracOutOfRange));
69        }
70
71        let tai_since_1958 = self
72            .target(Scale::TAI)
73            .to_scale_and_diff(Self::CCSDS_EPOCH, false);
74
75        let rem_attos = tai_since_1958.to_sec_ufrac();
76        let total_tai_seconds = tai_since_1958.to_sec64_floor();
77
78        if total_tai_seconds < 0 {
79            return Err(an_err!(DtErrKind::YearOutOfRange, "<1958"));
80        }
81
82        let coarse = total_tai_seconds as u64;
83        // Refuse silent truncation: coarse field must hold the full second count.
84        let coarse_bits = 8u32 * u32::from(n_coarse);
85        let max_coarse = if coarse_bits >= 64 {
86            u64::MAX
87        } else {
88            (1u64 << coarse_bits) - 1
89        };
90        if coarse > max_coarse {
91            return Err(an_err!(DtErrKind::OutOfRange, "n_coarse"));
92        }
93
94        // Binary-fraction fine time: state of a free-running counter
95        // (CCSDS 301.0-B-4 §3.2). Pure truncation — never round.
96        //
97        // Build big-endian octets via successive ×256 / 10¹⁸ so that large
98        // `n_frac` never needs an intermediate wider than u128.
99        let mut frac_bytes = [0u8; 10];
100        if n_frac > 0 {
101            let mut rem = rem_attos as u128;
102            for byte in frac_bytes.iter_mut().take(n_frac as usize) {
103                rem *= 256;
104                *byte = (rem / ATTOS_PER_SEC_U128) as u8;
105                rem %= ATTOS_PER_SEC_U128;
106            }
107        }
108
109        let mut buf = [0u8; Self::CCSDS_C_AND_D_MAX_SIZE];
110        let mut pos = 0usize;
111
112        // Decide whether we need the extended (2-byte) P-field.
113        // Required when coarse > 4 octets, fractional > 3 octets, or caller forces it.
114        let needs_extension = n_coarse > 4 || n_frac > 3 || extension;
115
116        // Base values that fit in the first P-field octet.
117        // Coarse: 2 bits (0-3) → actual octets = base + 1
118        // Fractional: 2 bits (0-3)
119        let base_coarse = if n_coarse <= 4 { n_coarse - 1 } else { 3 };
120        let base_frac = if n_frac <= 3 { n_frac } else { 3 };
121
122        // Build P-field octet 1
123        // Bit 7     = extension flag
124        // Bits 6-4  = Code ID (001 for CUC Level 1)
125        // Bits 3-2  = base coarse (octets - 1)
126        // Bits 1-0  = base fractional octets
127        let mut p1 = 0b0001_0000u8; // Code ID = 001
128        p1 |= (base_coarse << 2) & 0b0000_1100;
129        p1 |= base_frac & 0b0000_0011;
130        if needs_extension {
131            p1 |= 0b1000_0000;
132        }
133        buf[pos] = p1;
134        pos += 1;
135
136        if needs_extension {
137            // Build P-field octet 2 (extended P-field)
138            // Bit 7     = further extension (must be 0)
139            // Bits 6-5  = additional coarse octets (0-3)
140            // Bits 4-2  = additional fractional octets (0-7)
141            // Bits 1-0  = reserved
142            let add_coarse = n_coarse - 4;
143            let add_frac = n_frac.saturating_sub(3);
144
145            let mut p2 = 0u8;
146            p2 |= (add_coarse & 0b11) << 5;
147            p2 |= (add_frac & 0b111) << 2;
148            buf[pos] = p2;
149            pos += 1;
150        }
151
152        // Write coarse time (big-endian)
153        for i in (0..n_coarse).rev() {
154            buf[pos] = (coarse >> (i as u32 * 8)) as u8;
155            pos += 1;
156        }
157
158        // Write fractional time (big-endian, MSB already in frac_bytes[0])
159        for &byte in frac_bytes.iter().take(n_frac as usize) {
160            buf[pos] = byte;
161            pos += 1;
162        }
163
164        Ok((buf, pos))
165    }
166
167    /// Formats this [`Dt`] as a **CCSDS D (CDS – Day Segmented Time Code)** binary packet.
168    ///
169    /// Fully configurable for round-tripping with
170    /// [`Dt::from_ccsds_cds`](../struct.Dt.html#method.from_ccsds_cds).
171    /// Conforms to **CCSDS 301.0-B-4 §3.3 (Level 1)**.
172    ///
173    /// The time is always encoded on the **UTC** timescale (day count + milliseconds
174    /// of day, with optional sub-millisecond segment). Leap-second handling follows
175    /// civil UTC (`second == 60` maps into the 86_400_000 ms range of Annex A).
176    ///
177    /// ## Parameters
178    ///
179    /// - `n_day`: Number of day-count octets. Must be `2` or `3`.
180    /// - `sub_ms_code`: Sub-millisecond resolution (cascaded unit counters):
181    ///   - `0`: none (millisecond resolution only)
182    ///   - `1`: 2 bytes — **microsecond-of-millisecond**, range **0–999** (Annex A)
183    ///   - `2`: 4 bytes — **picosecond-of-millisecond**, range **0–999_999_999**
184    /// - `extension`: If `true`, emits the second P-field octet.
185    ///
186    /// ## Segment counters (truncation)
187    ///
188    /// Each segment is a right-adjusted binary counter (CCSDS 301.0-B-4 §3.3).
189    /// Sub-second fields are **truncated** to the segment unit — never rounded —
190    /// so cascaded segments do not double-count residual time.
191    ///
192    /// ## Epoch
193    ///
194    /// Day count is calendar days since **1958-01-01 00:00:00 UTC**.
195    ///
196    /// ## Returns
197    ///
198    /// `(buffer, len)` where `buffer` is a fixed-size array of length
199    /// [`Dt::CCSDS_C_AND_D_MAX_SIZE`](../struct.Dt.html#associatedconstant.CCSDS_C_AND_D_MAX_SIZE)
200    /// and `len` is the number of bytes written.
201    ///
202    /// ## Errors
203    ///
204    /// - [`DtErrKind::InvalidNumber`] if `n_day` is not `2` or `3`.
205    /// - [`DtErrKind::InvalidSubmillisecond`] if `sub_ms_code` is not in `0..=2`.
206    /// - [`DtErrKind::YearOutOfRange`] if this instant is before
207    ///   **1958-01-01 00:00:00 UTC** (the CDS Level 1 epoch).
208    /// - [`DtErrKind::OutOfRange`] if the day count does not fit in `n_day` octets.
209    ///
210    /// ## See also
211    ///
212    /// - [`Dt::from_ccsds_cds`](../struct.Dt.html#method.from_ccsds_cds)
213    pub fn to_ccsds_cds(
214        &self,
215        n_day: u8,
216        sub_ms_code: u8,
217        extension: bool,
218    ) -> Result<([u8; Self::CCSDS_C_AND_D_MAX_SIZE], usize), DtErr> {
219        if !matches!(n_day, 2 | 3) {
220            return Err(an_err!(DtErrKind::InvalidNumber, "n_day: {}", n_day));
221        } else if !matches!(sub_ms_code, 0..=2) {
222            return Err(an_err!(DtErrKind::InvalidSubmillisecond));
223        }
224
225        // Civil UTC so leap seconds map to Annex A ms-of-day ranges.
226        let ymd = self.target(Scale::UTC).to_ymd();
227
228        let day_count_i = Self::ymd_to_days_since_1958(ymd.yr, ymd.mo, ymd.day);
229        if day_count_i < 0 {
230            return Err(an_err!(DtErrKind::YearOutOfRange, "<1958"));
231        }
232        let day_count = day_count_i as u64;
233
234        let max_day = if n_day == 2 {
235            0xFFFF_u64
236        } else {
237            0xFF_FFFF_u64
238        };
239        if day_count > max_day {
240            return Err(an_err!(DtErrKind::OutOfRange, "day count"));
241        }
242
243        // Cascaded counters, pure truncation (no rounding between segments).
244        let attos = ymd.attos as u128;
245        let sec_of_day = u64::from(ymd.hr) * 3600 + u64::from(ymd.min) * 60 + u64::from(ymd.sec);
246
247        let ms_in_sec = (attos / ATTOS_PER_MS_I128 as u128) as u64; // 0..999
248        // Civil fields already bound sec_of_day (≤ 86400 on leap second) and ms_in_sec.
249        let millis_of_day = sec_of_day
250            .checked_mul(1000)
251            .and_then(|v| v.checked_add(ms_in_sec))
252            .ok_or_else(|| an_err!(DtErrKind::OutOfRange, "ms of day"))?;
253
254        let attos_in_ms = attos % ATTOS_PER_MS_I128 as u128;
255
256        let frac_scaled = match sub_ms_code {
257            0 => 0u64,
258            // Microsecond-of-millisecond: Annex A range 0..=999
259            1 => (attos_in_ms / ATTOS_PER_US_I128 as u128) as u64,
260            // Picosecond-of-millisecond: 0..=999_999_999
261            2 => (attos_in_ms / ATTOS_PER_PS_I128 as u128) as u64,
262            // sub_ms_code validated above
263            _ => 0u64,
264        };
265
266        let mut buf = [0u8; Self::CCSDS_C_AND_D_MAX_SIZE];
267        let mut pos = 0usize;
268
269        let mut p1 = 0b0100_0000u8;
270        if extension {
271            p1 |= 0b1000_0000;
272        }
273        if n_day == 3 {
274            p1 |= 0b0000_0100;
275        }
276        p1 |= sub_ms_code;
277        buf[pos] = p1;
278        pos += 1;
279
280        if extension {
281            buf[pos] = 0;
282            pos += 1;
283        }
284
285        for i in (0..n_day).rev() {
286            buf[pos] = (day_count >> (i * 8)) as u8;
287            pos += 1;
288        }
289
290        for i in (0..4).rev() {
291            buf[pos] = (millis_of_day >> (i * 8)) as u8;
292            pos += 1;
293        }
294
295        let n_frac = match sub_ms_code {
296            0 => 0,
297            1 => 2,
298            2 => 4,
299            _ => 0,
300        };
301        for i in (0..n_frac).rev() {
302            buf[pos] = (frac_scaled >> (i * 8)) as u8;
303            pos += 1;
304        }
305
306        Ok((buf, pos))
307    }
308
309    /// Maximum size needed for a CCSDS CCS binary packet (P-field + T-field).
310    pub const CCSDS_CCS_MAX_SIZE: usize = 14; // 1 + 2(year) + 2(date) + 3(HMS) + 6(subsec)
311
312    /// Formats this [`Dt`] as a **CCSDS CCS (Calendar Segmented Time Code)** binary packet.
313    ///
314    /// Fully configurable for round-tripping with
315    /// [`Dt::from_ccsds_ccs`](../struct.Dt.html#method.from_ccsds_ccs).
316    /// Conforms to **CCSDS 301.0-B-4 §3.4** (Level 1 only).
317    ///
318    /// Both CCS variants are **UTC-based** and use BCD encoding.
319    /// Leap seconds are supported (`second = 60`).
320    ///
321    /// ## Parameters
322    ///
323    /// - `use_doy`: `false` = Month/Day variant (most common), `true` = Day-of-Year variant.
324    /// - `n_subsec`: Number of subsecond BCD octets (`0`–`6`). Each octet holds two decimal digits
325    ///   (so `n_subsec = 6` gives up to 12 decimal digits of subsecond precision).
326    ///
327    /// ## Subsecond digits
328    ///
329    /// Fractional seconds are **truncated** to the requested number of decimal digits
330    /// (no rounding, no carry into the seconds field). This matches the cascaded-segment
331    /// model used for CDS and avoids second-boundary overflow when the discarded
332    /// residual would have rounded up.
333    ///
334    /// ## Year Range
335    ///
336    /// The year must be in the range **1 to 9999** (as defined by the CCSDS standard).
337    ///
338    /// ## Returns
339    ///
340    /// `(buffer, len)` where `buffer` is a fixed-size array of length
341    /// [`Dt::CCSDS_CCS_MAX_SIZE`](../struct.Dt.html#associatedconstant.CCSDS_CCS_MAX_SIZE)
342    /// and `len` is the number of bytes written.
343    ///
344    /// ## Errors
345    ///
346    /// - [`DtErrKind::FracOutOfRange`] if `n_subsec > 6`.
347    /// - [`DtErrKind::YearOutOfRange`] if the year is outside `1..=9999`.
348    ///
349    /// ## See also
350    ///
351    /// - [`Dt::from_ccsds_ccs`](../struct.Dt.html#method.from_ccsds_ccs)
352    pub fn to_ccsds_ccs(
353        &self,
354        use_doy: bool,
355        n_subsec: u8,
356    ) -> Result<([u8; Self::CCSDS_CCS_MAX_SIZE], usize), DtErr> {
357        if n_subsec > 6 {
358            return Err(an_err!(DtErrKind::FracOutOfRange));
359        }
360
361        // ── Convert to UTC civil time ─────────────────────────────────────────
362        let ymd = self.target(Scale::UTC).to_ymd();
363
364        let year = ymd.yr;
365        if !(1..=9999).contains(&year) {
366            return Err(an_err!(DtErrKind::YearOutOfRange));
367        }
368
369        let mut buf = [0u8; Self::CCSDS_CCS_MAX_SIZE];
370        let mut pos = 0usize;
371
372        // ── P-field (exactly 1 byte, no extension) ─────────────────────────────────────
373        let mut p1 = 0b0101_0000u8; // bits 6-4 = 101 (Code ID)
374        if use_doy {
375            p1 |= 0b0000_1000; // bit 3 = 1 for DOY
376        }
377        p1 |= n_subsec & 0b0000_0111; // bits 2-0 = subsecond count
378        buf[pos] = p1;
379        pos += 1;
380
381        // ── BCD encoder helper (2 decimal digits per byte) ─────────────────────────────
382        let bcd = |val: u32| -> u8 {
383            let hi = (val / 10) as u8;
384            let lo = (val % 10) as u8;
385            (hi << 4) | lo
386        };
387
388        // ── Year (4 BCD digits) ───────────────────────────────────────────────────────
389        let year = year as u32;
390        let y_hi = year / 100;
391        let y_lo = year % 100;
392        buf[pos] = bcd(y_hi);
393        buf[pos + 1] = bcd(y_lo);
394        pos += 2;
395
396        // ── Date field (Month+Day or Day-of-Year) ─────────────────────────────────────
397        if !use_doy {
398            // Month/Day variant
399            buf[pos] = bcd(ymd.mo as u32);
400            buf[pos + 1] = bcd(ymd.day as u32);
401        } else {
402            // Day-of-Year variant
403            let doy = ymd.day_of_yr() as u32;
404            buf[pos] = bcd(doy / 100);
405            buf[pos + 1] = bcd(doy % 100);
406        }
407        pos += 2;
408
409        // ── Hour / Minute / Second (BCD) ──────────────────────────────────────────────
410        buf[pos] = bcd(ymd.hr as u32);
411        buf[pos + 1] = bcd(ymd.min as u32);
412        buf[pos + 2] = bcd(ymd.sec as u32); // leap second 60 is allowed by spec
413        pos += 3;
414
415        // ── Subsecond BCD (0–12 decimal digits, 2 per byte, truncated) ─────────────────
416        if n_subsec > 0 {
417            let decimal_places = (2 * n_subsec) as u32;
418            let scale = 10u128.pow(decimal_places);
419
420            // Truncate attos to the requested decimal resolution (no rounding/carry).
421            let frac_scaled = (ymd.attos as u128 * scale) / ATTOS_PER_SEC_U128;
422
423            let mut remaining = frac_scaled;
424            for i in (0..n_subsec).rev() {
425                let pair = (remaining % 100) as u32;
426                remaining /= 100;
427                buf[pos + i as usize] = bcd(pair);
428            }
429            pos += n_subsec as usize;
430        }
431
432        Ok((buf, pos))
433    }
434
435    /// Convenience method that picks a default CCSDS binary time code from this
436    /// [`Dt`]'s **`target`** [`Scale`].
437    ///
438    /// | Condition | Code | Defaults |
439    /// |-----------|------|----------|
440    /// | `target.uses_leap_seconds()` (UTC, UtcSpice, UtcHist) | **CDS** | 2-byte day, µs-of-ms (`sub_ms_code = 1`), no P-field extension |
441    /// | otherwise (TAI, TT, GPS, …) | **CUC** | 4 coarse + 4 fine octets, no forced extension |
442    ///
443    /// For full control over field widths, call
444    /// [`Dt::to_ccsds_cds`](../struct.Dt.html#method.to_ccsds_cds) or
445    /// [`Dt::to_ccsds_cuc`](../struct.Dt.html#method.to_ccsds_cuc) directly.
446    #[inline(always)]
447    pub fn to_ccsds_bin(&self) -> Result<([u8; Self::CCSDS_C_AND_D_MAX_SIZE], usize), DtErr> {
448        if self.target.uses_leap_seconds() {
449            self.to_ccsds_cds(2, 1, false)
450        } else {
451            self.to_ccsds_cuc(4, 4, false)
452        }
453    }
454}