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