Skip to main content

dvb_si/descriptors/
satellite_delivery_system.rs

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