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 – 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();
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 [`from_ccsds_cds`](Self::from_ccsds_cds).
146 /// Conforms to **CCSDS 301.0-B-4 §3.3 (Level 1)**.
147 ///
148 /// The time is always encoded on the **UTC** timescale (day count + milliseconds
149 /// since midnight UTC). Leap-second handling follows the library’s conversion rules.
150 ///
151 /// ## Parameters
152 ///
153 /// - `n_day`: Number of day-count octets. Must be `2` or `3`.
154 /// - `sub_ms_code`: Sub-millisecond resolution:
155 /// - `0`: none
156 /// - `1`: 2 bytes (microseconds within the millisecond)
157 /// - `2`: 4 bytes (fraction of a millisecond as 2⁻³²)
158 /// - `extension`: If `true`, emits the second P-field octet.
159 ///
160 /// ## Epoch
161 ///
162 /// Day count is days since **1958-01-01 00:00:00 UTC**.
163 ///
164 /// ## Returns
165 ///
166 /// `(buffer, len)` where `buffer` is a fixed-size array of length
167 /// [`CCSDS_C_AND_D_MAX_SIZE`](../struct.Dt.html#associatedconstant.CCSDS_C_AND_D_MAX_SIZE) and `len` is the number of bytes written.
168 ///
169 /// ## Errors
170 ///
171 /// - [`DtErrKind::InvalidNumber`] if `n_day` is not `2` or `3`.
172 /// - [`DtErrKind::InvalidSubmillisecond`] if `sub_ms_code` is not in `0..=2`.
173 /// - [`DtErrKind::YearOutOfRange`] if this instant is before
174 /// **1958-01-01 00:00:00 UTC** (the CDS Level 1 epoch).
175 ///
176 /// ## See also
177 ///
178 /// - [`Dt::from_ccsds_cds`](../struct.Dt.html#method.from_ccsds_cds)
179 pub fn to_ccsds_cds(
180 &self,
181 n_day: u8,
182 sub_ms_code: u8,
183 extension: bool,
184 ) -> Result<([u8; Self::CCSDS_C_AND_D_MAX_SIZE], usize), DtErr> {
185 if !matches!(n_day, 2 | 3) {
186 return Err(an_err!(DtErrKind::InvalidNumber, "n_day: {}", n_day));
187 } else if !matches!(sub_ms_code, 0..=2) {
188 return Err(an_err!(DtErrKind::InvalidSubmillisecond));
189 }
190
191 let utc_since_1958 = self
192 .target(Scale::UTC)
193 .to_scale_and_diff(Self::CCSDS_EPOCH, false);
194
195 let rem_attos = utc_since_1958.to_sec_ufrac();
196 let total_utc_seconds = utc_since_1958.to_sec64();
197
198 if total_utc_seconds < 0 {
199 return Err(an_err!(DtErrKind::YearOutOfRange, "<1958"));
200 }
201
202 let day_count = (total_utc_seconds / SEC_PER_DAYI64) as u64;
203 let sec_of_day = (total_utc_seconds % SEC_PER_DAYI64) as u64;
204
205 // Round to nearest millisecond
206 let additional_ms =
207 ((rem_attos as u128 + 500_000_000_000_000) / 1_000_000_000_000_000) as u64;
208
209 let millis_of_day = sec_of_day * 1000 + additional_ms;
210
211 // Remaining attoseconds inside the current millisecond
212 let remaining_attos_in_ms = (rem_attos as u128) % 1_000_000_000_000_000;
213
214 let frac_scaled = match sub_ms_code {
215 0 => 0u64,
216 1 => ((remaining_attos_in_ms * 65_536u128) / 1_000_000_000_000_000u128) as u64,
217 2 => {
218 const PS_SCALE: u128 = 1u128 << 32;
219 ((remaining_attos_in_ms * PS_SCALE) / 1_000_000_000_000_000u128) as u64
220 }
221 _ => unreachable!(),
222 };
223
224 let mut buf = [0u8; Self::CCSDS_C_AND_D_MAX_SIZE];
225 let mut pos = 0usize;
226
227 let mut p1 = 0b0100_0000u8;
228 if extension {
229 p1 |= 0b1000_0000;
230 }
231 if n_day == 3 {
232 p1 |= 0b0000_0100;
233 }
234 p1 |= sub_ms_code;
235 buf[pos] = p1;
236 pos += 1;
237
238 if extension {
239 buf[pos] = 0;
240 pos += 1;
241 }
242
243 for i in (0..n_day).rev() {
244 buf[pos] = (day_count >> (i * 8)) as u8;
245 pos += 1;
246 }
247
248 for i in (0..4).rev() {
249 buf[pos] = (millis_of_day >> (i * 8)) as u8;
250 pos += 1;
251 }
252
253 let n_frac = match sub_ms_code {
254 0 => 0,
255 1 => 2,
256 2 => 4,
257 _ => unreachable!(),
258 };
259 for i in (0..n_frac).rev() {
260 buf[pos] = (frac_scaled >> (i * 8)) as u8;
261 pos += 1;
262 }
263
264 Ok((buf, pos))
265 }
266
267 /// Maximum size needed for a CCSDS CCS binary packet (P-field + T-field).
268 pub const CCSDS_CCS_MAX_SIZE: usize = 14; // 1 + 2(year) + 2(date) + 3(HMS) + 6(subsec)
269
270 /// Formats this [`Dt`] as a **CCSDS CCS (Calendar Segmented Time Code)** binary packet.
271 ///
272 /// Fully configurable for round-tripping with [`from_ccsds_ccs`](Self::from_ccsds_ccs).
273 /// Conforms to **CCSDS 301.0-B-4 §3.4** (Level 1 only).
274 ///
275 /// Both CCS variants are **UTC-based** and use BCD encoding.
276 /// Leap seconds are supported (`second = 60`).
277 ///
278 /// ## Parameters
279 ///
280 /// - `use_doy`: `false` = Month/Day variant (most common), `true` = Day-of-Year variant.
281 /// - `n_subsec`: Number of subsecond BCD octets (`0`–`6`). Each octet holds two decimal digits
282 /// (so `n_subsec = 6` gives up to 12 decimal digits of subsecond precision).
283 ///
284 /// ## Year Range
285 ///
286 /// The year must be in the range **1 to 9999** (as defined by the CCSDS standard).
287 ///
288 /// ## Returns
289 ///
290 /// `(buffer, len)` where `buffer` is a fixed-size array of length
291 /// [`CCSDS_CCS_MAX_SIZE`](../struct.Dt.html#associatedconstant.CCSDS_CCS_MAX_SIZE) and `len` is the number of bytes written.
292 ///
293 /// ## Errors
294 ///
295 /// - [`DtErrKind::FracOutOfRange`] if `n_subsec > 6`.
296 /// - [`DtErrKind::YearOutOfRange`] if the year is outside `1..=9999`.
297 ///
298 /// ## See also
299 ///
300 /// - [`Dt::from_ccsds_ccs`](../struct.Dt.html#method.from_ccsds_ccs)
301 pub fn to_ccsds_ccs(
302 &self,
303 use_doy: bool,
304 n_subsec: u8,
305 ) -> Result<([u8; Self::CCSDS_CCS_MAX_SIZE], usize), DtErr> {
306 if n_subsec > 6 {
307 return Err(an_err!(DtErrKind::FracOutOfRange));
308 }
309
310 // ── Convert to UTC civil time (CCS uses the same 1958-01-01 UTC epoch as CDS) ─────
311 let ymd = self.target(Scale::UTC).to_ymd();
312
313 let year = ymd.yr;
314 if !(1..=9999).contains(&year) {
315 return Err(an_err!(DtErrKind::YearOutOfRange));
316 }
317
318 let mut buf = [0u8; Self::CCSDS_CCS_MAX_SIZE];
319 let mut pos = 0usize;
320
321 // ── P-field (exactly 1 byte, no extension) ─────────────────────────────────────
322 let mut p1 = 0b0101_0000u8; // bits 6-4 = 101 (Code ID)
323 if use_doy {
324 p1 |= 0b0000_1000; // bit 3 = 1 for DOY
325 }
326 p1 |= n_subsec & 0b0000_0111; // bits 2-0 = subsecond count
327 buf[pos] = p1;
328 pos += 1;
329
330 // ── BCD encoder helper (2 decimal digits per byte) ─────────────────────────────
331 let bcd = |val: u32| -> u8 {
332 let hi = (val / 10) as u8;
333 let lo = (val % 10) as u8;
334 (hi << 4) | lo
335 };
336
337 // ── Year (4 BCD digits) ───────────────────────────────────────────────────────
338 let year = year as u32;
339 let y_hi = year / 100;
340 let y_lo = year % 100;
341 buf[pos] = bcd(y_hi);
342 buf[pos + 1] = bcd(y_lo);
343 pos += 2;
344
345 // ── Date field (Month+Day or Day-of-Year) ─────────────────────────────────────
346 if !use_doy {
347 // Month/Day variant
348 buf[pos] = bcd(ymd.mo as u32);
349 buf[pos + 1] = bcd(ymd.day as u32);
350 } else {
351 // Day-of-Year variant
352 let doy = ymd.day_of_yr() as u32;
353 buf[pos] = bcd(doy / 100);
354 buf[pos + 1] = bcd(doy % 100);
355 }
356 pos += 2;
357
358 // ── Hour / Minute / Second (BCD) ──────────────────────────────────────────────
359 buf[pos] = bcd(ymd.hr as u32);
360 buf[pos + 1] = bcd(ymd.min as u32);
361 buf[pos + 2] = bcd(ymd.sec as u32); // leap second 60 is allowed by spec
362 pos += 3;
363
364 // ── Subsecond BCD (0–12 decimal digits, 2 per byte, rounded) ──────────────────
365 if n_subsec > 0 {
366 let decimal_places = (2 * n_subsec) as u32;
367 let scale = 10u128.pow(decimal_places);
368
369 // Round attos to nearest representable value at this precision
370 let frac_scaled =
371 (ymd.attos as u128 * scale + 500_000_000_000_000_000) / 1_000_000_000_000_000_000;
372
373 let mut remaining = frac_scaled;
374 for i in (0..n_subsec).rev() {
375 let pair = (remaining % 100) as u32;
376 remaining /= 100;
377 buf[pos + i as usize] = bcd(pair);
378 }
379 pos += n_subsec as usize;
380 }
381
382 Ok((buf, pos))
383 }
384
385 /// Convenience method that automatically selects the most appropriate
386 /// CCSDS binary time code based on this [`Dt`]'s `target` time [`Scale`].
387 ///
388 /// - If the `target` [`Scale`] **uses leap seconds** then **ccsds_cds is chosen**.
389 /// - Otherwise ccsds_cuc is chosen.
390 #[inline(always)]
391 pub fn to_ccsds_bin(&self) -> Result<([u8; Self::CCSDS_C_AND_D_MAX_SIZE], usize), DtErr> {
392 if self.target.uses_leap_seconds() {
393 self.to_ccsds_cds(2, 1, false)
394 } else {
395 self.to_ccsds_cuc(4, 4, false)
396 }
397 }
398}