Skip to main content

viva_u3v/
control.rs

1//! GenCP control channel over USB3 Vision bulk endpoints.
2//!
3//! USB3 Vision wraps GenCP commands/acknowledgements in a 12-byte prefix
4//! that carries the same semantic fields (opcode, flags, status, request ID)
5//! but in a different wire layout than the standard 8-byte GenCP header.
6//!
7//! This module encodes/decodes the U3V prefix and delegates to
8//! [`viva_gencp`] types for opcodes, status codes, and command flags.
9
10use std::sync::Arc;
11use std::time::Duration;
12
13use bytes::{BufMut, BytesMut};
14use viva_gencp::{CommandFlags, OpCode, PENDING_ACK_COMMAND, StatusCode};
15
16use crate::U3vError;
17use crate::usb::UsbTransfer;
18
19/// U3V command prefix magic: "U3VC" in little-endian.
20const CMD_PREFIX: u32 = 0x4356_3355;
21/// U3V acknowledge prefix magic: "U3VA" in little-endian (unused for encoding,
22/// validated on decode).
23const ACK_PREFIX: u32 = 0x4356_3341;
24
25/// Size of the U3V command/ack prefix in bytes.
26const PREFIX_SIZE: usize = 12;
27
28/// Maximum number of PENDING_ACK loops before giving up.
29const MAX_PENDING_RETRIES: usize = 100;
30
31/// Upper bound on a single pending-acknowledge wait.
32///
33/// A device asking for more time should not be able to park a control
34/// transaction indefinitely, and `MAX_PENDING_RETRIES` alone does not bound
35/// the total wait.
36const MAX_PENDING_WAIT: Duration = Duration::from_millis(10_000);
37
38/// Wait used when a pending-acknowledge carries no usable timeout field.
39const DEFAULT_PENDING_WAIT_MS: u64 = 100;
40
41/// Extract the requested wait from a pending-acknowledge SCD.
42///
43/// Layout is 2 reserved bytes followed by a little-endian `u16` of
44/// milliseconds — the GenCP framing is little-endian throughout, and aravis
45/// reads the same field the same way (`ArvUvcpPendingAckInfos` is
46/// `{ guint16 unknown; guint16 timeout; }`, read with `GUINT16_FROM_LE`).
47///
48/// The previous reading took all four bytes as a **big-endian `u32`**, which
49/// is wrong in both offset and byte order: a device asking for 1 000 ms sends
50/// `00 00 E8 03`, which that reading turned into 59 395 ms. See backlog TC-12
51/// for the GVCP side of the same field, which is still unsettled against
52/// hardware.
53fn pending_ack_timeout_ms(scd: &[u8]) -> u64 {
54    if scd.len() < 4 {
55        return DEFAULT_PENDING_WAIT_MS;
56    }
57    let requested = u16::from_le_bytes([scd[2], scd[3]]) as u64;
58    if requested == 0 {
59        return DEFAULT_PENDING_WAIT_MS;
60    }
61    requested.min(MAX_PENDING_WAIT.as_millis() as u64)
62}
63
64/// Default control channel timeout.
65const DEFAULT_TIMEOUT: Duration = Duration::from_millis(1000);
66
67/// GenCP control channel over a USB3 Vision bulk endpoint pair.
68///
69/// Sends GenCP commands (ReadReg, WriteReg, ReadMem, WriteMem) wrapped in
70/// the U3V prefix and receives acknowledgements from the device.
71pub struct ControlChannel<T: UsbTransfer> {
72    transport: Arc<T>,
73    ep_in: u8,
74    ep_out: u8,
75    request_id: u16,
76    max_cmd_transfer: u32,
77    max_ack_transfer: u32,
78    timeout: Duration,
79}
80
81impl<T: UsbTransfer> ControlChannel<T> {
82    /// Create a new control channel.
83    ///
84    /// `max_cmd_transfer` and `max_ack_transfer` come from the device's
85    /// SBRM (or USB descriptor) and cap the largest single bulk transfer.
86    pub fn new(
87        transport: Arc<T>,
88        ep_in: u8,
89        ep_out: u8,
90        max_cmd_transfer: u32,
91        max_ack_transfer: u32,
92    ) -> Self {
93        Self {
94            transport,
95            ep_in,
96            ep_out,
97            request_id: 0,
98            max_cmd_transfer,
99            max_ack_transfer,
100            timeout: DEFAULT_TIMEOUT,
101        }
102    }
103
104    /// Override the default control transaction timeout.
105    pub fn set_timeout(&mut self, timeout: Duration) {
106        self.timeout = timeout;
107    }
108
109    /// Access the underlying transport (e.g. to share with a stream).
110    pub fn transport(&self) -> &Arc<T> {
111        &self.transport
112    }
113
114    /// Read a single 32-bit register at `addr`.
115    pub fn read_register(&mut self, addr: u64) -> Result<u32, U3vError> {
116        let mut payload = BytesMut::with_capacity(8);
117        payload.put_u64(addr);
118        let ack = self.transact(OpCode::ReadRegister, &payload)?;
119        if ack.len() < 4 {
120            return Err(U3vError::Protocol(format!(
121                "ReadRegister ack too short: {} bytes",
122                ack.len()
123            )));
124        }
125        Ok(u32::from_be_bytes([ack[0], ack[1], ack[2], ack[3]]))
126    }
127
128    /// Write a single 32-bit register at `addr`.
129    pub fn write_register(&mut self, addr: u64, value: u32) -> Result<(), U3vError> {
130        let mut payload = BytesMut::with_capacity(12);
131        payload.put_u64(addr);
132        payload.put_u32(value);
133        let _ack = self.transact(OpCode::WriteRegister, &payload)?;
134        Ok(())
135    }
136
137    /// Read `len` bytes starting at `addr`, automatically chunking across
138    /// the device's maximum acknowledgement transfer size.
139    pub fn read_mem(&mut self, addr: u64, len: usize) -> Result<Vec<u8>, U3vError> {
140        let max_payload = self.max_read_chunk();
141        let mut result = Vec::with_capacity(len);
142        let mut offset = 0usize;
143
144        while offset < len {
145            let chunk = (len - offset).min(max_payload);
146            let mut payload = BytesMut::with_capacity(12);
147            payload.put_u64(addr + offset as u64);
148            // ReadMem SCD: 8-byte address + 2-byte reserved + 2-byte count
149            payload.put_u16(0); // reserved
150            payload.put_u16(chunk as u16);
151            let ack = self.transact(OpCode::ReadMem, &payload)?;
152            if ack.len() != chunk {
153                return Err(U3vError::Protocol(format!(
154                    "ReadMem ACK length mismatch at offset {offset}: \
155                     requested {chunk} bytes, got {}",
156                    ack.len()
157                )));
158            }
159            result.extend_from_slice(&ack);
160            offset += chunk;
161        }
162        Ok(result)
163    }
164
165    /// Write `data` starting at `addr`, automatically chunking across
166    /// the device's maximum command transfer size.
167    pub fn write_mem(&mut self, addr: u64, data: &[u8]) -> Result<(), U3vError> {
168        let max_payload = self.max_write_chunk();
169        let mut offset = 0usize;
170
171        while offset < data.len() {
172            let chunk = (data.len() - offset).min(max_payload);
173            let mut payload = BytesMut::with_capacity(8 + chunk);
174            payload.put_u64(addr + offset as u64);
175            payload.extend_from_slice(&data[offset..offset + chunk]);
176            let _ack = self.transact(OpCode::WriteMem, &payload)?;
177            offset += chunk;
178        }
179        Ok(())
180    }
181
182    /// Maximum read chunk size: ack transfer limit minus the prefix.
183    fn max_read_chunk(&self) -> usize {
184        (self.max_ack_transfer as usize).saturating_sub(PREFIX_SIZE)
185    }
186
187    /// Maximum write chunk size: cmd transfer limit minus prefix and 8-byte address.
188    fn max_write_chunk(&self) -> usize {
189        (self.max_cmd_transfer as usize).saturating_sub(PREFIX_SIZE + 8)
190    }
191
192    // -----------------------------------------------------------------------
193    // Core transaction: encode → send → receive (with PENDING_ACK retry)
194    // -----------------------------------------------------------------------
195
196    fn transact(&mut self, opcode: OpCode, payload: &[u8]) -> Result<Vec<u8>, U3vError> {
197        let request_id = self.next_request_id();
198        let packet = encode_command(opcode, CommandFlags::ACK_REQUIRED, request_id, payload);
199        self.transport
200            .bulk_write(self.ep_out, &packet, self.timeout)?;
201
202        // Read ack, handling PENDING_ACK loops.
203        let mut ack_buf = vec![0u8; self.max_ack_transfer as usize];
204        for _ in 0..MAX_PENDING_RETRIES {
205            let n = self
206                .transport
207                .bulk_read(self.ep_in, &mut ack_buf, self.timeout)?;
208            let ack = decode_ack(&ack_buf[..n])?;
209
210            if ack.request_id != request_id {
211                return Err(U3vError::Protocol(format!(
212                    "request ID mismatch: expected {request_id:#06x}, got {:#06x}",
213                    ack.request_id
214                )));
215            }
216
217            // A pending-acknowledge is identified by its command id, before
218            // the status is consulted at all: its status is `SUCCESS`, so
219            // status-first dispatch would accept it as the real answer and
220            // return its timeout SCD as register data. aravis orders the two
221            // checks the same way (`arvuvdevice.c`).
222            if ack.command == PENDING_ACK_COMMAND {
223                let wait_ms = pending_ack_timeout_ms(&ack.payload);
224                tracing::debug!(wait_ms, "PENDING_ACK, extending deadline");
225                std::thread::sleep(Duration::from_millis(wait_ms));
226                continue;
227            }
228
229            // Anything else must be the acknowledgement for the command we
230            // sent. Without this the request id alone was the only guard, so
231            // any other ack that happened to carry a matching id was accepted.
232            if ack.command != opcode.ack_code() {
233                return Err(U3vError::Protocol(format!(
234                    "unexpected acknowledgement command {:#06x}, expected {:#06x}",
235                    ack.command,
236                    opcode.ack_code()
237                )));
238            }
239
240            match ack.status {
241                StatusCode::Success => return Ok(ack.payload),
242                status => return Err(U3vError::Status { status }),
243            }
244        }
245        Err(U3vError::Timeout)
246    }
247
248    fn next_request_id(&mut self) -> u16 {
249        let id = self.request_id;
250        self.request_id = self.request_id.wrapping_add(1);
251        id
252    }
253}
254
255// ---------------------------------------------------------------------------
256// Wire encoding / decoding
257// ---------------------------------------------------------------------------
258
259/// Encode a U3V command packet (prefix + GenCP payload).
260///
261/// Layout (little-endian):
262/// ```text
263/// [0..4]  prefix   0x43563355 ("U3VC")
264/// [4..6]  flags    CommandFlags bits
265/// [6..8]  command  OpCode command code
266/// [8..10] length   payload length in bytes
267/// [10..12] req_id  request identifier
268/// [12..]  payload  GenCP-specific payload
269/// ```
270fn encode_command(opcode: OpCode, flags: CommandFlags, request_id: u16, payload: &[u8]) -> Vec<u8> {
271    let mut buf = BytesMut::with_capacity(PREFIX_SIZE + payload.len());
272    buf.put_u32_le(CMD_PREFIX);
273    buf.put_u16_le(flags.bits());
274    buf.put_u16_le(opcode.command_code());
275    buf.put_u16_le(payload.len() as u16);
276    buf.put_u16_le(request_id);
277    buf.extend_from_slice(payload);
278    buf.to_vec()
279}
280
281/// Decoded U3V acknowledgement fields.
282struct AckPacket {
283    status: StatusCode,
284    /// The acknowledgement's command id.
285    ///
286    /// Must be kept: a pending-acknowledge is distinguished from a real answer
287    /// by this field and not by `status`, which is `SUCCESS` on both. This was
288    /// previously discarded, so a pending-ack's timeout SCD was returned to
289    /// the caller as if it were register data.
290    command: u16,
291    request_id: u16,
292    payload: Vec<u8>,
293}
294
295/// Decode a U3V acknowledgement packet.
296fn decode_ack(buf: &[u8]) -> Result<AckPacket, U3vError> {
297    if buf.len() < PREFIX_SIZE {
298        return Err(U3vError::Protocol(format!(
299            "ack too short: {} bytes, need at least {PREFIX_SIZE}",
300            buf.len()
301        )));
302    }
303
304    let prefix = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
305    if prefix != ACK_PREFIX {
306        return Err(U3vError::Protocol(format!(
307            "bad ack prefix: {prefix:#010x}, expected {ACK_PREFIX:#010x}"
308        )));
309    }
310
311    let status_raw = u16::from_le_bytes([buf[4], buf[5]]);
312    let command = u16::from_le_bytes([buf[6], buf[7]]);
313    let length = u16::from_le_bytes([buf[8], buf[9]]) as usize;
314    let request_id = u16::from_le_bytes([buf[10], buf[11]]);
315
316    let expected = PREFIX_SIZE + length;
317    if buf.len() < expected {
318        return Err(U3vError::Protocol(format!(
319            "ack truncated: got {} bytes, header says {expected}",
320            buf.len()
321        )));
322    }
323
324    let status = StatusCode::from_raw(status_raw);
325    let payload = buf[PREFIX_SIZE..PREFIX_SIZE + length].to_vec();
326
327    Ok(AckPacket {
328        status,
329        command,
330        request_id,
331        payload,
332    })
333}
334
335// ---------------------------------------------------------------------------
336// Tests
337// ---------------------------------------------------------------------------
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342    use crate::usb::MockUsbTransfer;
343
344    const EP_OUT: u8 = 0x01;
345    const EP_IN: u8 = 0x81;
346
347    /// Build a mock ack response with the given status, request_id, and payload.
348    // Acknowledgement command ids, so fixtures can name the command they are
349    // impersonating without burying it in `OpCode::…::ack_code()` noise.
350    const ACK_READ_REG: u16 = OpCode::ReadRegister.ack_code();
351    const ACK_WRITE_REG: u16 = OpCode::WriteRegister.ack_code();
352    const ACK_READ_MEM: u16 = OpCode::ReadMem.ack_code();
353    const ACK_WRITE_MEM: u16 = OpCode::WriteMem.ack_code();
354
355    /// Build an acknowledgement with an explicit command id.
356    ///
357    /// The command id used to be hardcoded to `ReadMem`'s, with the comment
358    /// "opcode doesn't matter for most tests" — which was true only because
359    /// `transact` ignored the field. It is exactly the assumption that let a
360    /// pending-acknowledge be accepted as a real answer, so every caller now
361    /// states the command it is impersonating.
362    fn build_ack(command: u16, status: StatusCode, request_id: u16, payload: &[u8]) -> Vec<u8> {
363        let mut buf = BytesMut::with_capacity(PREFIX_SIZE + payload.len());
364        buf.put_u32_le(ACK_PREFIX);
365        buf.put_u16_le(status.to_raw());
366        buf.put_u16_le(command);
367        buf.put_u16_le(payload.len() as u16);
368        buf.put_u16_le(request_id);
369        buf.extend_from_slice(payload);
370        buf.to_vec()
371    }
372
373    fn make_channel(mock: &Arc<MockUsbTransfer>) -> ControlChannel<MockUsbTransfer> {
374        ControlChannel::new(Arc::clone(mock), EP_IN, EP_OUT, 1024, 1024)
375    }
376
377    #[test]
378    fn encode_command_format() {
379        let payload = [0xAA, 0xBB, 0xCC, 0xDD];
380        let pkt = encode_command(
381            OpCode::ReadMem,
382            CommandFlags::ACK_REQUIRED,
383            0x0042,
384            &payload,
385        );
386        assert_eq!(pkt.len(), PREFIX_SIZE + 4);
387
388        // Prefix
389        assert_eq!(
390            u32::from_le_bytes([pkt[0], pkt[1], pkt[2], pkt[3]]),
391            CMD_PREFIX
392        );
393        // Flags
394        assert_eq!(
395            u16::from_le_bytes([pkt[4], pkt[5]]),
396            CommandFlags::ACK_REQUIRED.bits()
397        );
398        // Opcode
399        assert_eq!(
400            u16::from_le_bytes([pkt[6], pkt[7]]),
401            OpCode::ReadMem.command_code()
402        );
403        // Length
404        assert_eq!(u16::from_le_bytes([pkt[8], pkt[9]]), 4);
405        // Request ID
406        assert_eq!(u16::from_le_bytes([pkt[10], pkt[11]]), 0x0042);
407        // Payload
408        assert_eq!(&pkt[12..], &payload);
409    }
410
411    #[test]
412    fn decode_ack_success() {
413        let payload = vec![0x01, 0x02, 0x03, 0x04];
414        let buf = build_ack(ACK_READ_MEM, StatusCode::Success, 0x0042, &payload);
415        let ack = decode_ack(&buf).unwrap();
416        assert_eq!(ack.status, StatusCode::Success);
417        assert_eq!(ack.request_id, 0x0042);
418        assert_eq!(ack.payload, payload);
419    }
420
421    #[test]
422    fn decode_ack_bad_prefix() {
423        let mut buf = build_ack(ACK_READ_MEM, StatusCode::Success, 0x0001, &[]);
424        // Corrupt prefix
425        buf[0] = 0xFF;
426        assert!(decode_ack(&buf).is_err());
427    }
428
429    #[test]
430    fn decode_ack_truncated() {
431        let buf = build_ack(ACK_READ_MEM, StatusCode::Success, 0x0001, &[1, 2, 3, 4]);
432        // Truncate: send header but chop off payload
433        assert!(decode_ack(&buf[..PREFIX_SIZE]).is_err());
434    }
435
436    #[test]
437    fn read_register_roundtrip() {
438        let mock = Arc::new(MockUsbTransfer::new());
439        let mut ch = make_channel(&mock);
440
441        // Enqueue a success ack with a 4-byte register value
442        let value: u32 = 0xDEAD_BEEF;
443        let ack = build_ack(
444            ACK_READ_REG,
445            StatusCode::Success,
446            0x0000,
447            &value.to_be_bytes(),
448        );
449        mock.enqueue_read(EP_IN, ack);
450
451        let result = ch.read_register(0x0000_1000).unwrap();
452        assert_eq!(result, value);
453
454        // Verify the command was sent
455        let writes = mock.take_writes(EP_OUT);
456        assert_eq!(writes.len(), 1);
457        assert_eq!(writes[0].len(), PREFIX_SIZE + 8); // prefix + 8-byte addr
458    }
459
460    #[test]
461    fn write_register_roundtrip() {
462        let mock = Arc::new(MockUsbTransfer::new());
463        let mut ch = make_channel(&mock);
464
465        // Enqueue success ack (empty payload is fine for write)
466        let ack = build_ack(ACK_WRITE_REG, StatusCode::Success, 0x0000, &[]);
467        mock.enqueue_read(EP_IN, ack);
468
469        ch.write_register(0x0000_1000, 0x1234_5678).unwrap();
470
471        let writes = mock.take_writes(EP_OUT);
472        assert_eq!(writes.len(), 1);
473        assert_eq!(writes[0].len(), PREFIX_SIZE + 12); // prefix + 8-byte addr + 4-byte value
474    }
475
476    #[test]
477    fn read_mem_single_chunk() {
478        let mock = Arc::new(MockUsbTransfer::new());
479        let mut ch = make_channel(&mock);
480
481        let data = vec![0xAA; 64];
482        let ack = build_ack(ACK_READ_MEM, StatusCode::Success, 0x0000, &data);
483        mock.enqueue_read(EP_IN, ack);
484
485        let result = ch.read_mem(0x0000_2000, 64).unwrap();
486        assert_eq!(result, data);
487    }
488
489    #[test]
490    fn read_mem_chunked() {
491        // Set max_ack_transfer small enough to force 2 chunks for 64 bytes.
492        // max_read_chunk = max_ack_transfer - PREFIX_SIZE = 48 - 12 = 36
493        let mock = Arc::new(MockUsbTransfer::new());
494        let mut ch = ControlChannel::new(Arc::clone(&mock), EP_IN, EP_OUT, 1024, 48);
495
496        let chunk1 = vec![0xAA; 36];
497        let chunk2 = vec![0xBB; 28];
498        mock.enqueue_read(
499            EP_IN,
500            build_ack(ACK_READ_MEM, StatusCode::Success, 0x0000, &chunk1),
501        );
502        mock.enqueue_read(
503            EP_IN,
504            build_ack(ACK_READ_MEM, StatusCode::Success, 0x0001, &chunk2),
505        );
506
507        let result = ch.read_mem(0x0000_3000, 64).unwrap();
508        assert_eq!(result.len(), 64);
509        assert_eq!(&result[..36], &chunk1[..]);
510        assert_eq!(&result[36..], &chunk2[..]);
511
512        // Should have issued 2 write commands
513        let writes = mock.take_writes(EP_OUT);
514        assert_eq!(writes.len(), 2);
515    }
516
517    #[test]
518    fn write_mem_chunked() {
519        let mock = Arc::new(MockUsbTransfer::new());
520        // max_write_chunk = 48 - 12 - 8 = 28
521        let mut ch = ControlChannel::new(Arc::clone(&mock), EP_IN, EP_OUT, 48, 1024);
522
523        mock.enqueue_read(
524            EP_IN,
525            build_ack(ACK_WRITE_MEM, StatusCode::Success, 0x0000, &[]),
526        );
527        mock.enqueue_read(
528            EP_IN,
529            build_ack(ACK_WRITE_MEM, StatusCode::Success, 0x0001, &[]),
530        );
531
532        let data = vec![0xCC; 50]; // > 28, so needs 2 chunks
533        ch.write_mem(0x0000_4000, &data).unwrap();
534
535        let writes = mock.take_writes(EP_OUT);
536        assert_eq!(writes.len(), 2);
537    }
538
539    #[test]
540    fn pending_ack_retry() {
541        let mock = Arc::new(MockUsbTransfer::new());
542        let mut ch = make_channel(&mock);
543
544        // First ack: a real GenCP pending-acknowledge. Note the status is
545        // SUCCESS -- it is the command id that marks it -- and the SCD is
546        // 2 reserved bytes plus a little-endian u16 of milliseconds.
547        let pending_scd = [0x00, 0x00, 0x00, 0x00];
548        mock.enqueue_read(
549            EP_IN,
550            build_ack(
551                PENDING_ACK_COMMAND,
552                StatusCode::Success,
553                0x0000,
554                &pending_scd,
555            ),
556        );
557        // Second ack: success
558        let value = 0x1234_5678u32;
559        mock.enqueue_read(
560            EP_IN,
561            build_ack(
562                ACK_READ_REG,
563                StatusCode::Success,
564                0x0000,
565                &value.to_be_bytes(),
566            ),
567        );
568
569        let result = ch.read_register(0x0000_5000).unwrap();
570        assert_eq!(result, value);
571    }
572
573    #[test]
574    fn device_error_status() {
575        let mock = Arc::new(MockUsbTransfer::new());
576        let mut ch = make_channel(&mock);
577
578        mock.enqueue_read(
579            EP_IN,
580            build_ack(ACK_READ_REG, StatusCode::InvalidAddress, 0x0000, &[]),
581        );
582
583        let err = ch.read_register(0xFFFF_FFFF).unwrap_err();
584        assert!(matches!(
585            err,
586            U3vError::Status {
587                status: StatusCode::InvalidAddress
588            }
589        ));
590    }
591
592    #[test]
593    fn pending_ack_scd_is_a_little_endian_u16_after_two_reserved_bytes() {
594        // A device asking for 1000 ms sends 00 00 E8 03.
595        assert_eq!(pending_ack_timeout_ms(&[0x00, 0x00, 0xE8, 0x03]), 1000);
596        // The previous reading took all four bytes as a big-endian u32, which
597        // turned that same request into a 59-second sleep.
598        assert_ne!(
599            pending_ack_timeout_ms(&[0x00, 0x00, 0xE8, 0x03]),
600            u32::from_be_bytes([0x00, 0x00, 0xE8, 0x03]) as u64
601        );
602        // Too short, and zero, fall back rather than busy-looping.
603        assert_eq!(pending_ack_timeout_ms(&[0x00]), DEFAULT_PENDING_WAIT_MS);
604        assert_eq!(
605            pending_ack_timeout_ms(&[0x00, 0x00, 0x00, 0x00]),
606            DEFAULT_PENDING_WAIT_MS
607        );
608        // A device cannot park the transaction indefinitely.
609        assert_eq!(
610            pending_ack_timeout_ms(&[0x00, 0x00, 0xFF, 0xFF]),
611            MAX_PENDING_WAIT.as_millis() as u64
612        );
613    }
614
615    #[test]
616    fn access_denied_is_reported_not_retried_as_pending() {
617        // 0x8006 used to be read as "device needs more time", so a refusal was
618        // slept over 100 times and then reported as a timeout. It is
619        // ACCESS_DENIED -- exactly what #45's FLIR returned on a locked node.
620        let mock = Arc::new(MockUsbTransfer::new());
621        let mut ch = make_channel(&mock);
622
623        mock.enqueue_read(
624            EP_IN,
625            build_ack(ACK_READ_REG, StatusCode::AccessDenied, 0x0000, &[]),
626        );
627
628        let err = ch.read_register(0x0000_1000).unwrap_err();
629        assert!(matches!(
630            err,
631            U3vError::Status {
632                status: StatusCode::AccessDenied
633            }
634        ));
635        // One exchange, not 100.
636        assert_eq!(mock.take_writes(EP_OUT).len(), 1);
637    }
638
639    #[test]
640    fn acknowledgement_for_another_command_is_rejected() {
641        // The request id alone used to be the only guard, so any ack carrying a
642        // matching id was accepted and its payload returned as register data.
643        let mock = Arc::new(MockUsbTransfer::new());
644        let mut ch = make_channel(&mock);
645
646        mock.enqueue_read(
647            EP_IN,
648            build_ack(
649                ACK_WRITE_MEM,
650                StatusCode::Success,
651                0x0000,
652                &0xDEAD_BEEFu32.to_be_bytes(),
653            ),
654        );
655
656        let err = ch.read_register(0x0000_1000).unwrap_err();
657        assert!(
658            matches!(err, U3vError::Protocol(ref msg) if msg.contains("unexpected acknowledgement command")),
659            "expected a protocol error naming the command, got {err:?}"
660        );
661    }
662
663    #[test]
664    fn request_id_increments() {
665        let mock = Arc::new(MockUsbTransfer::new());
666        let mut ch = make_channel(&mock);
667
668        // Two reads, request ID should increment
669        mock.enqueue_read(
670            EP_IN,
671            build_ack(ACK_READ_REG, StatusCode::Success, 0x0000, &[0; 4]),
672        );
673        mock.enqueue_read(
674            EP_IN,
675            build_ack(ACK_READ_REG, StatusCode::Success, 0x0001, &[0; 4]),
676        );
677
678        ch.read_register(0x1000).unwrap();
679        ch.read_register(0x2000).unwrap();
680
681        let writes = mock.take_writes(EP_OUT);
682        assert_eq!(writes.len(), 2);
683        // First command: request_id = 0
684        assert_eq!(u16::from_le_bytes([writes[0][10], writes[0][11]]), 0x0000);
685        // Second command: request_id = 1
686        assert_eq!(u16::from_le_bytes([writes[1][10], writes[1][11]]), 0x0001);
687    }
688}