Skip to main content

dvb_ci/ci_ext/
stream_input.rs

1//! StreamInput objects — ETSI TS 101 699 V1.1.1 §6.1.2, Tables 12-20
2//! (PDF pp. 25-28). See `docs/ci_plus/input-modules.md`.
3//!
4//! Resource ID `0x00801ii1` (`ii` = Module ID), single session. A Type 'A' input
5//! module presents StreamInput: it delivers broadcast services at the **TS
6//! level**; the host scans for, and tunes to, transport streams.
7//!
8//! - `DeliverySystemInfoReq` (`9F 80 00`, Table 13) — host → module: header-only.
9//! - `DeliverySystemInfoAck` (`9F 80 01`, Table 14) — module → host: a list of
10//!   `SystemIdentifier`s (Table 15: 0=Abstract, 1=DVB-C, 2=DVB-S, 3=DVB-T).
11//! - `ScanStartReq` (`9F 80 02`, Table 16) — host → module: header-only.
12//! - `ScanNextReq` (`9F 80 03`, Table 17) — host → module: header-only.
13//! - `ScanAck` (`9F 80 04`, Table 18) — module → host: `TSState` + 11-byte
14//!   `TuningInformationMessage` + `ScanProgress`.
15//! - `TuneTSReq` (`9F 80 05`, Table 19) — host → module: an (optional) 11-byte
16//!   `TuningInformationMessage` (absent = disconnect from network).
17//! - `TuneTSAck` (`9F 80 06`, Table 20) — module → host: `TSState`.
18
19use crate::error::{Error, Result};
20use crate::objects;
21use alloc::vec::Vec;
22use broadcast_common::{Parse, Serialize};
23
24/// Resource-scoped `apdu_tag`s for StreamInput (Tables 13-20).
25pub mod tag {
26    use crate::tag::ApduTag;
27    /// `DeliverySystemInfoReqTag` = `9F 80 00`.
28    pub const DELIVERY_SYSTEM_INFO_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x00);
29    /// `DeliverySystemInfoAckTag` = `9F 80 01`.
30    pub const DELIVERY_SYSTEM_INFO_ACK: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x01);
31    /// `ScanStartReqTag` = `9F 80 02`.
32    pub const SCAN_START_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x02);
33    /// `ScanNextReqTag` = `9F 80 03`.
34    pub const SCAN_NEXT_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x03);
35    /// `ScanAckTag` = `9F 80 04`.
36    pub const SCAN_ACK: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x04);
37    /// `TuneTSReqTag` = `9F 80 05`.
38    pub const TUNE_TS_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x05);
39    /// `TuneTSAckTag` = `9F 80 06`.
40    pub const TUNE_TS_ACK: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x06);
41}
42
43/// Length of a `TuningInformationMessage` — always 11 bytes (`11 x 8`, §6.1.1).
44pub const TUNING_INFO_MESSAGE_LEN: usize = 11;
45
46/// `SystemIdentifier` — the delivery system a module connects to (Table 15).
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize))]
49#[non_exhaustive]
50pub enum SystemIdentifier {
51    /// `0` — "Abstract" (tuning info is module-specific).
52    Abstract,
53    /// `1` — DVB-C (tuning info as the DVB SI cable delivery system descriptor).
54    DvbC,
55    /// `2` — DVB-S (tuning info as the DVB SI satellite delivery system descriptor).
56    DvbS,
57    /// `3` — DVB-T (tuning info as the DVB SI terrestrial delivery system descriptor).
58    DvbT,
59    /// `> 3` — reserved for future use.
60    Reserved(u8),
61}
62
63impl SystemIdentifier {
64    /// Decode a `SystemIdentifier` byte.
65    #[must_use]
66    pub fn from_u8(v: u8) -> Self {
67        match v {
68            0 => Self::Abstract,
69            1 => Self::DvbC,
70            2 => Self::DvbS,
71            3 => Self::DvbT,
72            other => Self::Reserved(other),
73        }
74    }
75    /// Wire byte.
76    #[must_use]
77    pub const fn to_u8(self) -> u8 {
78        match self {
79            Self::Abstract => 0,
80            Self::DvbC => 1,
81            Self::DvbS => 2,
82            Self::DvbT => 3,
83            Self::Reserved(v) => v,
84        }
85    }
86    /// Spec token, or `"reserved"`.
87    #[must_use]
88    pub fn name(&self) -> &'static str {
89        match self {
90            Self::Abstract => "Abstract",
91            Self::DvbC => "DVB-C",
92            Self::DvbS => "DVB-S",
93            Self::DvbT => "DVB-T",
94            Self::Reserved(_) => "reserved",
95        }
96    }
97}
98broadcast_common::impl_spec_display!(SystemIdentifier, Reserved);
99
100/// `DeliverySystemInfoReq()` (Table 13) — host → module: header-only.
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
102#[cfg_attr(feature = "serde", derive(serde::Serialize))]
103pub struct DeliverySystemInfoReq;
104
105/// `ScanStartReq()` (Table 16) — host → module: header-only.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
107#[cfg_attr(feature = "serde", derive(serde::Serialize))]
108pub struct ScanStartReq;
109
110/// `ScanNextReq()` (Table 17) — host → module: header-only.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
112#[cfg_attr(feature = "serde", derive(serde::Serialize))]
113pub struct ScanNextReq;
114
115/// `DeliverySystemInfoAck()` (Table 14) — module → host: the delivery systems the
116/// module is connected to (one `SystemIdentifier` per byte).
117#[derive(Debug, Clone, PartialEq, Eq, Default)]
118#[cfg_attr(feature = "serde", derive(serde::Serialize))]
119pub struct DeliverySystemInfoAck {
120    /// `SystemIdentifier`s in wire order (`length_field = N`).
121    pub systems: Vec<SystemIdentifier>,
122}
123
124/// `ScanAck()` (Table 18) — module → host: a TS found during a scan.
125#[derive(Debug, Clone, PartialEq, Eq)]
126#[cfg_attr(feature = "serde", derive(serde::Serialize))]
127pub struct ScanAck<'a> {
128    /// `TSState` — `0` = no signal (or, when auto-scanning, all frequencies
129    /// searched); `1`-`255` = normalized signal-quality (bigger is better).
130    pub ts_state: u8,
131    /// `TuningInformationMessage` — 11-byte delivery-system-dependent coding to
132    /// re-acquire the TS. Undefined when `ts_state == 0`.
133    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
134    pub tuning_information_message: &'a [u8],
135    /// `ScanProgress` — 0-255, approximate proportional indication of scan progress.
136    pub scan_progress: u8,
137}
138
139/// `TuneTSReq()` (Table 19) — host → module: tune to a TS. An absent
140/// `TuningInformationMessage` (zero following bytes) requests a network disconnect.
141#[derive(Debug, Clone, PartialEq, Eq, Default)]
142#[cfg_attr(feature = "serde", derive(serde::Serialize))]
143pub struct TuneTSReq<'a> {
144    /// `TuningInformationMessage` — 11 bytes, or empty (= disconnect from network).
145    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
146    pub tuning_information_message: &'a [u8],
147}
148
149/// `TuneTSAck()` (Table 20) — module → host: tune result.
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
151#[cfg_attr(feature = "serde", derive(serde::Serialize))]
152pub struct TuneTSAck {
153    /// `TSState` — identical coding to the [`ScanAck`] `TSState`; `0` after a
154    /// disconnect request.
155    pub ts_state: u8,
156}
157
158// --- header-only objects ---
159
160crate::dispatch::declare_empty_apdu!(
161    DeliverySystemInfoReq,
162    tag::DELIVERY_SYSTEM_INFO_REQ,
163    "DeliverySystemInfoReq"
164);
165crate::dispatch::declare_empty_apdu!(ScanStartReq, tag::SCAN_START_REQ, "ScanStartReq");
166crate::dispatch::declare_empty_apdu!(ScanNextReq, tag::SCAN_NEXT_REQ, "ScanNextReq");
167
168// --- DeliverySystemInfoAck ---
169
170impl<'a> Parse<'a> for DeliverySystemInfoAck {
171    type Error = Error;
172    fn parse(bytes: &'a [u8]) -> Result<Self> {
173        let body = objects::parse_apdu_header(
174            bytes,
175            tag::DELIVERY_SYSTEM_INFO_ACK,
176            "DeliverySystemInfoAck",
177        )?;
178        let mut systems = Vec::with_capacity(body.len());
179        for &b in body {
180            systems.push(SystemIdentifier::from_u8(b));
181        }
182        Ok(Self { systems })
183    }
184}
185impl Serialize for DeliverySystemInfoAck {
186    type Error = Error;
187    fn serialized_len(&self) -> usize {
188        objects::apdu_len(self.systems.len())
189    }
190    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
191        let body_len = self.systems.len();
192        let mut pos = objects::write_apdu_header(tag::DELIVERY_SYSTEM_INFO_ACK, body_len, buf)?;
193        for s in &self.systems {
194            buf[pos] = s.to_u8();
195            pos += 1;
196        }
197        Ok(pos)
198    }
199}
200
201// --- ScanAck ---
202
203// TSState(1) + TuningInformationMessage(11) + ScanProgress(1).
204const SCAN_ACK_BODY: usize = 1 + TUNING_INFO_MESSAGE_LEN + 1;
205
206impl<'a> Parse<'a> for ScanAck<'a> {
207    type Error = Error;
208    fn parse(bytes: &'a [u8]) -> Result<Self> {
209        let body = objects::parse_apdu_header(bytes, tag::SCAN_ACK, "ScanAck")?;
210        if body.len() < SCAN_ACK_BODY {
211            return Err(Error::BufferTooShort {
212                need: SCAN_ACK_BODY,
213                have: body.len(),
214                what: "ScanAck",
215            });
216        }
217        crate::objects::reject_trailing_body(body, SCAN_ACK_BODY, "ScanAck")?;
218        Ok(Self {
219            ts_state: body[0],
220            tuning_information_message: &body[1..1 + TUNING_INFO_MESSAGE_LEN],
221            scan_progress: body[1 + TUNING_INFO_MESSAGE_LEN],
222        })
223    }
224}
225impl Serialize for ScanAck<'_> {
226    type Error = Error;
227    fn serialized_len(&self) -> usize {
228        objects::apdu_len(SCAN_ACK_BODY)
229    }
230    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
231        if self.tuning_information_message.len() != TUNING_INFO_MESSAGE_LEN {
232            return Err(Error::InvalidObject {
233                what: "ScanAck",
234                reason: "TuningInformationMessage must be exactly 11 bytes",
235            });
236        }
237        let mut pos = objects::write_apdu_header(tag::SCAN_ACK, SCAN_ACK_BODY, buf)?;
238        buf[pos] = self.ts_state;
239        pos += 1;
240        buf[pos..pos + TUNING_INFO_MESSAGE_LEN].copy_from_slice(self.tuning_information_message);
241        pos += TUNING_INFO_MESSAGE_LEN;
242        buf[pos] = self.scan_progress;
243        Ok(pos + 1)
244    }
245}
246
247// --- TuneTSReq ---
248
249impl<'a> Parse<'a> for TuneTSReq<'a> {
250    type Error = Error;
251    fn parse(bytes: &'a [u8]) -> Result<Self> {
252        let body = objects::parse_apdu_header(bytes, tag::TUNE_TS_REQ, "TuneTSReq")?;
253        // The TuningInformationMessage is either absent (disconnect) or 11 bytes.
254        if !body.is_empty() && body.len() != TUNING_INFO_MESSAGE_LEN {
255            return Err(Error::InvalidObject {
256                what: "TuneTSReq",
257                reason: "TuningInformationMessage must be absent or exactly 11 bytes",
258            });
259        }
260        Ok(Self {
261            tuning_information_message: body,
262        })
263    }
264}
265impl Serialize for TuneTSReq<'_> {
266    type Error = Error;
267    fn serialized_len(&self) -> usize {
268        objects::apdu_len(self.tuning_information_message.len())
269    }
270    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
271        if !self.tuning_information_message.is_empty()
272            && self.tuning_information_message.len() != TUNING_INFO_MESSAGE_LEN
273        {
274            return Err(Error::InvalidObject {
275                what: "TuneTSReq",
276                reason: "TuningInformationMessage must be absent or exactly 11 bytes",
277            });
278        }
279        let body_len = self.tuning_information_message.len();
280        let pos = objects::write_apdu_header(tag::TUNE_TS_REQ, body_len, buf)?;
281        buf[pos..pos + body_len].copy_from_slice(self.tuning_information_message);
282        Ok(pos + body_len)
283    }
284}
285
286// --- TuneTSAck ---
287
288// TSState(1).
289const TUNE_TS_ACK_BODY: usize = 1;
290
291impl<'a> Parse<'a> for TuneTSAck {
292    type Error = Error;
293    fn parse(bytes: &'a [u8]) -> Result<Self> {
294        let body = objects::parse_apdu_header(bytes, tag::TUNE_TS_ACK, "TuneTSAck")?;
295        if body.len() < TUNE_TS_ACK_BODY {
296            return Err(Error::BufferTooShort {
297                need: TUNE_TS_ACK_BODY,
298                have: body.len(),
299                what: "TuneTSAck",
300            });
301        }
302        crate::objects::reject_trailing_body(body, TUNE_TS_ACK_BODY, "TuneTSAck")?;
303        Ok(Self { ts_state: body[0] })
304    }
305}
306impl Serialize for TuneTSAck {
307    type Error = Error;
308    fn serialized_len(&self) -> usize {
309        objects::apdu_len(TUNE_TS_ACK_BODY)
310    }
311    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
312        let pos = objects::write_apdu_header(tag::TUNE_TS_ACK, TUNE_TS_ACK_BODY, buf)?;
313        buf[pos] = self.ts_state;
314        Ok(pos + TUNE_TS_ACK_BODY)
315    }
316}
317
318crate::dispatch::declare_resource_apdus! {
319    /// Resource-scoped dispatch over the StreamInput objects (Tables 13-20).
320    #[derive(Debug, Clone, PartialEq, Eq)]
321    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
322    #[non_exhaustive]
323    pub enum StreamInputApdu<'a> ("stream_input") {
324        /// `DeliverySystemInfoReq` (`9F 80 00`).
325        DeliverySystemInfoReq(DeliverySystemInfoReq) = tag::DELIVERY_SYSTEM_INFO_REQ,
326        /// `DeliverySystemInfoAck` (`9F 80 01`).
327        DeliverySystemInfoAck(DeliverySystemInfoAck) = tag::DELIVERY_SYSTEM_INFO_ACK,
328        /// `ScanStartReq` (`9F 80 02`).
329        ScanStartReq(ScanStartReq) = tag::SCAN_START_REQ,
330        /// `ScanNextReq` (`9F 80 03`).
331        ScanNextReq(ScanNextReq) = tag::SCAN_NEXT_REQ,
332        /// `ScanAck` (`9F 80 04`).
333        ScanAck(ScanAck<'a>) = tag::SCAN_ACK,
334        /// `TuneTSReq` (`9F 80 05`).
335        TuneTSReq(TuneTSReq<'a>) = tag::TUNE_TS_REQ,
336        /// `TuneTSAck` (`9F 80 06`).
337        TuneTSAck(TuneTSAck) = tag::TUNE_TS_ACK,
338    }
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344
345    #[test]
346    fn header_only_objects_round_trip() {
347        assert_eq!(DeliverySystemInfoReq.to_bytes(), [0x9F, 0x80, 0x00, 0x00]);
348        assert_eq!(ScanStartReq.to_bytes(), [0x9F, 0x80, 0x02, 0x00]);
349        assert_eq!(ScanNextReq.to_bytes(), [0x9F, 0x80, 0x03, 0x00]);
350        assert_eq!(
351            DeliverySystemInfoReq::parse(&[0x9F, 0x80, 0x00, 0x00]).unwrap(),
352            DeliverySystemInfoReq
353        );
354        assert_eq!(
355            ScanNextReq::parse(&[0x9F, 0x80, 0x03, 0x00]).unwrap(),
356            ScanNextReq
357        );
358    }
359
360    #[test]
361    fn delivery_system_info_ack_multi_round_trips_and_bites() {
362        let ack = DeliverySystemInfoAck {
363            systems: alloc::vec![
364                SystemIdentifier::DvbC,
365                SystemIdentifier::DvbS,
366                SystemIdentifier::DvbT,
367            ],
368        };
369        let bytes = ack.to_bytes();
370        // tag(3) + len(1) + 3 = 7; body len = 3 = 0x03.
371        assert_eq!(bytes, [0x9F, 0x80, 0x01, 0x03, 0x01, 0x02, 0x03]);
372        assert_eq!(DeliverySystemInfoAck::parse(&bytes).unwrap(), ack);
373        assert_eq!(ack.systems[0].name(), "DVB-C");
374        let mut other = ack.clone();
375        other.systems[2] = SystemIdentifier::Abstract;
376        assert_ne!(bytes, other.to_bytes());
377    }
378
379    #[test]
380    fn delivery_system_info_ack_reserved_value() {
381        let ack = DeliverySystemInfoAck {
382            systems: alloc::vec![SystemIdentifier::from_u8(0x7F)],
383        };
384        assert_eq!(ack.systems[0], SystemIdentifier::Reserved(0x7F));
385        let bytes = ack.to_bytes();
386        assert_eq!(bytes, [0x9F, 0x80, 0x01, 0x01, 0x7F]);
387        assert_eq!(DeliverySystemInfoAck::parse(&bytes).unwrap(), ack);
388    }
389
390    #[test]
391    fn scan_ack_round_trips_and_bites() {
392        let tim = [
393            0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB,
394        ];
395        let ack = ScanAck {
396            ts_state: 0xC8,
397            tuning_information_message: &tim,
398            scan_progress: 0x40,
399        };
400        let bytes = ack.to_bytes();
401        // tag(3) + len(1) + state(1) + tim(11) + progress(1) = 17; body = 13 = 0x0D.
402        assert_eq!(
403            bytes,
404            [
405                0x9F, 0x80, 0x04, 0x0D, 0xC8, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99,
406                0xAA, 0xBB, 0x40
407            ]
408        );
409        assert_eq!(ScanAck::parse(&bytes).unwrap(), ack);
410        let mut other = ack.clone();
411        other.scan_progress = 0x41;
412        assert_ne!(bytes, other.to_bytes());
413    }
414
415    #[test]
416    fn tune_ts_req_with_message_round_trips_and_bites() {
417        let tim = [
418            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B,
419        ];
420        let req = TuneTSReq {
421            tuning_information_message: &tim,
422        };
423        let bytes = req.to_bytes();
424        assert_eq!(
425            bytes,
426            [
427                0x9F, 0x80, 0x05, 0x0B, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
428                0x0B
429            ]
430        );
431        assert_eq!(TuneTSReq::parse(&bytes).unwrap(), req);
432        let mut tim2 = tim;
433        tim2[10] = 0xFF;
434        let other = TuneTSReq {
435            tuning_information_message: &tim2,
436        };
437        assert_ne!(bytes, other.to_bytes());
438    }
439
440    #[test]
441    fn tune_ts_req_disconnect_empty_message() {
442        let req = TuneTSReq {
443            tuning_information_message: &[],
444        };
445        let bytes = req.to_bytes();
446        assert_eq!(bytes, [0x9F, 0x80, 0x05, 0x00]);
447        assert_eq!(TuneTSReq::parse(&bytes).unwrap(), req);
448    }
449
450    #[test]
451    fn tune_ts_req_rejects_wrong_length() {
452        // 5-byte TuningInformationMessage is neither absent nor 11 bytes.
453        let bad = [0x9F, 0x80, 0x05, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05];
454        assert!(matches!(
455            TuneTSReq::parse(&bad),
456            Err(Error::InvalidObject { .. })
457        ));
458    }
459
460    #[test]
461    fn tune_ts_ack_round_trips_and_bites() {
462        let ack = TuneTSAck { ts_state: 0xFF };
463        let bytes = ack.to_bytes();
464        assert_eq!(bytes, [0x9F, 0x80, 0x06, 0x01, 0xFF]);
465        assert_eq!(TuneTSAck::parse(&bytes).unwrap(), ack);
466        let other = TuneTSAck { ts_state: 0x00 };
467        assert_ne!(bytes, other.to_bytes());
468    }
469
470    #[test]
471    fn dispatch_routes_each_tag() {
472        let req = DeliverySystemInfoReq.to_bytes();
473        assert!(matches!(
474            StreamInputApdu::parse(&req).unwrap(),
475            StreamInputApdu::DeliverySystemInfoReq(_)
476        ));
477        let ack = TuneTSAck { ts_state: 1 }.to_bytes();
478        let parsed = StreamInputApdu::parse(&ack).unwrap();
479        assert!(matches!(parsed, StreamInputApdu::TuneTSAck(_)));
480        assert_eq!(parsed.to_bytes(), ack);
481        // ScanNextReq is 9F8003 (Table 17) — distinct from ScanStartReq 9F8002.
482        let next = ScanNextReq.to_bytes();
483        assert!(matches!(
484            StreamInputApdu::parse(&next).unwrap(),
485            StreamInputApdu::ScanNextReq(_)
486        ));
487    }
488}