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    /// How the `length_field` was coded on the wire: a non-minimal long form
105    /// (e.g. `82 00 09`) is preserved so the object re-serializes
106    /// byte-for-byte; [`LengthForm::Minimal`](crate::length::LengthForm::Minimal) for a hand-built value.
107    #[cfg_attr(feature = "serde", serde(skip))]
108    pub length_form: crate::length::LengthForm,
109}
110
111/// `ScanStartReq()` (Table 16) — host → module: header-only.
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
113#[cfg_attr(feature = "serde", derive(serde::Serialize))]
114pub struct ScanStartReq {
115    /// How the `length_field` was coded on the wire: a non-minimal long form
116    /// (e.g. `82 00 09`) is preserved so the object re-serializes
117    /// byte-for-byte; [`LengthForm::Minimal`](crate::length::LengthForm::Minimal) for a hand-built value.
118    #[cfg_attr(feature = "serde", serde(skip))]
119    pub length_form: crate::length::LengthForm,
120}
121
122/// `ScanNextReq()` (Table 17) — host → module: header-only.
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
124#[cfg_attr(feature = "serde", derive(serde::Serialize))]
125pub struct ScanNextReq {
126    /// How the `length_field` was coded on the wire: a non-minimal long form
127    /// (e.g. `82 00 09`) is preserved so the object re-serializes
128    /// byte-for-byte; [`LengthForm::Minimal`](crate::length::LengthForm::Minimal) for a hand-built value.
129    #[cfg_attr(feature = "serde", serde(skip))]
130    pub length_form: crate::length::LengthForm,
131}
132
133/// `DeliverySystemInfoAck()` (Table 14) — module → host: the delivery systems the
134/// module is connected to (one `SystemIdentifier` per byte).
135#[derive(Debug, Clone, PartialEq, Eq, Default)]
136#[cfg_attr(feature = "serde", derive(serde::Serialize))]
137pub struct DeliverySystemInfoAck {
138    /// How the `length_field` was coded on the wire: a non-minimal long form
139    /// (e.g. `82 00 09`) is preserved so the object re-serializes
140    /// byte-for-byte; [`LengthForm::Minimal`](crate::length::LengthForm::Minimal) for a hand-built value.
141    #[cfg_attr(feature = "serde", serde(skip))]
142    pub length_form: crate::length::LengthForm,
143
144    /// `SystemIdentifier`s in wire order (`length_field = N`).
145    pub systems: Vec<SystemIdentifier>,
146}
147
148/// `ScanAck()` (Table 18) — module → host: a TS found during a scan.
149#[derive(Debug, Clone, PartialEq, Eq)]
150#[cfg_attr(feature = "serde", derive(serde::Serialize))]
151pub struct ScanAck<'a> {
152    /// How the `length_field` was coded on the wire: a non-minimal long form
153    /// (e.g. `82 00 09`) is preserved so the object re-serializes
154    /// byte-for-byte; [`LengthForm::Minimal`](crate::length::LengthForm::Minimal) for a hand-built value.
155    #[cfg_attr(feature = "serde", serde(skip))]
156    pub length_form: crate::length::LengthForm,
157
158    /// `TSState` — `0` = no signal (or, when auto-scanning, all frequencies
159    /// searched); `1`-`255` = normalized signal-quality (bigger is better).
160    pub ts_state: u8,
161    /// `TuningInformationMessage` — 11-byte delivery-system-dependent coding to
162    /// re-acquire the TS. Undefined when `ts_state == 0`.
163    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
164    pub tuning_information_message: &'a [u8],
165    /// `ScanProgress` — 0-255, approximate proportional indication of scan progress.
166    pub scan_progress: u8,
167}
168
169/// `TuneTSReq()` (Table 19) — host → module: tune to a TS. An absent
170/// `TuningInformationMessage` (zero following bytes) requests a network disconnect.
171#[derive(Debug, Clone, PartialEq, Eq, Default)]
172#[cfg_attr(feature = "serde", derive(serde::Serialize))]
173pub struct TuneTSReq<'a> {
174    /// How the `length_field` was coded on the wire: a non-minimal long form
175    /// (e.g. `82 00 09`) is preserved so the object re-serializes
176    /// byte-for-byte; [`LengthForm::Minimal`](crate::length::LengthForm::Minimal) for a hand-built value.
177    #[cfg_attr(feature = "serde", serde(skip))]
178    pub length_form: crate::length::LengthForm,
179
180    /// `TuningInformationMessage` — 11 bytes, or empty (= disconnect from network).
181    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
182    pub tuning_information_message: &'a [u8],
183}
184
185/// `TuneTSAck()` (Table 20) — module → host: tune result.
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
187#[cfg_attr(feature = "serde", derive(serde::Serialize))]
188pub struct TuneTSAck {
189    /// How the `length_field` was coded on the wire: a non-minimal long form
190    /// (e.g. `82 00 09`) is preserved so the object re-serializes
191    /// byte-for-byte; [`LengthForm::Minimal`](crate::length::LengthForm::Minimal) for a hand-built value.
192    #[cfg_attr(feature = "serde", serde(skip))]
193    pub length_form: crate::length::LengthForm,
194
195    /// `TSState` — identical coding to the [`ScanAck`] `TSState`; `0` after a
196    /// disconnect request.
197    pub ts_state: u8,
198}
199
200// --- header-only objects ---
201
202crate::dispatch::declare_empty_apdu!(
203    DeliverySystemInfoReq,
204    tag::DELIVERY_SYSTEM_INFO_REQ,
205    "DeliverySystemInfoReq"
206);
207crate::dispatch::declare_empty_apdu!(ScanStartReq, tag::SCAN_START_REQ, "ScanStartReq");
208crate::dispatch::declare_empty_apdu!(ScanNextReq, tag::SCAN_NEXT_REQ, "ScanNextReq");
209
210// --- DeliverySystemInfoAck ---
211
212impl<'a> Parse<'a> for DeliverySystemInfoAck {
213    type Error = Error;
214    fn parse(bytes: &'a [u8]) -> Result<Self> {
215        let (length_form, body) = objects::parse_apdu_header(
216            bytes,
217            tag::DELIVERY_SYSTEM_INFO_ACK,
218            "DeliverySystemInfoAck",
219        )?;
220        let mut systems = Vec::with_capacity(body.len());
221        for &b in body {
222            systems.push(SystemIdentifier::from_u8(b));
223        }
224        Ok(Self {
225            length_form,
226            systems,
227        })
228    }
229}
230impl Serialize for DeliverySystemInfoAck {
231    type Error = Error;
232    fn serialized_len(&self) -> usize {
233        objects::apdu_len(self.systems.len(), self.length_form)
234    }
235    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
236        let body_len = self.systems.len();
237        let mut pos = objects::write_apdu_header(
238            tag::DELIVERY_SYSTEM_INFO_ACK,
239            body_len,
240            self.length_form,
241            buf,
242        )?;
243        for s in &self.systems {
244            buf[pos] = s.to_u8();
245            pos += 1;
246        }
247        Ok(pos)
248    }
249}
250
251// --- ScanAck ---
252
253// TSState(1) + TuningInformationMessage(11) + ScanProgress(1).
254const SCAN_ACK_BODY: usize = 1 + TUNING_INFO_MESSAGE_LEN + 1;
255
256impl<'a> Parse<'a> for ScanAck<'a> {
257    type Error = Error;
258    fn parse(bytes: &'a [u8]) -> Result<Self> {
259        let (length_form, body) = objects::parse_apdu_header(bytes, tag::SCAN_ACK, "ScanAck")?;
260        if body.len() < SCAN_ACK_BODY {
261            return Err(Error::BufferTooShort {
262                need: SCAN_ACK_BODY,
263                have: body.len(),
264                what: "ScanAck",
265            });
266        }
267        crate::objects::reject_trailing_body(body, SCAN_ACK_BODY, "ScanAck")?;
268        Ok(Self {
269            length_form,
270            ts_state: body[0],
271            tuning_information_message: &body[1..1 + TUNING_INFO_MESSAGE_LEN],
272            scan_progress: body[1 + TUNING_INFO_MESSAGE_LEN],
273        })
274    }
275}
276impl Serialize for ScanAck<'_> {
277    type Error = Error;
278    fn serialized_len(&self) -> usize {
279        objects::apdu_len(SCAN_ACK_BODY, self.length_form)
280    }
281    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
282        if self.tuning_information_message.len() != TUNING_INFO_MESSAGE_LEN {
283            return Err(Error::InvalidObject {
284                what: "ScanAck",
285                reason: "TuningInformationMessage must be exactly 11 bytes",
286            });
287        }
288        let mut pos =
289            objects::write_apdu_header(tag::SCAN_ACK, SCAN_ACK_BODY, self.length_form, buf)?;
290        buf[pos] = self.ts_state;
291        pos += 1;
292        buf[pos..pos + TUNING_INFO_MESSAGE_LEN].copy_from_slice(self.tuning_information_message);
293        pos += TUNING_INFO_MESSAGE_LEN;
294        buf[pos] = self.scan_progress;
295        Ok(pos + 1)
296    }
297}
298
299// --- TuneTSReq ---
300
301impl<'a> Parse<'a> for TuneTSReq<'a> {
302    type Error = Error;
303    fn parse(bytes: &'a [u8]) -> Result<Self> {
304        let (length_form, body) = objects::parse_apdu_header(bytes, tag::TUNE_TS_REQ, "TuneTSReq")?;
305        // The TuningInformationMessage is either absent (disconnect) or 11 bytes.
306        if !body.is_empty() && body.len() != TUNING_INFO_MESSAGE_LEN {
307            return Err(Error::InvalidObject {
308                what: "TuneTSReq",
309                reason: "TuningInformationMessage must be absent or exactly 11 bytes",
310            });
311        }
312        Ok(Self {
313            length_form,
314            tuning_information_message: body,
315        })
316    }
317}
318impl Serialize for TuneTSReq<'_> {
319    type Error = Error;
320    fn serialized_len(&self) -> usize {
321        objects::apdu_len(self.tuning_information_message.len(), self.length_form)
322    }
323    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
324        if !self.tuning_information_message.is_empty()
325            && self.tuning_information_message.len() != TUNING_INFO_MESSAGE_LEN
326        {
327            return Err(Error::InvalidObject {
328                what: "TuneTSReq",
329                reason: "TuningInformationMessage must be absent or exactly 11 bytes",
330            });
331        }
332        let body_len = self.tuning_information_message.len();
333        let pos = objects::write_apdu_header(tag::TUNE_TS_REQ, body_len, self.length_form, buf)?;
334        buf[pos..pos + body_len].copy_from_slice(self.tuning_information_message);
335        Ok(pos + body_len)
336    }
337}
338
339// --- TuneTSAck ---
340
341// TSState(1).
342const TUNE_TS_ACK_BODY: usize = 1;
343
344impl<'a> Parse<'a> for TuneTSAck {
345    type Error = Error;
346    fn parse(bytes: &'a [u8]) -> Result<Self> {
347        let (length_form, body) = objects::parse_apdu_header(bytes, tag::TUNE_TS_ACK, "TuneTSAck")?;
348        if body.len() < TUNE_TS_ACK_BODY {
349            return Err(Error::BufferTooShort {
350                need: TUNE_TS_ACK_BODY,
351                have: body.len(),
352                what: "TuneTSAck",
353            });
354        }
355        crate::objects::reject_trailing_body(body, TUNE_TS_ACK_BODY, "TuneTSAck")?;
356        Ok(Self {
357            length_form,
358            ts_state: body[0],
359        })
360    }
361}
362impl Serialize for TuneTSAck {
363    type Error = Error;
364    fn serialized_len(&self) -> usize {
365        objects::apdu_len(TUNE_TS_ACK_BODY, self.length_form)
366    }
367    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
368        let pos =
369            objects::write_apdu_header(tag::TUNE_TS_ACK, TUNE_TS_ACK_BODY, self.length_form, buf)?;
370        buf[pos] = self.ts_state;
371        Ok(pos + TUNE_TS_ACK_BODY)
372    }
373}
374
375crate::dispatch::declare_resource_apdus! {
376    /// Resource-scoped dispatch over the StreamInput objects (Tables 13-20).
377    #[derive(Debug, Clone, PartialEq, Eq)]
378    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
379    #[non_exhaustive]
380    pub enum StreamInputApdu<'a> ("stream_input") {
381        /// `DeliverySystemInfoReq` (`9F 80 00`).
382        DeliverySystemInfoReq(DeliverySystemInfoReq) = tag::DELIVERY_SYSTEM_INFO_REQ,
383        /// `DeliverySystemInfoAck` (`9F 80 01`).
384        DeliverySystemInfoAck(DeliverySystemInfoAck) = tag::DELIVERY_SYSTEM_INFO_ACK,
385        /// `ScanStartReq` (`9F 80 02`).
386        ScanStartReq(ScanStartReq) = tag::SCAN_START_REQ,
387        /// `ScanNextReq` (`9F 80 03`).
388        ScanNextReq(ScanNextReq) = tag::SCAN_NEXT_REQ,
389        /// `ScanAck` (`9F 80 04`).
390        ScanAck(ScanAck<'a>) = tag::SCAN_ACK,
391        /// `TuneTSReq` (`9F 80 05`).
392        TuneTSReq(TuneTSReq<'a>) = tag::TUNE_TS_REQ,
393        /// `TuneTSAck` (`9F 80 06`).
394        TuneTSAck(TuneTSAck) = tag::TUNE_TS_ACK,
395    }
396}
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401
402    #[test]
403    fn header_only_objects_round_trip() {
404        assert_eq!(
405            DeliverySystemInfoReq::default().try_to_bytes().unwrap(),
406            [0x9F, 0x80, 0x00, 0x00]
407        );
408        assert_eq!(
409            ScanStartReq::default().try_to_bytes().unwrap(),
410            [0x9F, 0x80, 0x02, 0x00]
411        );
412        assert_eq!(
413            ScanNextReq::default().try_to_bytes().unwrap(),
414            [0x9F, 0x80, 0x03, 0x00]
415        );
416        assert_eq!(
417            DeliverySystemInfoReq::parse(&[0x9F, 0x80, 0x00, 0x00]).unwrap(),
418            DeliverySystemInfoReq::default()
419        );
420        assert_eq!(
421            ScanNextReq::parse(&[0x9F, 0x80, 0x03, 0x00]).unwrap(),
422            ScanNextReq::default()
423        );
424    }
425
426    #[test]
427    fn delivery_system_info_ack_multi_round_trips_and_bites() {
428        let ack = DeliverySystemInfoAck {
429            length_form: crate::length::LengthForm::Minimal,
430            systems: alloc::vec![
431                SystemIdentifier::DvbC,
432                SystemIdentifier::DvbS,
433                SystemIdentifier::DvbT,
434            ],
435        };
436        let bytes = ack.try_to_bytes().unwrap();
437        // tag(3) + len(1) + 3 = 7; body len = 3 = 0x03.
438        assert_eq!(bytes, [0x9F, 0x80, 0x01, 0x03, 0x01, 0x02, 0x03]);
439        assert_eq!(DeliverySystemInfoAck::parse(&bytes).unwrap(), ack);
440        assert_eq!(ack.systems[0].name(), "DVB-C");
441        let mut other = ack.clone();
442        other.systems[2] = SystemIdentifier::Abstract;
443        assert_ne!(bytes, other.try_to_bytes().unwrap());
444    }
445
446    #[test]
447    fn delivery_system_info_ack_reserved_value() {
448        let ack = DeliverySystemInfoAck {
449            length_form: crate::length::LengthForm::Minimal,
450            systems: alloc::vec![SystemIdentifier::from_u8(0x7F)],
451        };
452        assert_eq!(ack.systems[0], SystemIdentifier::Reserved(0x7F));
453        let bytes = ack.try_to_bytes().unwrap();
454        assert_eq!(bytes, [0x9F, 0x80, 0x01, 0x01, 0x7F]);
455        assert_eq!(DeliverySystemInfoAck::parse(&bytes).unwrap(), ack);
456    }
457
458    #[test]
459    fn scan_ack_round_trips_and_bites() {
460        let tim = [
461            0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB,
462        ];
463        let ack = ScanAck {
464            length_form: crate::length::LengthForm::Minimal,
465            ts_state: 0xC8,
466            tuning_information_message: &tim,
467            scan_progress: 0x40,
468        };
469        let bytes = ack.try_to_bytes().unwrap();
470        // tag(3) + len(1) + state(1) + tim(11) + progress(1) = 17; body = 13 = 0x0D.
471        assert_eq!(
472            bytes,
473            [
474                0x9F, 0x80, 0x04, 0x0D, 0xC8, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99,
475                0xAA, 0xBB, 0x40
476            ]
477        );
478        assert_eq!(ScanAck::parse(&bytes).unwrap(), ack);
479        let mut other = ack.clone();
480        other.scan_progress = 0x41;
481        assert_ne!(bytes, other.try_to_bytes().unwrap());
482    }
483
484    #[test]
485    fn tune_ts_req_with_message_round_trips_and_bites() {
486        let tim = [
487            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B,
488        ];
489        let req = TuneTSReq {
490            length_form: crate::length::LengthForm::Minimal,
491            tuning_information_message: &tim,
492        };
493        let bytes = req.try_to_bytes().unwrap();
494        assert_eq!(
495            bytes,
496            [
497                0x9F, 0x80, 0x05, 0x0B, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
498                0x0B
499            ]
500        );
501        assert_eq!(TuneTSReq::parse(&bytes).unwrap(), req);
502        let mut tim2 = tim;
503        tim2[10] = 0xFF;
504        let other = TuneTSReq {
505            length_form: crate::length::LengthForm::Minimal,
506            tuning_information_message: &tim2,
507        };
508        assert_ne!(bytes, other.try_to_bytes().unwrap());
509    }
510
511    #[test]
512    fn tune_ts_req_disconnect_empty_message() {
513        let req = TuneTSReq {
514            length_form: crate::length::LengthForm::Minimal,
515            tuning_information_message: &[],
516        };
517        let bytes = req.try_to_bytes().unwrap();
518        assert_eq!(bytes, [0x9F, 0x80, 0x05, 0x00]);
519        assert_eq!(TuneTSReq::parse(&bytes).unwrap(), req);
520    }
521
522    #[test]
523    fn tune_ts_req_rejects_wrong_length() {
524        // 5-byte TuningInformationMessage is neither absent nor 11 bytes.
525        let bad = [0x9F, 0x80, 0x05, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05];
526        assert!(matches!(
527            TuneTSReq::parse(&bad),
528            Err(Error::InvalidObject { .. })
529        ));
530    }
531
532    #[test]
533    fn tune_ts_ack_round_trips_and_bites() {
534        let ack = TuneTSAck {
535            length_form: crate::length::LengthForm::Minimal,
536            ts_state: 0xFF,
537        };
538        let bytes = ack.try_to_bytes().unwrap();
539        assert_eq!(bytes, [0x9F, 0x80, 0x06, 0x01, 0xFF]);
540        assert_eq!(TuneTSAck::parse(&bytes).unwrap(), ack);
541        let other = TuneTSAck {
542            length_form: crate::length::LengthForm::Minimal,
543            ts_state: 0x00,
544        };
545        assert_ne!(bytes, other.try_to_bytes().unwrap());
546    }
547
548    #[test]
549    fn dispatch_routes_each_tag() {
550        let req = DeliverySystemInfoReq::default().try_to_bytes().unwrap();
551        assert!(matches!(
552            StreamInputApdu::parse(&req).unwrap(),
553            StreamInputApdu::DeliverySystemInfoReq(_)
554        ));
555        let ack = TuneTSAck {
556            length_form: crate::length::LengthForm::Minimal,
557            ts_state: 1,
558        }
559        .try_to_bytes()
560        .unwrap();
561        let parsed = StreamInputApdu::parse(&ack).unwrap();
562        assert!(matches!(parsed, StreamInputApdu::TuneTSAck(_)));
563        assert_eq!(parsed.try_to_bytes().unwrap(), ack);
564        // ScanNextReq is 9F8003 (Table 17) — distinct from ScanStartReq 9F8002.
565        let next = ScanNextReq::default().try_to_bytes().unwrap();
566        assert!(matches!(
567            StreamInputApdu::parse(&next).unwrap(),
568            StreamInputApdu::ScanNextReq(_)
569        ));
570    }
571}