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