Skip to main content

dvb_si/descriptors/
satellite_delivery_system.rs

1//! Satellite Delivery System Descriptor -- ETSI EN 300 468 \u00a76.2.13.2 (tag 0x43).
2//!
3//! Carried inside the NIT's `transport_stream_loop`'s second descriptor loop.
4//! Conveys carrier tuning parameters for a DVB-S / DVB-S2 transponder.
5
6use crate::error::{Error, Result};
7use crate::traits::Descriptor;
8use dvb_common::{Parse, Serialize};
9
10/// Descriptor tag for satellite_delivery_system_descriptor.
11pub const TAG: u8 = 0x43;
12const HEADER_LEN: usize = 2;
13const BODY_LEN: u8 = 11;
14
15/// Polarization (\u00a76.2.13.2 Table 38).
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize))]
18pub enum Polarization {
19    /// Linear horizontal.
20    LinearHorizontal,
21    /// Linear vertical.
22    LinearVertical,
23    /// Circular left.
24    CircularLeft,
25    /// Circular right.
26    CircularRight,
27}
28
29/// Modulation system (§6.2.13.2 Table 40: DVB-S or DVB-S2).
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize))]
32pub enum ModulationSystem {
33    /// DVB-S (first generation).
34    DvbS,
35    /// DVB-S2 (second generation).
36    DvbS2,
37}
38
39/// Modulation type (\u00a76.2.13.2 Table 41).
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize))]
42pub enum ModulationType {
43    /// Auto-detect.
44    Auto,
45    /// QPSK.
46    Qpsk,
47    /// 8PSK.
48    Psk8,
49    /// 16QAM.
50    Qam16,
51}
52
53/// Roll-off factor (§6.2.13.2 Table 39, DVB-S2 only).
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55#[cfg_attr(feature = "serde", derive(serde::Serialize))]
56pub enum RollOff {
57    /// 0.35 (DVB-S default).
58    Alpha035,
59    /// 0.25 (DVB-S2 common).
60    Alpha025,
61    /// 0.20 (DVB-S2 narrow).
62    Alpha020,
63    /// Reserved -- do not emit.
64    Reserved,
65}
66
67/// Satellite Delivery System Descriptor.
68#[derive(Debug, Clone, PartialEq, Eq)]
69#[cfg_attr(feature = "serde", derive(serde::Serialize))]
70pub struct SatelliteDeliverySystemDescriptor {
71    /// 32-bit BCD frequency in GHz (e.g. 11_725_000 kHz \u2192 0x11725000 = 11.72500 GHz).
72    pub frequency_bcd: u32,
73    /// 16-bit BCD orbital position tenths of a degree (e.g. 0x1920 = 192.0\u00b0).
74    pub orbital_position_bcd: u16,
75    /// False = west, true = east.
76    pub east: bool,
77    /// Polarization.
78    pub polarization: Polarization,
79    /// DVB-S2 roll-off factor. Meaningful only when `modulation_system` is
80    /// DVB-S2 (Table 37); for DVB-S the bits are reserved_zero_future_use and
81    /// serialize emits them as 0 regardless of this field.
82    pub roll_off: RollOff,
83    /// Modulation system.
84    pub modulation_system: ModulationSystem,
85    /// Modulation type.
86    pub modulation_type: ModulationType,
87    /// 28-bit BCD symbol rate in Msym/s (e.g. 0x0275_000 = 27.500 Msym/s).
88    pub symbol_rate_bcd: u32,
89    /// 4-bit FEC inner code.
90    pub fec_inner: u8,
91}
92
93impl SatelliteDeliverySystemDescriptor {
94    /// Decode the 32-bit BCD `frequency` to Hz (1 kHz field resolution,
95    /// EN 300 468 §6.2.13.2). `None` if the BCD nibbles are out of range.
96    ///
97    /// e.g. `0x1172_5000` → `11_725_000_000` Hz (11.725 GHz).
98    #[must_use]
99    pub fn frequency_hz(&self) -> Option<u64> {
100        dvb_common::bcd::bcd_to_decimal(u64::from(self.frequency_bcd), 8).map(|khz| khz * 1_000)
101    }
102
103    /// Set `frequency` from Hz, encoding to the 8-digit BCD field at the field's
104    /// 1 kHz resolution (sub-kHz precision is truncated).
105    ///
106    /// # Errors
107    /// [`ValueOutOfRange`](crate::Error::ValueOutOfRange) if the value
108    /// exceeds the 8-digit BCD field.
109    pub fn set_frequency_hz(&mut self, hz: u64) -> crate::Result<()> {
110        self.frequency_bcd = super::encode_bcd_field(
111            hz / 1_000,
112            8,
113            "SatelliteDeliverySystemDescriptor::frequency",
114        )? as u32;
115        Ok(())
116    }
117
118    /// Decode the 28-bit BCD `symbol_rate` to symbols/second (100 sym/s
119    /// resolution). `None` if the BCD nibbles are out of range.
120    ///
121    /// e.g. `0x027_5000` → `27_500_000` (27.5 Msym/s).
122    #[must_use]
123    pub fn symbol_rate_sps(&self) -> Option<u64> {
124        dvb_common::bcd::bcd_to_decimal(u64::from(self.symbol_rate_bcd), 7).map(|v| v * 100)
125    }
126
127    /// Set `symbol_rate` from symbols/second (100 sym/s field resolution).
128    ///
129    /// # Errors
130    /// [`ValueOutOfRange`](crate::Error::ValueOutOfRange) on overflow of
131    /// the 7-digit BCD field.
132    pub fn set_symbol_rate_sps(&mut self, sps: u64) -> crate::Result<()> {
133        self.symbol_rate_bcd = super::encode_bcd_field(
134            sps / 100,
135            7,
136            "SatelliteDeliverySystemDescriptor::symbol_rate",
137        )? as u32;
138        Ok(())
139    }
140
141    /// Decode the 16-bit BCD `orbital_position` to degrees (tenths resolution).
142    /// `None` if the BCD nibbles are out of range. e.g. `0x1920` → `192.0`.
143    #[must_use]
144    pub fn orbital_position_deg(&self) -> Option<f64> {
145        dvb_common::bcd::bcd_to_decimal(u64::from(self.orbital_position_bcd), 4)
146            .map(|tenths| tenths as f64 / 10.0)
147    }
148
149    /// Set `orbital_position` in degrees, rounded to the field's tenth-degree
150    /// resolution. The east/west `east` flag is a separate field.
151    ///
152    /// # Errors
153    /// [`ValueOutOfRange`](crate::Error::ValueOutOfRange) if negative or
154    /// beyond the 4-digit BCD field.
155    pub fn set_orbital_position_deg(&mut self, deg: f64) -> crate::Result<()> {
156        if !(0.0..=6_553.5).contains(&deg) {
157            return Err(crate::Error::ValueOutOfRange {
158                field: "SatelliteDeliverySystemDescriptor::orbital_position",
159                reason: "degrees must be in 0.0..=6553.5",
160            });
161        }
162        let tenths = (deg * 10.0).round() as u64;
163        self.orbital_position_bcd = super::encode_bcd_field(
164            tenths,
165            4,
166            "SatelliteDeliverySystemDescriptor::orbital_position",
167        )? as u16;
168        Ok(())
169    }
170}
171
172impl<'a> Parse<'a> for SatelliteDeliverySystemDescriptor {
173    type Error = crate::error::Error;
174    fn parse(bytes: &'a [u8]) -> Result<Self> {
175        if bytes.len() < HEADER_LEN {
176            return Err(Error::BufferTooShort {
177                need: HEADER_LEN,
178                have: bytes.len(),
179                what: "satellite delivery system descriptor header",
180            });
181        }
182
183        let tag = bytes[0];
184        if tag != TAG {
185            return Err(Error::InvalidDescriptor {
186                tag,
187                reason: "expected tag 0x43",
188            });
189        }
190
191        let length = bytes[1] as usize;
192        let total = HEADER_LEN + length;
193
194        if bytes.len() < total {
195            return Err(Error::BufferTooShort {
196                need: total,
197                have: bytes.len(),
198                what: "satellite delivery system descriptor body",
199            });
200        }
201
202        if length != BODY_LEN as usize {
203            return Err(Error::InvalidDescriptor {
204                tag: TAG,
205                reason: "descriptor_length must equal 11",
206            });
207        }
208
209        let body = &bytes[HEADER_LEN..total];
210
211        // Frequency: 4 bytes BCD (GHz.MMMM)
212        let frequency_bcd = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
213
214        // Orbital position: 2 bytes BCD (tenths of a degree)
215        let orbital_position_bcd = u16::from_be_bytes([body[4], body[5]]);
216
217        // Flags byte 6: bit 7 = west_east_flag, bits 5-6 = polarization,
218        // bits 3-4 = roll_off, bit 2 = modulation_system, bits 1-0 = modulation_type
219        let flags = body[6];
220        let east = (flags & 0x80) != 0;
221
222        let pol_raw = (flags >> 5) & 0x03;
223        let polarization = match pol_raw {
224            0 => Polarization::LinearHorizontal,
225            1 => Polarization::LinearVertical,
226            2 => Polarization::CircularLeft,
227            _ => Polarization::CircularRight,
228        };
229
230        let roll_raw = (flags >> 3) & 0x03;
231        let roll_off = match roll_raw {
232            0 => RollOff::Alpha035,
233            1 => RollOff::Alpha025,
234            2 => RollOff::Alpha020,
235            _ => RollOff::Reserved,
236        };
237
238        let mod_sys_raw = (flags >> 2) & 0x01;
239        let modulation_system = match mod_sys_raw {
240            0 => ModulationSystem::DvbS,
241            _ => ModulationSystem::DvbS2,
242        };
243
244        let mod_type_raw = flags & 0x03;
245        let modulation_type = match mod_type_raw {
246            0 => ModulationType::Auto,
247            1 => ModulationType::Qpsk,
248            2 => ModulationType::Psk8,
249            _ => ModulationType::Qam16,
250        };
251
252        // Symbol rate: 28-bit BCD packed into 4 bytes (3.5 bytes + 4-bit FEC)
253        let symbol_rate_and_fec = u32::from_be_bytes([body[7], body[8], body[9], body[10]]);
254        let symbol_rate_bcd = symbol_rate_and_fec >> 4;
255        let fec_inner = (symbol_rate_and_fec & 0x0F) as u8;
256
257        Ok(SatelliteDeliverySystemDescriptor {
258            frequency_bcd,
259            orbital_position_bcd,
260            east,
261            polarization,
262            roll_off,
263            modulation_system,
264            modulation_type,
265            symbol_rate_bcd,
266            fec_inner,
267        })
268    }
269}
270
271impl Serialize for SatelliteDeliverySystemDescriptor {
272    type Error = crate::error::Error;
273    fn serialized_len(&self) -> usize {
274        HEADER_LEN + BODY_LEN as usize
275    }
276
277    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
278        let len = self.serialized_len();
279        if buf.len() < len {
280            return Err(Error::OutputBufferTooSmall {
281                need: len,
282                have: buf.len(),
283            });
284        }
285
286        buf[0] = TAG;
287        buf[1] = BODY_LEN;
288
289        // Frequency: 4 bytes BCD
290        let freq_bytes = self.frequency_bcd.to_be_bytes();
291        buf[2..6].copy_from_slice(&freq_bytes);
292
293        // Orbital position: 2 bytes BCD
294        let orb_bytes = self.orbital_position_bcd.to_be_bytes();
295        buf[6..8].copy_from_slice(&orb_bytes);
296
297        // Flags byte: combine west_east, polarization, roll_off, modulation_system, modulation_type
298        let mut flags: u8 = 0;
299        if self.east {
300            flags |= 0x80;
301        }
302        flags |= match self.polarization {
303            Polarization::LinearHorizontal => 0x00,
304            Polarization::LinearVertical => 0x20,
305            Polarization::CircularLeft => 0x40,
306            Polarization::CircularRight => 0x60,
307        };
308        // Table 37: roll_off exists only when modulation_system == DVB-S2;
309        // for DVB-S those 2 bits are reserved_zero_future_use and SHALL be 0.
310        if self.modulation_system == ModulationSystem::DvbS2 {
311            flags |= match self.roll_off {
312                RollOff::Alpha035 => 0x00,
313                RollOff::Alpha025 => 0x08,
314                RollOff::Alpha020 => 0x10,
315                RollOff::Reserved => 0x18,
316            };
317        }
318        flags |= match self.modulation_system {
319            ModulationSystem::DvbS => 0x00,
320            ModulationSystem::DvbS2 => 0x04,
321        };
322        flags |= match self.modulation_type {
323            ModulationType::Auto => 0x00,
324            ModulationType::Qpsk => 0x01,
325            ModulationType::Psk8 => 0x02,
326            ModulationType::Qam16 => 0x03,
327        };
328        buf[8] = flags;
329
330        // Symbol rate + FEC_inner: 28-bit BCD shifted left 4 bits, then OR with FEC.
331        // Mask to 28 bits so an over-range value can't spill past the field.
332        let sym_freq =
333            ((self.symbol_rate_bcd & 0x0FFF_FFFF) << 4) | (u32::from(self.fec_inner) & 0x0F);
334        let sym_bytes = sym_freq.to_be_bytes();
335        buf[9..13].copy_from_slice(&sym_bytes);
336
337        Ok(len)
338    }
339}
340
341impl<'a> Descriptor<'a> for SatelliteDeliverySystemDescriptor {
342    const TAG: u8 = TAG;
343
344    fn descriptor_length(&self) -> u8 {
345        BODY_LEN
346    }
347}
348
349impl<'a> crate::traits::DescriptorDef<'a> for SatelliteDeliverySystemDescriptor {
350    const TAG: u8 = TAG;
351    const NAME: &'static str = "SATELLITE_DELIVERY_SYSTEM";
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357
358    /// Build a valid 13-byte descriptor (2 header + 11 body) and confirm
359    /// parse extracts frequency and orbital position correctly.
360    #[test]
361    fn parse_extracts_frequency_and_orbital_position() {
362        // frequency: 11.7250 GHz \u2192 BCD 0x11 0x72 0x50 0x00
363        // orbital: 192.0\u00b0 \u2192 BCD 0x19 0x20
364        let raw: Vec<u8> = vec![
365            TAG, BODY_LEN, 0x11, 0x72, 0x50, 0x00, // frequency
366            0x19, 0x20, // orbital position
367            0x00, // flags (all defaults: west, linear-h, alpha-035, DVB-S, auto)
368            0x02, 0x75, 0x00, 0x00, // symbol rate 27.500 Msym/s, FEC 0
369        ];
370        let desc = SatelliteDeliverySystemDescriptor::parse(&raw).unwrap();
371        assert_eq!(desc.frequency_bcd, 0x11725000);
372        assert_eq!(desc.orbital_position_bcd, 0x1920);
373    }
374
375    /// Flags byte bit 7 encodes west/east direction.
376    #[test]
377    fn parse_extracts_west_east_flag() {
378        // East: bit 7 = 1
379        let raw_east: Vec<u8> = vec![
380            TAG, BODY_LEN, 0x11, 0x72, 0x50, 0x00, 0x19, 0x20,
381            0x80, // east flag set, everything else zero
382            0x02, 0x75, 0x00, 0x00,
383        ];
384        let desc_east = SatelliteDeliverySystemDescriptor::parse(&raw_east).unwrap();
385        assert!(desc_east.east, "east should be true when bit 7 is set");
386
387        // West: bit 7 = 0
388        let raw_west: Vec<u8> = vec![
389            TAG, BODY_LEN, 0x11, 0x72, 0x50, 0x00, 0x19, 0x20, 0x00, // east flag clear
390            0x02, 0x75, 0x00, 0x00,
391        ];
392        let desc_west = SatelliteDeliverySystemDescriptor::parse(&raw_west).unwrap();
393        assert!(!desc_west.east, "east should be false when bit 7 is clear");
394    }
395
396    /// All four polarization values are extracted correctly from bits 5-6.
397    #[test]
398    fn parse_extracts_polarization_variants() {
399        let pol_pairs: [(u8, Polarization); 4] = [
400            (0x00, Polarization::LinearHorizontal),
401            (0x20, Polarization::LinearVertical),
402            (0x40, Polarization::CircularLeft),
403            (0x60, Polarization::CircularRight),
404        ];
405
406        for (offset, expected_pol) in pol_pairs {
407            let raw: Vec<u8> = vec![
408                TAG, BODY_LEN, 0x11, 0x72, 0x50, 0x00, 0x19, 0x20,
409                offset, // polarization bits
410                0x02, 0x75, 0x00, 0x00,
411            ];
412            let desc = SatelliteDeliverySystemDescriptor::parse(&raw).unwrap();
413            assert_eq!(
414                desc.polarization, expected_pol,
415                "polarization mismatch for offset 0x{:02x}",
416                offset
417            );
418        }
419    }
420
421    /// Modulation system (bit 2) and modulation type (bits 1-0) are extracted.
422    #[test]
423    fn parse_extracts_modulation_system_and_type() {
424        // DVB-S (bit 2 = 0), QPSK (bits 1-0 = 01)
425        let raw: Vec<u8> = vec![
426            TAG, BODY_LEN, 0x11, 0x72, 0x50, 0x00, 0x19, 0x20, 0x01, // DVB-S, QPSK
427            0x02, 0x75, 0x00, 0x00,
428        ];
429        let desc = SatelliteDeliverySystemDescriptor::parse(&raw).unwrap();
430        assert_eq!(desc.modulation_system, ModulationSystem::DvbS);
431        assert_eq!(desc.modulation_type, ModulationType::Qpsk);
432
433        // DVB-S2 (bit 2 = 1), 8PSK (bits 1-0 = 10)
434        let raw2: Vec<u8> = vec![
435            TAG, BODY_LEN, 0x11, 0x72, 0x50, 0x00, 0x19, 0x20,
436            0x06, // DVB-S2 (0x04) + 8PSK (0x02)
437            0x02, 0x75, 0x00, 0x00,
438        ];
439        let desc2 = SatelliteDeliverySystemDescriptor::parse(&raw2).unwrap();
440        assert_eq!(desc2.modulation_system, ModulationSystem::DvbS2);
441        assert_eq!(desc2.modulation_type, ModulationType::Psk8);
442    }
443
444    /// Roll-off codes (bits 3-4) are extracted correctly.
445    #[test]
446    fn parse_extracts_roll_off() {
447        let roll_pairs: [(u8, RollOff); 4] = [
448            (0x00, RollOff::Alpha035),
449            (0x08, RollOff::Alpha025),
450            (0x10, RollOff::Alpha020),
451            (0x18, RollOff::Reserved),
452        ];
453
454        for (offset, expected_roll) in roll_pairs {
455            let raw: Vec<u8> = vec![
456                TAG, BODY_LEN, 0x11, 0x72, 0x50, 0x00, 0x19, 0x20, offset, // roll-off bits
457                0x02, 0x75, 0x00, 0x00,
458            ];
459            let desc = SatelliteDeliverySystemDescriptor::parse(&raw).unwrap();
460            assert_eq!(desc.roll_off, expected_roll);
461        }
462    }
463
464    /// Symbol rate (28-bit BCD) and FEC inner (4 bits) are extracted from
465    /// the last 4 bytes.
466    #[test]
467    fn parse_extracts_symbol_rate_and_fec() {
468        // symbol_rate: 27.500 Msym/s \u2192 BCD 0x027500, FEC: 5/6 \u2192 0x5
469        let raw: Vec<u8> = vec![
470            TAG, BODY_LEN, 0x11, 0x72, 0x50, 0x00, 0x19, 0x20, 0x00, 0x02, 0x75, 0x00,
471            0x05, // symbol_rate_bcd = 0x027500, fec_inner = 5
472        ];
473        let desc = SatelliteDeliverySystemDescriptor::parse(&raw).unwrap();
474        assert_eq!(desc.symbol_rate_bcd, 0x0275000);
475        assert_eq!(desc.fec_inner, 5);
476
477        // FEC full range test: 0x0 to 0xF
478        let raw2: Vec<u8> = vec![
479            TAG, BODY_LEN, 0x11, 0x72, 0x50, 0x00, 0x19, 0x20, 0x00, 0x02, 0x75, 0x00,
480            0x0F, // FEC = 0x0F
481        ];
482        let desc2 = SatelliteDeliverySystemDescriptor::parse(&raw2).unwrap();
483        assert_eq!(desc2.fec_inner, 0x0F);
484    }
485
486    /// Wrong tag byte should return InvalidDescriptor.
487    #[test]
488    fn parse_rejects_wrong_tag() {
489        let raw: Vec<u8> = vec![
490            0x44, // wrong tag (cable delivery system)
491            BODY_LEN, 0x11, 0x72, 0x50, 0x00, 0x19, 0x20, 0x00, 0x02, 0x75, 0x00, 0x00,
492        ];
493        let err = SatelliteDeliverySystemDescriptor::parse(&raw).unwrap_err();
494        assert!(
495            matches!(err, Error::InvalidDescriptor { tag: 0x44, .. }),
496            "expected InvalidDescriptor(tag=0x44), got {err:?}"
497        );
498    }
499
500    /// Body length must be exactly 11. Wrong length returns InvalidDescriptor.
501    #[test]
502    fn parse_rejects_wrong_length() {
503        let raw: Vec<u8> = vec![
504            TAG, 0x05, // wrong length (should be 11)
505            0x11, 0x72, 0x50, 0x00, 0x19, 0x20,
506        ];
507        let err = SatelliteDeliverySystemDescriptor::parse(&raw).unwrap_err();
508        assert!(
509            matches!(
510                err,
511                Error::InvalidDescriptor {
512                    reason: "descriptor_length must equal 11",
513                    ..
514                }
515            ),
516            "expected InvalidDescriptor about length, got {err:?}"
517        );
518    }
519
520    /// Parse \u2192 serialize \u2192 re-parse should yield an equal struct and
521    /// identical bytes.
522    #[test]
523    fn serialize_round_trip() {
524        let desc = SatelliteDeliverySystemDescriptor {
525            frequency_bcd: 0x11725000,
526            orbital_position_bcd: 0x1920,
527            east: true,
528            polarization: Polarization::CircularRight,
529            roll_off: RollOff::Alpha025,
530            modulation_system: ModulationSystem::DvbS2,
531            modulation_type: ModulationType::Psk8,
532            symbol_rate_bcd: 0x027500,
533            fec_inner: 5,
534        };
535
536        let mut buf = vec![0u8; desc.serialized_len()];
537        let written = desc.serialize_into(&mut buf).unwrap();
538        assert_eq!(written, desc.serialized_len());
539
540        let reparsed = SatelliteDeliverySystemDescriptor::parse(&buf).unwrap();
541        assert_eq!(desc, reparsed);
542    }
543}