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 std::fmt;
6
7use num_enum::TryFromPrimitive;
8
9use dvb_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)]
15pub enum S1Field {
16    /// S1 value V0 (000 = T2_SISO).
17    V0 = 0,
18    /// S1 value V1 (001 = T2_MISO).
19    V1 = 1,
20    /// S1 value V2 (010 = Non-T2).
21    V2 = 2,
22    /// S1 value V3 (011 = T2_LITE_SISO).
23    V3 = 3,
24    /// S1 value V4 (100 = T2_LITE_MISO).
25    V4 = 4,
26    /// S1 value V5 (101 = reserved).
27    V5 = 5,
28    /// S1 value V6 (110 = reserved).
29    V6 = 6,
30    /// S1 value V7 (111 = reserved).
31    V7 = 7,
32}
33
34impl From<S1Field> for u8 {
35    fn from(s: S1Field) -> Self {
36        s as u8
37    }
38}
39
40impl From<num_enum::TryFromPrimitiveError<S1Field>> for crate::error::Error {
41    fn from(_: num_enum::TryFromPrimitiveError<S1Field>) -> Self {
42        crate::error::Error::ReservedBitsViolation {
43            field: "s1_field",
44            reason: "Must be 0..=7",
45        }
46    }
47}
48
49impl S1Field {
50    /// Per EN 302 755 §7.2.1 Table 18.
51    #[must_use]
52    pub fn meaning(self) -> &'static str {
53        match self {
54            Self::V0 => "T2_SISO",
55            Self::V1 => "T2_MISO",
56            Self::V2 => "Non-T2",
57            Self::V3 => "T2_LITE_SISO",
58            Self::V4 => "T2_LITE_MISO",
59            Self::V5 => "reserved",
60            Self::V6 => "reserved",
61            Self::V7 => "reserved",
62        }
63    }
64}
65
66/// S2 field 1 (upper 3 bits of the 4-bit S2 field) per EN 302 755 §7.2.3 Tables 19-20.
67///
68/// Encodes the FFT size and guard-interval set for the T2 frame.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70#[cfg_attr(feature = "serde", derive(serde::Serialize))]
71#[non_exhaustive]
72pub enum S2Field1 {
73    /// FFT size 1k / GI 1/128, 1/32, 1/16, 19/256, 1/8, 19/128, 1/4.
74    Fft1k,
75    /// FFT size 2k / GI 1/128, 1/32, 1/16, 19/256, 1/8, 19/128, 1/4.
76    Fft2k,
77    /// FFT size 4k / GI 1/128, 1/32, 1/16, 19/256, 1/8, 19/128, 1/4.
78    Fft4k,
79    /// FFT size 8k / GI 1/128, 1/32, 1/16, 19/256, 1/8, 19/128, 1/4.
80    Fft8k,
81    /// FFT size 16k / GI 1/128, 1/32, 1/16, 1/8, 19/128, 1/4.
82    Fft16k,
83    /// FFT size 32k / GI 1/128, 1/32, 1/16, 19/256.
84    Fft32k,
85    /// Reserved.
86    Reserved1,
87    /// Reserved.
88    Reserved2,
89}
90
91impl S2Field1 {
92    /// Decode from the 3-bit S2 field 1 (bits `[6:4]` of the S2 byte).
93    /// Decode from the wire byte.  Every byte maps to a variant (lossless).
94    #[must_use]
95    pub fn from_u8(v: u8) -> Self {
96        match v & 0x07 {
97            0 => Self::Fft1k,
98            1 => Self::Fft2k,
99            2 => Self::Fft4k,
100            3 => Self::Fft8k,
101            4 => Self::Fft16k,
102            5 => Self::Fft32k,
103            6 => Self::Reserved1,
104            _ => Self::Reserved2,
105        }
106    }
107
108    /// Encode to 3-bit value.
109    /// Encode to the wire byte.  Inverse of `from_u8`.
110    #[must_use]
111    pub fn to_u8(self) -> u8 {
112        match self {
113            Self::Fft1k => 0,
114            Self::Fft2k => 1,
115            Self::Fft4k => 2,
116            Self::Fft8k => 3,
117            Self::Fft16k => 4,
118            Self::Fft32k => 5,
119            Self::Reserved1 => 6,
120            Self::Reserved2 => 7,
121        }
122    }
123
124    #[must_use]
125    /// FFT size name (e.g. "1k", "2k").
126    pub fn fft_size(self) -> &'static str {
127        match self {
128            Self::Fft1k => "1k",
129            Self::Fft2k => "2k",
130            Self::Fft4k => "4k",
131            Self::Fft8k => "8k",
132            Self::Fft16k => "16k",
133            Self::Fft32k => "32k",
134            Self::Reserved1 | Self::Reserved2 => "reserved",
135        }
136    }
137
138    #[must_use]
139    /// Guard-interval set description.
140    pub fn guard_interval_set(self) -> &'static str {
141        match self {
142            Self::Fft1k => "1/128, 1/32, 1/16, 19/256, 1/8, 19/128, 1/4",
143            Self::Fft2k => "1/128, 1/32, 1/16, 19/256, 1/8, 19/128, 1/4",
144            Self::Fft4k => "1/128, 1/32, 1/16, 19/256, 1/8, 19/128, 1/4",
145            Self::Fft8k => "1/128, 1/32, 1/16, 19/256, 1/8, 19/128, 1/4",
146            Self::Fft16k => "1/128, 1/32, 1/16, 1/8, 19/128, 1/4",
147            Self::Fft32k => "1/128, 1/32, 1/16, 19/256",
148            Self::Reserved1 | Self::Reserved2 => "reserved",
149        }
150    }
151}
152
153/// FEF part: Null payload (type 0x30) per ETSI TS 102 773 §5.2.9.
154#[derive(Debug, Clone, PartialEq, Eq)]
155#[cfg_attr(feature = "serde", derive(serde::Serialize))]
156pub struct FefNullPayload {
157    /// FEF index within super-frame.
158    pub fef_idx: u8,
159    /// S1 field per EN 302 755 §7.2.1.
160    pub s1_field: S1Field,
161    /// S2 field per EN 302 755 §7.2.1.
162    pub s2_field: u8,
163}
164
165impl FefNullPayload {
166    /// Decode S2 field 1 (3 bits, upper nibble minus rfu).
167    #[must_use]
168    pub fn s2_field1(&self) -> S2Field1 {
169        S2Field1::from_u8(self.s2_field >> 1)
170    }
171
172    /// S2 field 2: mixed flag (1 bit, bit 0).
173    #[must_use]
174    pub fn is_mixed(&self) -> bool {
175        (self.s2_field & 0x01) != 0
176    }
177}
178
179const FEF_NULL_LEN: usize = 3;
180
181impl<'a> Parse<'a> for FefNullPayload {
182    type Error = crate::error::Error;
183
184    fn parse(bytes: &'a [u8]) -> Result<Self, crate::error::Error> {
185        if bytes.len() < FEF_NULL_LEN {
186            return Err(crate::Error::BufferTooShort {
187                need: FEF_NULL_LEN,
188                have: bytes.len(),
189                what: "FefNullPayload",
190            });
191        }
192        // Layout (Figure 12): fef_idx(8) | rfu(9) | s1_field(3) | s2_field(4).
193        // rfu spans all of byte 1 plus the top bit of byte 2.
194        if bytes[1] != 0 || bytes[2] & 0x80 != 0 {
195            return Err(crate::Error::ReservedBitsViolation {
196                field: "9-bit rfu",
197                reason: "Must be zero (ETSI TS 102 773 §5.2.9)",
198            });
199        }
200        Ok(FefNullPayload {
201            fef_idx: bytes[0],
202            // byte 2: rfu(1) | s1_field(3) [6:4] | s2_field(4) [3:0]
203            s1_field: S1Field::try_from((bytes[2] >> 4) & 0x07)?,
204            s2_field: bytes[2] & 0x0F,
205        })
206    }
207}
208
209impl<'a> crate::traits::PayloadDef<'a> for FefNullPayload {
210    const PACKET_TYPE: u8 = 0x30;
211    const NAME: &'static str = "FEF_NULL";
212}
213
214impl Serialize for FefNullPayload {
215    type Error = crate::error::Error;
216
217    fn serialized_len(&self) -> usize {
218        FEF_NULL_LEN
219    }
220
221    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize, crate::error::Error> {
222        if buf.len() < self.serialized_len() {
223            return Err(crate::Error::OutputBufferTooSmall {
224                need: self.serialized_len(),
225                have: buf.len(),
226            });
227        }
228        if self.s2_field > 0x0F {
229            return Err(crate::Error::ReservedBitsViolation {
230                field: "s2_field",
231                reason: "Must fit in 4 bits",
232            });
233        }
234        buf[0] = self.fef_idx;
235        buf[1] = 0; // rfu (high 8 of the 9 reserved bits)
236        buf[2] = ((u8::from(self.s1_field) & 0x07) << 4) | (self.s2_field & 0x0F);
237        Ok(self.serialized_len())
238    }
239}
240
241impl fmt::Display for FefNullPayload {
242    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243        write!(
244            f,
245            "FEF Null {{ fef_idx: {}, s1: {:?}({}), s2: {:04b} }}",
246            self.fef_idx,
247            self.s1_field,
248            self.s1_field.meaning(),
249            self.s2_field
250        )
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn parse_extracts_fields() {
260        // fef_idx=5, rfu=0, byte2 = s1(1)<<4 | s2(0x0A) = 0x1A
261        let buf = [0x05u8, 0x00, 0x1A];
262        let result = FefNullPayload::parse(&buf).unwrap();
263        assert_eq!(result.fef_idx, 5);
264        assert_eq!(result.s1_field, S1Field::V1);
265        assert_eq!(result.s2_field, 0x0A);
266    }
267
268    #[test]
269    fn parse_rejects_nonzero_rfu() {
270        let buf = [0x00u8, 0x1F, 0x00];
271        assert!(FefNullPayload::parse(&buf).is_err());
272    }
273
274    #[test]
275    fn serialize_round_trip() {
276        let orig = FefNullPayload {
277            fef_idx: 3,
278            s1_field: S1Field::V4,
279            s2_field: 0x0C,
280        };
281        let mut buf = [0u8; FEF_NULL_LEN];
282        orig.serialize_into(&mut buf).unwrap();
283        let parsed = FefNullPayload::parse(&buf).unwrap();
284        assert_eq!(orig, parsed);
285    }
286
287    #[test]
288    fn display_output() {
289        let p = FefNullPayload {
290            fef_idx: 0,
291            s1_field: S1Field::V0,
292            s2_field: 0,
293        };
294        assert!(p.to_string().contains("FEF Null"));
295    }
296
297    #[test]
298    fn exhaustive_byte_sweep() {
299        let mut matched = 0u16;
300        for byte in 0u8..=0xFF {
301            if let Ok(v) = S1Field::try_from(byte) {
302                assert_eq!(v as u8, byte, "round-trip failed for {byte:#04x}");
303                matched += 1;
304            }
305        }
306        assert_eq!(matched, 8, "expected 8 matched variants");
307    }
308
309    #[test]
310    fn s1_meaning_values() {
311        assert_eq!(S1Field::V0.meaning(), "T2_SISO");
312        assert_eq!(S1Field::V1.meaning(), "T2_MISO");
313        assert_eq!(S1Field::V2.meaning(), "Non-T2");
314        assert_eq!(S1Field::V3.meaning(), "T2_LITE_SISO");
315        assert_eq!(S1Field::V4.meaning(), "T2_LITE_MISO");
316        assert_eq!(S1Field::V5.meaning(), "reserved");
317    }
318
319    #[test]
320    fn s2_field1_decode() {
321        // s2_field = 0b0001 -> S2 field 1 = 000, mixed = 1
322        let p = FefNullPayload {
323            fef_idx: 0,
324            s1_field: S1Field::V0,
325            s2_field: 0x01,
326        };
327        assert_eq!(p.s2_field1(), S2Field1::Fft1k);
328        assert!(p.is_mixed());
329
330        // s2_field = 0b1100 -> S2 field 1 = 110 (reserved), mixed = 0
331        let p = FefNullPayload {
332            fef_idx: 0,
333            s1_field: S1Field::V0,
334            s2_field: 0x0C,
335        };
336        assert_eq!(p.s2_field1(), S2Field1::Reserved1);
337        assert!(!p.is_mixed());
338    }
339
340    #[test]
341    fn s2_field1_round_trip() {
342        for v in 0u8..=7 {
343            let s2 = S2Field1::from_u8(v);
344            assert_eq!(s2.to_u8(), v, "S2Field1 round-trip failed for {v}");
345        }
346    }
347}