Skip to main content

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