dvb_t2mi/payload/
timestamp.rs1use num_enum::TryFromPrimitive;
8
9use dvb_common::{Parse, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[repr(u8)]
15pub enum Bandwidth {
16 Mhz1_7 = 0,
18 Mhz5 = 1,
20 Mhz6 = 2,
22 Mhz7 = 3,
24 Mhz8 = 4,
26 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#[derive(Debug, Clone, PartialEq, Eq)]
54#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
55pub struct T2TimestampPayload {
56 pub bw: Bandwidth,
58 pub seconds_since_2000: u64,
61 pub subseconds: u32,
63 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 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 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 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 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; 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; buf[1] = 0x00;
200 buf[2] = 0x00;
201 buf[3] = 0x01; buf[6] = 0x00;
203 buf[7] = 0x00;
204 buf[8] = 0x00;
205 buf[9] = 0x00; 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; buf[0] = 0x0F; buf[0] = 0x00; buf[0] = 0x02; 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); assert_eq!(parsed.subseconds, 0x7FFFFFF); assert_eq!(parsed.utco, 0x1FFF); }
258}