Skip to main content

deep_time/dt/
to_bin_ccsds.rs

1use crate::{Dt, DtErr, DtErrKind, SEC_PER_DAY_I64, Scale, an_err};
2
3impl Dt {
4    /// Maximum size needed for a CCSDS C & D (CUC) binary packet (with extended P-field).
5    pub const CCSDS_C_AND_D_MAX_SIZE: usize = 32;
6
7    /// Formats this [`Dt`] as a **CCSDS C (CUC – Unsegmented Time Code)** binary packet.
8    ///
9    /// Fully configurable for round-tripping with [`Dt::from_ccsds_cuc`].
10    /// Conforms to **CCSDS 301.0-B-4 §3.2 (Level 1)**, including full support for the
11    /// extended 2-byte P-field.
12    ///
13    /// - The time is always encoded on the **TAI** timescale (Code ID `001`).
14    /// - The `target` field of this [`Dt`] is ignored.
15    ///
16    /// ## Parameters
17    ///
18    /// - `n_coarse`: Number of bytes used for the coarse (integer) seconds since the
19    ///   1958 epoch. Must be in `1..=7`. The chosen value must be large enough to
20    ///   represent the full time; otherwise the high-order bytes are silently
21    ///   truncated. For example, a date in the year 2025 requires at least 4 bytes
22    ///   (`n_coarse >= 4`), while 3 bytes is only sufficient for dates up to roughly
23    ///   mid-1968.
24    /// - `n_frac`: Number of bytes used for the fractional seconds. Must be in `0..=10`.
25    ///   Higher values provide greater sub-second precision, but values of `8` or
26    ///   above may produce reduced accuracy when round-tripping due to internal
27    ///   128-bit integer limits.
28    /// - `extension`: If `true`, forces inclusion of the second P-field octet even
29    ///   when it is not strictly required by the field sizes.
30    ///
31    /// ## Epoch
32    ///
33    /// Seconds are counted since **1958-01-01 00:00:00 TAI**.
34    ///
35    /// ## Returns
36    ///
37    /// `(buffer, len)` where `buffer` is a fixed-size array of length
38    /// [`CCSDS_C_AND_D_MAX_SIZE`](../struct.Dt.html#associatedconstant.CCSDS_C_AND_D_MAX_SIZE) and `len` is the number of bytes written.
39    ///
40    /// ## Errors
41    ///
42    /// - [`DtErrKind::OutOfRange`] if `n_coarse` is not in `1..=7`.
43    /// - [`DtErrKind::FracOutOfRange`] if `n_frac > 10`.
44    /// - [`DtErrKind::YearOutOfRange`] if this instant is before
45    ///   **1958-01-01 00:00:00 TAI** (the CUC epoch).
46    ///
47    /// ## See also
48    ///
49    /// - [`Dt::from_ccsds_cuc`](../struct.Dt.html#method.from_ccsds_cuc)
50    pub fn to_ccsds_cuc(
51        &self,
52        n_coarse: u8,
53        n_frac: u8,
54        extension: bool,
55    ) -> Result<([u8; Self::CCSDS_C_AND_D_MAX_SIZE], usize), DtErr> {
56        if !(1..=7).contains(&n_coarse) {
57            return Err(an_err!(DtErrKind::OutOfRange));
58        } else if n_frac > 10 {
59            return Err(an_err!(DtErrKind::FracOutOfRange));
60        }
61
62        let tai_since_1958 = self
63            .target(Scale::TAI)
64            .to_scale_and_diff(Self::CCSDS_EPOCH, false);
65
66        let rem_attos = tai_since_1958.to_sec_ufrac();
67        let total_tai_seconds = tai_since_1958.to_sec64_floor();
68
69        if total_tai_seconds < 0 {
70            return Err(an_err!(DtErrKind::YearOutOfRange, "<1958"));
71        }
72
73        let frac_scaled = if n_frac == 0 {
74            0u128
75        } else {
76            let scale = 1u128 << (8 * n_frac as u32);
77            let half = scale / 2;
78            ((rem_attos as u128)
79                .saturating_mul(scale)
80                .saturating_add(half))
81                / 1_000_000_000_000_000_000
82        };
83
84        let mut buf = [0u8; Self::CCSDS_C_AND_D_MAX_SIZE];
85        let mut pos = 0usize;
86
87        // Decide whether we need the extended (2-byte) P-field.
88        // Required when coarse > 4 octets, fractional > 3 octets, or caller forces it.
89        let needs_extension = n_coarse > 4 || n_frac > 3 || extension;
90
91        // Base values that fit in the first P-field octet.
92        // Coarse: 2 bits (0-3) → actual octets = base + 1
93        // Fractional: 2 bits (0-3)
94        let base_coarse = if n_coarse <= 4 { n_coarse - 1 } else { 3 };
95        let base_frac = if n_frac <= 3 { n_frac } else { 3 };
96
97        // Build P-field octet 1
98        // Bit 7     = extension flag
99        // Bits 6-4  = Code ID (001 for CUC Level 1)
100        // Bits 3-2  = base coarse (octets - 1)
101        // Bits 1-0  = base fractional octets
102        let mut p1 = 0b0001_0000u8; // Code ID = 001
103        p1 |= (base_coarse << 2) & 0b0000_1100;
104        p1 |= base_frac & 0b0000_0011;
105        if needs_extension {
106            p1 |= 0b1000_0000;
107        }
108        buf[pos] = p1;
109        pos += 1;
110
111        if needs_extension {
112            // Build P-field octet 2 (extended P-field)
113            // Bit 7     = further extension (must be 0)
114            // Bits 6-5  = additional coarse octets (0-3)
115            // Bits 4-2  = additional fractional octets (0-7)
116            // Bits 1-0  = reserved
117            let add_coarse = n_coarse.saturating_sub(4);
118            let add_frac = n_frac.saturating_sub(3);
119
120            let mut p2 = 0u8;
121            p2 |= (add_coarse & 0b11) << 5;
122            p2 |= (add_frac & 0b111) << 2;
123            buf[pos] = p2;
124            pos += 1;
125        }
126
127        // Write coarse time (big-endian)
128        let coarse = total_tai_seconds as u64;
129        for i in (0..n_coarse).rev() {
130            buf[pos] = (coarse >> (i as u32 * 8)) as u8;
131            pos += 1;
132        }
133
134        // Write fractional time (big-endian)
135        for i in (0..n_frac).rev() {
136            buf[pos] = (frac_scaled >> (i as u32 * 8)) as u8;
137            pos += 1;
138        }
139
140        Ok((buf, pos))
141    }
142
143    /// Formats this [`Dt`] as a **CCSDS D (CDS – Day Segmented Time Code)** binary packet.
144    ///
145    /// Fully configurable for round-tripping with
146    /// [`from_ccsds_cds`](../struct.Dt.html#method.from_ccsds_cds).
147    /// Conforms to **CCSDS 301.0-B-4 §3.3 (Level 1)**.
148    ///
149    /// The time is always encoded on the **UTC** timescale (day count + milliseconds
150    /// since midnight UTC). Leap-second handling follows the library’s conversion rules.
151    ///
152    /// ## Parameters
153    ///
154    /// - `n_day`: Number of day-count octets. Must be `2` or `3`.
155    /// - `sub_ms_code`: Sub-millisecond resolution:
156    ///   - `0`: none
157    ///   - `1`: 2 bytes (microseconds within the millisecond)
158    ///   - `2`: 4 bytes (fraction of a millisecond as 2⁻³²)
159    /// - `extension`: If `true`, emits the second P-field octet.
160    ///
161    /// ## Epoch
162    ///
163    /// Day count is days since **1958-01-01 00:00:00 UTC**.
164    ///
165    /// ## Returns
166    ///
167    /// `(buffer, len)` where `buffer` is a fixed-size array of length
168    /// [`CCSDS_C_AND_D_MAX_SIZE`](../struct.Dt.html#associatedconstant.CCSDS_C_AND_D_MAX_SIZE) and `len` is the number of bytes written.
169    ///
170    /// ## Errors
171    ///
172    /// - [`DtErrKind::InvalidNumber`] if `n_day` is not `2` or `3`.
173    /// - [`DtErrKind::InvalidSubmillisecond`] if `sub_ms_code` is not in `0..=2`.
174    /// - [`DtErrKind::YearOutOfRange`] if this instant is before
175    ///   **1958-01-01 00:00:00 UTC** (the CDS Level 1 epoch).
176    ///
177    /// ## See also
178    ///
179    /// - [`Dt::from_ccsds_cds`](../struct.Dt.html#method.from_ccsds_cds)
180    pub fn to_ccsds_cds(
181        &self,
182        n_day: u8,
183        sub_ms_code: u8,
184        extension: bool,
185    ) -> Result<([u8; Self::CCSDS_C_AND_D_MAX_SIZE], usize), DtErr> {
186        if !matches!(n_day, 2 | 3) {
187            return Err(an_err!(DtErrKind::InvalidNumber, "n_day: {}", n_day));
188        } else if !matches!(sub_ms_code, 0..=2) {
189            return Err(an_err!(DtErrKind::InvalidSubmillisecond));
190        }
191
192        let utc_since_1958 = self
193            .target(Scale::UTC)
194            .to_scale_and_diff(Self::CCSDS_EPOCH, false);
195
196        let rem_attos = utc_since_1958.to_sec_ufrac();
197        let total_utc_seconds = utc_since_1958.to_sec64_floor();
198
199        if total_utc_seconds < 0 {
200            return Err(an_err!(DtErrKind::YearOutOfRange, "<1958"));
201        }
202
203        let day_count = (total_utc_seconds / SEC_PER_DAY_I64) as u64;
204        let sec_of_day = (total_utc_seconds % SEC_PER_DAY_I64) as u64;
205
206        // Round to nearest millisecond
207        let additional_ms =
208            ((rem_attos as u128 + 500_000_000_000_000) / 1_000_000_000_000_000) as u64;
209
210        let millis_of_day = sec_of_day * 1000 + additional_ms;
211
212        // Remaining attoseconds inside the current millisecond
213        let remaining_attos_in_ms = (rem_attos as u128) % 1_000_000_000_000_000;
214
215        let frac_scaled = match sub_ms_code {
216            0 => 0u64,
217            1 => ((remaining_attos_in_ms * 65_536u128) / 1_000_000_000_000_000u128) as u64,
218            2 => {
219                const PS_SCALE: u128 = 1u128 << 32;
220                ((remaining_attos_in_ms * PS_SCALE) / 1_000_000_000_000_000u128) as u64
221            }
222            _ => unreachable!(),
223        };
224
225        let mut buf = [0u8; Self::CCSDS_C_AND_D_MAX_SIZE];
226        let mut pos = 0usize;
227
228        let mut p1 = 0b0100_0000u8;
229        if extension {
230            p1 |= 0b1000_0000;
231        }
232        if n_day == 3 {
233            p1 |= 0b0000_0100;
234        }
235        p1 |= sub_ms_code;
236        buf[pos] = p1;
237        pos += 1;
238
239        if extension {
240            buf[pos] = 0;
241            pos += 1;
242        }
243
244        for i in (0..n_day).rev() {
245            buf[pos] = (day_count >> (i * 8)) as u8;
246            pos += 1;
247        }
248
249        for i in (0..4).rev() {
250            buf[pos] = (millis_of_day >> (i * 8)) as u8;
251            pos += 1;
252        }
253
254        let n_frac = match sub_ms_code {
255            0 => 0,
256            1 => 2,
257            2 => 4,
258            _ => unreachable!(),
259        };
260        for i in (0..n_frac).rev() {
261            buf[pos] = (frac_scaled >> (i * 8)) as u8;
262            pos += 1;
263        }
264
265        Ok((buf, pos))
266    }
267
268    /// Maximum size needed for a CCSDS CCS binary packet (P-field + T-field).
269    pub const CCSDS_CCS_MAX_SIZE: usize = 14; // 1 + 2(year) + 2(date) + 3(HMS) + 6(subsec)
270
271    /// Formats this [`Dt`] as a **CCSDS CCS (Calendar Segmented Time Code)** binary packet.
272    ///
273    /// Fully configurable for round-tripping with
274    /// [`from_ccsds_ccs`](../struct.Dt.html#method.from_ccsds_ccs).
275    /// Conforms to **CCSDS 301.0-B-4 §3.4** (Level 1 only).
276    ///
277    /// Both CCS variants are **UTC-based** and use BCD encoding.
278    /// Leap seconds are supported (`second = 60`).
279    ///
280    /// ## Parameters
281    ///
282    /// - `use_doy`: `false` = Month/Day variant (most common), `true` = Day-of-Year variant.
283    /// - `n_subsec`: Number of subsecond BCD octets (`0`–`6`). Each octet holds two decimal digits
284    ///   (so `n_subsec = 6` gives up to 12 decimal digits of subsecond precision).
285    ///
286    /// ## Year Range
287    ///
288    /// The year must be in the range **1 to 9999** (as defined by the CCSDS standard).
289    ///
290    /// ## Returns
291    ///
292    /// `(buffer, len)` where `buffer` is a fixed-size array of length
293    /// [`CCSDS_CCS_MAX_SIZE`](../struct.Dt.html#associatedconstant.CCSDS_CCS_MAX_SIZE) and `len` is the number of bytes written.
294    ///
295    /// ## Errors
296    ///
297    /// - [`DtErrKind::FracOutOfRange`] if `n_subsec > 6`.
298    /// - [`DtErrKind::YearOutOfRange`] if the year is outside `1..=9999`.
299    ///
300    /// ## See also
301    ///
302    /// - [`Dt::from_ccsds_ccs`](../struct.Dt.html#method.from_ccsds_ccs)
303    pub fn to_ccsds_ccs(
304        &self,
305        use_doy: bool,
306        n_subsec: u8,
307    ) -> Result<([u8; Self::CCSDS_CCS_MAX_SIZE], usize), DtErr> {
308        if n_subsec > 6 {
309            return Err(an_err!(DtErrKind::FracOutOfRange));
310        }
311
312        // ── Convert to UTC civil time (CCS uses the same 1958-01-01 UTC epoch as CDS) ─────
313        let ymd = self.target(Scale::UTC).to_ymd();
314
315        let year = ymd.yr;
316        if !(1..=9999).contains(&year) {
317            return Err(an_err!(DtErrKind::YearOutOfRange));
318        }
319
320        let mut buf = [0u8; Self::CCSDS_CCS_MAX_SIZE];
321        let mut pos = 0usize;
322
323        // ── P-field (exactly 1 byte, no extension) ─────────────────────────────────────
324        let mut p1 = 0b0101_0000u8; // bits 6-4 = 101 (Code ID)
325        if use_doy {
326            p1 |= 0b0000_1000; // bit 3 = 1 for DOY
327        }
328        p1 |= n_subsec & 0b0000_0111; // bits 2-0 = subsecond count
329        buf[pos] = p1;
330        pos += 1;
331
332        // ── BCD encoder helper (2 decimal digits per byte) ─────────────────────────────
333        let bcd = |val: u32| -> u8 {
334            let hi = (val / 10) as u8;
335            let lo = (val % 10) as u8;
336            (hi << 4) | lo
337        };
338
339        // ── Year (4 BCD digits) ───────────────────────────────────────────────────────
340        let year = year as u32;
341        let y_hi = year / 100;
342        let y_lo = year % 100;
343        buf[pos] = bcd(y_hi);
344        buf[pos + 1] = bcd(y_lo);
345        pos += 2;
346
347        // ── Date field (Month+Day or Day-of-Year) ─────────────────────────────────────
348        if !use_doy {
349            // Month/Day variant
350            buf[pos] = bcd(ymd.mo as u32);
351            buf[pos + 1] = bcd(ymd.day as u32);
352        } else {
353            // Day-of-Year variant
354            let doy = ymd.day_of_yr() as u32;
355            buf[pos] = bcd(doy / 100);
356            buf[pos + 1] = bcd(doy % 100);
357        }
358        pos += 2;
359
360        // ── Hour / Minute / Second (BCD) ──────────────────────────────────────────────
361        buf[pos] = bcd(ymd.hr as u32);
362        buf[pos + 1] = bcd(ymd.min as u32);
363        buf[pos + 2] = bcd(ymd.sec as u32); // leap second 60 is allowed by spec
364        pos += 3;
365
366        // ── Subsecond BCD (0–12 decimal digits, 2 per byte, rounded) ──────────────────
367        if n_subsec > 0 {
368            let decimal_places = (2 * n_subsec) as u32;
369            let scale = 10u128.pow(decimal_places);
370
371            // Round attos to nearest representable value at this precision
372            let frac_scaled =
373                (ymd.attos as u128 * scale + 500_000_000_000_000_000) / 1_000_000_000_000_000_000;
374
375            let mut remaining = frac_scaled;
376            for i in (0..n_subsec).rev() {
377                let pair = (remaining % 100) as u32;
378                remaining /= 100;
379                buf[pos + i as usize] = bcd(pair);
380            }
381            pos += n_subsec as usize;
382        }
383
384        Ok((buf, pos))
385    }
386
387    /// Convenience method that automatically selects the most appropriate
388    /// CCSDS binary time code based on this [`Dt`]'s `target` time [`Scale`].
389    ///
390    /// - If the `target` [`Scale`] **uses leap seconds** then **ccsds_cds is chosen**.
391    /// - Otherwise ccsds_cuc is chosen.
392    #[inline(always)]
393    pub fn to_ccsds_bin(&self) -> Result<([u8; Self::CCSDS_C_AND_D_MAX_SIZE], usize), DtErr> {
394        if self.target.uses_leap_seconds() {
395            self.to_ccsds_cds(2, 1, false)
396        } else {
397            self.to_ccsds_cuc(4, 4, false)
398        }
399    }
400}