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