1use num_enum::TryFromPrimitive;
39
40use broadcast_common::{Parse, Serialize};
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
44#[cfg_attr(feature = "serde", derive(serde::Serialize))]
45#[repr(u8)]
46#[non_exhaustive]
47pub enum Bandwidth {
48 Mhz1_7 = 0,
50 Mhz5 = 1,
52 Mhz6 = 2,
54 Mhz7 = 3,
56 Mhz8 = 4,
58 Mhz10 = 5,
60}
61
62impl From<Bandwidth> for u8 {
63 fn from(bw: Bandwidth) -> Self {
64 bw as u8
65 }
66}
67
68impl From<num_enum::TryFromPrimitiveError<Bandwidth>> for crate::error::Error {
69 fn from(_: num_enum::TryFromPrimitiveError<Bandwidth>) -> Self {
70 crate::error::Error::ReservedBitsViolation {
71 field: "bw",
72 reason: "Must be 0..=5 per ETSI TS 102 773 §5.2.7 Table 3",
73 }
74 }
75}
76
77impl Bandwidth {
78 #[must_use]
80 pub fn name(&self) -> &'static str {
81 match self {
82 Self::Mhz1_7 => "1.7 MHz",
83 Self::Mhz5 => "5 MHz",
84 Self::Mhz6 => "6 MHz",
85 Self::Mhz7 => "7 MHz",
86 Self::Mhz8 => "8 MHz",
87 Self::Mhz10 => "10 MHz",
88 }
89 }
90}
91broadcast_common::impl_spec_display!(Bandwidth);
92
93const SUBSEC_DENOM_1_7MHZ: u64 = 131;
96
97const SUBSEC_DENOM_5MHZ: u64 = 40;
100
101const SUBSEC_DENOM_6MHZ: u64 = 48;
104
105const SUBSEC_DENOM_7MHZ: u64 = 56;
108
109const SUBSEC_DENOM_8MHZ: u64 = 64;
112
113const SUBSEC_DENOM_10MHZ: u64 = 80;
116
117impl Bandwidth {
118 #[must_use]
141 pub fn t_sub(self) -> (u32, u32) {
142 let denom = match self {
144 Bandwidth::Mhz1_7 => SUBSEC_DENOM_1_7MHZ,
145 Bandwidth::Mhz5 => SUBSEC_DENOM_5MHZ,
146 Bandwidth::Mhz6 => SUBSEC_DENOM_6MHZ,
147 Bandwidth::Mhz7 => SUBSEC_DENOM_7MHZ,
148 Bandwidth::Mhz8 => SUBSEC_DENOM_8MHZ,
149 Bandwidth::Mhz10 => SUBSEC_DENOM_10MHZ,
150 };
151 (1000, denom as u32)
152 }
153
154 pub fn subseconds_per_second(self) -> u64 {
159 match self {
160 Bandwidth::Mhz1_7 => SUBSEC_DENOM_1_7MHZ * 1_000_000,
161 Bandwidth::Mhz5 => SUBSEC_DENOM_5MHZ * 1_000_000,
162 Bandwidth::Mhz6 => SUBSEC_DENOM_6MHZ * 1_000_000,
163 Bandwidth::Mhz7 => SUBSEC_DENOM_7MHZ * 1_000_000,
164 Bandwidth::Mhz8 => SUBSEC_DENOM_8MHZ * 1_000_000,
165 Bandwidth::Mhz10 => SUBSEC_DENOM_10MHZ * 1_000_000,
166 }
167 }
168}
169
170const SECONDS_SINCE_2000_MAX: u64 = 0xFF_FFFF_FFFF;
172
173const SUBSECONDS_MAX: u32 = 0x7FF_FFFF;
175
176const UTCO_MAX: u16 = 0x1FFF;
178
179#[derive(Debug, Clone, PartialEq, Eq)]
192#[cfg_attr(feature = "serde", derive(serde::Serialize))]
193pub struct T2TimestampPayload {
194 pub bw: Bandwidth,
196 pub seconds_since_2000: u64,
199 pub subseconds: u32,
201 pub utco: u16,
203}
204
205const TIMESTAMP_HEADER_LEN: usize = 11;
206
207impl<'a> Parse<'a> for T2TimestampPayload {
208 type Error = crate::error::Error;
209
210 fn parse(bytes: &'a [u8]) -> Result<Self, crate::error::Error> {
211 if bytes.len() < TIMESTAMP_HEADER_LEN {
212 return Err(crate::Error::BufferTooShort {
213 need: TIMESTAMP_HEADER_LEN,
214 have: bytes.len(),
215 what: "T2TimestampPayload header",
216 });
217 }
218
219 if bytes[0] & 0xF0 != 0 {
221 return Err(crate::Error::ReservedBitsViolation {
222 field: "4-bit RFU",
223 reason: "Must be zero (ETSI TS 102 773 §5.2.7)",
224 });
225 }
226
227 let bw = Bandwidth::try_from(bytes[0] & 0x0F)?;
228
229 let seconds_since_2000 = (bytes[1] as u64) << 32
231 | (bytes[2] as u64) << 24
232 | (bytes[3] as u64) << 16
233 | (bytes[4] as u64) << 8
234 | (bytes[5] as u64);
235
236 let subseconds = (bytes[6] as u32) << 19
240 | (bytes[7] as u32) << 11
241 | (bytes[8] as u32) << 3
242 | ((bytes[9] >> 5) as u32 & 0x7);
243
244 let utco = ((bytes[9] as u16 & 0x1F) << 8) | (bytes[10] as u16);
246
247 Ok(T2TimestampPayload {
248 bw,
249 seconds_since_2000,
250 subseconds,
251 utco,
252 })
253 }
254}
255
256impl<'a> crate::traits::PayloadDef<'a> for T2TimestampPayload {
257 const PACKET_TYPE: u8 = 0x20;
258 const NAME: &'static str = "TIMESTAMP";
259}
260
261impl Serialize for T2TimestampPayload {
262 type Error = crate::error::Error;
263
264 fn serialized_len(&self) -> usize {
265 TIMESTAMP_HEADER_LEN
266 }
267
268 fn serialize_into(&self, buf: &mut [u8]) -> Result<usize, crate::error::Error> {
269 if buf.len() < self.serialized_len() {
270 return Err(crate::Error::OutputBufferTooSmall {
271 need: self.serialized_len(),
272 have: buf.len(),
273 });
274 }
275
276 if self.seconds_since_2000 > 0xFF_FFFF_FFFF {
277 return Err(crate::Error::ReservedBitsViolation {
278 field: "seconds_since_2000",
279 reason: "Must fit in 40 bits",
280 });
281 }
282 if self.subseconds > 0x7FFFFFF {
283 return Err(crate::Error::ReservedBitsViolation {
284 field: "subseconds",
285 reason: "Must fit in 27 bits",
286 });
287 }
288 if self.utco > 0x1FFF {
289 return Err(crate::Error::ReservedBitsViolation {
290 field: "utco",
291 reason: "Must fit in 13 bits",
292 });
293 }
294
295 buf[0] = u8::from(self.bw) & 0x0F; buf[1] = (self.seconds_since_2000 >> 32 & 0xFF) as u8;
297 buf[2] = (self.seconds_since_2000 >> 24 & 0xFF) as u8;
298 buf[3] = (self.seconds_since_2000 >> 16 & 0xFF) as u8;
299 buf[4] = (self.seconds_since_2000 >> 8 & 0xFF) as u8;
300 buf[5] = (self.seconds_since_2000 & 0xFF) as u8;
301 buf[6] = (self.subseconds >> 19 & 0xFF) as u8;
302 buf[7] = (self.subseconds >> 11 & 0xFF) as u8;
303 buf[8] = (self.subseconds >> 3 & 0xFF) as u8;
304 buf[9] = ((self.subseconds & 0x7) as u8) << 5 | ((self.utco >> 8) as u8 & 0x1F);
305 buf[10] = (self.utco & 0xFF) as u8;
306
307 Ok(self.serialized_len())
308 }
309}
310
311impl T2TimestampPayload {
312 pub fn is_null(&self) -> bool {
315 self.seconds_since_2000 == SECONDS_SINCE_2000_MAX
316 && self.subseconds == SUBSECONDS_MAX
317 && self.utco == UTCO_MAX
318 }
319
320 pub fn is_relative(&self) -> bool {
323 self.seconds_since_2000 == 0 && !self.is_null()
324 }
325
326 pub fn emission_offset(&self) -> Option<core::time::Duration> {
333 if self.is_null() {
334 return None;
335 }
336 let sps = self.bw.subseconds_per_second();
337 let total_nanos: u128 = self.subseconds as u128 * 1_000_000_000u128 / sps as u128;
338 let secs = self.seconds_since_2000 + (total_nanos / 1_000_000_000) as u64;
339 let sub_nanos = (total_nanos % 1_000_000_000) as u32;
340 Some(core::time::Duration::new(secs, sub_nanos))
341 }
342
343 pub fn set_emission_offset(
351 &mut self,
352 offset: core::time::Duration,
353 ) -> Result<(), crate::error::Error> {
354 let secs = offset.as_secs();
355 if secs > SECONDS_SINCE_2000_MAX {
356 return Err(crate::error::Error::ReservedBitsViolation {
357 field: "seconds_since_2000",
358 reason: "exceeds 40 bits",
359 });
360 }
361 let sps = self.bw.subseconds_per_second();
362 let subseconds = (offset.subsec_nanos() as u128 * sps as u128 / 1_000_000_000u128) as u32;
363 if subseconds > SUBSECONDS_MAX {
364 return Err(crate::error::Error::ReservedBitsViolation {
365 field: "subseconds",
366 reason: "exceeds 27 bits",
367 });
368 }
369 self.seconds_since_2000 = secs;
370 self.subseconds = subseconds;
371 Ok(())
372 }
373}
374
375#[cfg(feature = "chrono")]
376#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
377impl T2TimestampPayload {
378 #[must_use]
389 pub fn emission_time_utc(&self) -> Option<chrono::DateTime<chrono::Utc>> {
390 let offset = self.emission_offset()?;
391 broadcast_common::time::decode_seconds_since_2000_utc(
392 offset.as_secs(),
393 offset.subsec_nanos(),
394 self.utco,
395 )
396 }
397
398 pub fn set_emission_time_utc(
407 &mut self,
408 dt: chrono::DateTime<chrono::Utc>,
409 utco: u16,
410 ) -> Result<(), crate::error::Error> {
411 let (secs, nanos) = broadcast_common::time::encode_seconds_since_2000_utc(dt, utco).ok_or(
412 crate::error::Error::ReservedBitsViolation {
413 field: "seconds_since_2000",
414 reason: "date before 2000 epoch or exceeds 40-bit range",
415 },
416 )?;
417 let sps = self.bw.subseconds_per_second();
418 let subseconds = (u128::from(nanos) * sps as u128 / 1_000_000_000u128) as u32;
419 if subseconds > SUBSECONDS_MAX {
420 return Err(crate::error::Error::ReservedBitsViolation {
421 field: "subseconds",
422 reason: "nanosecond component exceeds 27-bit subseconds range",
423 });
424 }
425 self.seconds_since_2000 = secs;
426 self.subseconds = subseconds;
427 self.utco = utco;
428 Ok(())
429 }
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435
436 #[test]
437 fn bandwidth_try_from_valid() {
438 assert_eq!(Bandwidth::try_from(0), Ok(Bandwidth::Mhz1_7));
439 assert_eq!(Bandwidth::try_from(5), Ok(Bandwidth::Mhz10));
440 }
441
442 #[test]
443 fn bandwidth_try_from_rejects_6() {
444 assert!(Bandwidth::try_from(6).is_err());
445 }
446
447 #[test]
448 fn exhaustive_byte_sweep() {
449 let mut matched = 0u16;
450 for byte in 0u8..=0xFF {
451 if let Ok(v) = Bandwidth::try_from(byte) {
452 assert_eq!(v as u8, byte, "round-trip failed for {byte:#04x}");
453 matched += 1;
454 }
455 }
456 assert_eq!(matched, 6, "expected 6 matched variants");
457 }
458
459 #[test]
460 fn parse_extracts_all_fields() {
461 let mut buf = [0u8; 11];
462 buf[0] = 0x02; buf[1] = 0x00;
464 buf[2] = 0x00;
465 buf[3] = 0x01; buf[6] = 0x00;
467 buf[7] = 0x00;
468 buf[8] = 0x00;
469 buf[9] = 0x00; buf[10] = 0x00;
471
472 let result = T2TimestampPayload::parse(&buf).unwrap();
473 assert_eq!(result.bw, Bandwidth::Mhz6);
474 assert_eq!(result.seconds_since_2000, 0x00_00_01_00_00);
475 }
476
477 #[test]
478 fn parse_rejects_nonzero_rfu() {
479 let buf = [
480 0x80u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
481 ];
482 assert!(T2TimestampPayload::parse(&buf).is_err());
483 }
484
485 #[test]
486 fn parse_rejects_short_buffer() {
487 assert!(T2TimestampPayload::parse(&[0x00; 10]).is_err());
488 }
489
490 #[test]
491 fn serialize_round_trip() {
492 let orig = T2TimestampPayload {
493 bw: Bandwidth::Mhz8,
494 seconds_since_2000: 0x00_00_01_02_03,
495 subseconds: 0x0123456,
496 utco: 0x7FF,
497 };
498 let mut buf = [0u8; 11];
499 orig.serialize_into(&mut buf).unwrap();
500 let parsed = T2TimestampPayload::parse(&buf).unwrap();
501 assert_eq!(orig, parsed);
502 }
503
504 #[test]
505 fn null_timestamp_all_ones() {
506 let mut buf = [0xFFu8; 11];
507 buf[0] = 0x02; buf[1..11].fill(0xFF);
509 let result = T2TimestampPayload::parse(&buf);
510 assert!(result.is_ok());
511 let parsed = result.unwrap();
512 assert_eq!(parsed.seconds_since_2000, 0xFFFFFFFFFF); assert_eq!(parsed.subseconds, 0x7FFFFFF); assert_eq!(parsed.utco, 0x1FFF); }
516
517 #[test]
518 fn subseconds_per_second_per_table4() {
519 assert_eq!(
520 Bandwidth::Mhz1_7.subseconds_per_second(),
521 131_000_000,
522 "1.7 MHz: D=131"
523 );
524 assert_eq!(
525 Bandwidth::Mhz5.subseconds_per_second(),
526 40_000_000,
527 "5 MHz: D=40"
528 );
529 assert_eq!(
530 Bandwidth::Mhz6.subseconds_per_second(),
531 48_000_000,
532 "6 MHz: D=48"
533 );
534 assert_eq!(
535 Bandwidth::Mhz7.subseconds_per_second(),
536 56_000_000,
537 "7 MHz: D=56"
538 );
539 assert_eq!(
540 Bandwidth::Mhz8.subseconds_per_second(),
541 64_000_000,
542 "8 MHz: D=64"
543 );
544 assert_eq!(
545 Bandwidth::Mhz10.subseconds_per_second(),
546 80_000_000,
547 "10 MHz: D=80"
548 );
549 }
550
551 #[test]
552 fn emission_offset_known_values() {
553 let p = T2TimestampPayload {
557 bw: Bandwidth::Mhz8,
558 seconds_since_2000: 100,
559 subseconds: 32_000_000,
560 utco: 0,
561 };
562 assert_eq!(
563 p.emission_offset(),
564 Some(core::time::Duration::new(100, 500_000_000))
565 );
566
567 let p2 = T2TimestampPayload {
571 bw: Bandwidth::Mhz6,
572 seconds_since_2000: 200,
573 subseconds: 12_000_000,
574 utco: 0,
575 };
576 assert_eq!(
577 p2.emission_offset(),
578 Some(core::time::Duration::new(200, 250_000_000))
579 );
580 }
581
582 #[test]
583 fn set_emission_offset_round_trips() {
584 let mut p = T2TimestampPayload {
585 bw: Bandwidth::Mhz8,
586 seconds_since_2000: 0,
587 subseconds: 0,
588 utco: 0,
589 };
590 let dur = core::time::Duration::new(12345, 500_000_000);
591 p.set_emission_offset(dur).unwrap();
592 assert_eq!(p.emission_offset(), Some(dur));
593 }
594
595 #[test]
596 fn null_timestamp_offset_is_none() {
597 let p = T2TimestampPayload {
598 bw: Bandwidth::Mhz8,
599 seconds_since_2000: SECONDS_SINCE_2000_MAX,
600 subseconds: SUBSECONDS_MAX,
601 utco: UTCO_MAX,
602 };
603 assert!(p.is_null());
604 assert_eq!(p.emission_offset(), None);
605 }
606
607 #[test]
608 fn relative_timestamp_flag() {
609 let p = T2TimestampPayload {
610 bw: Bandwidth::Mhz8,
611 seconds_since_2000: 0,
612 subseconds: 1000,
613 utco: 0,
614 };
615 assert!(p.is_relative());
616 assert!(!p.is_null());
617 assert!(p.emission_offset().is_some());
618 }
619
620 #[test]
625 fn t_sub_1_7mhz() {
626 let (n, d) = Bandwidth::Mhz1_7.t_sub();
629 assert_eq!((n, d), (1000, 131));
630 let nanos = 131_000u128 * u128::from(n) / u128::from(d);
631 assert_eq!(nanos, 1_000_000, "1.7 MHz: 131_000 ticks should be 1 ms");
632 }
633
634 #[test]
635 fn t_sub_5mhz() {
636 let (n, d) = Bandwidth::Mhz5.t_sub();
639 assert_eq!((n, d), (1000, 40));
640 let nanos = 40_000u128 * u128::from(n) / u128::from(d);
641 assert_eq!(nanos, 1_000_000, "5 MHz: 40_000 ticks should be 1 ms");
642 }
643
644 #[test]
645 fn t_sub_6mhz() {
646 let (n, d) = Bandwidth::Mhz6.t_sub();
649 assert_eq!((n, d), (1000, 48));
650 let nanos = 48_000u128 * u128::from(n) / u128::from(d);
651 assert_eq!(nanos, 1_000_000, "6 MHz: 48_000 ticks should be 1 ms");
652 }
653
654 #[test]
655 fn t_sub_7mhz() {
656 let (n, d) = Bandwidth::Mhz7.t_sub();
659 assert_eq!((n, d), (1000, 56));
660 let nanos = 56_000u128 * u128::from(n) / u128::from(d);
661 assert_eq!(nanos, 1_000_000, "7 MHz: 56_000 ticks should be 1 ms");
662 }
663
664 #[test]
665 fn t_sub_8mhz() {
666 let (n, d) = Bandwidth::Mhz8.t_sub();
669 assert_eq!((n, d), (1000, 64));
670 let nanos = 64_000u128 * u128::from(n) / u128::from(d);
671 assert_eq!(nanos, 1_000_000, "8 MHz: 64_000 ticks should be 1 ms");
672 }
673
674 #[test]
675 fn t_sub_10mhz() {
676 let (n, d) = Bandwidth::Mhz10.t_sub();
679 assert_eq!((n, d), (1000, 80));
680 let nanos = 80_000u128 * u128::from(n) / u128::from(d);
681 assert_eq!(nanos, 1_000_000, "10 MHz: 80_000 ticks should be 1 ms");
682 }
683
684 #[cfg(feature = "chrono")]
685 #[test]
686 fn emission_time_utc_known_value() {
687 use chrono::{Datelike, Timelike};
688 let p = T2TimestampPayload {
690 bw: Bandwidth::Mhz8,
691 seconds_since_2000: 0,
692 subseconds: 0,
693 utco: 0,
694 };
695 let dt = p.emission_time_utc().expect("should decode");
696 assert_eq!((dt.year(), dt.month(), dt.day()), (2000, 1, 1));
697 assert_eq!((dt.hour(), dt.minute(), dt.second()), (0, 0, 0));
698
699 let p2 = T2TimestampPayload {
701 bw: Bandwidth::Mhz8,
702 seconds_since_2000: 0,
703 subseconds: 0,
704 utco: 37,
705 };
706 let dt2 = p2.emission_time_utc().expect("should decode");
707 assert_eq!((dt2.year(), dt2.month(), dt2.day()), (1999, 12, 31));
708 assert_eq!((dt2.hour(), dt2.minute(), dt2.second()), (23, 59, 23));
709 }
710
711 #[cfg(feature = "chrono")]
712 #[test]
713 fn set_emission_time_utc_round_trips() {
714 use chrono::TimeZone;
715 let dt = chrono::Utc
716 .with_ymd_and_hms(2023, 6, 8, 12, 34, 56)
717 .unwrap();
718 let mut p = T2TimestampPayload {
719 bw: Bandwidth::Mhz8,
720 seconds_since_2000: 0,
721 subseconds: 0,
722 utco: 0,
723 };
724 p.set_emission_time_utc(dt, 37).unwrap();
725 let decoded = p.emission_time_utc().expect("decodes");
726 assert_eq!(decoded, dt);
727 }
728
729 #[cfg(feature = "chrono")]
730 #[test]
731 fn emission_time_utc_null_returns_none() {
732 let p = T2TimestampPayload {
733 bw: Bandwidth::Mhz8,
734 seconds_since_2000: SECONDS_SINCE_2000_MAX,
735 subseconds: SUBSECONDS_MAX,
736 utco: UTCO_MAX,
737 };
738 assert!(p.emission_time_utc().is_none());
739 }
740}