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 pub bus_id: String,
27 pub port_chain: Vec<u8>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
32pub struct BoardInfo {
33 pub location: UsbLocation,
34 pub address: u8,
36 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 #[default]
80 Only,
81 SerialNumber(String),
83 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 ¤t {
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}