Skip to main content

dvb_t2mi/payload/
timestamp.rs

1//! T2-MI payload type 0x20: DVB-T2 timestamp — §5.2.7.
2//!
3//! Absolute or relative emission time.
4//! Emission time = seconds_since_2000 + subseconds * Tsub (where Tsub depends on bandwidth).
5//! Null timestamp: all bits of seconds_since_2000, subseconds, utco = 1.
6//!
7//! Civil UTC conversion (applying the `utco` leap-second offset) is intentionally not
8//! provided yet; `utco` is exposed as a field. `emission_offset` is in the timestamp's
9//! own time base relative to 2000-01-01T00:00:00.
10
11use num_enum::TryFromPrimitive;
12
13use dvb_common::{Parse, Serialize};
14
15/// Bandwidth per §5.2.7 Table 3.
16#[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    /// 1.7 MHz bandwidth.
22    Mhz1_7 = 0,
23    /// 5 MHz bandwidth.
24    Mhz5 = 1,
25    /// 6 MHz bandwidth.
26    Mhz6 = 2,
27    /// 7 MHz bandwidth.
28    Mhz7 = 3,
29    /// 8 MHz bandwidth.
30    Mhz8 = 4,
31    /// 10 MHz bandwidth.
32    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
50/// Subsecond denominator D for 1.7 MHz bandwidth.
51/// ETSI TS 102 773 §5.2.7 Table 4: Tsub = 1/D µs, D = 131.
52const SUBSEC_DENOM_1_7MHZ: u64 = 131;
53
54/// Subsecond denominator D for 5 MHz bandwidth.
55/// ETSI TS 102 773 §5.2.7 Table 4: Tsub = 1/D µs, D = 40.
56const SUBSEC_DENOM_5MHZ: u64 = 40;
57
58/// Subsecond denominator D for 6 MHz bandwidth.
59/// ETSI TS 102 773 §5.2.7 Table 4: Tsub = 1/D µs, D = 48.
60const SUBSEC_DENOM_6MHZ: u64 = 48;
61
62/// Subsecond denominator D for 7 MHz bandwidth.
63/// ETSI TS 102 773 §5.2.7 Table 4: Tsub = 1/D µs, D = 56.
64const SUBSEC_DENOM_7MHZ: u64 = 56;
65
66/// Subsecond denominator D for 8 MHz bandwidth.
67/// ETSI TS 102 773 §5.2.7 Table 4: Tsub = 1/D µs, D = 64.
68const SUBSEC_DENOM_8MHZ: u64 = 64;
69
70/// Subsecond denominator D for 10 MHz bandwidth.
71/// ETSI TS 102 773 §5.2.7 Table 4: Tsub = 1/D µs, D = 80.
72const SUBSEC_DENOM_10MHZ: u64 = 80;
73
74impl Bandwidth {
75    /// Return the number of subsecond ticks per second for this bandwidth.
76    ///
77    /// Equals D × 1_000_000, where D is the denominator from
78    /// ETSI TS 102 773 §5.2.7 Table 4 (Tsub = 1/D µs).
79    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
91/// Maximum value for the 40-bit `seconds_since_2000` field.
92const SECONDS_SINCE_2000_MAX: u64 = 0xFF_FFFF_FFFF;
93
94/// Maximum value for the 27-bit `subseconds` field.
95const SUBSECONDS_MAX: u32 = 0x7FF_FFFF;
96
97/// Maximum value for the 13-bit `utco` field.
98const UTCO_MAX: u16 = 0x1FFF;
99
100/// DVB-T2 timestamp payload (type 0x20) per ETSI TS 102 773 §5.2.7.
101///
102/// Layout (88 bits = 11 bytes):
103/// - byte 0 `[7:4]`: rfu (4 bits) — must be 0
104/// - byte 0 `[3:0]`: bw (4 bits) — Table 3
105/// - bytes 1-5: seconds_since_2000 (40 bits)
106/// - subseconds (27 bits): bytes 6-8 + byte 9 `[7:5]`
107/// - utco (13 bits): byte 9 `[4:0]` + byte 10 — UTC offset in seconds
108///
109/// Civil UTC conversion (applying the `utco` leap-second offset) is intentionally not
110/// provided yet; `utco` is exposed as a field. `emission_offset` is in the timestamp's
111/// own time base relative to 2000-01-01T00:00:00.
112#[derive(Debug, Clone, PartialEq, Eq)]
113#[cfg_attr(feature = "serde", derive(serde::Serialize))]
114pub struct T2TimestampPayload {
115    /// Bandwidth (determines Tsub units).
116    pub bw: Bandwidth,
117    /// Seconds since 2000-01-01T00:00:00Z. 0 = relative timestamp.
118    /// If all bits are 1 along with subseconds + utco, this is a Null timestamp.
119    pub seconds_since_2000: u64,
120    /// Subsecond count (27 bits).
121    pub subseconds: u32,
122    /// UTC offset in seconds (e.g. 34 for leap seconds as of 2016).
123    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        // byte 0 [7:4] = rfu
141        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        // bytes 1-5: seconds_since_2000 (40 bits)
151        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        // bytes 6-8 [31:5]: subseconds (27 bits)
158        // bytes 6-7-8 = 24 bits, but subseconds extends into byte 9
159        // 27 bits: bytes 6-8 (24 bits) + byte 9 [7:5] (3 bits)
160        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        // byte 9 [4:0] + byte 10: utco (13 bits)
166        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; // RFU = 0
217        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    /// Returns `true` if this is a Null timestamp (all bits of
234    /// `seconds_since_2000`, `subseconds`, and `utco` are 1).
235    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    /// Returns `true` if this is a relative timestamp
242    /// (`seconds_since_2000` is 0 and is not null).
243    pub fn is_relative(&self) -> bool {
244        self.seconds_since_2000 == 0 && !self.is_null()
245    }
246
247    /// Time elapsed since 2000-01-01T00:00:00 in the timestamp's own time base.
248    ///
249    /// Returns `None` for a Null timestamp.
250    ///
251    /// Civil UTC conversion (applying the `utco` leap-second offset) is
252    /// intentionally not provided yet; `utco` is exposed as a field.
253    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    /// Set `seconds_since_2000` and `subseconds` from a [`core::time::Duration`]
265    /// using the current `bw`. Leaves `bw` and `utco` unchanged.
266    ///
267    /// # Errors
268    ///
269    /// Returns [`ReservedBitsViolation`](crate::error::Error::ReservedBitsViolation)
270    /// if the duration exceeds the 40-bit seconds or 27-bit subseconds range.
271    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; // bw = 6MHz
327        buf[1] = 0x00;
328        buf[2] = 0x00;
329        buf[3] = 0x01; // seconds_since_2000 = 65536 + 256 + ... let me just set simple values
330        buf[6] = 0x00;
331        buf[7] = 0x00;
332        buf[8] = 0x00;
333        buf[9] = 0x00; // subseconds=0, utco=0
334        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; // bw = 6 MHz; top 4 rfu bits = 0
372        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); // 40 bits all 1
377        assert_eq!(parsed.subseconds, 0x7FFFFFF); // 27 bits all 1
378        assert_eq!(parsed.utco, 0x1FFF); // 13 bits all 1
379    }
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        // 8 MHz: D=64, sps=64_000_000.
418        // subseconds=32_000_000 = half of sps => 0.5 s subsecond component.
419        // total_nanos = 32_000_000 * 1_000_000_000 / 64_000_000 = 500_000_000.
420        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        // 6 MHz: D=48, sps=48_000_000.
432        // subseconds=12_000_000 = 1/4 of sps => 0.25 s subsecond component.
433        // total_nanos = 12_000_000 * 1_000_000_000 / 48_000_000 = 250_000_000.
434        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}