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
7use num_enum::TryFromPrimitive;
8
9use dvb_common::{Parse, Serialize};
10
11/// Bandwidth per §5.2.7 Table 3.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[repr(u8)]
15pub enum Bandwidth {
16    /// 1.7 MHz bandwidth.
17    Mhz1_7 = 0,
18    /// 5 MHz bandwidth.
19    Mhz5 = 1,
20    /// 6 MHz bandwidth.
21    Mhz6 = 2,
22    /// 7 MHz bandwidth.
23    Mhz7 = 3,
24    /// 8 MHz bandwidth.
25    Mhz8 = 4,
26    /// 10 MHz bandwidth.
27    Mhz10 = 5,
28}
29
30impl From<Bandwidth> for u8 {
31    fn from(bw: Bandwidth) -> Self {
32        bw as u8
33    }
34}
35
36impl From<num_enum::TryFromPrimitiveError<Bandwidth>> for crate::error::Error {
37    fn from(_: num_enum::TryFromPrimitiveError<Bandwidth>) -> Self {
38        crate::error::Error::ReservedBitsViolation {
39            field: "bw",
40            reason: "Must be 0..=5 per ETSI TS 102 773 §5.2.7 Table 3",
41        }
42    }
43}
44
45/// DVB-T2 timestamp payload (type 0x20) per ETSI TS 102 773 §5.2.7.
46///
47/// Layout (88 bits = 11 bytes):
48/// - byte 0 `[7:4]`: rfu (4 bits) — must be 0
49/// - byte 0 `[3:0]`: bw (4 bits) — Table 3
50/// - bytes 1-5: seconds_since_2000 (40 bits)
51/// - subseconds (27 bits): bytes 6-8 + byte 9 `[7:5]`
52/// - utco (13 bits): byte 9 `[4:0]` + byte 10 — UTC offset in seconds
53#[derive(Debug, Clone, PartialEq, Eq)]
54#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
55pub struct T2TimestampPayload {
56    /// Bandwidth (determines Tsub units).
57    pub bw: Bandwidth,
58    /// Seconds since 2000-01-01T00:00:00Z. 0 = relative timestamp.
59    /// If all bits are 1 along with subseconds + utco, this is a Null timestamp.
60    pub seconds_since_2000: u64,
61    /// Subsecond count (27 bits).
62    pub subseconds: u32,
63    /// UTC offset in seconds (e.g. 34 for leap seconds as of 2016).
64    pub utco: u16,
65}
66
67const TIMESTAMP_HEADER_LEN: usize = 11;
68
69impl<'a> Parse<'a> for T2TimestampPayload {
70    type Error = crate::error::Error;
71
72    fn parse(bytes: &'a [u8]) -> Result<Self, crate::error::Error> {
73        if bytes.len() < TIMESTAMP_HEADER_LEN {
74            return Err(crate::Error::BufferTooShort {
75                need: TIMESTAMP_HEADER_LEN,
76                have: bytes.len(),
77                what: "T2TimestampPayload header",
78            });
79        }
80
81        // byte 0 [7:4] = rfu
82        if bytes[0] & 0xF0 != 0 {
83            return Err(crate::Error::ReservedBitsViolation {
84                field: "4-bit RFU",
85                reason: "Must be zero (ETSI TS 102 773 §5.2.7)",
86            });
87        }
88
89        let bw = Bandwidth::try_from(bytes[0] & 0x0F)?;
90
91        // bytes 1-5: seconds_since_2000 (40 bits)
92        let seconds_since_2000 = (bytes[1] as u64) << 32
93            | (bytes[2] as u64) << 24
94            | (bytes[3] as u64) << 16
95            | (bytes[4] as u64) << 8
96            | (bytes[5] as u64);
97
98        // bytes 6-8 [31:5]: subseconds (27 bits)
99        // bytes 6-7-8 = 24 bits, but subseconds extends into byte 9
100        // 27 bits: bytes 6-8 (24 bits) + byte 9 [7:5] (3 bits)
101        let subseconds = (bytes[6] as u32) << 19
102            | (bytes[7] as u32) << 11
103            | (bytes[8] as u32) << 3
104            | ((bytes[9] >> 5) as u32 & 0x7);
105
106        // byte 9 [4:0] + byte 10: utco (13 bits)
107        let utco = ((bytes[9] as u16 & 0x1F) << 8) | (bytes[10] as u16);
108
109        Ok(T2TimestampPayload {
110            bw,
111            seconds_since_2000,
112            subseconds,
113            utco,
114        })
115    }
116}
117
118impl Serialize for T2TimestampPayload {
119    type Error = crate::error::Error;
120
121    fn serialized_len(&self) -> usize {
122        TIMESTAMP_HEADER_LEN
123    }
124
125    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize, crate::error::Error> {
126        if buf.len() < self.serialized_len() {
127            return Err(crate::Error::OutputBufferTooSmall {
128                need: self.serialized_len(),
129                have: buf.len(),
130            });
131        }
132
133        if self.seconds_since_2000 > 0xFF_FFFF_FFFF {
134            return Err(crate::Error::ReservedBitsViolation {
135                field: "seconds_since_2000",
136                reason: "Must fit in 40 bits",
137            });
138        }
139        if self.subseconds > 0x7FFFFFF {
140            return Err(crate::Error::ReservedBitsViolation {
141                field: "subseconds",
142                reason: "Must fit in 27 bits",
143            });
144        }
145        if self.utco > 0x1FFF {
146            return Err(crate::Error::ReservedBitsViolation {
147                field: "utco",
148                reason: "Must fit in 13 bits",
149            });
150        }
151
152        buf[0] = u8::from(self.bw) & 0x0F; // RFU = 0
153        buf[1] = (self.seconds_since_2000 >> 32 & 0xFF) as u8;
154        buf[2] = (self.seconds_since_2000 >> 24 & 0xFF) as u8;
155        buf[3] = (self.seconds_since_2000 >> 16 & 0xFF) as u8;
156        buf[4] = (self.seconds_since_2000 >> 8 & 0xFF) as u8;
157        buf[5] = (self.seconds_since_2000 & 0xFF) as u8;
158        buf[6] = (self.subseconds >> 19 & 0xFF) as u8;
159        buf[7] = (self.subseconds >> 11 & 0xFF) as u8;
160        buf[8] = (self.subseconds >> 3 & 0xFF) as u8;
161        buf[9] = ((self.subseconds & 0x7) as u8) << 5 | ((self.utco >> 8) as u8 & 0x1F);
162        buf[10] = (self.utco & 0xFF) as u8;
163
164        Ok(self.serialized_len())
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn bandwidth_try_from_valid() {
174        assert_eq!(Bandwidth::try_from(0), Ok(Bandwidth::Mhz1_7));
175        assert_eq!(Bandwidth::try_from(5), Ok(Bandwidth::Mhz10));
176    }
177
178    #[test]
179    fn bandwidth_try_from_rejects_6() {
180        assert!(Bandwidth::try_from(6).is_err());
181    }
182
183    #[test]
184    fn exhaustive_byte_sweep() {
185        let mut matched = 0u16;
186        for byte in 0u8..=0xFF {
187            if let Ok(v) = Bandwidth::try_from(byte) {
188                assert_eq!(v as u8, byte, "round-trip failed for {byte:#04x}");
189                matched += 1;
190            }
191        }
192        assert_eq!(matched, 6, "expected 6 matched variants");
193    }
194
195    #[test]
196    fn parse_extracts_all_fields() {
197        let mut buf = [0u8; 11];
198        buf[0] = 0x02; // bw = 6MHz
199        buf[1] = 0x00;
200        buf[2] = 0x00;
201        buf[3] = 0x01; // seconds_since_2000 = 65536 + 256 + ... let me just set simple values
202        buf[6] = 0x00;
203        buf[7] = 0x00;
204        buf[8] = 0x00;
205        buf[9] = 0x00; // subseconds=0, utco=0
206        buf[10] = 0x00;
207
208        let result = T2TimestampPayload::parse(&buf).unwrap();
209        assert_eq!(result.bw, Bandwidth::Mhz6);
210        assert_eq!(result.seconds_since_2000, 0x00_00_01_00_00);
211    }
212
213    #[test]
214    fn parse_rejects_nonzero_rfu() {
215        let buf = [
216            0x80u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
217        ];
218        assert!(T2TimestampPayload::parse(&buf).is_err());
219    }
220
221    #[test]
222    fn parse_rejects_short_buffer() {
223        assert!(T2TimestampPayload::parse(&[0x00; 10]).is_err());
224    }
225
226    #[test]
227    fn serialize_round_trip() {
228        let orig = T2TimestampPayload {
229            bw: Bandwidth::Mhz8,
230            seconds_since_2000: 0x00_00_01_02_03,
231            subseconds: 0x0123456,
232            utco: 0x7FF,
233        };
234        let mut buf = [0u8; 11];
235        orig.serialize_into(&mut buf).unwrap();
236        let parsed = T2TimestampPayload::parse(&buf).unwrap();
237        assert_eq!(orig, parsed);
238    }
239
240    #[test]
241    fn null_timestamp_all_ones() {
242        let mut buf = [0xFFu8; 11];
243        buf[0] = 0x0F; // bw = max valid (5), rest RFU = 1 (should fail)
244                       // Actually for null timestamp, RFU bits should still be 0
245        buf[0] = 0x0F; // rfu=0(4 bits) + bw=1111=0xF (but F=15 is invalid per TryFrom)
246                       // Let me set bw=0, rest=1
247        buf[0] = 0x00; // rfu=0, bw=0 — but then seconds_since_2000 bits...
248                       // For null timestamp: all bits of seconds, subseconds, utco = 1, but bw + rfu normal
249        buf[0] = 0x02; // bw=6MHz
250        buf[1..11].fill(0xFF);
251        let result = T2TimestampPayload::parse(&buf);
252        assert!(result.is_ok());
253        let parsed = result.unwrap();
254        assert_eq!(parsed.seconds_since_2000, 0xFFFFFFFFFF); // 40 bits all 1
255        assert_eq!(parsed.subseconds, 0x7FFFFFF); // 27 bits all 1
256        assert_eq!(parsed.utco, 0x1FFF); // 13 bits all 1
257    }
258}