Skip to main content

dvb_ci/objects/
low_speed_comms.rs

1//! Low-Speed Communications objects — ETSI EN 50221 §8.7.1, Tables 52-56
2//! (PDF pp. 51-54).
3//!
4//! - `comms_cmd` (`9F 8C 00`, Table 52) — connect / disconnect / set-params /
5//!   enquire-status / get-next-buffer.
6//! - `connection_descriptor` (`9F 8C 01`, Table 53) — telephone / cable-return
7//!   connection info; also nested inside Connect_on_Channel.
8//! - `comms_reply` (`9F 8C 02`, Table 54) — id + return_value.
9//! - `comms_send` (`9F 8C 03` last / `9F 8C 04` more, Table 55) — phase + bytes.
10//! - `comms_rcv` (`9F 8C 05` last / `9F 8C 06` more, Table 56) — phase + bytes.
11
12use crate::error::{Error, Result};
13use crate::length;
14use crate::tag::{self, ApduTag};
15use crate::traits::ApduDef;
16use broadcast_common::{Parse, Serialize};
17
18/// `comms_command_id` values (Table 52, p. 52).
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize))]
21#[non_exhaustive]
22pub enum CommsCommandId {
23    /// `01` — establish communication on the comms resource.
24    ConnectOnChannel,
25    /// `02` — terminate the connection.
26    DisconnectOnChannel,
27    /// `03` — set buffer size / timeout.
28    SetParams,
29    /// `04` — request the current connection status.
30    EnquireStatus,
31    /// `05` — receive-side flow control.
32    GetNextBuffer,
33    /// Any other value (reserved).
34    Reserved(u8),
35}
36
37impl CommsCommandId {
38    /// Decode a `comms_command_id` byte.
39    #[must_use]
40    pub fn from_u8(v: u8) -> Self {
41        match v {
42            0x01 => Self::ConnectOnChannel,
43            0x02 => Self::DisconnectOnChannel,
44            0x03 => Self::SetParams,
45            0x04 => Self::EnquireStatus,
46            0x05 => Self::GetNextBuffer,
47            other => Self::Reserved(other),
48        }
49    }
50    /// Wire byte.
51    #[must_use]
52    pub const fn to_u8(self) -> u8 {
53        match self {
54            Self::ConnectOnChannel => 0x01,
55            Self::DisconnectOnChannel => 0x02,
56            Self::SetParams => 0x03,
57            Self::EnquireStatus => 0x04,
58            Self::GetNextBuffer => 0x05,
59            Self::Reserved(v) => v,
60        }
61    }
62    /// Spec token, or `"reserved"`.
63    #[must_use]
64    pub fn name(&self) -> &'static str {
65        match self {
66            Self::ConnectOnChannel => "Connect_on_Channel",
67            Self::DisconnectOnChannel => "Disconnect_on_Channel",
68            Self::SetParams => "Set_Params",
69            Self::EnquireStatus => "Enquire_Status",
70            Self::GetNextBuffer => "Get_Next_Buffer",
71            Self::Reserved(_) => "reserved",
72        }
73    }
74}
75broadcast_common::impl_spec_display!(CommsCommandId, Reserved);
76
77/// `connection_descriptor_type` values (Table 53, p. 52).
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79#[cfg_attr(feature = "serde", derive(serde::Serialize))]
80#[non_exhaustive]
81pub enum ConnectionDescriptorType {
82    /// `01` — an SI telephone_descriptor() follows.
83    SiTelephoneDescriptor,
84    /// `02` — a cable-return-channel `channel_id` follows.
85    CableReturnChannelDescriptor,
86    /// Any other value (reserved).
87    Reserved(u8),
88}
89
90impl ConnectionDescriptorType {
91    /// Decode a `connection_descriptor_type` byte.
92    #[must_use]
93    pub fn from_u8(v: u8) -> Self {
94        match v {
95            0x01 => Self::SiTelephoneDescriptor,
96            0x02 => Self::CableReturnChannelDescriptor,
97            other => Self::Reserved(other),
98        }
99    }
100    /// Wire byte.
101    #[must_use]
102    pub const fn to_u8(self) -> u8 {
103        match self {
104            Self::SiTelephoneDescriptor => 0x01,
105            Self::CableReturnChannelDescriptor => 0x02,
106            Self::Reserved(v) => v,
107        }
108    }
109    /// Spec token, or `"reserved"`.
110    #[must_use]
111    pub fn name(&self) -> &'static str {
112        match self {
113            Self::SiTelephoneDescriptor => "SI_Telephone_Descriptor",
114            Self::CableReturnChannelDescriptor => "Cable_Return_Channel_Descriptor",
115            Self::Reserved(_) => "reserved",
116        }
117    }
118}
119broadcast_common::impl_spec_display!(ConnectionDescriptorType, Reserved);
120
121/// `connection_descriptor()` object (Table 53): carries the connection info.
122///
123/// The body after the `connection_descriptor_type` byte is branch-dependent (an
124/// SI `telephone_descriptor()` whose internal layout EN 50221 does not reproduce,
125/// or a 1-byte `channel_id`); it is carried verbatim so it round-trips.
126#[derive(Debug, Clone, PartialEq, Eq)]
127#[cfg_attr(feature = "serde", derive(serde::Serialize))]
128pub struct ConnectionDescriptor<'a> {
129    /// `connection_descriptor_type`.
130    pub descriptor_type: ConnectionDescriptorType,
131    /// The branch-dependent payload (telephone_descriptor bytes, or the
132    /// `channel_id` byte), verbatim.
133    #[cfg_attr(feature = "serde", serde(borrow, with = "super::bytes_serde"))]
134    pub payload: &'a [u8],
135}
136
137impl<'a> ConnectionDescriptor<'a> {
138    /// Parse a nested `connection_descriptor()` at the front of `bytes`, returning
139    /// the object and how many bytes it consumed (tag + length + body).
140    pub(crate) fn parse_component(bytes: &'a [u8]) -> Result<(Self, usize)> {
141        if bytes.len() < 3 {
142            return Err(Error::BufferTooShort {
143                need: 3,
144                have: bytes.len(),
145                what: "connection_descriptor tag",
146            });
147        }
148        let t = ApduTag::from_bytes(bytes[0], bytes[1], bytes[2]);
149        if t != tag::CONNECTION_DESCRIPTOR {
150            return Err(Error::UnexpectedApduTag {
151                got: t.as_u24(),
152                expected: tag::CONNECTION_DESCRIPTOR.as_u24(),
153                what: "connection_descriptor",
154            });
155        }
156        let (len_value, len_hdr) = length::decode(&bytes[3..])?;
157        let body_start = 3 + len_hdr;
158        let body_end = body_start + len_value;
159        if bytes.len() < body_end {
160            return Err(Error::LengthMismatch {
161                what: "connection_descriptor",
162                declared: len_value,
163                actual: bytes.len().saturating_sub(body_start),
164            });
165        }
166        let body = &bytes[body_start..body_end];
167        let type_byte = *body.first().ok_or(Error::BufferTooShort {
168            need: 1,
169            have: 0,
170            what: "connection_descriptor type",
171        })?;
172        Ok((
173            Self {
174                descriptor_type: ConnectionDescriptorType::from_u8(type_byte),
175                payload: &body[1..],
176            },
177            body_end,
178        ))
179    }
180
181    pub(crate) fn component_len(&self) -> usize {
182        super::apdu_len(1 + self.payload.len())
183    }
184
185    pub(crate) fn serialize_component(&self, buf: &mut [u8]) -> Result<usize> {
186        let body_len = 1 + self.payload.len();
187        let mut pos = super::write_apdu_header(tag::CONNECTION_DESCRIPTOR, body_len, buf)?;
188        buf[pos] = self.descriptor_type.to_u8();
189        pos += 1;
190        buf[pos..pos + self.payload.len()].copy_from_slice(self.payload);
191        pos += self.payload.len();
192        Ok(pos)
193    }
194}
195
196impl<'a> Parse<'a> for ConnectionDescriptor<'a> {
197    type Error = Error;
198    fn parse(bytes: &'a [u8]) -> Result<Self> {
199        let (c, consumed) = Self::parse_component(bytes)?;
200        if consumed != bytes.len() {
201            return Err(Error::LengthMismatch {
202                what: "connection_descriptor",
203                declared: consumed,
204                actual: bytes.len(),
205            });
206        }
207        Ok(c)
208    }
209}
210
211impl Serialize for ConnectionDescriptor<'_> {
212    type Error = Error;
213    fn serialized_len(&self) -> usize {
214        self.component_len()
215    }
216    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
217        self.serialize_component(buf)
218    }
219}
220
221impl<'a> ApduDef<'a> for ConnectionDescriptor<'a> {
222    const TAG: ApduTag = tag::CONNECTION_DESCRIPTOR;
223    const NAME: &'static str = "CONNECTION_DESCRIPTOR";
224}
225
226/// The branch-dependent parameters of a `comms_cmd()` (Table 52).
227#[derive(Debug, Clone, PartialEq, Eq)]
228#[cfg_attr(feature = "serde", derive(serde::Serialize))]
229#[non_exhaustive]
230pub enum CommsCmdParams<'a> {
231    /// `Connect_on_Channel`: connection descriptor + retry_count + timeout.
232    Connect {
233        /// The nested `connection_descriptor()`.
234        #[cfg_attr(feature = "serde", serde(borrow))]
235        connection_descriptor: ConnectionDescriptor<'a>,
236        /// `retry_count`.
237        retry_count: u8,
238        /// `timeout` (seconds).
239        timeout: u8,
240    },
241    /// `Set_Params`: buffer_size + timeout.
242    SetParams {
243        /// `buffer_size` (1..=254).
244        buffer_size: u8,
245        /// `timeout` (units of 10 ms).
246        timeout: u8,
247    },
248    /// `Get_Next_Buffer`: comms_phase_id.
249    GetNextBuffer {
250        /// `comms_phase_id` (alternates 0,1,…).
251        comms_phase_id: u8,
252    },
253    /// No branch parameters (`Disconnect_on_Channel`, `Enquire_Status`, or a
254    /// reserved command id).
255    None,
256}
257
258/// `comms_cmd()` object (Table 52).
259#[derive(Debug, Clone, PartialEq, Eq)]
260#[cfg_attr(feature = "serde", derive(serde::Serialize))]
261pub struct CommsCmd<'a> {
262    /// `comms_command_id`.
263    pub command_id: CommsCommandId,
264    /// The branch-dependent parameters keyed by `command_id`.
265    #[cfg_attr(feature = "serde", serde(borrow))]
266    pub params: CommsCmdParams<'a>,
267}
268
269impl<'a> Parse<'a> for CommsCmd<'a> {
270    type Error = Error;
271    fn parse(bytes: &'a [u8]) -> Result<Self> {
272        let body = super::parse_apdu_header(bytes, tag::COMMS_CMD, "comms_cmd")?;
273        let id_byte = *body.first().ok_or(Error::BufferTooShort {
274            need: 1,
275            have: 0,
276            what: "comms_cmd command_id",
277        })?;
278        let command_id = CommsCommandId::from_u8(id_byte);
279        let rest = &body[1..];
280        let params = match command_id {
281            CommsCommandId::ConnectOnChannel => {
282                let (cd, consumed) = ConnectionDescriptor::parse_component(rest)?;
283                let tail = &rest[consumed..];
284                if tail.len() < 2 {
285                    return Err(Error::BufferTooShort {
286                        need: 2,
287                        have: tail.len(),
288                        what: "comms_cmd connect retry/timeout",
289                    });
290                }
291                CommsCmdParams::Connect {
292                    connection_descriptor: cd,
293                    retry_count: tail[0],
294                    timeout: tail[1],
295                }
296            }
297            CommsCommandId::SetParams => {
298                if rest.len() < 2 {
299                    return Err(Error::BufferTooShort {
300                        need: 2,
301                        have: rest.len(),
302                        what: "comms_cmd set_params",
303                    });
304                }
305                CommsCmdParams::SetParams {
306                    buffer_size: rest[0],
307                    timeout: rest[1],
308                }
309            }
310            CommsCommandId::GetNextBuffer => {
311                let comms_phase_id = *rest.first().ok_or(Error::BufferTooShort {
312                    need: 1,
313                    have: 0,
314                    what: "comms_cmd get_next_buffer",
315                })?;
316                CommsCmdParams::GetNextBuffer { comms_phase_id }
317            }
318            _ => CommsCmdParams::None,
319        };
320        Ok(Self { command_id, params })
321    }
322}
323
324impl CommsCmd<'_> {
325    fn body_len(&self) -> usize {
326        1 + match &self.params {
327            CommsCmdParams::Connect {
328                connection_descriptor,
329                ..
330            } => connection_descriptor.component_len() + 2,
331            CommsCmdParams::SetParams { .. } => 2,
332            CommsCmdParams::GetNextBuffer { .. } => 1,
333            CommsCmdParams::None => 0,
334        }
335    }
336}
337
338impl Serialize for CommsCmd<'_> {
339    type Error = Error;
340    fn serialized_len(&self) -> usize {
341        super::apdu_len(self.body_len())
342    }
343    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
344        let body_len = self.body_len();
345        let mut pos = super::write_apdu_header(tag::COMMS_CMD, body_len, buf)?;
346        buf[pos] = self.command_id.to_u8();
347        pos += 1;
348        match &self.params {
349            CommsCmdParams::Connect {
350                connection_descriptor,
351                retry_count,
352                timeout,
353            } => {
354                pos += connection_descriptor.serialize_component(&mut buf[pos..])?;
355                buf[pos] = *retry_count;
356                buf[pos + 1] = *timeout;
357                pos += 2;
358            }
359            CommsCmdParams::SetParams {
360                buffer_size,
361                timeout,
362            } => {
363                buf[pos] = *buffer_size;
364                buf[pos + 1] = *timeout;
365                pos += 2;
366            }
367            CommsCmdParams::GetNextBuffer { comms_phase_id } => {
368                buf[pos] = *comms_phase_id;
369                pos += 1;
370            }
371            CommsCmdParams::None => {}
372        }
373        Ok(pos)
374    }
375}
376
377impl<'a> ApduDef<'a> for CommsCmd<'a> {
378    const TAG: ApduTag = tag::COMMS_CMD;
379    const NAME: &'static str = "COMMS_CMD";
380}
381
382/// `comms_reply_id` values (Table 54, p. 53).
383#[derive(Debug, Clone, Copy, PartialEq, Eq)]
384#[cfg_attr(feature = "serde", derive(serde::Serialize))]
385#[non_exhaustive]
386pub enum CommsReplyId {
387    /// `01` — Connect acknowledgement.
388    ConnectAck,
389    /// `02` — Disconnect acknowledgement.
390    DisconnectAck,
391    /// `03` — Set Params acknowledgement.
392    SetParamsAck,
393    /// `04` — Status reply (return_value 0 = Disconnected, 1 = Connected).
394    StatusReply,
395    /// `05` — Get Next Buffer acknowledgement.
396    GetNextBufferAck,
397    /// `06` — Send acknowledgement (return_value = acknowledged phase).
398    SendAck,
399    /// Any other value (reserved).
400    Reserved(u8),
401}
402
403impl CommsReplyId {
404    /// Decode a `comms_reply_id` byte.
405    #[must_use]
406    pub fn from_u8(v: u8) -> Self {
407        match v {
408            0x01 => Self::ConnectAck,
409            0x02 => Self::DisconnectAck,
410            0x03 => Self::SetParamsAck,
411            0x04 => Self::StatusReply,
412            0x05 => Self::GetNextBufferAck,
413            0x06 => Self::SendAck,
414            other => Self::Reserved(other),
415        }
416    }
417    /// Wire byte.
418    #[must_use]
419    pub const fn to_u8(self) -> u8 {
420        match self {
421            Self::ConnectAck => 0x01,
422            Self::DisconnectAck => 0x02,
423            Self::SetParamsAck => 0x03,
424            Self::StatusReply => 0x04,
425            Self::GetNextBufferAck => 0x05,
426            Self::SendAck => 0x06,
427            Self::Reserved(v) => v,
428        }
429    }
430    /// Spec token, or `"reserved"`.
431    #[must_use]
432    pub fn name(&self) -> &'static str {
433        match self {
434            Self::ConnectAck => "Connect_Ack",
435            Self::DisconnectAck => "Disconnect_Ack",
436            Self::SetParamsAck => "Set_Params_Ack",
437            Self::StatusReply => "Status_Reply",
438            Self::GetNextBufferAck => "Get_Next_Buffer_Ack",
439            Self::SendAck => "Send_Ack",
440            Self::Reserved(_) => "reserved",
441        }
442    }
443}
444broadcast_common::impl_spec_display!(CommsReplyId, Reserved);
445
446/// `comms_reply()` object (Table 54): id + signed return_value.
447#[derive(Debug, Clone, Copy, PartialEq, Eq)]
448#[cfg_attr(feature = "serde", derive(serde::Serialize))]
449pub struct CommsReply {
450    /// `comms_reply_id`.
451    pub reply_id: CommsReplyId,
452    /// `return_value` — positive/zero = OK, negative = error (carried as raw u8).
453    pub return_value: u8,
454}
455
456// comms_reply_id(1) + return_value(1).
457const COMMS_REPLY_BODY: usize = 2;
458
459impl<'a> Parse<'a> for CommsReply {
460    type Error = Error;
461    fn parse(bytes: &'a [u8]) -> Result<Self> {
462        let body = super::parse_apdu_header(bytes, tag::COMMS_REPLY, "comms_reply")?;
463        if body.len() < COMMS_REPLY_BODY {
464            return Err(Error::BufferTooShort {
465                need: COMMS_REPLY_BODY,
466                have: body.len(),
467                what: "comms_reply",
468            });
469        }
470        Ok(Self {
471            reply_id: CommsReplyId::from_u8(body[0]),
472            return_value: body[1],
473        })
474    }
475}
476
477impl Serialize for CommsReply {
478    type Error = Error;
479    fn serialized_len(&self) -> usize {
480        super::apdu_len(COMMS_REPLY_BODY)
481    }
482    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
483        let mut pos = super::write_apdu_header(tag::COMMS_REPLY, COMMS_REPLY_BODY, buf)?;
484        buf[pos] = self.reply_id.to_u8();
485        buf[pos + 1] = self.return_value;
486        pos += COMMS_REPLY_BODY;
487        Ok(pos)
488    }
489}
490
491impl ApduDef<'_> for CommsReply {
492    const TAG: ApduTag = tag::COMMS_REPLY;
493    const NAME: &'static str = "COMMS_REPLY";
494}
495
496/// `comms_send()` object (Table 55): a `comms_phase_id` + message bytes.
497///
498/// The `_last` (`9F 8C 03`) / `_more` (`9F 8C 04`) tags share this body;
499/// [`CommsSend::more`] selects which tag is written and is set from the tag.
500#[derive(Debug, Clone, PartialEq, Eq)]
501#[cfg_attr(feature = "serde", derive(serde::Serialize))]
502pub struct CommsSend<'a> {
503    /// `true` = `comms_send_more`; `false` = `comms_send_last`.
504    pub more: bool,
505    /// `comms_phase_id` (0 or 1).
506    pub comms_phase_id: u8,
507    /// `message_byte`s (max 254).
508    #[cfg_attr(feature = "serde", serde(borrow, with = "super::bytes_serde"))]
509    pub message: &'a [u8],
510}
511
512impl CommsSend<'_> {
513    /// The `apdu_tag` for this object given [`CommsSend::more`].
514    #[must_use]
515    pub fn tag(&self) -> ApduTag {
516        if self.more {
517            tag::COMMS_SEND_MORE
518        } else {
519            tag::COMMS_SEND_LAST
520        }
521    }
522}
523
524impl<'a> Parse<'a> for CommsSend<'a> {
525    type Error = Error;
526    fn parse(bytes: &'a [u8]) -> Result<Self> {
527        if bytes.len() < 3 {
528            return Err(Error::BufferTooShort {
529                need: 3,
530                have: bytes.len(),
531                what: "comms_send tag",
532            });
533        }
534        let t = ApduTag::from_bytes(bytes[0], bytes[1], bytes[2]);
535        let (expected, more) = match t {
536            tag::COMMS_SEND_MORE => (tag::COMMS_SEND_MORE, true),
537            _ => (tag::COMMS_SEND_LAST, false),
538        };
539        let body = super::parse_apdu_header(bytes, expected, "comms_send")?;
540        let comms_phase_id = *body.first().ok_or(Error::BufferTooShort {
541            need: 1,
542            have: 0,
543            what: "comms_send comms_phase_id",
544        })?;
545        Ok(Self {
546            more,
547            comms_phase_id,
548            message: &body[1..],
549        })
550    }
551}
552
553impl Serialize for CommsSend<'_> {
554    type Error = Error;
555    fn serialized_len(&self) -> usize {
556        super::apdu_len(1 + self.message.len())
557    }
558    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
559        let body_len = 1 + self.message.len();
560        let mut pos = super::write_apdu_header(self.tag(), body_len, buf)?;
561        buf[pos] = self.comms_phase_id;
562        pos += 1;
563        buf[pos..pos + self.message.len()].copy_from_slice(self.message);
564        pos += self.message.len();
565        Ok(pos)
566    }
567}
568
569impl<'a> ApduDef<'a> for CommsSend<'a> {
570    const TAG: ApduTag = tag::COMMS_SEND_LAST;
571    const NAME: &'static str = "COMMS_SEND";
572}
573
574/// `comms_rcv()` object (Table 56): a `comms_phase_id` + message bytes.
575///
576/// The `_last` (`9F 8C 05`) / `_more` (`9F 8C 06`) tags share this body;
577/// [`CommsRcv::more`] selects which tag is written and is set from the tag.
578#[derive(Debug, Clone, PartialEq, Eq)]
579#[cfg_attr(feature = "serde", derive(serde::Serialize))]
580pub struct CommsRcv<'a> {
581    /// `true` = `comms_rcv_more`; `false` = `comms_rcv_last`.
582    pub more: bool,
583    /// `comms_phase_id`.
584    pub comms_phase_id: u8,
585    /// `message_byte`s.
586    #[cfg_attr(feature = "serde", serde(borrow, with = "super::bytes_serde"))]
587    pub message: &'a [u8],
588}
589
590impl CommsRcv<'_> {
591    /// The `apdu_tag` for this object given [`CommsRcv::more`].
592    #[must_use]
593    pub fn tag(&self) -> ApduTag {
594        if self.more {
595            tag::COMMS_RCV_MORE
596        } else {
597            tag::COMMS_RCV_LAST
598        }
599    }
600}
601
602impl<'a> Parse<'a> for CommsRcv<'a> {
603    type Error = Error;
604    fn parse(bytes: &'a [u8]) -> Result<Self> {
605        if bytes.len() < 3 {
606            return Err(Error::BufferTooShort {
607                need: 3,
608                have: bytes.len(),
609                what: "comms_rcv tag",
610            });
611        }
612        let t = ApduTag::from_bytes(bytes[0], bytes[1], bytes[2]);
613        let (expected, more) = match t {
614            tag::COMMS_RCV_MORE => (tag::COMMS_RCV_MORE, true),
615            _ => (tag::COMMS_RCV_LAST, false),
616        };
617        let body = super::parse_apdu_header(bytes, expected, "comms_rcv")?;
618        let comms_phase_id = *body.first().ok_or(Error::BufferTooShort {
619            need: 1,
620            have: 0,
621            what: "comms_rcv comms_phase_id",
622        })?;
623        Ok(Self {
624            more,
625            comms_phase_id,
626            message: &body[1..],
627        })
628    }
629}
630
631impl Serialize for CommsRcv<'_> {
632    type Error = Error;
633    fn serialized_len(&self) -> usize {
634        super::apdu_len(1 + self.message.len())
635    }
636    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
637        let body_len = 1 + self.message.len();
638        let mut pos = super::write_apdu_header(self.tag(), body_len, buf)?;
639        buf[pos] = self.comms_phase_id;
640        pos += 1;
641        buf[pos..pos + self.message.len()].copy_from_slice(self.message);
642        pos += self.message.len();
643        Ok(pos)
644    }
645}
646
647impl<'a> ApduDef<'a> for CommsRcv<'a> {
648    const TAG: ApduTag = tag::COMMS_RCV_LAST;
649    const NAME: &'static str = "COMMS_RCV";
650}
651
652#[cfg(test)]
653mod tests {
654    use super::*;
655
656    #[test]
657    fn comms_cmd_connect_round_trips_and_bites() {
658        let cmd = CommsCmd {
659            command_id: CommsCommandId::ConnectOnChannel,
660            params: CommsCmdParams::Connect {
661                connection_descriptor: ConnectionDescriptor {
662                    descriptor_type: ConnectionDescriptorType::CableReturnChannelDescriptor,
663                    payload: &[0x07], // channel_id
664                },
665                retry_count: 3,
666                timeout: 30,
667            },
668        };
669        let bytes = cmd.to_bytes();
670        // 9F8C00, body: id(01) + CD[9F8C01 len2 type02 chan07] + retry03 + to1E
671        assert_eq!(
672            bytes,
673            [
674                0x9F, 0x8C, 0x00, 0x09, 0x01, 0x9F, 0x8C, 0x01, 0x02, 0x02, 0x07, 0x03, 0x1E
675            ]
676        );
677        assert_eq!(CommsCmd::parse(&bytes).unwrap(), cmd);
678
679        let mut other = cmd.clone();
680        other.params = CommsCmdParams::Connect {
681            connection_descriptor: ConnectionDescriptor {
682                descriptor_type: ConnectionDescriptorType::CableReturnChannelDescriptor,
683                payload: &[0x08],
684            },
685            retry_count: 3,
686            timeout: 30,
687        };
688        assert_ne!(bytes, other.to_bytes());
689    }
690
691    #[test]
692    fn comms_cmd_set_params_and_get_next() {
693        let sp = CommsCmd {
694            command_id: CommsCommandId::SetParams,
695            params: CommsCmdParams::SetParams {
696                buffer_size: 254,
697                timeout: 10,
698            },
699        };
700        let bytes = sp.to_bytes();
701        assert_eq!(bytes, [0x9F, 0x8C, 0x00, 0x03, 0x03, 0xFE, 0x0A]);
702        assert_eq!(CommsCmd::parse(&bytes).unwrap(), sp);
703        assert_eq!(sp.command_id.name(), "Set_Params");
704
705        let gnb = CommsCmd {
706            command_id: CommsCommandId::GetNextBuffer,
707            params: CommsCmdParams::GetNextBuffer { comms_phase_id: 1 },
708        };
709        let gb = gnb.to_bytes();
710        assert_eq!(gb, [0x9F, 0x8C, 0x00, 0x02, 0x05, 0x01]);
711        assert_eq!(CommsCmd::parse(&gb).unwrap(), gnb);
712    }
713
714    #[test]
715    fn comms_cmd_disconnect_has_no_params() {
716        let d = CommsCmd {
717            command_id: CommsCommandId::DisconnectOnChannel,
718            params: CommsCmdParams::None,
719        };
720        let bytes = d.to_bytes();
721        assert_eq!(bytes, [0x9F, 0x8C, 0x00, 0x01, 0x02]);
722        assert_eq!(CommsCmd::parse(&bytes).unwrap(), d);
723    }
724
725    #[test]
726    fn connection_descriptor_standalone_round_trips() {
727        let cd = ConnectionDescriptor {
728            descriptor_type: ConnectionDescriptorType::SiTelephoneDescriptor,
729            payload: &[0xAA, 0xBB, 0xCC],
730        };
731        let bytes = cd.to_bytes();
732        assert_eq!(bytes, [0x9F, 0x8C, 0x01, 0x04, 0x01, 0xAA, 0xBB, 0xCC]);
733        assert_eq!(ConnectionDescriptor::parse(&bytes).unwrap(), cd);
734        let mut other = cd.clone();
735        other.descriptor_type = ConnectionDescriptorType::CableReturnChannelDescriptor;
736        assert_ne!(bytes, other.to_bytes());
737    }
738
739    #[test]
740    fn comms_reply_round_trips_and_bites() {
741        let r = CommsReply {
742            reply_id: CommsReplyId::StatusReply,
743            return_value: 0x01, // Connected
744        };
745        let bytes = r.to_bytes();
746        assert_eq!(bytes, [0x9F, 0x8C, 0x02, 0x02, 0x04, 0x01]);
747        assert_eq!(CommsReply::parse(&bytes).unwrap(), r);
748        assert_eq!(r.reply_id.name(), "Status_Reply");
749        let mut other = r;
750        other.return_value = 0x00;
751        assert_ne!(bytes, other.to_bytes());
752    }
753
754    #[test]
755    fn comms_send_multibyte_round_trips_and_more_bites() {
756        let s = CommsSend {
757            more: false,
758            comms_phase_id: 0,
759            message: b"AT&F\r",
760        };
761        let bytes = s.to_bytes();
762        assert_eq!(
763            bytes,
764            [0x9F, 0x8C, 0x03, 0x06, 0x00, b'A', b'T', b'&', b'F', b'\r']
765        );
766        assert_eq!(CommsSend::parse(&bytes).unwrap(), s);
767
768        // more bite: flipping `more` flips the tag.
769        let mut more = s.clone();
770        more.more = true;
771        let mb = more.to_bytes();
772        assert_eq!(mb[2], 0x04);
773        assert_ne!(bytes, mb);
774        assert_eq!(CommsSend::parse(&mb).unwrap(), more);
775
776        // field bite.
777        let mut other = s.clone();
778        other.comms_phase_id = 1;
779        assert_ne!(bytes, other.to_bytes());
780    }
781
782    #[test]
783    fn comms_rcv_round_trips_and_more_bites() {
784        let r = CommsRcv {
785            more: true,
786            comms_phase_id: 1,
787            message: b"OK\r\n",
788        };
789        let bytes = r.to_bytes();
790        assert_eq!(bytes[2], 0x06); // rcv_more
791        assert_eq!(CommsRcv::parse(&bytes).unwrap(), r);
792
793        let mut last = r.clone();
794        last.more = false;
795        let lb = last.to_bytes();
796        assert_eq!(lb[2], 0x05); // rcv_last
797        assert_ne!(bytes, lb);
798        assert_eq!(CommsRcv::parse(&lb).unwrap(), last);
799    }
800}