Skip to main content

dvb_t2mi/payload/
fef_null.rs

1//! T2-MI payload type 0x30: FEF part — Null — §5.2.9.
2//!
3//! Null FEF part — modulator generates P1 preamble per S1/S2, zeros for remainder.
4
5use core::fmt;
6
7use num_enum::TryFromPrimitive;
8
9use broadcast_common::{Parse, Serialize};
10
11/// S1 field (3 bits) per EN 302 755 §7.2.1.
12#[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    /// S1 value V0 (000 = T2_SISO).
18    V0 = 0,
19    /// S1 value V1 (001 = T2_MISO).
20    V1 = 1,
21    /// S1 value V2 (010 = Non-T2).
22    V2 = 2,
23    /// S1 value V3 (011 = T2_LITE_SISO).
24    V3 = 3,
25    /// S1 value V4 (100 = T2_LITE_MISO).
26    V4 = 4,
27    /// S1 value V5 (101 = reserved).
28    V5 = 5,
29    /// S1 value V6 (110 = reserved).
30    V6 = 6,
31    /// S1 value V7 (111 = reserved).
32    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    /// Per EN 302 755 §7.2.1 Table 18.
52    #[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    /// Human-readable spec label (EN 302 755 §7.2.1 Table 18) — the S1 meaning.
67    #[must_use]
68    pub fn name(&self) -> &'static str {
69        self.meaning()
70    }
71}
72broadcast_common::impl_spec_display!(S1Field);
73
74/// S2 field 1 (upper 3 bits of the 4-bit S2 field) per EN 302 755 §7.2.3 Tables 19-20.
75///
76/// Encodes the FFT size and guard-interval set for the T2 frame.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78#[cfg_attr(feature = "serde", derive(serde::Serialize))]
79#[non_exhaustive]
80pub enum S2Field1 {
81    /// FFT size 1k / GI 1/128, 1/32, 1/16, 19/256, 1/8, 19/128, 1/4.
82    Fft1k,
83    /// FFT size 2k / GI 1/128, 1/32, 1/16, 19/256, 1/8, 19/128, 1/4.
84    Fft2k,
85    /// FFT size 4k / GI 1/128, 1/32, 1/16, 19/256, 1/8, 19/128, 1/4.
86    Fft4k,
87    /// FFT size 8k / GI 1/128, 1/32, 1/16, 19/256, 1/8, 19/128, 1/4.
88    Fft8k,
89    /// FFT size 16k / GI 1/128, 1/32, 1/16, 1/8, 19/128, 1/4.
90    Fft16k,
91    /// FFT size 32k / GI 1/128, 1/32, 1/16, 19/256.
92    Fft32k,
93    /// Reserved.
94    Reserved1,
95    /// Reserved.
96    Reserved2,
97}
98
99impl S2Field1 {
100    /// Decode from the 3-bit S2 field 1 (bits `[6:4]` of the S2 byte).
101    /// Decode from the wire byte.  Every byte maps to a variant (lossless).
102    #[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    /// Encode to 3-bit value.
117    /// Encode to the wire byte.  Inverse of `from_u8`.
118    #[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    /// FFT size name (e.g. "1k", "2k").
134    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    /// Guard-interval set description.
148    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    /// Human-readable spec label (EN 302 755 §7.2.3 Tables 19-20) — the FFT size.
161    #[must_use]
162    pub fn name(&self) -> &'static str {
163        self.fft_size()
164    }
165}
166broadcast_common::impl_spec_display!(S2Field1);
167
168/// FEF part: Null payload (type 0x30) per ETSI TS 102 773 §5.2.9.
169#[derive(Debug, Clone, PartialEq, Eq)]
170#[cfg_attr(feature = "serde", derive(serde::Serialize))]
171pub struct FefNullPayload {
172    /// FEF index within super-frame.
173    pub fef_idx: u8,
174    /// S1 field per EN 302 755 §7.2.1.
175    pub s1_field: S1Field,
176    /// S2 field per EN 302 755 §7.2.1.
177    pub s2_field: u8,
178}
179
180impl FefNullPayload {
181    /// Decode S2 field 1 (3 bits, upper nibble minus rfu).
182    #[must_use]
183    pub fn s2_field1(&self) -> S2Field1 {
184        S2Field1::from_u8(self.s2_field >> 1)
185    }
186
187    /// S2 field 2: mixed flag (1 bit, bit 0).
188    #[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        // Layout (Figure 12): fef_idx(8) | rfu(9) | s1_field(3) | s2_field(4).
208        // rfu spans all of byte 1 plus the top bit of byte 2.
209        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            // byte 2: rfu(1) | s1_field(3) [6:4] | s2_field(4) [3:0]
218            s1_field: S1Field::try_from((bytes[2] >> 4) & 0x07)?,
219            s2_field: bytes[2] & 0x0F,
220        })
221    }
222}
223
224impl crate::traits::PayloadDef<'_> 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; // rfu (high 8 of the 9 reserved bits)
251        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        // fef_idx=5, rfu=0, byte2 = s1(1)<<4 | s2(0x0A) = 0x1A
276        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        // s2_field = 0b0001 -> S2 field 1 = 000, mixed = 1
337        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        // s2_field = 0b1100 -> S2 field 1 = 110 (reserved), mixed = 0
346        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}