Skip to main content

deep_time/dt/
to_bin_ccsds.rs

1use crate::{Dt, DtErr, DtErrKind, SEC_PER_DAYI64, 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)** binary time code.
8    ///
9    /// Fully configurable for round-tripping with [`from_ccsds_c`].
10    /// Conforms to **CCSDS 301.0-B-4 §3.2 (Level 1)**, including full support for the
11    /// extended P-field (second octet) when `n_coarse > 4` or `n_frac > 3`.
12    ///
13    /// ## Parameters
14    ///
15    /// - `n_coarse`: 1–7 (number of coarse-time octets)
16    /// - `n_frac`:   0–10 (number of fractional octets)
17    /// - `extension`: advisory flag (ignored when larger sizes force the second octet)
18    pub fn to_ccsds_c(
19        &self,
20        current: Scale,
21        n_coarse: u8,
22        n_frac: u8,
23        extension: bool,
24    ) -> Result<([u8; Self::CCSDS_C_AND_D_MAX_SIZE], usize), DtErr> {
25        if !(1..=7).contains(&n_coarse) {
26            return Err(an_err!(DtErrKind::OutOfRange, "coarse: {}", n_coarse,));
27        } else if n_frac > 10 {
28            return Err(an_err!(DtErrKind::OutOfRange, "frac: {}", n_frac));
29        }
30
31        let tai = self.to(current, Scale::TAI);
32
33        const EPOCH_OFFSET: i64 = 1_325_419_167;
34        let total_tai_seconds = tai.sec + EPOCH_OFFSET;
35
36        let frac_scaled = if n_frac == 0 {
37            0u128
38        } else {
39            let scale = 1u128 << (8 * n_frac as u32);
40            (tai.attos as u128 * scale + 500_000_000_000_000_000) / 1_000_000_000_000_000_000
41        };
42
43        let mut buf = [0u8; Self::CCSDS_C_AND_D_MAX_SIZE];
44        let mut pos = 0usize;
45
46        // Decide whether extension byte is needed
47        let needs_extension = n_coarse > 4 || n_frac > 3 || extension;
48
49        // Base values for Octet 1
50        let base_coarse = if n_coarse <= 4 { n_coarse - 1 } else { 3 };
51        let base_frac = if n_frac <= 3 { n_frac } else { 3 };
52
53        // ── Build P-field Octet 1 ─────────────────────────────
54        let mut p1 = 0b0001_0000u8; // Code ID = 001
55        p1 |= (base_coarse << 2) & 0b0000_1100;
56        p1 |= base_frac & 0b0000_0011;
57        if needs_extension {
58            p1 |= 0b1000_0000;
59        }
60        buf[pos] = p1;
61        pos += 1;
62
63        if needs_extension {
64            // ── Build P-field Octet 2 ─────────────────────────────
65            let add_coarse = n_coarse.saturating_sub(4); // 0–3
66            let add_frac = n_frac.saturating_sub(3); // 0–7
67
68            let mut p2 = 0u8;
69            p2 |= (add_coarse & 0b11) << 5; // spec Bits 1-2 → u8 bits 6-5
70            p2 |= (add_frac & 0b111) << 2; // spec Bits 3-5 → u8 bits 4-2
71            // Bit 0 (further extension) = 0
72            // Bits 6-7 reserved = 0
73            buf[pos] = p2;
74            pos += 1;
75        }
76
77        // ── Coarse time (big-endian) ─────────────────────────────
78        let coarse = total_tai_seconds as u64;
79        for i in (0..n_coarse).rev() {
80            buf[pos] = (coarse >> (i as u32 * 8)) as u8;
81            pos += 1;
82        }
83
84        // ── Fractional time (big-endian) ─────────────────────────────
85        for i in (0..n_frac).rev() {
86            buf[pos] = (frac_scaled >> (i as u32 * 8)) as u8;
87            pos += 1;
88        }
89
90        Ok((buf, pos))
91    }
92
93    /// Formats this [`Dt`] as a **CCSDS D (CDS)** binary time code.
94    ///
95    /// - Fully configurable for round-tripping with [`from_ccsds_d`].
96    /// - Conforms to CCSDS 301.0-B-4 §3.3 (Level 1): UTC day count + ms-of-day
97    ///   since 1958-01-01 UTC.
98    pub fn to_ccsds_d(
99        &self,
100        current: Scale,
101        n_day: u8,
102        sub_ms_code: u8,
103        extension: bool,
104    ) -> Result<([u8; Self::CCSDS_C_AND_D_MAX_SIZE], usize), DtErr> {
105        if !matches!(n_day, 2 | 3) {
106            return Err(an_err!(DtErrKind::InvalidNumber, "n_day: {}", n_day));
107        } else if !matches!(sub_ms_code, 0..=2) {
108            return Err(an_err!(DtErrKind::InvalidItem, "sub-millisecond code"));
109        }
110
111        let utc = self.to(current, Scale::UTC);
112
113        // UTC seconds since 1958-01-01 00:00:00 UTC (exact offset to library UTC zero,
114        // accounting for all leap seconds up to the library epoch)
115        const EPOCH_OFFSET: i64 = 1_325_419_135;
116        let total_utc_seconds = utc.sec + EPOCH_OFFSET;
117
118        let day_count = (total_utc_seconds / SEC_PER_DAYI64) as u64;
119        let sec_of_day = (total_utc_seconds % SEC_PER_DAYI64) as u64;
120
121        // Round to nearest millisecond (CORRECT 10^15 scaling)
122        let additional_ms =
123            ((utc.attos as u128 + 500_000_000_000_000) / 1_000_000_000_000_000) as u64;
124        let millis_of_day = sec_of_day * 1000 + additional_ms;
125
126        // Remaining attoseconds inside the current millisecond
127        let remaining_attos_in_ms = (utc.attos as u128) % 1_000_000_000_000_000;
128
129        let frac_scaled = match sub_ms_code {
130            0 => 0u64,
131            1 => ((remaining_attos_in_ms * 65_536u128) / 1_000_000_000_000_000u128) as u64,
132            2 => {
133                const PS_SCALE: u128 = 1u128 << 32;
134                ((remaining_attos_in_ms * PS_SCALE) / 1_000_000_000_000_000u128) as u64
135            }
136            _ => return Err(an_err!(DtErrKind::InvalidItem, "sub-millisecond code")),
137        };
138
139        let mut buf = [0u8; Self::CCSDS_C_AND_D_MAX_SIZE];
140        let mut pos = 0usize;
141
142        let mut p1 = 0b0100_0000u8;
143        if extension {
144            p1 |= 0b1000_0000;
145        }
146        if n_day == 3 {
147            p1 |= 0b0000_0100;
148        }
149        p1 |= sub_ms_code;
150        buf[pos] = p1;
151        pos += 1;
152
153        if extension {
154            buf[pos] = 0;
155            pos += 1;
156        }
157
158        for i in (0..n_day).rev() {
159            buf[pos] = (day_count >> (i * 8)) as u8;
160            pos += 1;
161        }
162
163        for i in (0..4).rev() {
164            buf[pos] = (millis_of_day >> (i * 8)) as u8;
165            pos += 1;
166        }
167
168        let n_frac = match sub_ms_code {
169            0 => 0,
170            1 => 2,
171            2 => 4,
172            _ => return Err(an_err!(DtErrKind::InvalidItem, "sub-millisecond code")),
173        };
174        for i in (0..n_frac).rev() {
175            buf[pos] = (frac_scaled >> (i * 8)) as u8;
176            pos += 1;
177        }
178
179        Ok((buf, pos))
180    }
181
182    /// Maximum size needed for a CCSDS CCS binary packet (P-field + T-field).
183    pub const CCSDS_CCS_MAX_SIZE: usize = 14; // 1 + 2(year) + 2(date) + 3(HMS) + 6(subsec)
184
185    /// Formats this [`Dt`] as a **CCSDS CCS (Calendar Segmented Time Code)**.
186    ///
187    /// Implements **CCSDS 301.0-B-4 §3.4** (Level 1 only).
188    ///
189    /// ## Parameters
190    ///
191    /// - `use_doy`: `false` = Month/Day variant (most common), `true` = Day-of-Year variant
192    /// - `n_subsec`: Number of subsecond BCD octets (`0`–`6`). Each octet holds 2 decimal digits.
193    ///
194    /// ## Returns
195    ///
196    /// `(buffer, written_len)` — the P-field + T-field (big-endian BCD).
197    ///
198    /// ## Precision & Rounding
199    ///
200    /// Fractional seconds are rounded to the nearest representable value at the chosen precision
201    /// (exactly as `to_ccsds_d` does for milliseconds).
202    pub fn to_ccsds_ccs(
203        &self,
204        current: Scale,
205        use_doy: bool,
206        n_subsec: u8,
207    ) -> Result<([u8; Self::CCSDS_CCS_MAX_SIZE], usize), DtErr> {
208        if n_subsec > 6 {
209            return Err(an_err!(DtErrKind::OutOfRange, "n_subsec: {}", n_subsec));
210        }
211
212        // ── Convert to UTC civil time (CCS uses the same 1958-01-01 UTC epoch as CDS) ─────
213        let gt = if current.uses_leap_seconds() {
214            self.to_ymdhms_rich_on(current, current)
215        } else {
216            let utc = self.to(current, Scale::UTC);
217            utc.to_ymdhms_rich_on(Scale::UTC, Scale::UTC)
218        };
219
220        let mut buf = [0u8; Self::CCSDS_CCS_MAX_SIZE];
221        let mut pos = 0usize;
222
223        // ── P-field (exactly 1 byte, no extension) ─────────────────────────────────────
224        let mut p1 = 0b0101_0000u8; // bits 6-4 = 101 (Code ID)
225        if use_doy {
226            p1 |= 0b0000_1000; // bit 3 = 1 for DOY
227        }
228        p1 |= n_subsec & 0b0000_0111; // bits 2-0 = subsecond count
229        buf[pos] = p1;
230        pos += 1;
231
232        // ── BCD encoder helper (2 decimal digits per byte) ─────────────────────────────
233        let bcd = |val: u32| -> u8 {
234            let hi = (val / 10) as u8;
235            let lo = (val % 10) as u8;
236            (hi << 4) | lo
237        };
238
239        // ── Year (4 BCD digits) ───────────────────────────────────────────────────────
240        let year = gt.yr as u32;
241        let y_hi = year / 100;
242        let y_lo = year % 100;
243        buf[pos] = bcd(y_hi);
244        buf[pos + 1] = bcd(y_lo);
245        pos += 2;
246
247        // ── Date field (Month+Day or Day-of-Year) ─────────────────────────────────────
248        if !use_doy {
249            // Month/Day variant
250            buf[pos] = bcd(gt.mo as u32);
251            buf[pos + 1] = bcd(gt.day as u32);
252        } else {
253            // Day-of-Year variant (high nibble of first byte is always 0)
254            let doy = gt.day_of_yr as u32;
255            buf[pos] = bcd(doy / 100); // high byte = 00–03 (but only 0-3 used)
256            buf[pos + 1] = bcd(doy % 100);
257        }
258        pos += 2;
259
260        // ── Hour / Minute / Second (BCD) ──────────────────────────────────────────────
261        buf[pos] = bcd(gt.hr as u32);
262        buf[pos + 1] = bcd(gt.min as u32);
263        buf[pos + 2] = bcd(gt.sec as u32); // leap second 60 is allowed by spec
264        pos += 3;
265
266        // ── Subsecond BCD (0–12 decimal digits, 2 per byte, rounded) ──────────────────
267        if n_subsec > 0 {
268            let decimal_places = (2 * n_subsec) as u32;
269            let scale = 10u128.pow(decimal_places);
270
271            // Round attos to nearest representable value at this precision
272            let frac_scaled =
273                (gt.attos as u128 * scale + 500_000_000_000_000_000) / 1_000_000_000_000_000_000;
274
275            let mut remaining = frac_scaled;
276            for i in (0..n_subsec).rev() {
277                let pair = (remaining % 100) as u32;
278                remaining /= 100;
279                buf[pos + i as usize] = bcd(pair);
280            }
281            pos += n_subsec as usize;
282        }
283
284        Ok((buf, pos))
285    }
286
287    /// Convenience method that automatically selects the most appropriate
288    /// CCSDS binary time code based on `current` [`Scale`].
289    ///
290    /// - If the `current` [`Scale`] **uses leap seconds** then **ccsds_d is chosen**.
291    /// - Otherwise ccsds_c is chosen.
292    #[inline]
293    pub fn to_ccsds_bin(
294        &self,
295        current: Scale,
296    ) -> Result<([u8; Self::CCSDS_C_AND_D_MAX_SIZE], usize), DtErr> {
297        if current.uses_leap_seconds() {
298            self.to_ccsds_d(current, 2, 1, false)
299        } else {
300            self.to_ccsds_c(current, 4, 4, false)
301        }
302    }
303}