deep_time/time_parts/from_bin_ccsds.rs
1use crate::{Dt, DtErr, DtErrKind, Scale, TimeParts, an_err};
2
3impl TimeParts {
4 /// Parses a CCSDS Calendar Segmented Time Code (CCS) into [`TimeParts`].
5 ///
6 /// Implements **CCSDS 301.0-B-4 §3.4 (Level 1 only)**.
7 ///
8 /// This function accepts a single-byte P-field followed by a BCD-encoded T-field.
9 /// It supports both calendar variants:
10 /// - Month/Day format (most common)
11 /// - Day-of-Year format
12 ///
13 /// ## P-field
14 ///
15 /// - Must not have the extension bit set (only 1-byte P-fields are supported).
16 /// - Code ID must be `101`.
17 /// - Subsecond resolution: 0 to 6 BCD octets (0–12 decimal digits).
18 ///
19 /// ## T-field
20 ///
21 /// - Year is encoded as 4 BCD digits (0001–9999).
22 /// - Time of day uses BCD with leap second support (`second == 60`).
23 /// - When a leap second is present, `second` is normalized to 59 and
24 /// `is_leap_second` is set to `true` in the returned [`TimeParts`].
25 ///
26 /// ## Epoch
27 ///
28 /// 1958-01-01 00:00:00 UTC (identical to CDS).
29 ///
30 /// ## Errors
31 ///
32 /// Returns an error if the P-field is extended, the Code ID is wrong,
33 /// BCD digits are invalid, field lengths are insufficient, or any
34 /// component (month, day, DOY, hour, minute, second) is out of range.
35 ///
36 /// The resulting [`TimeParts`] has `scale = UTC`.
37 pub fn from_ccsds_ccs(input: &[u8]) -> Result<TimeParts, DtErr> {
38 if input.is_empty() {
39 return Err(an_err!(DtErrKind::Incomplete, "empty"));
40 }
41
42 let p1 = input[0];
43 let mut idx = 1usize;
44
45 if (p1 & 0b1000_0000) != 0 {
46 return Err(an_err!(
47 DtErrKind::InvalidInput,
48 "p-field ext. not supported"
49 ));
50 }
51
52 let code_id = (p1 >> 4) & 0b0111;
53 if code_id != 0b101 {
54 return Err(an_err!(DtErrKind::InvalidItem, "code id"));
55 }
56
57 let is_doy = ((p1 >> 3) & 1) != 0;
58 let n_subsec = (p1 & 0b0000_0111) as usize;
59
60 if n_subsec > 6 {
61 return Err(an_err!(DtErrKind::InvalidItem, "subsecond count"));
62 }
63
64 let min_len = 1 + 2 + 2 + 3 + n_subsec;
65 if input.len() < min_len {
66 return Err(an_err!(DtErrKind::InvalidSyntax, "t-field too short"));
67 }
68
69 let bcd_byte = |b: u8| -> Result<u8, DtErr> {
70 let hi = b >> 4;
71 let lo = b & 0x0F;
72 if hi > 9 || lo > 9 {
73 Err(an_err!(DtErrKind::InvalidBytes, "invalid bcd digit"))
74 } else {
75 Ok(hi * 10 + lo)
76 }
77 };
78
79 // Year
80 let y1 = bcd_byte(input[idx])?;
81 let y2 = bcd_byte(input[idx + 1])?;
82 let year = (y1 as i64) * 100 + (y2 as i64);
83 idx += 2;
84
85 // Date field
86 let (month, day, day_of_year) = if !is_doy {
87 let mo = bcd_byte(input[idx])?;
88 let d = bcd_byte(input[idx + 1])?;
89 idx += 2;
90
91 if !(1..=12).contains(&mo) {
92 return Err(an_err!(DtErrKind::OutOfRange, "month"));
93 }
94 if !(1..=31).contains(&d) {
95 return Err(an_err!(DtErrKind::OutOfRange, "day"));
96 }
97 (Some(mo), Some(d), None)
98 } else {
99 let d1 = bcd_byte(input[idx])?;
100 let d2 = bcd_byte(input[idx + 1])?;
101 idx += 2;
102 let doy = (d1 as u16) * 100 + (d2 as u16);
103
104 if doy == 0 || doy > 366 || (doy == 366 && !Dt::is_leap_yr(year)) {
105 return Err(an_err!(DtErrKind::OutOfRange, "day of year"));
106 }
107 (None, None, Some(doy))
108 };
109
110 // Time
111 let hour = bcd_byte(input[idx])?;
112 let minute = bcd_byte(input[idx + 1])?;
113 let mut second = bcd_byte(input[idx + 2])?;
114 idx += 3;
115
116 if hour > 23 {
117 return Err(an_err!(DtErrKind::OutOfRange, "hour"));
118 }
119 if minute > 59 {
120 return Err(an_err!(DtErrKind::OutOfRange, "minute"));
121 }
122
123 if second == 60 {
124 second = 59;
125 } else if second > 59 {
126 return Err(an_err!(DtErrKind::OutOfRange, "second"));
127 }
128
129 // Subseconds (BCD → attoseconds)
130 let mut frac_value: u128 = 0;
131 for _ in 0..n_subsec {
132 let b = input[idx];
133 let hi = (b >> 4) as u128;
134 let lo = (b & 0x0F) as u128;
135 if hi > 9 || lo > 9 {
136 return Err(an_err!(DtErrKind::InvalidBytes, "invalid subsecond bcd"));
137 }
138 frac_value = frac_value * 100 + hi * 10 + lo;
139 idx += 1;
140 }
141
142 let attos = if n_subsec == 0 {
143 0
144 } else {
145 let decimal_places = (2 * n_subsec) as u32;
146 let denom = 10u128.pow(decimal_places);
147 ((frac_value * 1_000_000_000_000_000_000u128) / denom) as u64
148 };
149
150 let mut pd = TimeParts {
151 yr: Some(year),
152 mo: month,
153 day,
154 day_of_yr: day_of_year,
155 hr: hour,
156 min: minute,
157 sec: second,
158 attos,
159 scale: Scale::UTC,
160 ..TimeParts::default()
161 };
162
163 pd.finish(false)?;
164 Ok(pd)
165 }
166
167 /// Parses a **CCSDS C (CUC – Unsegmented Time Code)** binary time code
168 /// into a [`TimeParts`].
169 ///
170 /// Implements **CCSDS 301.0-B-4 §3.2 (Level 1)**, including full support
171 /// for the extended 2-byte P-field.
172 ///
173 /// ## Supported formats (Level 1 only)
174 ///
175 /// - 1-byte or 2-byte P-field (further extension beyond 2 bytes is rejected).
176 /// - Code ID must be `001` (1958-01-01 TAI epoch).
177 /// - Coarse time: 1–7 octets total.
178 /// - Fractional time: 0–10 octets total.
179 ///
180 /// ## P-field decoding
181 ///
182 /// - **First octet (P1)**:
183 /// - Bit 7: Extension flag (1 = second P-field octet follows)
184 /// - Bits 6-4: Code ID (must be `001`)
185 /// - Bits 3-2: Coarse time octets minus 1 (0–3 → 1–4 octets)
186 /// - Bits 1-0: Fractional time octets (0–3)
187 ///
188 /// - **Second octet (P2, when extension flag is set)**:
189 /// - Bit 7: Further-extension flag (must be 0; 3+-byte P-fields are rejected)
190 /// - Bits 6-5: Additional coarse octets (0–3)
191 /// - Bits 4-2: Additional fractional octets (0–7)
192 /// - Bits 1-0: Reserved (ignored)
193 ///
194 /// ## T-field
195 ///
196 /// - Coarse time is interpreted as seconds since **1958-01-01 00:00:00 TAI**.
197 /// - Fractional time is converted to attoseconds using exact integer arithmetic
198 /// (`value × 10¹⁸ / 2^(8·n_frac)`).
199 ///
200 /// ## Returns
201 ///
202 /// A [`TimeParts`] with `scale = TAI` and the decoded civil date/time.
203 ///
204 /// ## Errors
205 ///
206 /// - [`DtErrKind::Incomplete`] if `input` is empty.
207 /// - [`DtErrKind::InvalidItem`] if the Code ID is not `001`.
208 /// - [`DtErrKind::InvalidInput`] if the input is too short to contain the declared
209 /// extended P-field, or if the "further extension" flag (bit 7 of the second
210 /// P-field octet) is set.
211 /// - [`DtErrKind::InvalidSyntax`] if the declared coarse + fractional field lengths
212 /// make the T-field longer than the remaining input bytes.
213 pub fn from_ccsds_cuc(input: &[u8]) -> Result<TimeParts, DtErr> {
214 if input.is_empty() {
215 return Err(an_err!(DtErrKind::Incomplete, "empty"));
216 }
217
218 let p1 = input[0];
219 let mut idx = 1usize;
220
221 let extension = (p1 & 0b1000_0000) != 0;
222 let code_id = (p1 >> 4) & 0b0111;
223 if code_id != 0b001 {
224 return Err(an_err!(DtErrKind::InvalidItem, "code id"));
225 }
226
227 let base_coarse = (((p1 >> 2) & 0b0011) as usize) + 1;
228 let base_frac = (p1 & 0b0011) as usize;
229
230 let (n_coarse, n_frac) = if extension {
231 if input.len() < 2 {
232 return Err(an_err!(DtErrKind::InvalidInput, "p-field too short"));
233 }
234 let p2 = input[1];
235 idx += 1;
236
237 if (p2 & 0b1000_0000) != 0 {
238 return Err(an_err!(
239 DtErrKind::InvalidInput,
240 "further p-field ext. not supported"
241 ));
242 }
243
244 let add_coarse = ((p2 >> 5) & 0b0000_0011) as usize;
245 let add_frac = ((p2 >> 2) & 0b0000_0111) as usize;
246
247 (base_coarse + add_coarse, base_frac + add_frac)
248 } else {
249 (base_coarse, base_frac)
250 };
251
252 if n_coarse == 0 || input.len() < idx + n_coarse + n_frac {
253 return Err(an_err!(DtErrKind::InvalidSyntax, "t-field too short"));
254 }
255
256 // Read coarse time (big-endian)
257 let mut coarse_sec: u64 = 0;
258 for _ in 0..n_coarse {
259 coarse_sec = (coarse_sec << 8) | u64::from(input[idx]);
260 idx += 1;
261 }
262
263 // Read fractional time (big-endian)
264 let mut frac_raw: u128 = 0;
265 for _ in 0..n_frac {
266 frac_raw = (frac_raw << 8) | u128::from(input[idx]);
267 idx += 1;
268 }
269
270 let frac_attos = if n_frac == 0 {
271 0
272 } else {
273 let denom = 1u128 << (8 * n_frac as u32);
274 let numerator = frac_raw * 1_000_000_000_000_000_000u128;
275 // Add proper rounding (symmetric to the encoder)
276 ((numerator + (denom / 2)) / denom) as u64
277 };
278
279 // Convert to civil time using custom Gregorian conversion
280 let days_since_epoch = (coarse_sec / 86400) as i64;
281 let sec_of_day = (coarse_sec % 86400) as i64;
282
283 let (year, month, day) = Dt::days_since_1958_to_gregorian(days_since_epoch);
284
285 let hour = (sec_of_day / 3600) as u8;
286 let minute = ((sec_of_day % 3600) / 60) as u8;
287 let second = (sec_of_day % 60) as u8;
288
289 let mut pd = TimeParts {
290 yr: Some(year),
291 mo: Some(month),
292 day: Some(day),
293 hr: hour,
294 min: minute,
295 sec: second,
296 attos: frac_attos,
297 scale: Scale::TAI,
298 ..TimeParts::default()
299 };
300 pd.finish(false)?;
301 Ok(pd)
302 }
303
304 /// Parses a **CCSDS D (CDS – Day Segmented Time Code)** binary time code
305 /// into a [`TimeParts`].
306 ///
307 /// Implements **CCSDS 301.0-B-4 §3.3 (Level 1)**.
308 ///
309 /// ## Supported formats (Level 1 only)
310 ///
311 /// - 1-byte or 2-byte P-field.
312 /// - Code ID must be `100` and the Epoch bit must be `0` (1958-01-01 UTC epoch).
313 /// - Day count: 2 or 3 bytes.
314 /// - Milliseconds since midnight: always 4 bytes.
315 /// - Sub-millisecond field (bits 1-0 of P-field):
316 /// - `00`: no fractional field
317 /// - `01`: 2 bytes (microseconds within the millisecond, 0–65535)
318 /// - `10`: 4 bytes (fractional part of the millisecond as 2⁻³²)
319 /// - `11`: rejected (unsupported)
320 ///
321 /// ## P-field bit layout (first octet)
322 ///
323 /// - Bit 7: Extension flag (1 = second P-field octet follows)
324 /// - Bits 6-4: Code ID (must be `100`)
325 /// - Bit 3: Epoch (must be `0` for Level 1 / 1958 epoch)
326 /// - Bit 2: Day count size (`0` = 2 bytes, `1` = 3 bytes)
327 /// - Bits 1-0: Sub-millisecond code (see above)
328 ///
329 /// ## T-field
330 ///
331 /// - Day count is days since **1958-01-01 00:00:00 UTC**.
332 /// - Milliseconds since midnight are always present (4 bytes).
333 /// - Sub-millisecond data (if present) is converted to attoseconds with
334 /// exact integer scaling.
335 ///
336 /// ## Leap-second handling
337 ///
338 /// Correctly supports leap seconds. When the millisecond-of-day value
339 /// represents 23:59:60 (i.e. `millis_of_day >= 86_400_000`), `sec` is set
340 /// to `60` and `is_leap_second` is effectively indicated via the `sec` field
341 /// in the returned [`TimeParts`].
342 ///
343 /// ## Returns
344 ///
345 /// A [`TimeParts`] with `scale = UTC` and the decoded civil date/time.
346 ///
347 /// ## Errors
348 ///
349 /// - [`DtErrKind::Incomplete`] if `input` is empty.
350 /// - [`DtErrKind::InvalidInput`] if the P-field indicates an extended second
351 /// octet but the input is too short to contain it.
352 /// - [`DtErrKind::InvalidItem`] if the Code ID is not `100`, the Epoch bit is
353 /// set (non-Level-1 epoch), or the sub-millisecond code is `0b11`.
354 /// - [`DtErrKind::InvalidSyntax`] if the declared field lengths make the
355 /// T-field longer than the remaining input bytes.
356 pub fn from_ccsds_cds(input: &[u8]) -> Result<TimeParts, DtErr> {
357 if input.is_empty() {
358 return Err(an_err!(DtErrKind::Incomplete, "empty"));
359 }
360
361 let p1 = input[0];
362 let mut idx = 1usize;
363
364 let extension = (p1 & 0b1000_0000) != 0;
365 if extension {
366 if input.len() < 2 {
367 return Err(an_err!(DtErrKind::InvalidInput, "p-field too short"));
368 }
369 idx += 1;
370 }
371
372 let code_id = (p1 >> 4) & 0b0111;
373 if code_id != 0b100 {
374 return Err(an_err!(DtErrKind::InvalidItem, "code id"));
375 }
376
377 if (p1 & 0b0000_1000) != 0 {
378 return Err(an_err!(
379 DtErrKind::InvalidItem,
380 "non-level-1 epoch not supported"
381 ));
382 }
383
384 let n_day = if (p1 & 0b0000_0100) == 0 { 2 } else { 3 };
385 let sub_ms_code = p1 & 0b0000_0011;
386
387 let n_subsec = match sub_ms_code {
388 0b00 => 0,
389 0b01 => 2,
390 0b10 => 4,
391 _ => return Err(an_err!(DtErrKind::InvalidItem, "sub-millisecond code")),
392 };
393
394 if input.len() < idx + n_day + 4 + n_subsec {
395 return Err(an_err!(DtErrKind::InvalidSyntax, "t-field too short"));
396 }
397
398 // Read fields
399 let mut day_count: u64 = 0;
400 for _ in 0..n_day {
401 day_count = (day_count << 8) | u64::from(input[idx]);
402 idx += 1;
403 }
404
405 let mut millis_of_day: u64 = 0;
406 for _ in 0..4 {
407 millis_of_day = (millis_of_day << 8) | u64::from(input[idx]);
408 idx += 1;
409 }
410
411 let mut frac_raw: u64 = 0;
412 for _ in 0..n_subsec {
413 frac_raw = (frac_raw << 8) | u64::from(input[idx]);
414 idx += 1;
415 }
416
417 // === Leap second handling (robust) ===
418 let total_sec_in_day = millis_of_day / 1000;
419 let is_leap_second = total_sec_in_day == 86400;
420
421 let effective_sec = if is_leap_second {
422 86399
423 } else {
424 total_sec_in_day
425 };
426
427 let sec_of_day = effective_sec;
428 let remaining_ms = (millis_of_day % 1000) as u128;
429
430 // Sub-millisecond to attoseconds
431 let sub_ms_attos = if n_subsec == 0 {
432 0
433 } else if sub_ms_code == 0b01 {
434 (frac_raw as u128 * 1_000_000_000_000_000) / 65_536
435 } else {
436 (frac_raw as u128 * 1_000_000_000_000_000) / (1u128 << 32)
437 };
438
439 let frac_attos = remaining_ms * 1_000_000_000_000_000 + sub_ms_attos;
440
441 // Convert day count to Gregorian
442 let days_since_epoch = day_count as i64;
443 let (year, month, day) = Dt::days_since_1958_to_gregorian(days_since_epoch);
444
445 let hour = (sec_of_day / 3600) as u8;
446 let minute = ((sec_of_day % 3600) / 60) as u8;
447 let mut second = (sec_of_day % 60) as u8;
448
449 if is_leap_second {
450 second = 60;
451 }
452
453 let mut pd = TimeParts {
454 yr: Some(year),
455 mo: Some(month),
456 day: Some(day),
457 hr: hour,
458 min: minute,
459 sec: second,
460 attos: frac_attos as u64,
461 scale: Scale::UTC,
462 ..TimeParts::default()
463 };
464 pd.finish(false)?;
465 Ok(pd)
466 }
467
468 /// Auto-detects and parses a CCSDS binary time code (CUC, CDS, or CCS)
469 /// based on the Code ID in the first P-field byte.
470 ///
471 /// Examines the Code ID and dispatches to the appropriate parser:
472 /// - `001` → [`from_ccsds_cuc`](Self::from_ccsds_cuc) (CUC – Unsegmented Time Code)
473 /// - `100` → [`from_ccsds_cds`](Self::from_ccsds_cds) (CDS – Day Segmented Time Code)
474 /// - `101` → [`from_ccsds_ccs`](Self::from_ccsds_ccs) (CCS – Calendar Segmented Time Code)
475 ///
476 /// This is a convenience wrapper. For stricter control or when the format
477 /// is known in advance, prefer calling the specific `from_ccsds_*` function directly.
478 ///
479 /// ## Errors
480 ///
481 /// - [`DtErrKind::Incomplete`] if `input` is empty.
482 /// - [`DtErrKind::InvalidItem`] if the Code ID is not one of the three
483 /// recognized Level 1 values (`001`, `100`, or `101`).
484 ///
485 /// The resulting [`TimeParts`] has `scale` set according to the detected format
486 /// (TAI for CUC, UTC for CDS, and format-dependent for CCS).
487 pub fn from_ccsds_bin(input: &[u8]) -> Result<TimeParts, DtErr> {
488 if input.is_empty() {
489 return Err(an_err!(DtErrKind::Incomplete, "empty"));
490 }
491 let code_id = (input[0] >> 4) & 0b0111;
492 match code_id {
493 0b001 => Self::from_ccsds_cuc(input),
494 0b100 => Self::from_ccsds_cds(input),
495 0b101 => Self::from_ccsds_ccs(input),
496 _ => Err(an_err!(DtErrKind::InvalidItem, "unknown code id")),
497 }
498 }
499}