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