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