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 dvb_common::{Parse, Serialize};
9
10/// Frequency source per §5.2.4 Table 2.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13#[repr(u8)]
14pub enum FrequencySource {
15    /// Use L1-current data field.
16    UseL1CurrentData = 0b00,
17    /// Use individual addressing frequency function.
18    UseIndividualAddressing = 0b01,
19    /// Manually set per modulator.
20    ManualPerModulator = 0b10,
21}
22
23impl From<FrequencySource> for u8 {
24    fn from(fs: FrequencySource) -> Self {
25        fs as u8
26    }
27}
28
29impl From<num_enum::TryFromPrimitiveError<FrequencySource>> for crate::error::Error {
30    fn from(_: num_enum::TryFromPrimitiveError<FrequencySource>) -> Self {
31        crate::error::Error::ReservedBitsViolation {
32            field: "freq_source",
33            reason: "Must be 0b00, 0b01, or 0b10 (ETSI TS 102 773 §5.2.4)",
34        }
35    }
36}
37
38/// L1-current payload (type 0x10) per ETSI TS 102 773 §5.2.4.
39///
40/// Layout:
41/// - byte 0: frame_idx (8 bits) — T2 frame where L1 is carried
42/// - byte 1 `[7:6]`: freq_source (2 bits) — Table 2
43/// - byte 1 `[5:0]`: rfu (6 bits) — must be 0
44/// - bytes 2..: l1_current_data (variable bytes)
45#[derive(Debug, Clone, PartialEq, Eq)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
47pub struct L1CurrentPayload<'a> {
48    /// FRAME_IDX of T2 frame where L1 is carried.
49    pub frame_idx: u8,
50    /// Frequency source per §5.2.4 Table 2.
51    pub freq_source: FrequencySource,
52    /// L1-current data: L1PRE + L1CONF + L1DYN_CURR + L1EXT (all per EN 302 755).
53    #[cfg_attr(feature = "serde", serde(borrow))]
54    pub l1_current_data: &'a [u8],
55}
56
57const L1_CURRENT_HEADER_LEN: usize = 2;
58
59impl<'a> Parse<'a> for L1CurrentPayload<'a> {
60    type Error = crate::error::Error;
61
62    fn parse(bytes: &'a [u8]) -> Result<Self, crate::error::Error> {
63        if bytes.len() < L1_CURRENT_HEADER_LEN {
64            return Err(crate::Error::BufferTooShort {
65                need: L1_CURRENT_HEADER_LEN,
66                have: bytes.len(),
67                what: "L1CurrentPayload header",
68            });
69        }
70
71        let frame_idx = bytes[0];
72        let freq_source = FrequencySource::try_from(bytes[1] >> 6)?;
73
74        // RFU: byte 1 bottom 6 bits — must be 0
75        let rfu = bytes[1] & 0x3F;
76        if rfu != 0 {
77            return Err(crate::Error::ReservedBitsViolation {
78                field: "6-bit RFU after freq_source",
79                reason: "Must be zero (ETSI TS 102 773 §5.2.4)",
80            });
81        }
82
83        Ok(L1CurrentPayload {
84            frame_idx,
85            freq_source,
86            l1_current_data: &bytes[L1_CURRENT_HEADER_LEN..],
87        })
88    }
89}
90
91impl<'a> crate::traits::PayloadDef<'a> for L1CurrentPayload<'a> {
92    const PACKET_TYPE: u8 = 0x10;
93    const NAME: &'static str = "L1_CURRENT";
94}
95
96impl Serialize for L1CurrentPayload<'_> {
97    type Error = crate::error::Error;
98
99    fn serialized_len(&self) -> usize {
100        L1_CURRENT_HEADER_LEN + self.l1_current_data.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
111        buf[0] = self.frame_idx;
112        buf[1] = (u8::from(self.freq_source) << 6) & 0xC0; // freq_source in top 2 bits, RFU = 0
113
114        if !self.l1_current_data.is_empty() {
115            buf[L1_CURRENT_HEADER_LEN..L1_CURRENT_HEADER_LEN + self.l1_current_data.len()]
116                .copy_from_slice(self.l1_current_data);
117        }
118
119        Ok(self.serialized_len())
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn frequency_source_try_from_valid() {
129        assert_eq!(
130            FrequencySource::try_from(0b00),
131            Ok(FrequencySource::UseL1CurrentData)
132        );
133        assert_eq!(
134            FrequencySource::try_from(0b01),
135            Ok(FrequencySource::UseIndividualAddressing)
136        );
137        assert_eq!(
138            FrequencySource::try_from(0b10),
139            Ok(FrequencySource::ManualPerModulator)
140        );
141    }
142
143    #[test]
144    fn frequency_source_try_from_rejects_11() {
145        assert!(FrequencySource::try_from(0b11).is_err());
146    }
147
148    #[test]
149    fn exhaustive_byte_sweep() {
150        let mut matched = 0u16;
151        for byte in 0u8..=0xFF {
152            if let Ok(v) = FrequencySource::try_from(byte) {
153                assert_eq!(v as u8, byte, "round-trip failed for {byte:#04x}");
154                matched += 1;
155            }
156        }
157        assert_eq!(matched, 3, "expected 3 matched variants");
158    }
159
160    #[test]
161    fn parse_extracts_frame_idx_and_freq_source() {
162        let buf = [0x42u8, 0x80, 0xDE, 0xAD]; // frame=0x42, freq_src=0b10 (Manual), rfu=0
163        let result = L1CurrentPayload::parse(&buf).unwrap();
164        assert_eq!(result.frame_idx, 0x42);
165        assert_eq!(result.freq_source, FrequencySource::ManualPerModulator);
166        assert_eq!(result.l1_current_data, &[0xDE, 0xAD]);
167    }
168
169    #[test]
170    fn parse_rejects_nonzero_rfu() {
171        let buf = [0x00u8, 0x01, 0x00]; // freq_source=00, bottom 6 RFU bits nonzero
172        assert!(L1CurrentPayload::parse(&buf).is_err());
173    }
174
175    #[test]
176    fn serialize_round_trip() {
177        let orig = L1CurrentPayload {
178            frame_idx: 0xAB,
179            freq_source: FrequencySource::UseL1CurrentData,
180            l1_current_data: &[0x12, 0x34, 0x56],
181        };
182        let mut buf = vec![0u8; orig.serialized_len()];
183        orig.serialize_into(&mut buf).unwrap();
184        let parsed = L1CurrentPayload::parse(&buf).unwrap();
185        assert_eq!(orig, parsed);
186    }
187
188    #[test]
189    fn serialize_zeros_rfu_bits() {
190        let payload = L1CurrentPayload {
191            frame_idx: 0x10,
192            freq_source: FrequencySource::UseIndividualAddressing,
193            l1_current_data: &[],
194        };
195        let mut buf = [0xFFu8; 2];
196        payload.serialize_into(&mut buf).unwrap();
197        assert_eq!(buf[1] & 0x3F, 0x00);
198    }
199}