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))]
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))]
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<'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; 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; buf[1] = 0x00;
205 buf[2] = 0x00;
206 buf[3] = 0x01; buf[6] = 0x00;
208 buf[7] = 0x00;
209 buf[8] = 0x00;
210 buf[9] = 0x00; 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; buf[0] = 0x0F; buf[0] = 0x00; buf[0] = 0x02; 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); assert_eq!(parsed.subseconds, 0x7FFFFFF); assert_eq!(parsed.utco, 0x1FFF); }
263}