1use num_enum::TryFromPrimitive;
12
13use dvb_common::{Parse, Serialize};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize))]
18#[repr(u8)]
19#[non_exhaustive]
20pub enum Bandwidth {
21 Mhz1_7 = 0,
23 Mhz5 = 1,
25 Mhz6 = 2,
27 Mhz7 = 3,
29 Mhz8 = 4,
31 Mhz10 = 5,
33}
34
35impl From<Bandwidth> for u8 {
36 fn from(bw: Bandwidth) -> Self {
37 bw as u8
38 }
39}
40
41impl From<num_enum::TryFromPrimitiveError<Bandwidth>> for crate::error::Error {
42 fn from(_: num_enum::TryFromPrimitiveError<Bandwidth>) -> Self {
43 crate::error::Error::ReservedBitsViolation {
44 field: "bw",
45 reason: "Must be 0..=5 per ETSI TS 102 773 §5.2.7 Table 3",
46 }
47 }
48}
49
50const SUBSEC_DENOM_1_7MHZ: u64 = 131;
53
54const SUBSEC_DENOM_5MHZ: u64 = 40;
57
58const SUBSEC_DENOM_6MHZ: u64 = 48;
61
62const SUBSEC_DENOM_7MHZ: u64 = 56;
65
66const SUBSEC_DENOM_8MHZ: u64 = 64;
69
70const SUBSEC_DENOM_10MHZ: u64 = 80;
73
74impl Bandwidth {
75 pub fn subseconds_per_second(self) -> u64 {
80 match self {
81 Bandwidth::Mhz1_7 => SUBSEC_DENOM_1_7MHZ * 1_000_000,
82 Bandwidth::Mhz5 => SUBSEC_DENOM_5MHZ * 1_000_000,
83 Bandwidth::Mhz6 => SUBSEC_DENOM_6MHZ * 1_000_000,
84 Bandwidth::Mhz7 => SUBSEC_DENOM_7MHZ * 1_000_000,
85 Bandwidth::Mhz8 => SUBSEC_DENOM_8MHZ * 1_000_000,
86 Bandwidth::Mhz10 => SUBSEC_DENOM_10MHZ * 1_000_000,
87 }
88 }
89}
90
91const SECONDS_SINCE_2000_MAX: u64 = 0xFF_FFFF_FFFF;
93
94const SUBSECONDS_MAX: u32 = 0x7FF_FFFF;
96
97const UTCO_MAX: u16 = 0x1FFF;
99
100#[derive(Debug, Clone, PartialEq, Eq)]
113#[cfg_attr(feature = "serde", derive(serde::Serialize))]
114pub struct T2TimestampPayload {
115 pub bw: Bandwidth,
117 pub seconds_since_2000: u64,
120 pub subseconds: u32,
122 pub utco: u16,
124}
125
126const TIMESTAMP_HEADER_LEN: usize = 11;
127
128impl<'a> Parse<'a> for T2TimestampPayload {
129 type Error = crate::error::Error;
130
131 fn parse(bytes: &'a [u8]) -> Result<Self, crate::error::Error> {
132 if bytes.len() < TIMESTAMP_HEADER_LEN {
133 return Err(crate::Error::BufferTooShort {
134 need: TIMESTAMP_HEADER_LEN,
135 have: bytes.len(),
136 what: "T2TimestampPayload header",
137 });
138 }
139
140 if bytes[0] & 0xF0 != 0 {
142 return Err(crate::Error::ReservedBitsViolation {
143 field: "4-bit RFU",
144 reason: "Must be zero (ETSI TS 102 773 §5.2.7)",
145 });
146 }
147
148 let bw = Bandwidth::try_from(bytes[0] & 0x0F)?;
149
150 let seconds_since_2000 = (bytes[1] as u64) << 32
152 | (bytes[2] as u64) << 24
153 | (bytes[3] as u64) << 16
154 | (bytes[4] as u64) << 8
155 | (bytes[5] as u64);
156
157 let subseconds = (bytes[6] as u32) << 19
161 | (bytes[7] as u32) << 11
162 | (bytes[8] as u32) << 3
163 | ((bytes[9] >> 5) as u32 & 0x7);
164
165 let utco = ((bytes[9] as u16 & 0x1F) << 8) | (bytes[10] as u16);
167
168 Ok(T2TimestampPayload {
169 bw,
170 seconds_since_2000,
171 subseconds,
172 utco,
173 })
174 }
175}
176
177impl<'a> crate::traits::PayloadDef<'a> for T2TimestampPayload {
178 const PACKET_TYPE: u8 = 0x20;
179 const NAME: &'static str = "TIMESTAMP";
180}
181
182impl Serialize for T2TimestampPayload {
183 type Error = crate::error::Error;
184
185 fn serialized_len(&self) -> usize {
186 TIMESTAMP_HEADER_LEN
187 }
188
189 fn serialize_into(&self, buf: &mut [u8]) -> Result<usize, crate::error::Error> {
190 if buf.len() < self.serialized_len() {
191 return Err(crate::Error::OutputBufferTooSmall {
192 need: self.serialized_len(),
193 have: buf.len(),
194 });
195 }
196
197 if self.seconds_since_2000 > 0xFF_FFFF_FFFF {
198 return Err(crate::Error::ReservedBitsViolation {
199 field: "seconds_since_2000",
200 reason: "Must fit in 40 bits",
201 });
202 }
203 if self.subseconds > 0x7FFFFFF {
204 return Err(crate::Error::ReservedBitsViolation {
205 field: "subseconds",
206 reason: "Must fit in 27 bits",
207 });
208 }
209 if self.utco > 0x1FFF {
210 return Err(crate::Error::ReservedBitsViolation {
211 field: "utco",
212 reason: "Must fit in 13 bits",
213 });
214 }
215
216 buf[0] = u8::from(self.bw) & 0x0F; buf[1] = (self.seconds_since_2000 >> 32 & 0xFF) as u8;
218 buf[2] = (self.seconds_since_2000 >> 24 & 0xFF) as u8;
219 buf[3] = (self.seconds_since_2000 >> 16 & 0xFF) as u8;
220 buf[4] = (self.seconds_since_2000 >> 8 & 0xFF) as u8;
221 buf[5] = (self.seconds_since_2000 & 0xFF) as u8;
222 buf[6] = (self.subseconds >> 19 & 0xFF) as u8;
223 buf[7] = (self.subseconds >> 11 & 0xFF) as u8;
224 buf[8] = (self.subseconds >> 3 & 0xFF) as u8;
225 buf[9] = ((self.subseconds & 0x7) as u8) << 5 | ((self.utco >> 8) as u8 & 0x1F);
226 buf[10] = (self.utco & 0xFF) as u8;
227
228 Ok(self.serialized_len())
229 }
230}
231
232impl T2TimestampPayload {
233 pub fn is_null(&self) -> bool {
236 self.seconds_since_2000 == SECONDS_SINCE_2000_MAX
237 && self.subseconds == SUBSECONDS_MAX
238 && self.utco == UTCO_MAX
239 }
240
241 pub fn is_relative(&self) -> bool {
244 self.seconds_since_2000 == 0 && !self.is_null()
245 }
246
247 pub fn emission_offset(&self) -> Option<core::time::Duration> {
254 if self.is_null() {
255 return None;
256 }
257 let sps = self.bw.subseconds_per_second();
258 let total_nanos: u128 = self.subseconds as u128 * 1_000_000_000u128 / sps as u128;
259 let secs = self.seconds_since_2000 + (total_nanos / 1_000_000_000) as u64;
260 let sub_nanos = (total_nanos % 1_000_000_000) as u32;
261 Some(core::time::Duration::new(secs, sub_nanos))
262 }
263
264 pub fn set_emission_offset(
272 &mut self,
273 offset: core::time::Duration,
274 ) -> Result<(), crate::error::Error> {
275 let secs = offset.as_secs();
276 if secs > SECONDS_SINCE_2000_MAX {
277 return Err(crate::error::Error::ReservedBitsViolation {
278 field: "seconds_since_2000",
279 reason: "exceeds 40 bits",
280 });
281 }
282 let sps = self.bw.subseconds_per_second();
283 let subseconds = (offset.subsec_nanos() as u128 * sps as u128 / 1_000_000_000u128) as u32;
284 if subseconds > SUBSECONDS_MAX {
285 return Err(crate::error::Error::ReservedBitsViolation {
286 field: "subseconds",
287 reason: "exceeds 27 bits",
288 });
289 }
290 self.seconds_since_2000 = secs;
291 self.subseconds = subseconds;
292 Ok(())
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn bandwidth_try_from_valid() {
302 assert_eq!(Bandwidth::try_from(0), Ok(Bandwidth::Mhz1_7));
303 assert_eq!(Bandwidth::try_from(5), Ok(Bandwidth::Mhz10));
304 }
305
306 #[test]
307 fn bandwidth_try_from_rejects_6() {
308 assert!(Bandwidth::try_from(6).is_err());
309 }
310
311 #[test]
312 fn exhaustive_byte_sweep() {
313 let mut matched = 0u16;
314 for byte in 0u8..=0xFF {
315 if let Ok(v) = Bandwidth::try_from(byte) {
316 assert_eq!(v as u8, byte, "round-trip failed for {byte:#04x}");
317 matched += 1;
318 }
319 }
320 assert_eq!(matched, 6, "expected 6 matched variants");
321 }
322
323 #[test]
324 fn parse_extracts_all_fields() {
325 let mut buf = [0u8; 11];
326 buf[0] = 0x02; buf[1] = 0x00;
328 buf[2] = 0x00;
329 buf[3] = 0x01; buf[6] = 0x00;
331 buf[7] = 0x00;
332 buf[8] = 0x00;
333 buf[9] = 0x00; buf[10] = 0x00;
335
336 let result = T2TimestampPayload::parse(&buf).unwrap();
337 assert_eq!(result.bw, Bandwidth::Mhz6);
338 assert_eq!(result.seconds_since_2000, 0x00_00_01_00_00);
339 }
340
341 #[test]
342 fn parse_rejects_nonzero_rfu() {
343 let buf = [
344 0x80u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
345 ];
346 assert!(T2TimestampPayload::parse(&buf).is_err());
347 }
348
349 #[test]
350 fn parse_rejects_short_buffer() {
351 assert!(T2TimestampPayload::parse(&[0x00; 10]).is_err());
352 }
353
354 #[test]
355 fn serialize_round_trip() {
356 let orig = T2TimestampPayload {
357 bw: Bandwidth::Mhz8,
358 seconds_since_2000: 0x00_00_01_02_03,
359 subseconds: 0x0123456,
360 utco: 0x7FF,
361 };
362 let mut buf = [0u8; 11];
363 orig.serialize_into(&mut buf).unwrap();
364 let parsed = T2TimestampPayload::parse(&buf).unwrap();
365 assert_eq!(orig, parsed);
366 }
367
368 #[test]
369 fn null_timestamp_all_ones() {
370 let mut buf = [0xFFu8; 11];
371 buf[0] = 0x02; buf[1..11].fill(0xFF);
373 let result = T2TimestampPayload::parse(&buf);
374 assert!(result.is_ok());
375 let parsed = result.unwrap();
376 assert_eq!(parsed.seconds_since_2000, 0xFFFFFFFFFF); assert_eq!(parsed.subseconds, 0x7FFFFFF); assert_eq!(parsed.utco, 0x1FFF); }
380
381 #[test]
382 fn subseconds_per_second_per_table4() {
383 assert_eq!(
384 Bandwidth::Mhz1_7.subseconds_per_second(),
385 131_000_000,
386 "1.7 MHz: D=131"
387 );
388 assert_eq!(
389 Bandwidth::Mhz5.subseconds_per_second(),
390 40_000_000,
391 "5 MHz: D=40"
392 );
393 assert_eq!(
394 Bandwidth::Mhz6.subseconds_per_second(),
395 48_000_000,
396 "6 MHz: D=48"
397 );
398 assert_eq!(
399 Bandwidth::Mhz7.subseconds_per_second(),
400 56_000_000,
401 "7 MHz: D=56"
402 );
403 assert_eq!(
404 Bandwidth::Mhz8.subseconds_per_second(),
405 64_000_000,
406 "8 MHz: D=64"
407 );
408 assert_eq!(
409 Bandwidth::Mhz10.subseconds_per_second(),
410 80_000_000,
411 "10 MHz: D=80"
412 );
413 }
414
415 #[test]
416 fn emission_offset_known_values() {
417 let p = T2TimestampPayload {
421 bw: Bandwidth::Mhz8,
422 seconds_since_2000: 100,
423 subseconds: 32_000_000,
424 utco: 0,
425 };
426 assert_eq!(
427 p.emission_offset(),
428 Some(core::time::Duration::new(100, 500_000_000))
429 );
430
431 let p2 = T2TimestampPayload {
435 bw: Bandwidth::Mhz6,
436 seconds_since_2000: 200,
437 subseconds: 12_000_000,
438 utco: 0,
439 };
440 assert_eq!(
441 p2.emission_offset(),
442 Some(core::time::Duration::new(200, 250_000_000))
443 );
444 }
445
446 #[test]
447 fn set_emission_offset_round_trips() {
448 let mut p = T2TimestampPayload {
449 bw: Bandwidth::Mhz8,
450 seconds_since_2000: 0,
451 subseconds: 0,
452 utco: 0,
453 };
454 let dur = core::time::Duration::new(12345, 500_000_000);
455 p.set_emission_offset(dur).unwrap();
456 assert_eq!(p.emission_offset(), Some(dur));
457 }
458
459 #[test]
460 fn null_timestamp_offset_is_none() {
461 let p = T2TimestampPayload {
462 bw: Bandwidth::Mhz8,
463 seconds_since_2000: SECONDS_SINCE_2000_MAX,
464 subseconds: SUBSECONDS_MAX,
465 utco: UTCO_MAX,
466 };
467 assert!(p.is_null());
468 assert_eq!(p.emission_offset(), None);
469 }
470
471 #[test]
472 fn relative_timestamp_flag() {
473 let p = T2TimestampPayload {
474 bw: Bandwidth::Mhz8,
475 seconds_since_2000: 0,
476 subseconds: 1000,
477 utco: 0,
478 };
479 assert!(p.is_relative());
480 assert!(!p.is_null());
481 assert!(p.emission_offset().is_some());
482 }
483}