dvb_gse/
bbheader.rs

1//! DVB-S2 Base-Band Header (BBHEADER).
2//!
3//! The BBHEADER (Base-Band Header) is a 10-byte header at the start of every
4//! DVB-S2 BBFRAME (Base-Band Frame). See Section 5.1.6 in
5//! [EN 302 307-1](https://www.etsi.org/deliver/etsi_en/302300_302399/30230701/01.04.01_60/en_30230701v010401p.pdf) and in the
6//! the [DVB BlueBook A083-2r3](https://dvb.org/wp-content/uploads/2021/07/A083-2r3_DVB-S2X_Draft-EN-302-307-2-v141_Feb_2022.pdf)
7//! for the features specific to DVB-S2X.
8
9use super::BitSlice;
10use bitvec::prelude::*;
11use num_enum::TryFromPrimitive;
12use std::fmt::{Display, Formatter};
13
14/// DVB-S2 BBHEADER.
15///
16/// This struct is used to parse the fields of a BBHEADER. It is simply a
17/// wrapper over an array reference `&[u8; 10]`.
18#[derive(Debug, Clone, Eq, PartialEq, Hash)]
19pub struct BBHeader<'a>(&'a [u8; BBHeader::LEN]);
20
21lazy_static::lazy_static! {
22    static ref CRC8: crc::Crc<u8> = crc::Crc::<u8>::new(&crc::CRC_8_DVB_S2);
23}
24
25impl BBHeader<'_> {
26    /// Length of a BBHEADER in bytes.
27    pub const LEN: usize = 10;
28
29    /// Creates a new BBHEADER.
30    pub fn new(data: &[u8; BBHeader::LEN]) -> BBHeader {
31        BBHeader(data)
32    }
33
34    fn matype1(&self) -> &BitSlice {
35        BitSlice::from_slice(&self.0[..1])
36    }
37
38    /// Gives the value of the TS/GS (Transport Stream Input or Generic Stream
39    /// Input) field.
40    pub fn tsgs(&self) -> TsGs {
41        TsGs::try_from(self.matype1()[..2].load_be::<u8>()).unwrap()
42    }
43
44    /// Returns `true` if the BBFRAME is a GSE-HEM BBFRAME.
45    ///
46    /// GSE-HEM BBFRAMEs have a different layout and omit some fields.
47    pub fn is_gse_hem(&self) -> bool {
48        matches!(self.tsgs(), TsGs::GseHem)
49    }
50
51    /// Give the value of the SIS/MIS (Single Input Stream or Multiple Input
52    /// Stream) field.
53    pub fn sismis(&self) -> SisMis {
54        SisMis::try_from(self.matype1()[2..3].load_be::<u8>()).unwrap()
55    }
56
57    /// Gives the value of the CCM/ACM (Constant Coding and Modulation or
58    /// Adaptive Coding and Modulation) field.
59    pub fn ccmacm(&self) -> CcmAcm {
60        CcmAcm::try_from(self.matype1()[3..4].load_be::<u8>()).unwrap()
61    }
62
63    /// Gives the value of ISSY (Input Stream Synchronization Indicator) field.
64    pub fn issyi(&self) -> bool {
65        self.matype1()[4]
66    }
67
68    /// Gives the value of the NPD (Null-packet Deletion) field.
69    pub fn npd(&self) -> bool {
70        self.matype1()[5]
71    }
72
73    /// Gives the value of the GSE-Lite field.
74    ///
75    /// This field is used in DVB-S2X only and replaces the NPD field in GSE modes.
76    pub fn gse_lite(&self) -> bool {
77        self.npd()
78    }
79
80    /// Gives the value of the roll-off (RO) field.
81    pub fn rolloff(&self) -> RollOff {
82        RollOff::try_from_primitive(self.matype1()[6..8].load_be::<u8>()).unwrap()
83    }
84
85    /// Gives the value of the ISI (Input Stream Identifier) field.
86    ///
87    /// This field corresponds to the MATYPE-2 byte and is only used in Multiple
88    /// Input Stream mode.
89    pub fn isi(&self) -> u8 {
90        self.0[1]
91    }
92
93    /// Gives the value of the UPL (User Packet Length) field.
94    ///
95    /// The function returns `None` if the UPL field is not present, which is
96    /// the case in GSE-HEM mode.
97    pub fn upl(&self) -> Option<u16> {
98        if self.is_gse_hem() {
99            None
100        } else {
101            Some(u16::from_be_bytes(self.0[2..4].try_into().unwrap()))
102        }
103    }
104
105    /// Gives the value of the ISSY field.
106    ///
107    /// The ISSY field is only present in GSE-HEM BBFRAMEs which have the ISSYI
108    /// bit asserted. If the ISSY field is not present, this function returns
109    /// `None`. The ISSY field is split into 2-byte field and a 1-byte field in
110    /// the BBHEADER. These are concatenated into a 3-byte array in the output
111    /// of this function.
112    pub fn issy(&self) -> Option<[u8; 3]> {
113        if self.is_gse_hem() && self.issyi() {
114            let mut field = [0; 3];
115            field[0] = self.0[2];
116            field[1] = self.0[3];
117            field[2] = self.0[6];
118            Some(field)
119        } else {
120            None
121        }
122    }
123
124    /// Gives the value of the DFL (Data Field Length) field.
125    pub fn dfl(&self) -> u16 {
126        u16::from_be_bytes(self.0[4..6].try_into().unwrap())
127    }
128
129    /// Gives the value of the SYNC (User Packet Sync-byte) field.
130    ///
131    /// The function returns `None` if the SYNC field is not present, which is
132    /// the case in GSE-HEM mode.
133    pub fn sync(&self) -> Option<u8> {
134        if self.is_gse_hem() {
135            None
136        } else {
137            Some(self.0[6])
138        }
139    }
140
141    /// Gives the value of the SYNCD field.
142    pub fn syncd(&self) -> u16 {
143        u16::from_be_bytes(self.0[7..9].try_into().unwrap())
144    }
145
146    /// Gives the value of the CRC-8 field.
147    pub fn crc8(&self) -> u8 {
148        self.0[BBHeader::LEN - 1]
149    }
150
151    /// Computes and returns the CRC-8 of the BBHEADER.
152    pub fn compute_crc8(&self) -> u8 {
153        let crc = CRC8.checksum(&self.0[..BBHeader::LEN - 1]);
154        if self.is_gse_hem() {
155            // ETSI EN 302 307-2 V1.3.1 (2021-07) says that the CRC8_MODE field
156            // in GSE-HEM is the EXOR of the MODE field with CRC-8, and that the
157            // MODE field has the value 1_D.
158            //
159            // To confirm if this indeeds refers to the value 1 in decimal.
160            crc ^ 1
161        } else {
162            crc
163        }
164    }
165
166    /// Checks if the CRC-8 of the BBHEADER is valid.
167    pub fn crc_is_valid(&self) -> bool {
168        self.crc8() == self.compute_crc8()
169    }
170}
171
172impl Display for BBHeader<'_> {
173    fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
174        write!(
175            f,
176            "BBHEADER(TS/GS = {}, SIS/MIS = {}, CCM/ACM = {}, ISSYI = {}, \
177		   NPD/GSE-Lite = {}, {}, ISI = {}, ",
178            self.tsgs(),
179            self.sismis(),
180            self.ccmacm(),
181            self.issyi(),
182            self.npd(),
183            self.rolloff(),
184            self.isi(),
185        )?;
186        if let Some(upl) = self.upl() {
187            write!(f, "UPL = {} bits, ", upl)?;
188        }
189        if let Some(issy) = self.issy() {
190            let issy = (u32::from(issy[0]) << 16) | (u32::from(issy[1]) << 8) | u32::from(issy[2]);
191            write!(f, "ISSY = {:#06x}, ", issy)?;
192        }
193        write!(f, "DFL = {} bits, ", self.dfl())?;
194        if let Some(sync) = self.sync() {
195            write!(f, "SYNC = {:#04x}, ", sync)?;
196        }
197        write!(f, "SYNCD = {:#06x})", self.syncd())
198    }
199}
200
201/// TS/GS (Transport Stream Input or Generic Stream Input) field value.
202#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, TryFromPrimitive)]
203#[repr(u8)]
204pub enum TsGs {
205    /// Transport stream mode.
206    Transport = 0b11,
207    /// Generic packetized mode.
208    GenericPacketized = 0b00,
209    /// Generic continuous mode.
210    GenericContinuous = 0b01,
211    /// GSE-HEM (GSE High Efficiency Mode).
212    ///
213    /// This is used only in DVB-S2X.
214    GseHem = 0b10,
215}
216
217impl Display for TsGs {
218    fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
219        write!(
220            f,
221            "{}",
222            match self {
223                TsGs::Transport => "Transport",
224                TsGs::GenericPacketized => "Generic packetized",
225                TsGs::GenericContinuous => "Generic continuous",
226                TsGs::GseHem => "GSE-HEM",
227            }
228        )
229    }
230}
231
232/// SIS/MIS (Single Input Stream or Multiple Input Stream) field value.
233#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, TryFromPrimitive)]
234#[repr(u8)]
235pub enum SisMis {
236    /// Single input stream.
237    Sis = 0b1,
238    /// Multiple input streams.
239    Mis = 0b0,
240}
241
242impl Display for SisMis {
243    fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
244        write!(
245            f,
246            "{}",
247            match self {
248                SisMis::Sis => "single",
249                SisMis::Mis => "multiple",
250            }
251        )
252    }
253}
254
255/// CCM/ACM (Constant Coding and Modulation or Adaptive Coding and Modulation)
256/// field value.
257#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, TryFromPrimitive)]
258#[repr(u8)]
259pub enum CcmAcm {
260    /// Constant coding and modulation.
261    Ccm = 0b1,
262    /// Adaptive coding and modulation.
263    ///
264    /// This value is also used for VCM (variable coding and modulation).
265    Acm = 0b0,
266}
267
268impl Display for CcmAcm {
269    fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
270        write!(
271            f,
272            "{}",
273            match self {
274                CcmAcm::Ccm => "CCM",
275                CcmAcm::Acm => "ACM",
276            }
277        )
278    }
279}
280
281/// Roll-off field values.
282///
283/// In DVB-S2X, there are two tables of roll-off field values, depending on
284/// whether the value is alternated with the reserved value `0b11` in every
285/// other BBFRAME or not. The values given here correspond to the case when
286/// alternation with `0b11` is not used. These values are backwards compatible
287/// with DVB-S2.
288#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, TryFromPrimitive)]
289#[repr(u8)]
290// For DVB-S2X, these values correspond to no alternation with 0b11
291pub enum RollOff {
292    /// Roll-off factor 0.35.
293    Ro0_35 = 0b00,
294    /// Roll-off factor 0.25.
295    Ro0_25 = 0b01,
296    /// Roll-off factor 0.20.
297    Ro0_20 = 0b10,
298    /// Reserved value.
299    ///
300    /// In DVB-S2, this value is reserved. In DVB-S2X, this value is used to
301    /// access an additional table with 3 narrower roll-off factors, by using
302    /// alternation as described above.
303    Reserved = 0b11,
304}
305
306impl Display for RollOff {
307    fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
308        write!(
309            f,
310            "α = {}",
311            match self {
312                RollOff::Ro0_35 => "0.35",
313                RollOff::Ro0_25 => "0.25",
314                RollOff::Ro0_20 => "0.20",
315                RollOff::Reserved => "reserved",
316            }
317        )
318    }
319}
320
321#[cfg(test)]
322mod test {
323    use super::*;
324    use hex_literal::hex;
325
326    const CONTINUOUS_GSE_HEADER: [u8; 10] = hex!("72 00 00 00 02 f0 00 00 00 15");
327    const GSE_HEM_HEADER: [u8; 10] = hex!("b2 00 00 00 02 f0 00 00 00 87");
328    const GSE_HEM_HEADER_ISSY: [u8; 10] = hex!("ba 00 12 34 02 f0 56 02 11 7c");
329
330    #[test]
331    fn continuous_gse_header() {
332        let header = BBHeader::new(&CONTINUOUS_GSE_HEADER);
333        assert_eq!(
334            format!("{}", header),
335            "BBHEADER(TS/GS = Generic continuous, SIS/MIS = single, CCM/ACM = CCM, \
336	     ISSYI = false, NPD/GSE-Lite = false, α = 0.20, ISI = 0, UPL = 0 bits, \
337	     DFL = 752 bits, SYNC = 0x00, SYNCD = 0x0000)"
338        );
339        assert_eq!(header.tsgs(), TsGs::GenericContinuous);
340        assert_eq!(header.sismis(), SisMis::Sis);
341        assert_eq!(header.ccmacm(), CcmAcm::Ccm);
342        assert!(!header.issyi());
343        assert!(!header.npd());
344        assert!(!header.gse_lite());
345        assert_eq!(header.rolloff(), RollOff::Ro0_20);
346        assert_eq!(header.isi(), 0);
347        assert_eq!(header.issy(), None);
348        assert_eq!(header.upl(), Some(0));
349        assert_eq!(header.dfl(), 752);
350        assert_eq!(header.sync(), Some(0));
351        assert_eq!(header.syncd(), 0);
352        assert_eq!(header.crc8(), CONTINUOUS_GSE_HEADER[9]);
353        assert_eq!(header.compute_crc8(), header.crc8());
354        assert!(header.crc_is_valid());
355    }
356
357    #[test]
358    fn gse_hem_header() {
359        let header = BBHeader::new(&GSE_HEM_HEADER);
360        assert_eq!(
361            format!("{}", header),
362            "BBHEADER(TS/GS = GSE-HEM, SIS/MIS = single, CCM/ACM = CCM, \
363	     ISSYI = false, NPD/GSE-Lite = false, α = 0.20, ISI = 0, \
364	     DFL = 752 bits, SYNCD = 0x0000)"
365        );
366        assert_eq!(header.tsgs(), TsGs::GseHem);
367        assert_eq!(header.sismis(), SisMis::Sis);
368        assert_eq!(header.ccmacm(), CcmAcm::Ccm);
369        assert!(!header.issyi());
370        assert!(!header.npd());
371        assert!(!header.gse_lite());
372        assert_eq!(header.rolloff(), RollOff::Ro0_20);
373        assert_eq!(header.isi(), 0);
374        assert_eq!(header.issy(), None);
375        assert_eq!(header.upl(), None);
376        assert_eq!(header.dfl(), 752);
377        assert_eq!(header.sync(), None);
378        assert_eq!(header.syncd(), 0);
379        assert_eq!(header.crc8(), GSE_HEM_HEADER[9]);
380        assert_eq!(header.compute_crc8(), header.crc8());
381        assert!(header.crc_is_valid());
382    }
383
384    #[test]
385    fn gse_hem_header_issy() {
386        let header = BBHeader::new(&GSE_HEM_HEADER_ISSY);
387        assert_eq!(
388            format!("{}", header),
389            "BBHEADER(TS/GS = GSE-HEM, SIS/MIS = single, CCM/ACM = CCM, \
390	     ISSYI = true, NPD/GSE-Lite = false, α = 0.20, ISI = 0, \
391	     ISSY = 0x123456, DFL = 752 bits, SYNCD = 0x0211)"
392        );
393        assert_eq!(header.tsgs(), TsGs::GseHem);
394        assert_eq!(header.sismis(), SisMis::Sis);
395        assert_eq!(header.ccmacm(), CcmAcm::Ccm);
396        assert!(header.issyi());
397        assert!(!header.npd());
398        assert!(!header.gse_lite());
399        assert_eq!(header.rolloff(), RollOff::Ro0_20);
400        assert_eq!(header.isi(), 0);
401        assert_eq!(header.issy(), Some([0x12, 0x34, 0x56]));
402        assert_eq!(header.upl(), None);
403        assert_eq!(header.dfl(), 752);
404        assert_eq!(header.sync(), None);
405        assert_eq!(header.syncd(), 0x211);
406        assert_eq!(header.crc8(), GSE_HEM_HEADER_ISSY[9]);
407        assert_eq!(header.compute_crc8(), header.crc8());
408        assert!(header.crc_is_valid());
409    }
410}
411
412#[cfg(test)]
413mod proptests {
414    use super::*;
415    use proptest::prelude::*;
416
417    proptest! {
418        #[test]
419        fn doesnt_panic(header: [u8; 10]) {
420            let header = BBHeader::new(&header);
421            header.tsgs();
422            header.sismis();
423            header.ccmacm();
424            header.issyi();
425            header.npd();
426            header.gse_lite();
427            header.rolloff();
428            header.isi();
429            header.upl();
430            header.dfl();
431            header.sync();
432            header.syncd();
433            header.crc8();
434            header.compute_crc8();
435            header.crc_is_valid();
436            let _ = format!("{header}");
437        }
438    }
439}