Skip to main content

dvb_t2mi/payload/
fef_subpart.rs

1//! T2-MI payload type 0x33: FEF sub-part — §5.2.12.
2//!
3//! Carries individual sub-parts of a composite FEF part.
4//! Sub-part variety determines the body format:
5//! - 0x0000: Null (reserved_for_future_use 32 bits)
6//! - 0x0001: IQ (reserved 32 bits + iq_data variable)
7//! - 0x0002: PRBS (prbs_type 8 bits + reserved 96 bits)
8//! - 0x0003: TX-SIG FEF (reserved 32 bits)
9//! - 0x0004..0xFFFF: Reserved for future use
10
11use std::fmt;
12
13use num_enum::TryFromPrimitive;
14
15use dvb_common::{Parse, Serialize};
16
17/// Sub-part variety per §5.2.12 Table 13.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20#[repr(u16)]
21pub enum SubpartVariety {
22    /// Null — `reserved_for_future_use(32)` = 0.
23    Null = 0x0000,
24    /// IQ — `reserved_for_future_use(32)` + `iq_data`.
25    Iq = 0x0001,
26    /// PRBS — `prbs_type(8)` + `reserved_for_future_use(96)`.
27    Prbs = 0x0002,
28    /// TX-SIG FEF — `reserved_for_future_use(32)`.
29    TxSigFef = 0x0003,
30}
31
32impl From<SubpartVariety> for u16 {
33    fn from(sv: SubpartVariety) -> Self {
34        sv as u16
35    }
36}
37
38impl From<num_enum::TryFromPrimitiveError<SubpartVariety>> for crate::error::Error {
39    fn from(_: num_enum::TryFromPrimitiveError<SubpartVariety>) -> Self {
40        // subpart_variety is a 16-bit field — casting the offending value to u8
41        // would truncate it, and InvalidPacketType is the wrong category anyway.
42        crate::error::Error::ReservedBitsViolation {
43            field: "subpart_variety",
44            reason: "Must be 0x0000..=0x0003 per ETSI TS 102 773 §5.2.12 Table 13",
45        }
46    }
47}
48
49/// PRBS type for SubpartVariety::Prbs.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52#[repr(u8)]
53pub enum PrbsType {
54    /// User-defined test/measurement.
55    UserDefined = 0x00,
56}
57
58impl From<PrbsType> for u8 {
59    fn from(pt: PrbsType) -> Self {
60        pt as u8
61    }
62}
63
64/// FEF sub-part payload (type 0x33) per ETSI TS 102 773 §5.2.12.
65///
66/// Layout (Figure 16):
67/// - byte 0: fef_idx (8 bits)
68/// - bytes 1-2: tx_identifier (16 bits)
69/// - bytes 3-6: rfu1 (32 bits) — must be 0
70/// - bytes 7-8: subpart_idx (16 bits)
71/// - bytes 9-10: subpart_variety (16 bits)
72/// - bytes 11-14: rfu2 (10 bits) + subpart_length (22 bits)
73/// - bytes 15..: subpart data (variable, format per variety)
74#[derive(Debug, Clone, PartialEq, Eq)]
75#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
76pub struct FefSubPartPayload<'a> {
77    /// FEF index within super-frame.
78    pub fef_idx: u8,
79    /// Transmitter identifier (0x0000 = broadcast).
80    pub tx_identifier: u16,
81    /// Sub-part index p.
82    pub subpart_idx: u16,
83    /// Sub-part variety (Null/IQ/PRBS/TX-SIG).
84    pub subpart_variety: SubpartVariety,
85    /// Length in elementary time periods.
86    pub subpart_length: u32,
87    /// Raw sub-part data (format depends on variety).
88    #[cfg_attr(feature = "serde", serde(borrow))]
89    pub subpart_data: &'a [u8],
90}
91
92/// Total header bytes before subpart_data: fef_idx(1) + tx_id(2) + rfu1(4) +
93/// subpart_idx(2) + variety(2) + rfu2+subpart_len(4) = 15.
94const FEF_SUBPART_HEADER_LEN: usize = 15;
95
96/// Mask for 22-bit subpart_length field.
97const SUBPART_LENGTH_MASK: u32 = 0x003F_FFFF;
98
99impl<'a> Parse<'a> for FefSubPartPayload<'a> {
100    type Error = crate::error::Error;
101
102    fn parse(bytes: &'a [u8]) -> Result<Self, crate::error::Error> {
103        if bytes.len() < FEF_SUBPART_HEADER_LEN {
104            return Err(crate::Error::BufferTooShort {
105                need: FEF_SUBPART_HEADER_LEN,
106                have: bytes.len(),
107                what: "FefSubPartPayload header",
108            });
109        }
110
111        // rfu1: bytes 3-6 (32 bits) — must be 0
112        if bytes[3] != 0 || bytes[4] != 0 || bytes[5] != 0 || bytes[6] != 0 {
113            return Err(crate::Error::ReservedBitsViolation {
114                field: "32-bit RFU (rfu1)",
115                reason: "Must be zero (ETSI TS 102 773 §5.2.12)",
116            });
117        }
118
119        let variety = SubpartVariety::try_from(u16::from_be_bytes([bytes[9], bytes[10]]))?;
120
121        // rfu2: byte 11 (8 bits) + byte 12 [7:6] (2 bits) = 10 bits — must be 0
122        if bytes[11] != 0 || (bytes[12] & 0xC0 != 0) {
123            return Err(crate::Error::ReservedBitsViolation {
124                field: "10-bit RFU (rfu2)",
125                reason: "Must be zero (ETSI TS 102 773 §5.2.12)",
126            });
127        }
128
129        // subpart_length: byte 12 [5:0] (6 bits) + byte 13 (8 bits) + byte 14 (8 bits) = 22 bits
130        let subpart_length =
131            ((bytes[12] & 0x3F) as u32) << 16 | (bytes[13] as u32) << 8 | (bytes[14] as u32);
132
133        Ok(FefSubPartPayload {
134            fef_idx: bytes[0],
135            tx_identifier: u16::from_be_bytes([bytes[1], bytes[2]]),
136            subpart_idx: u16::from_be_bytes([bytes[7], bytes[8]]),
137            subpart_variety: variety,
138            subpart_length,
139            subpart_data: &bytes[FEF_SUBPART_HEADER_LEN..],
140        })
141    }
142}
143
144impl<'a> crate::traits::PayloadDef<'a> for FefSubPartPayload<'a> {
145    const PACKET_TYPE: u8 = 0x33;
146    const NAME: &'static str = "FEF_SUBPART";
147}
148
149impl Serialize for FefSubPartPayload<'_> {
150    type Error = crate::error::Error;
151
152    fn serialized_len(&self) -> usize {
153        FEF_SUBPART_HEADER_LEN + self.subpart_data.len()
154    }
155
156    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize, crate::error::Error> {
157        if buf.len() < self.serialized_len() {
158            return Err(crate::Error::OutputBufferTooSmall {
159                need: self.serialized_len(),
160                have: buf.len(),
161            });
162        }
163
164        if self.subpart_length > SUBPART_LENGTH_MASK {
165            return Err(crate::Error::ReservedBitsViolation {
166                field: "subpart_length",
167                reason: "Must fit in 22 bits",
168            });
169        }
170
171        buf[0] = self.fef_idx;
172        let tx_id = self.tx_identifier.to_be_bytes();
173        buf[1] = tx_id[0];
174        buf[2] = tx_id[1];
175        // rfu1 = 0
176        buf[3] = 0;
177        buf[4] = 0;
178        buf[5] = 0;
179        buf[6] = 0;
180        let sub_idx = self.subpart_idx.to_be_bytes();
181        buf[7] = sub_idx[0];
182        buf[8] = sub_idx[1];
183        let variety = u16::from(self.subpart_variety);
184        buf[9] = (variety >> 8) as u8;
185        buf[10] = (variety & 0xFF) as u8;
186        // rfu2 = 0 (top 2 bits byte 12) + rfu2 = 0 (byte 11)
187        buf[11] = 0;
188        // subpart_length: 22 bits → byte 12 [5:0] = top 6, byte 13 = mid 8, byte 14 = bot 8
189        buf[12] = ((self.subpart_length >> 16) & 0x3F) as u8;
190        buf[13] = (self.subpart_length >> 8) as u8;
191        buf[14] = (self.subpart_length & 0xFF) as u8;
192
193        if !self.subpart_data.is_empty() {
194            buf[FEF_SUBPART_HEADER_LEN..FEF_SUBPART_HEADER_LEN + self.subpart_data.len()]
195                .copy_from_slice(self.subpart_data);
196        }
197
198        Ok(self.serialized_len())
199    }
200}
201
202impl fmt::Display for FefSubPartPayload<'_> {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        write!(
205            f,
206            "FEF SubPart {{ fef_idx: {}, tx_id: 0x{:04X}, subpart_idx: {}, variety: {:?}, length: {} }}",
207            self.fef_idx, self.tx_identifier, self.subpart_idx, self.subpart_variety, self.subpart_length
208        )
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn parse_extracts_fields() {
218        // subpart_length = 4096 = 0x1000.
219        // 22-bit: byte 12 [5:0] = (0x1000 >> 16) & 0x3F = 0,
220        //         byte 13 = (0x1000 >> 8) & 0xFF = 0x10,
221        //         byte 14 = 0x1000 & 0xFF = 0x00.
222        let mut buf = [0u8; 15];
223        buf[0] = 0x02; // fef_idx
224        buf[1] = 0x00;
225        buf[2] = 0x01; // tx_id = 1
226                       // bytes 3-6 = rfu1 = 0
227        buf[7] = 0x00;
228        buf[8] = 0x05; // subpart_idx = 5
229        buf[9] = 0x00;
230        buf[10] = 0x01; // variety = IQ
231                        // byte 11 = rfu2 = 0
232                        // byte 12 top 2 = rfu2 = 0, bottom 6 = subpart top = 0
233                        // byte 13 = subpart mid = 0x10
234        buf[13] = 0x10;
235        // byte 14 = subpart bot = 0
236        // subpart_data starts at offset 15
237
238        let result = FefSubPartPayload::parse(&buf).unwrap();
239        assert_eq!(result.fef_idx, 2);
240        assert_eq!(result.tx_identifier, 0x0001);
241        assert_eq!(result.subpart_idx, 5);
242        assert_eq!(result.subpart_variety, SubpartVariety::Iq);
243        assert_eq!(result.subpart_length, 0x1000);
244    }
245
246    #[test]
247    fn parse_rejects_invalid_variety() {
248        let mut buf = [0u8; 15];
249        buf[9] = 0x00;
250        buf[10] = 0x04; // variety = 0x0004 (reserved)
251        assert!(FefSubPartPayload::parse(&buf).is_err());
252    }
253
254    #[test]
255    fn parse_rejects_nonzero_rfu1() {
256        let mut buf = [0u8; 15];
257        buf[3] = 0x01;
258        assert!(FefSubPartPayload::parse(&buf).is_err());
259    }
260
261    #[test]
262    fn parse_rejects_nonzero_rfu2_byte11() {
263        let mut buf = [0u8; 15];
264        buf[11] = 0x01;
265        assert!(FefSubPartPayload::parse(&buf).is_err());
266    }
267
268    #[test]
269    fn parse_rejects_nonzero_rfu2_byte12_top() {
270        let mut buf = [0u8; 15];
271        buf[12] = 0xC0;
272        assert!(FefSubPartPayload::parse(&buf).is_err());
273    }
274
275    #[test]
276    fn parse_rejects_short_buffer() {
277        let buf = [0u8; 14];
278        assert!(FefSubPartPayload::parse(&buf).is_err());
279    }
280
281    #[test]
282    fn serialize_round_trip() {
283        let orig = FefSubPartPayload {
284            fef_idx: 1,
285            tx_identifier: 0x0000,
286            subpart_idx: 3,
287            subpart_variety: SubpartVariety::Null,
288            subpart_length: 2048,
289            subpart_data: &[],
290        };
291        let mut buf = vec![0u8; orig.serialized_len()];
292        orig.serialize_into(&mut buf).unwrap();
293        let parsed = FefSubPartPayload::parse(&buf).unwrap();
294        assert_eq!(orig, parsed);
295    }
296
297    #[test]
298    fn broadcast_tx_identifier() {
299        let buf = [0u8; 15];
300        let result = FefSubPartPayload::parse(&buf).unwrap();
301        assert_eq!(result.tx_identifier, 0x0000);
302    }
303
304    #[test]
305    fn subpart_variety_try_from_all() {
306        assert_eq!(SubpartVariety::try_from(0x0000), Ok(SubpartVariety::Null));
307        assert_eq!(SubpartVariety::try_from(0x0001), Ok(SubpartVariety::Iq));
308        assert_eq!(SubpartVariety::try_from(0x0002), Ok(SubpartVariety::Prbs));
309        assert_eq!(
310            SubpartVariety::try_from(0x0003),
311            Ok(SubpartVariety::TxSigFef)
312        );
313    }
314
315    #[test]
316    fn subpart_variety_try_from_reserved() {
317        assert!(SubpartVariety::try_from(0x0004).is_err());
318        assert!(SubpartVariety::try_from(0xFFFF).is_err());
319    }
320
321    #[test]
322    fn exhaustive_subpart_variety_sweep() {
323        let mut matched = 0u32;
324        for value in 0u16..=0xFFFF {
325            if let Ok(v) = SubpartVariety::try_from(value) {
326                assert_eq!(v as u16, value, "round-trip failed for {value:#06x}");
327                matched += 1;
328            }
329        }
330        assert_eq!(matched, 4, "expected 4 matched variants");
331    }
332
333    #[test]
334    fn exhaustive_prbs_type_sweep() {
335        let mut matched = 0u16;
336        for byte in 0u8..=0xFF {
337            if let Ok(v) = PrbsType::try_from(byte) {
338                assert_eq!(v as u8, byte, "round-trip failed for {byte:#04x}");
339                matched += 1;
340            }
341        }
342        assert_eq!(matched, 1, "expected 1 matched variant");
343    }
344}