use crate::error::{Error, Result};
use rusb::{
self, Context, Device, DeviceHandle, Hotplug, HotplugBuilder, Registration, UsbContext,
};
use std::{
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
thread,
time::Duration,
};
#[cfg(target_endian = "big")]
compile_error!("vlfd-rs currently supports little-endian hosts only");
const INTERFACE: u8 = 0;
const HOTPLUG_POLL_INTERVAL: Duration = Duration::from_millis(100);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TransportConfig {
pub usb_timeout: Duration,
pub sync_timeout: Duration,
pub reset_on_open: bool,
pub clear_halt_on_open: bool,
}
impl Default for TransportConfig {
fn default() -> Self {
Self {
usb_timeout: Duration::from_millis(1_000),
sync_timeout: Duration::from_secs(1),
reset_on_open: false,
clear_halt_on_open: true,
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum Endpoint {
FifoWrite = 0x02,
Command = 0x04,
FifoRead = 0x86,
Sync = 0x88,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HotplugEventKind {
Arrived,
Left,
}
#[derive(Debug, Clone)]
pub struct HotplugDeviceInfo {
pub bus_number: u8,
pub address: u8,
pub port_numbers: Vec<u8>,
pub vendor_id: Option<u16>,
pub product_id: Option<u16>,
pub class_code: Option<u8>,
pub sub_class_code: Option<u8>,
pub protocol_code: Option<u8>,
}
impl HotplugDeviceInfo {
fn from_device(device: &Device<Context>) -> Self {
let descriptor = device.device_descriptor().ok();
Self {
bus_number: device.bus_number(),
address: device.address(),
port_numbers: device.port_numbers().unwrap_or_default(),
vendor_id: descriptor.as_ref().map(|desc| desc.vendor_id()),
product_id: descriptor.as_ref().map(|desc| desc.product_id()),
class_code: descriptor.as_ref().map(|desc| desc.class_code()),
sub_class_code: descriptor.as_ref().map(|desc| desc.sub_class_code()),
protocol_code: descriptor.as_ref().map(|desc| desc.protocol_code()),
}
}
}
#[derive(Debug, Clone)]
pub struct HotplugEvent {
pub kind: HotplugEventKind,
pub device: HotplugDeviceInfo,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct HotplugOptions {
pub vendor_id: Option<u16>,
pub product_id: Option<u16>,
pub class_code: Option<u8>,
pub enumerate: bool,
}
pub struct UsbDevice {
context: Context,
handle: Option<DeviceHandle<Context>>,
transport: TransportConfig,
}
impl UsbDevice {
pub fn new() -> Result<Self> {
Self::with_transport_config(TransportConfig::default())
}
pub fn with_transport_config(transport: TransportConfig) -> Result<Self> {
let context = Context::new().map_err(|err| usb_error(err, "libusb_init"))?;
Ok(Self {
context,
handle: None,
transport,
})
}
pub fn is_open(&self) -> bool {
self.handle.is_some()
}
pub fn transport_config(&self) -> &TransportConfig {
&self.transport
}
pub fn set_transport_config(&mut self, transport: TransportConfig) {
self.transport = transport;
}
pub fn open(&mut self, vid: u16, pid: u16) -> Result<()> {
if self.is_open() {
return Ok(());
}
let handle = self
.context
.open_device_with_vid_pid(vid, pid)
.ok_or(Error::DeviceNotFound { vid, pid })?;
if self.transport.reset_on_open {
handle
.reset()
.map_err(|err| usb_error(err, "libusb_reset_device"))?;
}
handle
.claim_interface(INTERFACE)
.map_err(|err| usb_error(err, "libusb_claim_interface"))?;
if self.transport.clear_halt_on_open {
for endpoint in [
Endpoint::FifoWrite,
Endpoint::Command,
Endpoint::FifoRead,
Endpoint::Sync,
] {
handle
.clear_halt(endpoint as u8)
.map_err(|err| usb_error(err, "libusb_clear_halt"))?;
}
}
self.handle = Some(handle);
Ok(())
}
pub fn close(&mut self) -> Result<()> {
if let Some(handle) = self.handle.take() {
match handle.release_interface(INTERFACE) {
Ok(_) | Err(rusb::Error::NoDevice) => {}
Err(err) => return Err(usb_error(err, "libusb_release_interface")),
}
}
Ok(())
}
pub fn read_bytes(&self, endpoint: Endpoint, buffer: &mut [u8]) -> Result<()> {
let handle = self.handle.as_ref().ok_or(Error::DeviceNotOpen)?;
bulk_read(handle, endpoint, buffer, self.transport.usb_timeout)
}
pub fn read_words(&self, endpoint: Endpoint, buffer: &mut [u16]) -> Result<()> {
let raw = words_as_bytes_mut(buffer);
self.read_bytes(endpoint, raw)
}
pub fn write_bytes(&self, endpoint: Endpoint, buffer: &[u8]) -> Result<()> {
let handle = self.handle.as_ref().ok_or(Error::DeviceNotOpen)?;
bulk_write(handle, endpoint, buffer, self.transport.usb_timeout)
}
pub fn write_words(&self, endpoint: Endpoint, buffer: &[u16]) -> Result<()> {
let raw = words_as_bytes(buffer);
self.write_bytes(endpoint, raw)
}
pub fn register_hotplug_callback<F>(
&self,
options: HotplugOptions,
callback: F,
) -> Result<HotplugRegistration>
where
F: FnMut(HotplugEvent) + Send + 'static,
{
if !rusb::has_hotplug() {
return Err(Error::FeatureUnavailable("usb_hotplug"));
}
let mut builder = HotplugBuilder::new();
if let Some(vendor) = options.vendor_id {
builder.vendor_id(vendor);
}
if let Some(product) = options.product_id {
builder.product_id(product);
}
if let Some(class_code) = options.class_code {
builder.class(class_code);
}
builder.enumerate(options.enumerate);
let handler = CallbackHotplug { callback };
let registration = builder
.register(&self.context, Box::new(handler))
.map_err(|err| usb_error(err, "libusb_hotplug_register_callback"))?;
HotplugRegistration::new(self.context.clone(), registration)
}
}
impl Drop for UsbDevice {
fn drop(&mut self) {
let _ = self.close();
}
}
#[derive(Debug)]
pub struct HotplugRegistration {
registration: Option<Registration<Context>>,
running: Arc<AtomicBool>,
thread: Option<thread::JoinHandle<()>>,
}
impl HotplugRegistration {
fn new(context: Context, registration: Registration<Context>) -> Result<Self> {
let running = Arc::new(AtomicBool::new(true));
let thread_running = Arc::clone(&running);
let thread = thread::Builder::new()
.name("vlfd-usb-hotplug".into())
.spawn(move || {
while thread_running.load(Ordering::Relaxed) {
match context.handle_events(Some(HOTPLUG_POLL_INTERVAL)) {
Ok(_) => {}
Err(rusb::Error::Interrupted) | Err(rusb::Error::Timeout) => continue,
Err(_) => break,
}
}
})
.map_err(Error::Io)?;
Ok(Self {
registration: Some(registration),
running,
thread: Some(thread),
})
}
}
impl Drop for HotplugRegistration {
fn drop(&mut self) {
let _ = self.registration.take();
self.running.store(false, Ordering::SeqCst);
if let Some(handle) = self.thread.take() {
let _ = handle.join();
}
}
}
struct CallbackHotplug<F>
where
F: FnMut(HotplugEvent) + Send + 'static,
{
callback: F,
}
impl<F> Hotplug<Context> for CallbackHotplug<F>
where
F: FnMut(HotplugEvent) + Send + 'static,
{
fn device_arrived(&mut self, device: Device<Context>) {
(self.callback)(HotplugEvent {
kind: HotplugEventKind::Arrived,
device: HotplugDeviceInfo::from_device(&device),
});
}
fn device_left(&mut self, device: Device<Context>) {
(self.callback)(HotplugEvent {
kind: HotplugEventKind::Left,
device: HotplugDeviceInfo::from_device(&device),
});
}
}
fn bulk_read<T: UsbContext>(
handle: &DeviceHandle<T>,
endpoint: Endpoint,
buffer: &mut [u8],
timeout: Duration,
) -> Result<()> {
let mut offset = 0;
while offset < buffer.len() {
let chunk = &mut buffer[offset..];
let bytes_read = handle
.read_bulk(endpoint as u8, chunk, timeout)
.map_err(|err| usb_error(err, "libusb_bulk_read"))?;
if bytes_read == 0 {
return Err(Error::UnexpectedResponse("bulk read returned zero bytes"));
}
offset += bytes_read;
}
Ok(())
}
fn bulk_write<T: UsbContext>(
handle: &DeviceHandle<T>,
endpoint: Endpoint,
buffer: &[u8],
timeout: Duration,
) -> Result<()> {
let mut offset = 0;
while offset < buffer.len() {
let chunk = &buffer[offset..];
let bytes_written = handle
.write_bulk(endpoint as u8, chunk, timeout)
.map_err(|err| usb_error(err, "libusb_bulk_write"))?;
if bytes_written == 0 {
return Err(Error::UnexpectedResponse("bulk write returned zero bytes"));
}
offset += bytes_written;
}
Ok(())
}
fn words_as_bytes(words: &[u16]) -> &[u8] {
unsafe { std::slice::from_raw_parts(words.as_ptr() as *const u8, std::mem::size_of_val(words)) }
}
fn words_as_bytes_mut(words: &mut [u16]) -> &mut [u8] {
unsafe {
std::slice::from_raw_parts_mut(words.as_mut_ptr() as *mut u8, std::mem::size_of_val(words))
}
}
fn usb_error(err: rusb::Error, context: &'static str) -> Error {
Error::Usb {
source: err,
context,
}
}
#[cfg(test)]
mod tests {
use super::TransportConfig;
use std::time::Duration;
#[test]
fn default_transport_config_prefers_stable_open_behavior() {
let config = TransportConfig::default();
assert_eq!(config.usb_timeout, Duration::from_millis(1_000));
assert_eq!(config.sync_timeout, Duration::from_secs(1));
assert!(!config.reset_on_open);
assert!(config.clear_halt_on_open);
}
}