Skip to main content

dvb_t2mi/payload/
l1_current.rs

1//! T2-MI payload type 0x10: L1-current signalling — §5.2.4.
2//!
3//! L1-current carries the complete L1 signalling for the current T2 frame:
4//! L1PRE + L1CONF + L1DYN_CURR + optionally L1EXT.
5
6use num_enum::TryFromPrimitive;
7
8use broadcast_common::{Parse, Serialize};
9
10use super::l1::post::parse_l1_post_from_framed;
11use super::l1::pre::L1PRE_BYTES;
12use super::l1::{L1Post, L1Pre};
13
14/// Frequency source per §5.2.4 Table 2.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize))]
17#[repr(u8)]
18#[non_exhaustive]
19pub enum FrequencySource {
20    /// Use L1-current data field.
21    UseL1CurrentData = 0b00,
22    /// Use individual addressing frequency function.
23    UseIndividualAddressing = 0b01,
24    /// Manually set per modulator.
25    ManualPerModulator = 0b10,
26}
27
28impl From<FrequencySource> for u8 {
29    fn from(fs: FrequencySource) -> Self {
30        fs as u8
31    }
32}
33
34impl From<num_enum::TryFromPrimitiveError<FrequencySource>> for crate::error::Error {
35    fn from(_: num_enum::TryFromPrimitiveError<FrequencySource>) -> Self {
36        crate::error::Error::ReservedBitsViolation {
37            field: "freq_source",
38            reason: "Must be 0b00, 0b01, or 0b10 (ETSI TS 102 773 §5.2.4)",
39        }
40    }
41}
42
43impl FrequencySource {
44    /// Human-readable spec label (ETSI TS 102 773 §5.2.4 Table 2).
45    #[must_use]
46    pub fn name(&self) -> &'static str {
47        match self {
48            Self::UseL1CurrentData => "L1-current data",
49            Self::UseIndividualAddressing => "individual addressing",
50            Self::ManualPerModulator => "manual per modulator",
51        }
52    }
53}
54broadcast_common::impl_spec_display!(FrequencySource);
55
56/// L1-current payload (type 0x10) per ETSI TS 102 773 §5.2.4.
57///
58/// Layout:
59/// - byte 0: frame_idx (8 bits) — T2 frame where L1 is carried
60/// - byte 1 `[7:6]`: freq_source (2 bits) — Table 2
61/// - byte 1 `[5:0]`: rfu (6 bits) — must be 0
62/// - bytes 2..: l1_current_data (variable bytes)
63#[derive(Debug, Clone, PartialEq, Eq)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize))]
65#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
66pub struct L1CurrentPayload<'a> {
67    /// FRAME_IDX of T2 frame where L1 is carried.
68    pub frame_idx: u8,
69    /// Frequency source per §5.2.4 Table 2.
70    pub freq_source: FrequencySource,
71    /// L1-current data: L1PRE + L1CONF + L1DYN_CURR + L1EXT (all per EN 302 755).
72    pub l1_current_data: &'a [u8],
73}
74
75const L1_CURRENT_HEADER_LEN: usize = 2;
76
77impl<'a> L1CurrentPayload<'a> {
78    /// Parse the L1-pre block from the first 21 bytes of `l1_current_data`.
79    ///
80    /// # Errors
81    /// [`crate::Error::BufferTooShort`] if `l1_current_data` is shorter than 21 bytes.
82    /// [`crate::Error::L1Bits`] on bit-field errors.
83    pub fn l1_pre(&self) -> crate::error::Result<L1Pre> {
84        if self.l1_current_data.len() < L1PRE_BYTES {
85            return Err(crate::Error::BufferTooShort {
86                need: L1PRE_BYTES,
87                have: self.l1_current_data.len(),
88                what: "L1PRE in l1_current_data",
89            });
90        }
91        L1Pre::parse(&self.l1_current_data[..L1PRE_BYTES])
92    }
93
94    /// Parse the full L1-post block from `l1_current_data`.
95    ///
96    /// Internally parses the L1-pre block first to extract `num_rf` and
97    /// `fef_present` (S2 LSB), then parses the framed CONF/DYN/EXT sections.
98    ///
99    /// # Errors
100    /// [`crate::Error::BufferTooShort`] if any framing section is truncated.
101    /// [`crate::Error::L1Bits`] on bit-field errors.
102    pub fn l1_post(&self) -> crate::error::Result<L1Post> {
103        let pre = self.l1_pre()?;
104        let num_rf = pre.num_rf;
105        let fef_present = (pre.s2 & 0x01) == 1;
106        let framed = &self.l1_current_data[L1PRE_BYTES..];
107        parse_l1_post_from_framed(framed, num_rf, fef_present)
108    }
109}
110
111impl<'a> Parse<'a> for L1CurrentPayload<'a> {
112    type Error = crate::error::Error;
113
114    fn parse(bytes: &'a [u8]) -> Result<Self, crate::error::Error> {
115        if bytes.len() < L1_CURRENT_HEADER_LEN {
116            return Err(crate::Error::BufferTooShort {
117                need: L1_CURRENT_HEADER_LEN,
118                have: bytes.len(),
119                what: "L1CurrentPayload header",
120            });
121        }
122
123        let frame_idx = bytes[0];
124        let freq_source = FrequencySource::try_from(bytes[1] >> 6)?;
125
126        // RFU: byte 1 bottom 6 bits — must be 0
127        let rfu = bytes[1] & 0x3F;
128        if rfu != 0 {
129            return Err(crate::Error::ReservedBitsViolation {
130                field: "6-bit RFU after freq_source",
131                reason: "Must be zero (ETSI TS 102 773 §5.2.4)",
132            });
133        }
134
135        Ok(L1CurrentPayload {
136            frame_idx,
137            freq_source,
138            l1_current_data: &bytes[L1_CURRENT_HEADER_LEN..],
139        })
140    }
141}
142
143impl<'a> crate::traits::PayloadDef<'a> for L1CurrentPayload<'a> {
144    const PACKET_TYPE: u8 = 0x10;
145    const NAME: &'static str = "L1_CURRENT";
146}
147
148impl Serialize for L1CurrentPayload<'_> {
149    type Error = crate::error::Error;
150
151    fn serialized_len(&self) -> usize {
152        L1_CURRENT_HEADER_LEN + self.l1_current_data.len()
153    }
154
155    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize, crate::error::Error> {
156        if buf.len() < self.serialized_len() {
157            return Err(crate::Error::OutputBufferTooSmall {
158                need: self.serialized_len(),
159                have: buf.len(),
160            });
161        }
162
163        buf[0] = self.frame_idx;
164        buf[1] = (u8::from(self.freq_source) << 6) & 0xC0; // freq_source in top 2 bits, RFU = 0
165
166        if !self.l1_current_data.is_empty() {
167            buf[L1_CURRENT_HEADER_LEN..L1_CURRENT_HEADER_LEN + self.l1_current_data.len()]
168                .copy_from_slice(self.l1_current_data);
169        }
170
171        Ok(self.serialized_len())
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn frequency_source_try_from_valid() {
181        assert_eq!(
182            FrequencySource::try_from(0b00),
183            Ok(FrequencySource::UseL1CurrentData)
184        );
185        assert_eq!(
186            FrequencySource::try_from(0b01),
187            Ok(FrequencySource::UseIndividualAddressing)
188        );
189        assert_eq!(
190            FrequencySource::try_from(0b10),
191            Ok(FrequencySource::ManualPerModulator)
192        );
193    }
194
195    #[test]
196    fn frequency_source_try_from_rejects_11() {
197        assert!(FrequencySource::try_from(0b11).is_err());
198    }
199
200    #[test]
201    fn exhaustive_byte_sweep() {
202        let mut matched = 0u16;
203        for byte in 0u8..=0xFF {
204            if let Ok(v) = FrequencySource::try_from(byte) {
205                assert_eq!(v as u8, byte, "round-trip failed for {byte:#04x}");
206                matched += 1;
207            }
208        }
209        assert_eq!(matched, 3, "expected 3 matched variants");
210    }
211
212    #[test]
213    fn parse_extracts_frame_idx_and_freq_source() {
214        let buf = [0x42u8, 0x80, 0xDE, 0xAD]; // frame=0x42, freq_src=0b10 (Manual), rfu=0
215        let result = L1CurrentPayload::parse(&buf).unwrap();
216        assert_eq!(result.frame_idx, 0x42);
217        assert_eq!(result.freq_source, FrequencySource::ManualPerModulator);
218        assert_eq!(result.l1_current_data, &[0xDE, 0xAD]);
219    }
220
221    #[test]
222    fn parse_rejects_nonzero_rfu() {
223        let buf = [0x00u8, 0x01, 0x00]; // freq_source=00, bottom 6 RFU bits nonzero
224        assert!(L1CurrentPayload::parse(&buf).is_err());
225    }
226
227    #[test]
228    fn serialize_round_trip() {
229        let orig = L1CurrentPayload {
230            frame_idx: 0xAB,
231            freq_source: FrequencySource::UseL1CurrentData,
232            l1_current_data: &[0x12, 0x34, 0x56],
233        };
234        let mut buf = vec![0u8; orig.serialized_len()];
235        orig.serialize_into(&mut buf).unwrap();
236        let parsed = L1CurrentPayload::parse(&buf).unwrap();
237        assert_eq!(orig, parsed);
238    }
239
240    #[test]
241    fn serialize_zeros_rfu_bits() {
242        let payload = L1CurrentPayload {
243            frame_idx: 0x10,
244            freq_source: FrequencySource::UseIndividualAddressing,
245            l1_current_data: &[],
246        };
247        let mut buf = [0xFFu8; 2];
248        payload.serialize_into(&mut buf).unwrap();
249        assert_eq!(buf[1] & 0x3F, 0x00);
250    }
251}