Skip to main content

viva_u3v/
usb.rs

1//! USB transfer abstraction for testability.
2//!
3//! [`UsbTransfer`] decouples all U3V protocol logic from the `rusb` crate,
4//! allowing the control channel, streaming, and bootstrap parsing to be
5//! tested with [`MockUsbTransfer`] — no USB hardware required.
6
7use std::time::Duration;
8
9use crate::U3vError;
10
11/// Abstraction over a claimed USB device for bulk endpoint I/O.
12///
13/// Implementations must be safe to share across threads. The control and
14/// stream channels use different endpoints, so a single `UsbTransfer` can
15/// serve both concurrently (with internal synchronization if needed).
16pub trait UsbTransfer: Send + Sync {
17    /// Write `data` to the bulk OUT `endpoint`. Returns bytes written.
18    fn bulk_write(&self, endpoint: u8, data: &[u8], timeout: Duration) -> Result<usize, U3vError>;
19
20    /// Read up to `buf.len()` bytes from the bulk IN `endpoint`.
21    /// Returns the number of bytes actually read.
22    fn bulk_read(&self, endpoint: u8, buf: &mut [u8], timeout: Duration)
23    -> Result<usize, U3vError>;
24}
25
26// ---------------------------------------------------------------------------
27// rusb-backed implementation (behind `usb` feature)
28// ---------------------------------------------------------------------------
29
30#[cfg(feature = "usb")]
31pub use self::rusb_impl::RusbTransfer;
32
33#[cfg(feature = "usb")]
34mod rusb_impl {
35    use super::*;
36    use std::sync::Arc;
37
38    /// [`UsbTransfer`] backed by a real `rusb::DeviceHandle`.
39    pub struct RusbTransfer {
40        handle: Arc<rusb::DeviceHandle<rusb::Context>>,
41    }
42
43    impl RusbTransfer {
44        /// Wrap an already-opened and claimed device handle.
45        pub fn new(handle: Arc<rusb::DeviceHandle<rusb::Context>>) -> Self {
46            Self { handle }
47        }
48    }
49
50    impl UsbTransfer for RusbTransfer {
51        fn bulk_write(
52            &self,
53            endpoint: u8,
54            data: &[u8],
55            timeout: Duration,
56        ) -> Result<usize, U3vError> {
57            self.handle
58                .write_bulk(endpoint, data, timeout)
59                .map_err(|e| U3vError::Usb(e.to_string()))
60        }
61
62        fn bulk_read(
63            &self,
64            endpoint: u8,
65            buf: &mut [u8],
66            timeout: Duration,
67        ) -> Result<usize, U3vError> {
68            self.handle
69                .read_bulk(endpoint, buf, timeout)
70                .map_err(|e| U3vError::Usb(e.to_string()))
71        }
72    }
73}
74
75// ---------------------------------------------------------------------------
76// Mock implementation for testing
77// ---------------------------------------------------------------------------
78
79use std::collections::{HashMap, VecDeque};
80use std::sync::Mutex;
81
82/// In-memory mock of [`UsbTransfer`] for unit tests.
83///
84/// Pre-load expected read responses per endpoint, then execute protocol
85/// logic. After the test, inspect captured writes to verify correctness.
86///
87/// # Thread safety
88///
89/// State lives behind a `Mutex`, so the `Send + Sync` that [`UsbTransfer`]
90/// requires is derived rather than asserted. The mock used to hold `RefCell`
91/// and claim the bounds with `unsafe impl`, which was unsound: `RefCell`'s
92/// borrow flag is not atomic, so two threads sharing one mock could hold
93/// overlapping `borrow_mut()`s and alias the same `&mut`.
94pub struct MockUsbTransfer {
95    /// Queued read responses per endpoint address.
96    reads: Mutex<HashMap<u8, VecDeque<Vec<u8>>>>,
97    /// Captured write payloads per endpoint address.
98    writes: Mutex<HashMap<u8, Vec<Vec<u8>>>>,
99}
100
101impl MockUsbTransfer {
102    /// Create an empty mock with no pre-loaded responses.
103    pub fn new() -> Self {
104        Self {
105            reads: Mutex::new(HashMap::new()),
106            writes: Mutex::new(HashMap::new()),
107        }
108    }
109
110    /// Enqueue a response that will be returned by the next `bulk_read`
111    /// on the given `endpoint`.
112    pub fn enqueue_read(&self, endpoint: u8, data: Vec<u8>) {
113        self.reads
114            .lock()
115            .expect("mock read queue poisoned")
116            .entry(endpoint)
117            .or_default()
118            .push_back(data);
119    }
120
121    /// Return all captured write payloads for the given `endpoint`.
122    pub fn take_writes(&self, endpoint: u8) -> Vec<Vec<u8>> {
123        self.writes
124            .lock()
125            .expect("mock write log poisoned")
126            .remove(&endpoint)
127            .unwrap_or_default()
128    }
129}
130
131impl Default for MockUsbTransfer {
132    fn default() -> Self {
133        Self::new()
134    }
135}
136
137impl UsbTransfer for MockUsbTransfer {
138    fn bulk_write(&self, endpoint: u8, data: &[u8], _timeout: Duration) -> Result<usize, U3vError> {
139        let len = data.len();
140        self.writes
141            .lock()
142            .expect("mock write log poisoned")
143            .entry(endpoint)
144            .or_default()
145            .push(data.to_vec());
146        Ok(len)
147    }
148
149    fn bulk_read(
150        &self,
151        endpoint: u8,
152        buf: &mut [u8],
153        _timeout: Duration,
154    ) -> Result<usize, U3vError> {
155        let mut reads = self.reads.lock().expect("mock read queue poisoned");
156        let queue = reads.get_mut(&endpoint).ok_or_else(|| {
157            U3vError::Protocol(format!("no queued read for endpoint {endpoint:#04x}"))
158        })?;
159        let data = queue.pop_front().ok_or_else(|| {
160            U3vError::Protocol(format!("read queue exhausted for endpoint {endpoint:#04x}"))
161        })?;
162        let n = data.len().min(buf.len());
163        buf[..n].copy_from_slice(&data[..n]);
164        Ok(n)
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn mock_write_then_read() {
174        let mock = MockUsbTransfer::new();
175        let ep_out = 0x01;
176        let ep_in = 0x81;
177
178        // Write some data
179        let written = mock
180            .bulk_write(ep_out, &[1, 2, 3], Duration::from_millis(100))
181            .unwrap();
182        assert_eq!(written, 3);
183
184        // Enqueue a read response and read it back
185        mock.enqueue_read(ep_in, vec![4, 5, 6, 7]);
186        let mut buf = [0u8; 8];
187        let n = mock
188            .bulk_read(ep_in, &mut buf, Duration::from_millis(100))
189            .unwrap();
190        assert_eq!(n, 4);
191        assert_eq!(&buf[..4], &[4, 5, 6, 7]);
192
193        // Verify captured writes
194        let writes = mock.take_writes(ep_out);
195        assert_eq!(writes.len(), 1);
196        assert_eq!(writes[0], &[1, 2, 3]);
197    }
198
199    #[test]
200    fn mock_read_exhausted_returns_error() {
201        let mock = MockUsbTransfer::new();
202        let mut buf = [0u8; 4];
203        let err = mock
204            .bulk_read(0x81, &mut buf, Duration::from_millis(100))
205            .unwrap_err();
206        assert!(matches!(err, U3vError::Protocol(_)));
207    }
208}