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))]
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))]
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<'a> crate::traits::PayloadDef<'a> for T2TimestampPayload {
119    const PACKET_TYPE: u8 = 0x20;
120    const NAME: &'static str = "TIMESTAMP";
121}
122
123impl Serialize for T2TimestampPayload {
124    type Error = crate::error::Error;
125
126    fn serialized_len(&self) -> usize {
127        TIMESTAMP_HEADER_LEN
128    }
129
130    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize, crate::error::Error> {
131        if buf.len() < self.serialized_len() {
132            return Err(crate::Error::OutputBufferTooSmall {
133                need: self.serialized_len(),
134                have: buf.len(),
135            });
136        }
137
138        if self.seconds_since_2000 > 0xFF_FFFF_FFFF {
139            return Err(crate::Error::ReservedBitsViolation {
140                field: "seconds_since_2000",
141                reason: "Must fit in 40 bits",
142            });
143        }
144        if self.subseconds > 0x7FFFFFF {
145            return Err(crate::Error::ReservedBitsViolation {
146                field: "subseconds",
147                reason: "Must fit in 27 bits",
148            });
149        }
150        if self.utco > 0x1FFF {
151            return Err(crate::Error::ReservedBitsViolation {
152                field: "utco",
153                reason: "Must fit in 13 bits",
154            });
155        }
156
157        buf[0] = u8::from(self.bw) & 0x0F; // RFU = 0
158        buf[1] = (self.seconds_since_2000 >> 32 & 0xFF) as u8;
159        buf[2] = (self.seconds_since_2000 >> 24 & 0xFF) as u8;
160        buf[3] = (self.seconds_since_2000 >> 16 & 0xFF) as u8;
161        buf[4] = (self.seconds_since_2000 >> 8 & 0xFF) as u8;
162        buf[5] = (self.seconds_since_2000 & 0xFF) as u8;
163        buf[6] = (self.subseconds >> 19 & 0xFF) as u8;
164        buf[7] = (self.subseconds >> 11 & 0xFF) as u8;
165        buf[8] = (self.subseconds >> 3 & 0xFF) as u8;
166        buf[9] = ((self.subseconds & 0x7) as u8) << 5 | ((self.utco >> 8) as u8 & 0x1F);
167        buf[10] = (self.utco & 0xFF) as u8;
168
169        Ok(self.serialized_len())
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn bandwidth_try_from_valid() {
179        assert_eq!(Bandwidth::try_from(0), Ok(Bandwidth::Mhz1_7));
180        assert_eq!(Bandwidth::try_from(5), Ok(Bandwidth::Mhz10));
181    }
182
183    #[test]
184    fn bandwidth_try_from_rejects_6() {
185        assert!(Bandwidth::try_from(6).is_err());
186    }
187
188    #[test]
189    fn exhaustive_byte_sweep() {
190        let mut matched = 0u16;
191        for byte in 0u8..=0xFF {
192            if let Ok(v) = Bandwidth::try_from(byte) {
193                assert_eq!(v as u8, byte, "round-trip failed for {byte:#04x}");
194                matched += 1;
195            }
196        }
197        assert_eq!(matched, 6, "expected 6 matched variants");
198    }
199
200    #[test]
201    fn parse_extracts_all_fields() {
202        let mut buf = [0u8; 11];
203        buf[0] = 0x02; // bw = 6MHz
204        buf[1] = 0x00;
205        buf[2] = 0x00;
206        buf[3] = 0x01; // seconds_since_2000 = 65536 + 256 + ... let me just set simple values
207        buf[6] = 0x00;
208        buf[7] = 0x00;
209        buf[8] = 0x00;
210        buf[9] = 0x00; // subseconds=0, utco=0
211        buf[10] = 0x00;
212
213        let result = T2TimestampPayload::parse(&buf).unwrap();
214        assert_eq!(result.bw, Bandwidth::Mhz6);
215        assert_eq!(result.seconds_since_2000, 0x00_00_01_00_00);
216    }
217
218    #[test]
219    fn parse_rejects_nonzero_rfu() {
220        let buf = [
221            0x80u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
222        ];
223        assert!(T2TimestampPayload::parse(&buf).is_err());
224    }
225
226    #[test]
227    fn parse_rejects_short_buffer() {
228        assert!(T2TimestampPayload::parse(&[0x00; 10]).is_err());
229    }
230
231    #[test]
232    fn serialize_round_trip() {
233        let orig = T2TimestampPayload {
234            bw: Bandwidth::Mhz8,
235            seconds_since_2000: 0x00_00_01_02_03,
236            subseconds: 0x0123456,
237            utco: 0x7FF,
238        };
239        let mut buf = [0u8; 11];
240        orig.serialize_into(&mut buf).unwrap();
241        let parsed = T2TimestampPayload::parse(&buf).unwrap();
242        assert_eq!(orig, parsed);
243    }
244
245    #[test]
246    fn null_timestamp_all_ones() {
247        let mut buf = [0xFFu8; 11];
248        buf[0] = 0x0F; // bw = max valid (5), rest RFU = 1 (should fail)
249                       // Actually for null timestamp, RFU bits should still be 0
250        buf[0] = 0x0F; // rfu=0(4 bits) + bw=1111=0xF (but F=15 is invalid per TryFrom)
251                       // Let me set bw=0, rest=1
252        buf[0] = 0x00; // rfu=0, bw=0 — but then seconds_since_2000 bits...
253                       // For null timestamp: all bits of seconds, subseconds, utco = 1, but bw + rfu normal
254        buf[0] = 0x02; // bw=6MHz
255        buf[1..11].fill(0xFF);
256        let result = T2TimestampPayload::parse(&buf);
257        assert!(result.is_ok());
258        let parsed = result.unwrap();
259        assert_eq!(parsed.seconds_since_2000, 0xFFFFFFFFFF); // 40 bits all 1
260        assert_eq!(parsed.subseconds, 0x7FFFFFF); // 27 bits all 1
261        assert_eq!(parsed.utco, 0x1FFF); // 13 bits all 1
262    }
263}