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, serde::Deserialize))]
14#[repr(u8)]
15pub enum S1Field {
16    /// S1 value V0.
17    V0 = 0,
18    /// S1 value V1.
19    V1 = 1,
20    /// S1 value V2.
21    V2 = 2,
22    /// S1 value V3.
23    V3 = 3,
24    /// S1 value V4.
25    V4 = 4,
26    /// S1 value V5.
27    V5 = 5,
28    /// S1 value V6.
29    V6 = 6,
30    /// S1 value V7.
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
49/// FEF part: Null payload (type 0x30) per ETSI TS 102 773 §5.2.9.
50#[derive(Debug, Clone, PartialEq, Eq)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52pub struct FefNullPayload {
53    /// FEF index within super-frame.
54    pub fef_idx: u8,
55    /// S1 field per EN 302 755 §7.2.1.
56    pub s1_field: S1Field,
57    /// S2 field per EN 302 755 §7.2.1.
58    pub s2_field: u8,
59}
60
61const FEF_NULL_LEN: usize = 3;
62
63impl<'a> Parse<'a> for FefNullPayload {
64    type Error = crate::error::Error;
65
66    fn parse(bytes: &'a [u8]) -> Result<Self, crate::error::Error> {
67        if bytes.len() < FEF_NULL_LEN {
68            return Err(crate::Error::BufferTooShort {
69                need: FEF_NULL_LEN,
70                have: bytes.len(),
71                what: "FefNullPayload",
72            });
73        }
74        // Layout (Figure 12): fef_idx(8) | rfu(9) | s1_field(3) | s2_field(4).
75        // rfu spans all of byte 1 plus the top bit of byte 2.
76        if bytes[1] != 0 || bytes[2] & 0x80 != 0 {
77            return Err(crate::Error::ReservedBitsViolation {
78                field: "9-bit rfu",
79                reason: "Must be zero (ETSI TS 102 773 §5.2.9)",
80            });
81        }
82        Ok(FefNullPayload {
83            fef_idx: bytes[0],
84            // byte 2: rfu(1) | s1_field(3) [6:4] | s2_field(4) [3:0]
85            s1_field: S1Field::try_from((bytes[2] >> 4) & 0x07)?,
86            s2_field: bytes[2] & 0x0F,
87        })
88    }
89}
90
91impl<'a> crate::traits::PayloadDef<'a> for FefNullPayload {
92    const PACKET_TYPE: u8 = 0x30;
93    const NAME: &'static str = "FEF_NULL";
94}
95
96impl Serialize for FefNullPayload {
97    type Error = crate::error::Error;
98
99    fn serialized_len(&self) -> usize {
100        FEF_NULL_LEN
101    }
102
103    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize, crate::error::Error> {
104        if buf.len() < self.serialized_len() {
105            return Err(crate::Error::OutputBufferTooSmall {
106                need: self.serialized_len(),
107                have: buf.len(),
108            });
109        }
110        if self.s2_field > 0x0F {
111            return Err(crate::Error::ReservedBitsViolation {
112                field: "s2_field",
113                reason: "Must fit in 4 bits",
114            });
115        }
116        buf[0] = self.fef_idx;
117        buf[1] = 0; // rfu (high 8 of the 9 reserved bits)
118        buf[2] = ((u8::from(self.s1_field) & 0x07) << 4) | (self.s2_field & 0x0F);
119        Ok(self.serialized_len())
120    }
121}
122
123impl fmt::Display for FefNullPayload {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        write!(
126            f,
127            "FEF Null {{ fef_idx: {}, s1: {:?}, s2: {:04b} }}",
128            self.fef_idx, self.s1_field, self.s2_field
129        )
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn parse_extracts_fields() {
139        // fef_idx=5, rfu=0, byte2 = s1(1)<<4 | s2(0x0A) = 0x1A
140        let buf = [0x05u8, 0x00, 0x1A];
141        let result = FefNullPayload::parse(&buf).unwrap();
142        assert_eq!(result.fef_idx, 5);
143        assert_eq!(result.s1_field, S1Field::V1);
144        assert_eq!(result.s2_field, 0x0A);
145    }
146
147    #[test]
148    fn parse_rejects_nonzero_rfu() {
149        let buf = [0x00u8, 0x1F, 0x00];
150        assert!(FefNullPayload::parse(&buf).is_err());
151    }
152
153    #[test]
154    fn serialize_round_trip() {
155        let orig = FefNullPayload {
156            fef_idx: 3,
157            s1_field: S1Field::V4,
158            s2_field: 0x0C,
159        };
160        let mut buf = [0u8; FEF_NULL_LEN];
161        orig.serialize_into(&mut buf).unwrap();
162        let parsed = FefNullPayload::parse(&buf).unwrap();
163        assert_eq!(orig, parsed);
164    }
165
166    #[test]
167    fn display_output() {
168        let p = FefNullPayload {
169            fef_idx: 0,
170            s1_field: S1Field::V0,
171            s2_field: 0,
172        };
173        assert!(p.to_string().contains("FEF Null"));
174    }
175
176    #[test]
177    fn exhaustive_byte_sweep() {
178        let mut matched = 0u16;
179        for byte in 0u8..=0xFF {
180            if let Ok(v) = S1Field::try_from(byte) {
181                assert_eq!(v as u8, byte, "round-trip failed for {byte:#04x}");
182                matched += 1;
183            }
184        }
185        assert_eq!(matched, 8, "expected 8 matched variants");
186    }
187}