1use core::fmt;
6
7use num_enum::TryFromPrimitive;
8
9use broadcast_common::{Parse, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize))]
14#[repr(u8)]
15#[non_exhaustive]
16pub enum S1Field {
17 V0 = 0,
19 V1 = 1,
21 V2 = 2,
23 V3 = 3,
25 V4 = 4,
27 V5 = 5,
29 V6 = 6,
31 V7 = 7,
33}
34
35impl From<S1Field> for u8 {
36 fn from(s: S1Field) -> Self {
37 s as u8
38 }
39}
40
41impl From<num_enum::TryFromPrimitiveError<S1Field>> for crate::error::Error {
42 fn from(_: num_enum::TryFromPrimitiveError<S1Field>) -> Self {
43 crate::error::Error::ReservedBitsViolation {
44 field: "s1_field",
45 reason: "Must be 0..=7",
46 }
47 }
48}
49
50impl S1Field {
51 #[must_use]
53 pub fn meaning(self) -> &'static str {
54 match self {
55 Self::V0 => "T2_SISO",
56 Self::V1 => "T2_MISO",
57 Self::V2 => "Non-T2",
58 Self::V3 => "T2_LITE_SISO",
59 Self::V4 => "T2_LITE_MISO",
60 Self::V5 => "reserved",
61 Self::V6 => "reserved",
62 Self::V7 => "reserved",
63 }
64 }
65
66 #[must_use]
68 pub fn name(&self) -> &'static str {
69 self.meaning()
70 }
71}
72broadcast_common::impl_spec_display!(S1Field);
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78#[cfg_attr(feature = "serde", derive(serde::Serialize))]
79#[non_exhaustive]
80pub enum S2Field1 {
81 Fft1k,
83 Fft2k,
85 Fft4k,
87 Fft8k,
89 Fft16k,
91 Fft32k,
93 Reserved1,
95 Reserved2,
97}
98
99impl S2Field1 {
100 #[must_use]
103 pub fn from_u8(v: u8) -> Self {
104 match v & 0x07 {
105 0 => Self::Fft1k,
106 1 => Self::Fft2k,
107 2 => Self::Fft4k,
108 3 => Self::Fft8k,
109 4 => Self::Fft16k,
110 5 => Self::Fft32k,
111 6 => Self::Reserved1,
112 _ => Self::Reserved2,
113 }
114 }
115
116 #[must_use]
119 pub const fn to_u8(self) -> u8 {
120 match self {
121 Self::Fft1k => 0,
122 Self::Fft2k => 1,
123 Self::Fft4k => 2,
124 Self::Fft8k => 3,
125 Self::Fft16k => 4,
126 Self::Fft32k => 5,
127 Self::Reserved1 => 6,
128 Self::Reserved2 => 7,
129 }
130 }
131
132 #[must_use]
133 pub fn fft_size(self) -> &'static str {
135 match self {
136 Self::Fft1k => "1k",
137 Self::Fft2k => "2k",
138 Self::Fft4k => "4k",
139 Self::Fft8k => "8k",
140 Self::Fft16k => "16k",
141 Self::Fft32k => "32k",
142 Self::Reserved1 | Self::Reserved2 => "reserved",
143 }
144 }
145
146 #[must_use]
147 pub fn guard_interval_set(self) -> &'static str {
149 match self {
150 Self::Fft1k => "1/128, 1/32, 1/16, 19/256, 1/8, 19/128, 1/4",
151 Self::Fft2k => "1/128, 1/32, 1/16, 19/256, 1/8, 19/128, 1/4",
152 Self::Fft4k => "1/128, 1/32, 1/16, 19/256, 1/8, 19/128, 1/4",
153 Self::Fft8k => "1/128, 1/32, 1/16, 19/256, 1/8, 19/128, 1/4",
154 Self::Fft16k => "1/128, 1/32, 1/16, 1/8, 19/128, 1/4",
155 Self::Fft32k => "1/128, 1/32, 1/16, 19/256",
156 Self::Reserved1 | Self::Reserved2 => "reserved",
157 }
158 }
159
160 #[must_use]
162 pub fn name(&self) -> &'static str {
163 self.fft_size()
164 }
165}
166broadcast_common::impl_spec_display!(S2Field1);
167
168#[derive(Debug, Clone, PartialEq, Eq)]
170#[cfg_attr(feature = "serde", derive(serde::Serialize))]
171pub struct FefNullPayload {
172 pub fef_idx: u8,
174 pub s1_field: S1Field,
176 pub s2_field: u8,
178}
179
180impl FefNullPayload {
181 #[must_use]
183 pub fn s2_field1(&self) -> S2Field1 {
184 S2Field1::from_u8(self.s2_field >> 1)
185 }
186
187 #[must_use]
189 pub fn is_mixed(&self) -> bool {
190 (self.s2_field & 0x01) != 0
191 }
192}
193
194const FEF_NULL_LEN: usize = 3;
195
196impl<'a> Parse<'a> for FefNullPayload {
197 type Error = crate::error::Error;
198
199 fn parse(bytes: &'a [u8]) -> Result<Self, crate::error::Error> {
200 if bytes.len() < FEF_NULL_LEN {
201 return Err(crate::Error::BufferTooShort {
202 need: FEF_NULL_LEN,
203 have: bytes.len(),
204 what: "FefNullPayload",
205 });
206 }
207 if bytes[1] != 0 || bytes[2] & 0x80 != 0 {
210 return Err(crate::Error::ReservedBitsViolation {
211 field: "9-bit rfu",
212 reason: "Must be zero (ETSI TS 102 773 §5.2.9)",
213 });
214 }
215 Ok(FefNullPayload {
216 fef_idx: bytes[0],
217 s1_field: S1Field::try_from((bytes[2] >> 4) & 0x07)?,
219 s2_field: bytes[2] & 0x0F,
220 })
221 }
222}
223
224impl<'a> crate::traits::PayloadDef<'a> for FefNullPayload {
225 const PACKET_TYPE: u8 = 0x30;
226 const NAME: &'static str = "FEF_NULL";
227}
228
229impl Serialize for FefNullPayload {
230 type Error = crate::error::Error;
231
232 fn serialized_len(&self) -> usize {
233 FEF_NULL_LEN
234 }
235
236 fn serialize_into(&self, buf: &mut [u8]) -> Result<usize, crate::error::Error> {
237 if buf.len() < self.serialized_len() {
238 return Err(crate::Error::OutputBufferTooSmall {
239 need: self.serialized_len(),
240 have: buf.len(),
241 });
242 }
243 if self.s2_field > 0x0F {
244 return Err(crate::Error::ReservedBitsViolation {
245 field: "s2_field",
246 reason: "Must fit in 4 bits",
247 });
248 }
249 buf[0] = self.fef_idx;
250 buf[1] = 0; buf[2] = ((u8::from(self.s1_field) & 0x07) << 4) | (self.s2_field & 0x0F);
252 Ok(self.serialized_len())
253 }
254}
255
256impl fmt::Display for FefNullPayload {
257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258 write!(
259 f,
260 "FEF Null {{ fef_idx: {}, s1: {:?}({}), s2: {:04b} }}",
261 self.fef_idx,
262 self.s1_field,
263 self.s1_field.meaning(),
264 self.s2_field
265 )
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn parse_extracts_fields() {
275 let buf = [0x05u8, 0x00, 0x1A];
277 let result = FefNullPayload::parse(&buf).unwrap();
278 assert_eq!(result.fef_idx, 5);
279 assert_eq!(result.s1_field, S1Field::V1);
280 assert_eq!(result.s2_field, 0x0A);
281 }
282
283 #[test]
284 fn parse_rejects_nonzero_rfu() {
285 let buf = [0x00u8, 0x1F, 0x00];
286 assert!(FefNullPayload::parse(&buf).is_err());
287 }
288
289 #[test]
290 fn serialize_round_trip() {
291 let orig = FefNullPayload {
292 fef_idx: 3,
293 s1_field: S1Field::V4,
294 s2_field: 0x0C,
295 };
296 let mut buf = [0u8; FEF_NULL_LEN];
297 orig.serialize_into(&mut buf).unwrap();
298 let parsed = FefNullPayload::parse(&buf).unwrap();
299 assert_eq!(orig, parsed);
300 }
301
302 #[test]
303 fn display_output() {
304 let p = FefNullPayload {
305 fef_idx: 0,
306 s1_field: S1Field::V0,
307 s2_field: 0,
308 };
309 assert!(p.to_string().contains("FEF Null"));
310 }
311
312 #[test]
313 fn exhaustive_byte_sweep() {
314 let mut matched = 0u16;
315 for byte in 0u8..=0xFF {
316 if let Ok(v) = S1Field::try_from(byte) {
317 assert_eq!(v as u8, byte, "round-trip failed for {byte:#04x}");
318 matched += 1;
319 }
320 }
321 assert_eq!(matched, 8, "expected 8 matched variants");
322 }
323
324 #[test]
325 fn s1_meaning_values() {
326 assert_eq!(S1Field::V0.meaning(), "T2_SISO");
327 assert_eq!(S1Field::V1.meaning(), "T2_MISO");
328 assert_eq!(S1Field::V2.meaning(), "Non-T2");
329 assert_eq!(S1Field::V3.meaning(), "T2_LITE_SISO");
330 assert_eq!(S1Field::V4.meaning(), "T2_LITE_MISO");
331 assert_eq!(S1Field::V5.meaning(), "reserved");
332 }
333
334 #[test]
335 fn s2_field1_decode() {
336 let p = FefNullPayload {
338 fef_idx: 0,
339 s1_field: S1Field::V0,
340 s2_field: 0x01,
341 };
342 assert_eq!(p.s2_field1(), S2Field1::Fft1k);
343 assert!(p.is_mixed());
344
345 let p = FefNullPayload {
347 fef_idx: 0,
348 s1_field: S1Field::V0,
349 s2_field: 0x0C,
350 };
351 assert_eq!(p.s2_field1(), S2Field1::Reserved1);
352 assert!(!p.is_mixed());
353 }
354
355 #[test]
356 fn s2_field1_round_trip() {
357 for v in 0u8..=7 {
358 let s2 = S2Field1::from_u8(v);
359 assert_eq!(s2.to_u8(), v, "S2Field1 round-trip failed for {v}");
360 }
361 }
362}