Skip to main content

vlfd_rs/
usb.rs

1use crate::error::{Error, Result};
2use nusb::{
3    self, Device, DeviceId, DeviceInfo, Interface, MaybeFuture,
4    transfer::{Bulk, In, Out},
5};
6use std::{
7    io::{Read, Write},
8    sync::{
9        Arc,
10        atomic::{AtomicBool, Ordering},
11    },
12    thread,
13    time::Duration,
14};
15
16#[cfg(target_endian = "big")]
17compile_error!("vlfd-rs currently supports little-endian hosts only");
18
19const INTERFACE: u8 = 0;
20const HOTPLUG_POLL_INTERVAL: Duration = Duration::from_millis(100);
21const IO_BUFFER_SIZE: usize = 16 * 1024;
22
23#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
24pub struct UsbLocation {
25    /// Host-controller identifier reported by the operating system.
26    pub bus_id: String,
27    /// Stable physical hub-port path below that controller.
28    pub port_chain: Vec<u8>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
32pub struct BoardInfo {
33    pub location: UsbLocation,
34    /// Transient USB address, useful for diagnostics but not stable selection.
35    pub address: u8,
36    /// USB descriptor serial number, when the operating system exposes one.
37    pub serial_number: Option<String>,
38    pub vendor_id: u16,
39    pub product_id: u16,
40}
41
42impl BoardInfo {
43    fn from_device_info(device: &DeviceInfo) -> Self {
44        Self {
45            location: UsbLocation {
46                #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
47                bus_id: device.bus_id().to_owned(),
48                #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
49                bus_id: String::new(),
50                #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
51                port_chain: device.port_chain().to_vec(),
52                #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
53                port_chain: Vec::new(),
54            },
55            #[cfg(any(
56                target_os = "linux",
57                target_os = "macos",
58                target_os = "windows",
59                target_os = "android"
60            ))]
61            address: device.device_address(),
62            #[cfg(not(any(
63                target_os = "linux",
64                target_os = "macos",
65                target_os = "windows",
66                target_os = "android"
67            )))]
68            address: 0,
69            serial_number: device.serial_number().map(str::to_owned),
70            vendor_id: device.vendor_id(),
71            product_id: device.product_id(),
72        }
73    }
74}
75
76#[derive(Debug, Clone, Default, PartialEq, Eq)]
77pub enum BoardSelector {
78    /// Require exactly one matching VLFD board to be connected.
79    #[default]
80    Only,
81    /// Match the USB descriptor serial number exactly.
82    SerialNumber(String),
83    /// Match a stable physical USB topology location.
84    UsbLocation(UsbLocation),
85}
86
87impl BoardSelector {
88    fn matches(&self, board: &BoardInfo) -> bool {
89        match self {
90            Self::Only => true,
91            Self::SerialNumber(serial) => board.serial_number.as_ref() == Some(serial),
92            Self::UsbLocation(location) => board.location == *location,
93        }
94    }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub struct TransportConfig {
99    pub usb_timeout: Duration,
100    pub sync_timeout: Duration,
101    pub reset_on_open: bool,
102    pub clear_halt_on_open: bool,
103}
104
105impl Default for TransportConfig {
106    fn default() -> Self {
107        Self {
108            usb_timeout: Duration::from_millis(1_000),
109            sync_timeout: Duration::from_secs(1),
110            reset_on_open: false,
111            clear_halt_on_open: true,
112        }
113    }
114}
115
116#[derive(Debug, Clone, Copy)]
117pub enum Endpoint {
118    FifoWrite = 0x02,
119    Command = 0x04,
120    FifoRead = 0x86,
121    Sync = 0x88,
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum HotplugEventKind {
126    Arrived,
127    Left,
128}
129
130#[derive(Debug, Clone)]
131pub struct HotplugDeviceInfo {
132    pub bus_number: u8,
133    pub address: u8,
134    pub port_numbers: Vec<u8>,
135    pub vendor_id: Option<u16>,
136    pub product_id: Option<u16>,
137    pub class_code: Option<u8>,
138    pub sub_class_code: Option<u8>,
139    pub protocol_code: Option<u8>,
140}
141
142impl HotplugDeviceInfo {
143    fn from_device_info(device: &DeviceInfo) -> Self {
144        Self {
145            #[cfg(target_os = "linux")]
146            bus_number: device.busnum(),
147            #[cfg(not(target_os = "linux"))]
148            bus_number: 0,
149            address: device.device_address(),
150            #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
151            port_numbers: device.port_chain().to_vec(),
152            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
153            port_numbers: Vec::new(),
154            vendor_id: Some(device.vendor_id()),
155            product_id: Some(device.product_id()),
156            class_code: Some(device.class()),
157            sub_class_code: Some(device.subclass()),
158            protocol_code: Some(device.protocol()),
159        }
160    }
161}
162
163#[derive(Debug, Clone)]
164pub struct HotplugEvent {
165    pub kind: HotplugEventKind,
166    pub device: HotplugDeviceInfo,
167}
168
169#[derive(Debug, Clone, Copy, Default)]
170pub struct HotplugOptions {
171    pub vendor_id: Option<u16>,
172    pub product_id: Option<u16>,
173    pub class_code: Option<u8>,
174    pub enumerate: bool,
175}
176
177#[derive(Debug, Clone, Default)]
178pub struct Probe {
179    transport: TransportConfig,
180}
181
182impl Probe {
183    pub fn new() -> Self {
184        Self::default()
185    }
186
187    pub fn with_transport_config(transport: TransportConfig) -> Self {
188        Self { transport }
189    }
190
191    pub fn transport_config(&self) -> &TransportConfig {
192        &self.transport
193    }
194
195    pub fn boards(&self) -> Result<Vec<BoardInfo>> {
196        matching_board_devices(crate::constants::DW_VID, crate::constants::DW_PID).map(|devices| {
197            let mut boards = devices
198                .iter()
199                .map(BoardInfo::from_device_info)
200                .collect::<Vec<_>>();
201            boards.sort();
202            boards
203        })
204    }
205
206    pub fn watch<F>(&self, options: HotplugOptions, callback: F) -> Result<HotplugRegistration>
207    where
208        F: FnMut(HotplugEvent) + Send + 'static,
209    {
210        UsbDevice::with_transport_config(self.transport)?
211            .register_hotplug_callback(options, callback)
212    }
213}
214
215pub struct UsbDevice {
216    handle: Option<Device>,
217    interface: Option<Interface>,
218    transport: TransportConfig,
219}
220
221impl UsbDevice {
222    pub fn with_transport_config(transport: TransportConfig) -> Result<Self> {
223        Ok(Self {
224            handle: None,
225            interface: None,
226            transport,
227        })
228    }
229
230    pub fn is_open(&self) -> bool {
231        self.interface.is_some()
232    }
233
234    pub fn transport_config(&self) -> &TransportConfig {
235        &self.transport
236    }
237
238    pub fn open_selected(&mut self, vid: u16, pid: u16, selector: &BoardSelector) -> Result<()> {
239        if self.is_open() {
240            return Ok(());
241        }
242
243        let devices = matching_board_devices(vid, pid)?;
244        if devices.is_empty() {
245            return Err(Error::DeviceNotFound { vid, pid });
246        }
247        let device_info = select_device(&devices, selector)?;
248
249        let device = device_info
250            .open()
251            .wait()
252            .map_err(|err| usb_error(err, "nusb_open_device"))?;
253
254        if self.transport.reset_on_open {
255            device
256                .reset()
257                .wait()
258                .map_err(|err| usb_error(err, "nusb_reset_device"))?;
259        }
260
261        let interface = device
262            .detach_and_claim_interface(INTERFACE)
263            .wait()
264            .map_err(|err| usb_error(err, "nusb_claim_interface"))?;
265
266        let mut usb_device = Self {
267            handle: Some(device),
268            interface: Some(interface),
269            transport: self.transport,
270        };
271
272        if usb_device.transport.clear_halt_on_open {
273            usb_device.clear_halt_all()?;
274        }
275
276        *self = usb_device;
277        Ok(())
278    }
279
280    pub fn close(&mut self) -> Result<()> {
281        self.interface.take();
282        self.handle.take();
283        Ok(())
284    }
285
286    pub fn read_bytes(&self, endpoint: Endpoint, buffer: &mut [u8]) -> Result<()> {
287        let interface = self.interface.as_ref().ok_or(Error::DeviceNotOpen)?;
288        bulk_read(interface, endpoint, buffer, self.transport.usb_timeout)
289    }
290
291    pub fn read_words(&self, endpoint: Endpoint, buffer: &mut [u16]) -> Result<()> {
292        let raw = words_as_bytes_mut(buffer);
293        self.read_bytes(endpoint, raw)
294    }
295
296    pub fn write_bytes(&self, endpoint: Endpoint, buffer: &[u8]) -> Result<()> {
297        let interface = self.interface.as_ref().ok_or(Error::DeviceNotOpen)?;
298        bulk_write(interface, endpoint, buffer, self.transport.usb_timeout)
299    }
300
301    pub fn write_words(&self, endpoint: Endpoint, buffer: &[u16]) -> Result<()> {
302        let raw = words_as_bytes(buffer);
303        self.write_bytes(endpoint, raw)
304    }
305
306    pub fn open_in_endpoint(&self, endpoint: Endpoint) -> Result<nusb::Endpoint<Bulk, In>> {
307        let interface = self.interface.as_ref().ok_or(Error::DeviceNotOpen)?;
308        interface
309            .endpoint::<Bulk, In>(endpoint as u8)
310            .map_err(|err| usb_error(err, "nusb_open_in_endpoint"))
311    }
312
313    pub fn open_out_endpoint(&self, endpoint: Endpoint) -> Result<nusb::Endpoint<Bulk, Out>> {
314        let interface = self.interface.as_ref().ok_or(Error::DeviceNotOpen)?;
315        interface
316            .endpoint::<Bulk, Out>(endpoint as u8)
317            .map_err(|err| usb_error(err, "nusb_open_out_endpoint"))
318    }
319
320    pub fn register_hotplug_callback<F>(
321        &self,
322        options: HotplugOptions,
323        mut callback: F,
324    ) -> Result<HotplugRegistration>
325    where
326        F: FnMut(HotplugEvent) + Send + 'static,
327    {
328        let mut seen_devices = Vec::<(DeviceId, HotplugDeviceInfo)>::new();
329        let initial_devices = matching_devices(options)?;
330        if options.enumerate {
331            for device in &initial_devices {
332                callback(HotplugEvent {
333                    kind: HotplugEventKind::Arrived,
334                    device: HotplugDeviceInfo::from_device_info(device),
335                });
336            }
337        }
338        seen_devices.extend(
339            initial_devices
340                .iter()
341                .map(|device| (device.id(), HotplugDeviceInfo::from_device_info(device))),
342        );
343
344        let running = Arc::new(AtomicBool::new(true));
345        let thread_running = Arc::clone(&running);
346        let thread = thread::Builder::new()
347            .name("vlfd-usb-hotplug".into())
348            .spawn(move || {
349                let mut known = seen_devices;
350                while thread_running.load(Ordering::Relaxed) {
351                    if let Ok(devices) = matching_devices(options) {
352                        let mut current = devices
353                            .iter()
354                            .map(|device| {
355                                (device.id(), HotplugDeviceInfo::from_device_info(device))
356                            })
357                            .collect::<Vec<_>>();
358
359                        for (id, info) in &current {
360                            if !known.iter().any(|(known_id, _)| known_id == id) {
361                                callback(HotplugEvent {
362                                    kind: HotplugEventKind::Arrived,
363                                    device: info.clone(),
364                                });
365                            }
366                        }
367
368                        for (id, info) in &known {
369                            if !current.iter().any(|(current_id, _)| current_id == id) {
370                                callback(HotplugEvent {
371                                    kind: HotplugEventKind::Left,
372                                    device: info.clone(),
373                                });
374                            }
375                        }
376
377                        known.clear();
378                        known.append(&mut current);
379                    }
380
381                    thread::sleep(HOTPLUG_POLL_INTERVAL);
382                }
383            })
384            .map_err(Error::Io)?;
385
386        Ok(HotplugRegistration {
387            running,
388            thread: Some(thread),
389        })
390    }
391
392    pub(crate) fn clear_halt_all(&mut self) -> Result<()> {
393        for endpoint in [
394            Endpoint::FifoWrite,
395            Endpoint::Command,
396            Endpoint::FifoRead,
397            Endpoint::Sync,
398        ] {
399            self.clear_halt(endpoint)?;
400        }
401        Ok(())
402    }
403
404    fn clear_halt(&mut self, endpoint: Endpoint) -> Result<()> {
405        let interface = self.interface.as_ref().ok_or(Error::DeviceNotOpen)?;
406        match endpoint {
407            Endpoint::FifoWrite | Endpoint::Command => {
408                let mut ep = interface
409                    .endpoint::<Bulk, Out>(endpoint as u8)
410                    .map_err(|err| usb_error(err, "nusb_open_out_endpoint"))?;
411                ep.clear_halt()
412                    .wait()
413                    .map_err(|err| usb_error(err, "nusb_clear_halt"))?;
414            }
415            Endpoint::FifoRead | Endpoint::Sync => {
416                let mut ep = interface
417                    .endpoint::<Bulk, In>(endpoint as u8)
418                    .map_err(|err| usb_error(err, "nusb_open_in_endpoint"))?;
419                ep.clear_halt()
420                    .wait()
421                    .map_err(|err| usb_error(err, "nusb_clear_halt"))?;
422            }
423        }
424        Ok(())
425    }
426}
427
428impl Drop for UsbDevice {
429    fn drop(&mut self) {
430        let _ = self.close();
431    }
432}
433
434#[derive(Debug)]
435pub struct HotplugRegistration {
436    running: Arc<AtomicBool>,
437    thread: Option<thread::JoinHandle<()>>,
438}
439
440impl Drop for HotplugRegistration {
441    fn drop(&mut self) {
442        self.running.store(false, Ordering::SeqCst);
443        if let Some(handle) = self.thread.take() {
444            let _ = handle.join();
445        }
446    }
447}
448
449fn bulk_read(
450    interface: &Interface,
451    endpoint: Endpoint,
452    buffer: &mut [u8],
453    timeout: Duration,
454) -> Result<()> {
455    let mut reader = interface
456        .endpoint::<Bulk, In>(endpoint as u8)
457        .map_err(|err| usb_error(err, "nusb_open_in_endpoint"))?
458        .reader(IO_BUFFER_SIZE)
459        .with_read_timeout(timeout);
460
461    reader
462        .read_exact(buffer)
463        .map_err(|err| io_error(err, "nusb_bulk_read"))?;
464    Ok(())
465}
466
467fn bulk_write(
468    interface: &Interface,
469    endpoint: Endpoint,
470    buffer: &[u8],
471    timeout: Duration,
472) -> Result<()> {
473    let mut writer = interface
474        .endpoint::<Bulk, Out>(endpoint as u8)
475        .map_err(|err| usb_error(err, "nusb_open_out_endpoint"))?
476        .writer(IO_BUFFER_SIZE)
477        .with_write_timeout(timeout);
478
479    writer
480        .write_all(buffer)
481        .map_err(|err| io_error(err, "nusb_bulk_write"))?;
482    writer
483        .flush()
484        .map_err(|err| io_error(err, "nusb_bulk_flush"))?;
485    Ok(())
486}
487
488fn matching_devices(options: HotplugOptions) -> Result<Vec<DeviceInfo>> {
489    let devices = nusb::list_devices()
490        .wait()
491        .map_err(|err| usb_error(err, "nusb_list_devices"))?;
492    Ok(devices
493        .filter(|device| {
494            options
495                .vendor_id
496                .is_none_or(|vendor_id| device.vendor_id() == vendor_id)
497                && options
498                    .product_id
499                    .is_none_or(|product_id| device.product_id() == product_id)
500                && options
501                    .class_code
502                    .is_none_or(|class_code| device.class() == class_code)
503        })
504        .collect())
505}
506
507fn matching_board_devices(vid: u16, pid: u16) -> Result<Vec<DeviceInfo>> {
508    matching_devices(HotplugOptions {
509        vendor_id: Some(vid),
510        product_id: Some(pid),
511        ..HotplugOptions::default()
512    })
513}
514
515fn select_device<'a>(
516    devices: &'a [DeviceInfo],
517    selector: &BoardSelector,
518) -> Result<&'a DeviceInfo> {
519    select_unique(devices, |device| {
520        selector.matches(&BoardInfo::from_device_info(device))
521    })
522}
523
524fn select_unique<T>(items: &[T], mut predicate: impl FnMut(&T) -> bool) -> Result<&T> {
525    let mut selected = None;
526    let mut matches = 0;
527    for item in items.iter().filter(|item| predicate(item)) {
528        selected = Some(item);
529        matches += 1;
530    }
531
532    match matches {
533        0 => Err(Error::DeviceSelectionNoMatch),
534        1 => Ok(selected.expect("one device matched")),
535        _ => Err(Error::DeviceSelectionAmbiguous { matches }),
536    }
537}
538
539fn words_as_bytes(words: &[u16]) -> &[u8] {
540    unsafe { std::slice::from_raw_parts(words.as_ptr() as *const u8, std::mem::size_of_val(words)) }
541}
542
543fn words_as_bytes_mut(words: &mut [u16]) -> &mut [u8] {
544    unsafe {
545        std::slice::from_raw_parts_mut(words.as_mut_ptr() as *mut u8, std::mem::size_of_val(words))
546    }
547}
548
549fn usb_error(err: nusb::Error, context: &'static str) -> Error {
550    Error::Usb {
551        source: Box::new(err),
552        context,
553    }
554}
555
556fn io_error(err: std::io::Error, context: &'static str) -> Error {
557    if err.kind() == std::io::ErrorKind::TimedOut {
558        Error::Timeout(context)
559    } else {
560        Error::Usb {
561            source: Box::new(err),
562            context,
563        }
564    }
565}
566
567#[cfg(test)]
568mod tests {
569    use super::{BoardInfo, BoardSelector, TransportConfig, UsbLocation};
570    use crate::Error;
571    use std::time::Duration;
572
573    #[test]
574    fn default_transport_config_prefers_stable_open_behavior() {
575        let config = TransportConfig::default();
576        assert_eq!(config.usb_timeout, Duration::from_millis(1_000));
577        assert_eq!(config.sync_timeout, Duration::from_secs(1));
578        assert!(!config.reset_on_open);
579        assert!(config.clear_halt_on_open);
580    }
581
582    #[test]
583    fn selectors_match_stable_identity_fields() {
584        let board = BoardInfo {
585            location: UsbLocation {
586                bus_id: "usb0".into(),
587                port_chain: vec![2, 4],
588            },
589            address: 7,
590            serial_number: Some("board-a".into()),
591            vendor_id: 0x04b4,
592            product_id: 0x1004,
593        };
594
595        assert!(BoardSelector::Only.matches(&board));
596        assert!(BoardSelector::SerialNumber("board-a".into()).matches(&board));
597        assert!(
598            BoardSelector::UsbLocation(UsbLocation {
599                bus_id: "usb0".into(),
600                port_chain: vec![2, 4],
601            })
602            .matches(&board)
603        );
604        assert!(!BoardSelector::SerialNumber("board-b".into()).matches(&board));
605    }
606
607    #[test]
608    fn selection_rejects_zero_or_multiple_matches() {
609        assert!(matches!(
610            super::select_unique(&[1, 2], |_| false),
611            Err(Error::DeviceSelectionNoMatch)
612        ));
613        assert!(matches!(
614            super::select_unique(&[1, 2], |_| true),
615            Err(Error::DeviceSelectionAmbiguous { matches: 2 })
616        ));
617        assert_eq!(
618            *super::select_unique(&[1, 2], |value| *value == 2).unwrap(),
619            2
620        );
621    }
622}