Skip to main content

viva_gencp/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2//! GenCP: generic control protocol encode/decode (transport-agnostic).
3
4use bitflags::bitflags;
5use bytes::{Buf, BufMut, Bytes, BytesMut};
6use thiserror::Error;
7
8/// Size of the GenCP header (in bytes).
9pub const HEADER_SIZE: usize = 8;
10
11bitflags! {
12    /// Flags that can be set on a GenCP command packet.
13    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
14    pub struct CommandFlags: u16 {
15        /// Request an acknowledgement for this command.
16        const ACK_REQUIRED = 0x0001;
17        /// Mark the command as a broadcast.
18        const BROADCAST = 0x8000;
19        /// GVCP `ACTION_CMD` only: the payload carries a scheduled action time.
20        ///
21        /// These bit values are this crate's own representation; the mapping to
22        /// the single GVCP flags byte lives in `viva_gige::gvcp`.
23        const SCHEDULED_ACTION = 0x0002;
24    }
25}
26
27/// Command id of the GenCP pending-acknowledge.
28///
29/// A device that cannot answer within the controller's timeout replies with
30/// this instead of the real acknowledgement, and puts the extra time it wants
31/// in the SCD. It is a **command id, not a status** — the status field of a
32/// pending-ack is `SUCCESS`, so a receiver that only inspects the status
33/// cannot tell one from a real answer and will hand the timeout bytes back as
34/// payload. GVCP models the same mechanism the same way, as opcode `0x0089`
35/// (`viva_gige::gvcp::consts::PENDING_ACK`).
36///
37/// Corroborated by aravis `ARV_UVCP_COMMAND_PENDING_ACK`
38/// (`src/arvuvcpprivate.h`), which sits in the same command-id table as
39/// `READ_MEMORY_CMD` `0x0800` and `WRITE_MEMORY_CMD` `0x0802`.
40pub const PENDING_ACK_COMMAND: u16 = 0x0805;
41
42/// GenCP operation codes supported by this crate.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum OpCode {
45    /// Read a single bootstrap or device register.
46    ReadRegister,
47    /// Write a single bootstrap or device register.
48    WriteRegister,
49    /// Read a block of memory from the device.
50    ReadMem,
51    /// Write a block of memory to the device.
52    WriteMem,
53}
54
55impl OpCode {
56    /// Raw command value as defined by the GenCP/GVCP specification.
57    pub const fn command_code(self) -> u16 {
58        match self {
59            OpCode::ReadRegister => 0x0080,
60            OpCode::WriteRegister => 0x0082,
61            OpCode::ReadMem => 0x0084,
62            OpCode::WriteMem => 0x0086,
63        }
64    }
65
66    /// Raw acknowledgement value as defined by the specification.
67    pub const fn ack_code(self) -> u16 {
68        self.command_code() + 1
69    }
70
71    #[allow(dead_code)]
72    fn from_command(code: u16) -> Result<Self, GenCpError> {
73        match code {
74            0x0080 => Ok(OpCode::ReadRegister),
75            0x0082 => Ok(OpCode::WriteRegister),
76            0x0084 => Ok(OpCode::ReadMem),
77            0x0086 => Ok(OpCode::WriteMem),
78            _ => Err(GenCpError::UnknownOpcode(code)),
79        }
80    }
81
82    fn from_ack(code: u16) -> Result<Self, GenCpError> {
83        match code {
84            0x0081 => Ok(OpCode::ReadRegister),
85            0x0083 => Ok(OpCode::WriteRegister),
86            0x0085 => Ok(OpCode::ReadMem),
87            0x0087 => Ok(OpCode::WriteMem),
88            _ => Err(GenCpError::UnknownOpcode(code)),
89        }
90    }
91}
92
93/// Status codes shared by the GVCP and GenCP acknowledgement tables.
94///
95/// Only the codes **both** protocols define identically live here:
96/// `0x0000`, `0x8001`–`0x8007` and `0x8FFF`. Above that the tables diverge,
97/// and at `0x800B` they actively disagree — GVCP calls it `NO_MSG`
98/// (deprecated), GenCP calls it `MSG_TIMEOUT` — so a transport-specific code
99/// must be decoded by that transport, not here. See ADR-0020.
100///
101/// Values corroborated by Wireshark's GVCP dissector
102/// (`epan/dissectors/packet-gvcp.c`, `GEV_STATUS_*`) and aravis
103/// (`arvgvcpprivate.h`, `arvuvcpprivate.h`), which agree with each other.
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum StatusCode {
106    /// Command completed successfully.
107    Success,
108    /// The requested command is not implemented by the device.
109    NotImplemented,
110    /// One of the command parameters was invalid or out of range.
111    InvalidParameter,
112    /// The requested address does not exist on the device.
113    InvalidAddress,
114    /// Attempt to write to a read-only register.
115    ///
116    /// A permanent condition: retrying cannot make it succeed.
117    WriteProtect,
118    /// The access was not aligned as the underlying technology requires.
119    BadAlignment,
120    /// Attempt to read a non-readable, or write a non-writable, register.
121    ///
122    /// Distinct from [`StatusCode::WriteProtect`]: the register accepts
123    /// writes in principle but the device is refusing this one, typically
124    /// because a GenApi lock is engaged or control privilege is not held.
125    AccessDenied,
126    /// The device is busy and the command may succeed if retried.
127    ///
128    /// The only status in this table worth a retry.
129    Busy,
130    /// The device reported a generic error with nothing more specific.
131    GenericError,
132    /// A status code not known to this implementation, or specific to one
133    /// transport. Carries the raw value so a caller can still report it.
134    Unknown(u16),
135}
136
137impl StatusCode {
138    /// Convert from the raw status field in an acknowledgement header.
139    pub fn from_raw(raw: u16) -> Self {
140        match raw {
141            0x0000 => StatusCode::Success,
142            0x8001 => StatusCode::NotImplemented,
143            0x8002 => StatusCode::InvalidParameter,
144            0x8003 => StatusCode::InvalidAddress,
145            0x8004 => StatusCode::WriteProtect,
146            0x8005 => StatusCode::BadAlignment,
147            0x8006 => StatusCode::AccessDenied,
148            0x8007 => StatusCode::Busy,
149            0x8FFF => StatusCode::GenericError,
150            other => StatusCode::Unknown(other),
151        }
152    }
153
154    /// Convert to the raw value stored in the packet header.
155    pub const fn to_raw(self) -> u16 {
156        match self {
157            StatusCode::Success => 0x0000,
158            StatusCode::NotImplemented => 0x8001,
159            StatusCode::InvalidParameter => 0x8002,
160            StatusCode::InvalidAddress => 0x8003,
161            StatusCode::WriteProtect => 0x8004,
162            StatusCode::BadAlignment => 0x8005,
163            StatusCode::AccessDenied => 0x8006,
164            StatusCode::Busy => 0x8007,
165            StatusCode::GenericError => 0x8FFF,
166            StatusCode::Unknown(code) => code,
167        }
168    }
169
170    /// The specification's name for this code, for diagnostics.
171    pub const fn name(self) -> &'static str {
172        match self {
173            StatusCode::Success => "SUCCESS",
174            StatusCode::NotImplemented => "NOT_IMPLEMENTED",
175            StatusCode::InvalidParameter => "INVALID_PARAMETER",
176            StatusCode::InvalidAddress => "INVALID_ADDRESS",
177            StatusCode::WriteProtect => "WRITE_PROTECT",
178            StatusCode::BadAlignment => "BAD_ALIGNMENT",
179            StatusCode::AccessDenied => "ACCESS_DENIED",
180            StatusCode::Busy => "BUSY",
181            StatusCode::GenericError => "ERROR",
182            StatusCode::Unknown(_) => "unknown status",
183        }
184    }
185
186    /// Whether retrying the command could plausibly succeed.
187    ///
188    /// True only for [`StatusCode::Busy`]. Notably *not* `WriteProtect` or
189    /// `AccessDenied`, which are refusals rather than congestion.
190    pub const fn is_retryable(self) -> bool {
191        matches!(self, StatusCode::Busy)
192    }
193}
194
195/// Prints the specification name **and** the raw hex value.
196///
197/// Both halves matter: the name is what makes an error actionable, and the
198/// raw value is what lets a reporter match it against a capture. A bare
199/// decimal — which is what `{:?}` on the old `Unknown(32774)` produced —
200/// sent a user to the issue tracker in #45 unable to tell us anything.
201impl std::fmt::Display for StatusCode {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        write!(f, "{} (0x{:04X})", self.name(), self.to_raw())
204    }
205}
206
207/// Errors that can occur when dealing with GenCP packets.
208#[derive(Debug, Error)]
209#[non_exhaustive]
210pub enum GenCpError {
211    #[error("invalid packet: {0}")]
212    InvalidPacket(&'static str),
213    #[error("unknown opcode: {0:#06x}")]
214    UnknownOpcode(u16),
215    #[error("io: {0}")]
216    Io(#[from] std::io::Error),
217}
218
219/// Command header for GenCP requests.
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub struct CommandHeader {
222    /// Request flags (ack required, broadcast, …).
223    pub flags: CommandFlags,
224    /// Operation code for the request.
225    pub opcode: OpCode,
226    /// Length of the payload in bytes.
227    pub length: u16,
228    /// Request identifier chosen by the client.
229    pub request_id: u16,
230}
231
232/// Header for GenCP acknowledgements.
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234pub struct AckHeader {
235    /// Status returned by the device.
236    pub status: StatusCode,
237    /// Operation code associated with the acknowledgement.
238    pub opcode: OpCode,
239    /// Length of the payload in bytes.
240    pub length: u16,
241    /// Request identifier that this acknowledgement answers.
242    pub request_id: u16,
243}
244
245/// GenCP command packet.
246#[derive(Debug, Clone)]
247pub struct GenCpCmd {
248    /// Packet header fields.
249    pub header: CommandHeader,
250    /// Command payload.
251    pub payload: Bytes,
252}
253
254/// GenCP acknowledgement packet.
255#[derive(Debug, Clone)]
256pub struct GenCpAck {
257    /// Header fields returned by the device.
258    pub header: AckHeader,
259    /// Payload data (command specific).
260    pub payload: Bytes,
261}
262
263/// Encode a GenCP command into the on-the-wire representation.
264///
265/// The returned buffer is ready to be transmitted by the transport layer.
266pub fn encode_cmd(cmd: &GenCpCmd) -> Bytes {
267    debug_assert_eq!(cmd.header.length as usize, cmd.payload.len());
268    let mut buffer = BytesMut::with_capacity(HEADER_SIZE + cmd.payload.len());
269    buffer.put_u16(cmd.header.flags.bits());
270    buffer.put_u16(cmd.header.opcode.command_code());
271    buffer.put_u16(cmd.header.length);
272    buffer.put_u16(cmd.header.request_id);
273    buffer.extend_from_slice(&cmd.payload);
274    buffer.freeze()
275}
276
277/// Decode a GenCP acknowledgement from raw bytes.
278pub fn decode_ack(buf: &[u8]) -> Result<GenCpAck, GenCpError> {
279    if buf.len() < HEADER_SIZE {
280        return Err(GenCpError::InvalidPacket("too short"));
281    }
282    let mut cursor = buf;
283    let status_raw = cursor.get_u16();
284    let opcode_raw = cursor.get_u16();
285    let length = cursor.get_u16();
286    let request_id = cursor.get_u16();
287
288    let expected = HEADER_SIZE + length as usize;
289    if buf.len() != expected {
290        return Err(GenCpError::InvalidPacket("length mismatch"));
291    }
292
293    let opcode = OpCode::from_ack(opcode_raw)?;
294    let status = StatusCode::from_raw(status_raw);
295
296    let payload = Bytes::copy_from_slice(&buf[HEADER_SIZE..]);
297    Ok(GenCpAck {
298        header: AckHeader {
299            status,
300            opcode,
301            length,
302            request_id,
303        },
304        payload,
305    })
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    /// The shared status table, written as literal values rather than derived
313    /// from `to_raw` — per ADR-0019, a fixture that reuses our own encoder
314    /// cannot catch our own misreading. Cross-checked against Wireshark's
315    /// `GEV_STATUS_*` defines and aravis's `ArvUvcpStatus`.
316    const SPEC_STATUS_TABLE: &[(u16, StatusCode, &str)] = &[
317        (0x0000, StatusCode::Success, "SUCCESS"),
318        (0x8001, StatusCode::NotImplemented, "NOT_IMPLEMENTED"),
319        (0x8002, StatusCode::InvalidParameter, "INVALID_PARAMETER"),
320        (0x8003, StatusCode::InvalidAddress, "INVALID_ADDRESS"),
321        (0x8004, StatusCode::WriteProtect, "WRITE_PROTECT"),
322        (0x8005, StatusCode::BadAlignment, "BAD_ALIGNMENT"),
323        (0x8006, StatusCode::AccessDenied, "ACCESS_DENIED"),
324        (0x8007, StatusCode::Busy, "BUSY"),
325        (0x8FFF, StatusCode::GenericError, "ERROR"),
326    ];
327
328    #[test]
329    fn status_table_matches_the_specification() {
330        for &(raw, expected, name) in SPEC_STATUS_TABLE {
331            assert_eq!(
332                StatusCode::from_raw(raw),
333                expected,
334                "decoding {raw:#06x} ({name})"
335            );
336            assert_eq!(expected.to_raw(), raw, "re-encoding {name}");
337            assert_eq!(expected.name(), name);
338        }
339    }
340
341    #[test]
342    fn regression_the_three_codes_we_used_to_mislabel() {
343        // 0x8004 was `DeviceBusy`, so a write to a read-only register reported
344        // "device busy" and the GVCP retry loop kept retrying it.
345        assert_eq!(StatusCode::from_raw(0x8004), StatusCode::WriteProtect);
346        assert!(!StatusCode::from_raw(0x8004).is_retryable());
347
348        // 0x8005 was the catch-all `Error`.
349        assert_eq!(StatusCode::from_raw(0x8005), StatusCode::BadAlignment);
350
351        // 0x8006 had no variant at all: #45's FLIR returned it on a locked
352        // node and the user saw `Unknown(32774)` — 32774 being 0x8006.
353        assert_eq!(StatusCode::from_raw(0x8006), StatusCode::AccessDenied);
354        assert_eq!(StatusCode::from_raw(32774), StatusCode::AccessDenied);
355
356        // 0x8007 is the one status a retry can help.
357        assert!(StatusCode::from_raw(0x8007).is_retryable());
358        assert!(!StatusCode::from_raw(0x0000).is_retryable());
359    }
360
361    #[test]
362    fn display_carries_both_the_name_and_the_raw_value() {
363        assert_eq!(
364            StatusCode::AccessDenied.to_string(),
365            "ACCESS_DENIED (0x8006)"
366        );
367        // An unrecognised code still prints as hex, never bare decimal.
368        assert_eq!(
369            StatusCode::from_raw(0x800C).to_string(),
370            "unknown status (0x800C)"
371        );
372    }
373
374    #[test]
375    fn transport_specific_codes_stay_unknown_here() {
376        // 0x800B is the reason this enum holds only the shared core: GVCP
377        // calls it NO_MSG (deprecated), GenCP calls it MSG_TIMEOUT. Decoding
378        // it here would have to pick one and be wrong for the other
379        // transport, so it is deliberately left to the transport (ADR-0020).
380        assert_eq!(StatusCode::from_raw(0x800B), StatusCode::Unknown(0x800B));
381        // Likewise the GVCP packet-resend family and the GenCP 0xA0xx range.
382        assert_eq!(StatusCode::from_raw(0x800C), StatusCode::Unknown(0x800C));
383        assert_eq!(StatusCode::from_raw(0xA001), StatusCode::Unknown(0xA001));
384        // Round-tripping an unknown code must not lose it.
385        assert_eq!(StatusCode::from_raw(0x800B).to_raw(), 0x800B);
386    }
387
388    #[test]
389    fn pending_ack_is_a_command_id_not_a_status() {
390        // The bug this constant replaces: 0x8006 was treated as "pending".
391        assert_eq!(PENDING_ACK_COMMAND, 0x0805);
392        assert_ne!(PENDING_ACK_COMMAND, StatusCode::AccessDenied.to_raw());
393        // It sits in the command-id table beside the ones we already model.
394        assert_eq!(OpCode::ReadMem.command_code(), 0x0084);
395        assert_eq!(OpCode::WriteMem.command_code(), 0x0086);
396    }
397
398    #[test]
399    fn encode_read_register_roundtrip() {
400        let payload = {
401            let mut p = BytesMut::with_capacity(4);
402            p.put_u32(0x0000_0a00);
403            p.freeze()
404        };
405        let cmd = GenCpCmd {
406            header: CommandHeader {
407                flags: CommandFlags::ACK_REQUIRED,
408                opcode: OpCode::ReadRegister,
409                length: payload.len() as u16,
410                request_id: 0x41,
411            },
412            payload,
413        };
414
415        let encoded = encode_cmd(&cmd);
416        assert_eq!(
417            &encoded[..2],
418            &CommandFlags::ACK_REQUIRED.bits().to_be_bytes()
419        );
420        assert_eq!(&encoded[2..4], &0x0080u16.to_be_bytes());
421        assert_eq!(&encoded[4..6], &(cmd.payload.len() as u16).to_be_bytes());
422        assert_eq!(&encoded[6..8], &0x0041u16.to_be_bytes());
423        assert_eq!(&encoded[8..], &cmd.payload[..]);
424    }
425
426    #[test]
427    fn encode_write_register_roundtrip() {
428        let payload = {
429            let mut p = BytesMut::with_capacity(8);
430            p.put_u32(0x0000_0a00);
431            p.put_u32(0x0000_0002);
432            p.freeze()
433        };
434        let cmd = GenCpCmd {
435            header: CommandHeader {
436                flags: CommandFlags::ACK_REQUIRED,
437                opcode: OpCode::WriteRegister,
438                length: payload.len() as u16,
439                request_id: 0x43,
440            },
441            payload,
442        };
443
444        let encoded = encode_cmd(&cmd);
445        assert_eq!(
446            &encoded[..2],
447            &CommandFlags::ACK_REQUIRED.bits().to_be_bytes()
448        );
449        assert_eq!(&encoded[2..4], &0x0082u16.to_be_bytes());
450        assert_eq!(&encoded[4..6], &(cmd.payload.len() as u16).to_be_bytes());
451        assert_eq!(&encoded[6..8], &0x0043u16.to_be_bytes());
452        assert_eq!(&encoded[8..], &cmd.payload[..]);
453    }
454
455    #[test]
456    fn decode_read_register_ack() {
457        let value = 0x0000_0002u32;
458        let mut buf = BytesMut::with_capacity(HEADER_SIZE + 4);
459        buf.put_u16(0x0000);
460        buf.put_u16(0x0081);
461        buf.put_u16(4);
462        buf.put_u16(0x4141);
463        buf.put_u32(value);
464
465        let ack = decode_ack(&buf).expect("decode");
466        assert_eq!(ack.header.status, StatusCode::Success);
467        assert_eq!(ack.header.opcode, OpCode::ReadRegister);
468        assert_eq!(ack.header.length, 4);
469        assert_eq!(ack.header.request_id, 0x4141);
470        assert_eq!(&ack.payload[..], &value.to_be_bytes());
471    }
472
473    #[test]
474    fn decode_write_register_ack() {
475        let index = 1u32;
476        let mut buf = BytesMut::with_capacity(HEADER_SIZE + 4);
477        buf.put_u16(0x0000);
478        buf.put_u16(0x0083);
479        buf.put_u16(4);
480        buf.put_u16(0x4343);
481        buf.put_u32(index);
482
483        let ack = decode_ack(&buf).expect("decode");
484        assert_eq!(ack.header.status, StatusCode::Success);
485        assert_eq!(ack.header.opcode, OpCode::WriteRegister);
486        assert_eq!(ack.header.length, 4);
487        assert_eq!(ack.header.request_id, 0x4343);
488        assert_eq!(&ack.payload[..], &index.to_be_bytes());
489    }
490
491    #[test]
492    fn encode_read_mem_roundtrip() {
493        let payload = {
494            let mut p = BytesMut::with_capacity(12);
495            p.put_u64(0x0010_0200);
496            p.put_u32(64);
497            p.freeze()
498        };
499        let cmd = GenCpCmd {
500            header: CommandHeader {
501                flags: CommandFlags::ACK_REQUIRED,
502                opcode: OpCode::ReadMem,
503                length: payload.len() as u16,
504                request_id: 0x42,
505            },
506            payload,
507        };
508
509        let encoded = encode_cmd(&cmd);
510        assert_eq!(
511            &encoded[..2],
512            &CommandFlags::ACK_REQUIRED.bits().to_be_bytes()
513        );
514        assert_eq!(&encoded[2..4], &0x0084u16.to_be_bytes());
515        assert_eq!(&encoded[4..6], &(cmd.payload.len() as u16).to_be_bytes());
516        assert_eq!(&encoded[6..8], &0x0042u16.to_be_bytes());
517        assert_eq!(&encoded[8..], &cmd.payload[..]);
518    }
519
520    #[test]
521    fn decode_read_mem_ack() {
522        let payload = vec![0xAA; 4];
523        let mut buf = BytesMut::with_capacity(HEADER_SIZE + payload.len());
524        buf.put_u16(0x0000);
525        buf.put_u16(0x0085);
526        buf.put_u16(payload.len() as u16);
527        buf.put_u16(0x4242);
528        buf.extend_from_slice(&payload);
529
530        let ack = decode_ack(&buf).expect("decode");
531        assert_eq!(ack.header.status, StatusCode::Success);
532        assert_eq!(ack.header.opcode, OpCode::ReadMem);
533        assert_eq!(ack.header.length as usize, payload.len());
534        assert_eq!(ack.header.request_id, 0x4242);
535        assert_eq!(&ack.payload[..], &payload[..]);
536    }
537
538    #[test]
539    fn decode_write_mem_ack() {
540        let payload: Vec<u8> = Vec::new();
541        let mut buf = BytesMut::with_capacity(HEADER_SIZE + payload.len());
542        buf.put_u16(0x0000);
543        buf.put_u16(0x0087);
544        buf.put_u16(0);
545        buf.put_u16(0x1001);
546        let ack = decode_ack(&buf).expect("decode");
547        assert_eq!(ack.header.opcode, OpCode::WriteMem);
548        assert_eq!(ack.header.status, StatusCode::Success);
549        assert_eq!(ack.payload.len(), 0);
550    }
551
552    // ── Spec-derived acknowledgement header (backlog TC-04) ────────────────
553    //
554    // The tests above build their input with the same `put_u16` calls the
555    // encoder uses, in the order the decoder reads them. That proves the two
556    // agree; it cannot show either matches the standard. These assert the
557    // header as a literal byte array written from the specification's field
558    // table, and index it by offset.
559    //
560    // | Offset | Size | Field                    |
561    // |--------|------|--------------------------|
562    // |      0 |    2 | Status                   |
563    // |      2 |    2 | Acknowledge command id   |
564    // |      4 |    2 | Length of the payload    |
565    // |      6 |    2 | Request id (echoed)      |
566    // |      8 |    n | Payload                  |
567    //
568    // All fields are big-endian.
569
570    /// A `READREG_ACK` returning `0x0000_3EF2`, byte for byte.
571    const GOLDEN_READ_REGISTER_ACK: [u8; 12] = [
572        0x00, 0x00, // status: SUCCESS
573        0x00, 0x81, // acknowledge command id: READREG_ACK
574        0x00, 0x04, // length: 4 — the payload alone
575        0x12, 0x34, // request id
576        0x00, 0x00, 0x3E, 0xF2, // payload: 16114
577    ];
578
579    #[test]
580    fn ack_header_fields_sit_at_the_specified_offsets() {
581        let b = &GOLDEN_READ_REGISTER_ACK;
582        assert_eq!(u16::from_be_bytes([b[0], b[1]]), 0x0000, "status at 0");
583        assert_eq!(u16::from_be_bytes([b[2], b[3]]), 0x0081, "ack id at 2");
584        assert_eq!(u16::from_be_bytes([b[4], b[5]]), 4, "length at 4");
585        assert_eq!(u16::from_be_bytes([b[6], b[7]]), 0x1234, "request id at 6");
586        assert_eq!(HEADER_SIZE, 8, "the payload begins at offset 8");
587
588        let ack = decode_ack(b).expect("decode the golden ack");
589        assert_eq!(ack.header.status, StatusCode::Success);
590        assert_eq!(ack.header.opcode, OpCode::ReadRegister);
591        assert_eq!(ack.header.length, 4);
592        assert_eq!(ack.header.request_id, 0x1234);
593        assert_eq!(&ack.payload[..], &[0x00, 0x00, 0x3E, 0xF2]);
594    }
595
596    /// `length` counts the payload, not the whole datagram.
597    ///
598    /// Off by exactly `HEADER_SIZE`, a fake and a client still round-trip
599    /// perfectly with each other while every real device disagrees — the
600    /// ADR-0019 shape. Pin the interpretation rather than the arithmetic.
601    #[test]
602    fn ack_length_counts_the_payload_only() {
603        let mut counts_the_header = GOLDEN_READ_REGISTER_ACK;
604        counts_the_header[4..6].copy_from_slice(&12u16.to_be_bytes());
605        assert!(
606            decode_ack(&counts_the_header).is_err(),
607            "a length of 12 describes a 20-byte datagram, not this one"
608        );
609
610        // And the truncation case: the field promises more than arrived.
611        let mut over_promises = GOLDEN_READ_REGISTER_ACK;
612        over_promises[4..6].copy_from_slice(&8u16.to_be_bytes());
613        assert!(decode_ack(&over_promises).is_err());
614    }
615
616    /// The acknowledge command id is the command id plus one, and the decoder
617    /// must reject a *command* id arriving where an acknowledgement belongs.
618    #[test]
619    fn ack_ids_are_command_ids_plus_one() {
620        for (cmd, ack) in [
621            (0x0080u16, 0x0081u16),
622            (0x0082, 0x0083),
623            (0x0084, 0x0085),
624            (0x0086, 0x0087),
625        ] {
626            assert_eq!(
627                OpCode::from_command(cmd).expect("command id").ack_code(),
628                ack
629            );
630            assert!(
631                OpCode::from_ack(cmd).is_err(),
632                "{cmd:#06x} is a command id, not an acknowledgement"
633            );
634        }
635    }
636
637    /// A pending-acknowledge is a distinct command id carrying a *success*
638    /// status, so nothing about the status word distinguishes it from the real
639    /// answer (backlog TC-16). It must not decode as one.
640    #[test]
641    fn pending_ack_is_not_a_normal_acknowledgement() {
642        let mut pending = GOLDEN_READ_REGISTER_ACK;
643        pending[2..4].copy_from_slice(&PENDING_ACK_COMMAND.to_be_bytes());
644        assert_eq!(
645            u16::from_be_bytes([pending[0], pending[1]]),
646            0x0000,
647            "a pending-ack reports SUCCESS — the status cannot be the signal"
648        );
649        assert!(
650            decode_ack(&pending).is_err(),
651            "0x0805 is not an acknowledgement command id"
652        );
653    }
654
655    /// The command header uses the same four fields in the same order, so the
656    /// encoder is pinned by offset too.
657    #[test]
658    fn command_header_fields_sit_at_the_specified_offsets() {
659        let cmd = GenCpCmd {
660            header: CommandHeader {
661                flags: CommandFlags::ACK_REQUIRED,
662                opcode: OpCode::ReadMem,
663                length: 8,
664                request_id: 0x00AB,
665            },
666            payload: Bytes::from_static(&[0, 0, 0x0D, 0x04, 0, 0, 0, 4]),
667        };
668        let b = encode_cmd(&cmd);
669
670        assert_eq!(u16::from_be_bytes([b[0], b[1]]), 0x0001, "flags at 0");
671        assert_eq!(u16::from_be_bytes([b[2], b[3]]), 0x0084, "READMEM_CMD at 2");
672        assert_eq!(u16::from_be_bytes([b[4], b[5]]), 8, "payload length at 4");
673        assert_eq!(u16::from_be_bytes([b[6], b[7]]), 0x00AB, "request id at 6");
674        assert_eq!(b.len(), HEADER_SIZE + 8);
675    }
676}