diff --git a/README.md b/README.md index 9f4cc25..10c2cf6 100644 --- a/README.md +++ b/README.md @@ -3,24 +3,24 @@ nusb A new pure-Rust library for cross-platform low-level access to USB devices. -[Documentation](https://docs.rs/nusb) +* [Documentation](https://docs.rs/nusb) +* [Changelog](https://github.com/kevinmehall/nusb/releases) +* [Issues](https://github.com/kevinmehall/nusb/issues) +* [Discussions](https://github.com/kevinmehall/nusb/discussions) + +`nusb` supports Windows, macOS, and Linux, and provides both async and +blocking APIs for listing and watching USB devices, reading descriptor +details, opening and managing devices and interfaces, and performing +transfers on control, bulk, and interrupt endpoints. ### Compared to [rusb](https://docs.rs/rusb/latest/rusb/) and [libusb](https://libusb.info/) * Pure Rust, no dependency on libusb or any other C library. -* Async-first, while not requiring an async runtime like `tokio` or - `async-std`. Still easily supports blocking with - `futures_lite::block_on`. +* Async-first, while not requiring an async runtime. * No context object. You just open a device. There is a global event loop thread that is started when opening the first device. * Thinner layer over OS APIs, with less internal state. -### Current status - -* Support for Linux, Windows, and macOS -* Device listing and descriptor parsing -* Transfers on control, bulk and interrupt endpoints -* Used in production by [probe-rs](https://github.com/probe-rs/probe-rs) and others, but still relatively new. Please test with your device and report issues. - ### License + MIT or Apache 2.0, at your option diff --git a/src/descriptors.rs b/src/descriptors.rs index c88d65a..11e4d6e 100644 --- a/src/descriptors.rs +++ b/src/descriptors.rs @@ -657,7 +657,7 @@ descriptor_fields! { /// Get the raw value of the `bmAttributes` descriptor field. /// - /// See [`transfer_type``][Self::transfer_type] for the transfer type field. + /// See [`transfer_type`][Self::transfer_type] for the transfer type field. #[doc(alias = "bmAttributes")] pub fn attributes at 3 -> u8; diff --git a/src/device.rs b/src/device.rs index 38e3703..7c057da 100644 --- a/src/device.rs +++ b/src/device.rs @@ -56,7 +56,15 @@ impl Device { platform::Device::from_device_info(d).map(|d| d.map(Device::wrap)) } - /// Wraps a device that is already open. + /// Wrap a usbdevfs file descriptor that is already open. + /// + /// This opens a device from a file descriptor for a `/dev/bus/usb/*` device + /// provided externally, such as from + /// [Android](https://developer.android.com/reference/android/hardware/usb/UsbDeviceConnection#getFileDescriptor()), + /// [xdg-desktop-portal](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Usb.html), + /// etc. + /// + /// *Supported on Linux and Android only.* #[cfg(any(target_os = "android", target_os = "linux"))] pub fn from_fd(fd: std::os::fd::OwnedFd) -> impl MaybeFuture> { platform::Device::from_fd(fd).map(|d| d.map(Device::wrap)) @@ -75,7 +83,7 @@ impl Device { /// Detach kernel drivers and open an interface of the device and claim it for exclusive use. /// - /// ### Platform notes + /// ### Platform-specific details /// This function can only detach kernel drivers on Linux. Calling on other platforms has /// the same effect as [`claim_interface`][`Device::claim_interface`]. pub fn detach_and_claim_interface( @@ -90,7 +98,7 @@ impl Device { /// Detach kernel drivers for the specified interface. /// - /// ### Platform notes + /// ### Platform-specific details /// This function can only detach kernel drivers on Linux. Calling on other platforms has /// no effect. pub fn detach_kernel_driver(&self, interface: u8) -> Result<(), Error> { @@ -103,7 +111,7 @@ impl Device { /// Attach kernel drivers for the specified interface. /// - /// ### Platform notes + /// ### Platform-specific details /// This function can only attach kernel drivers on Linux. Calling on other platforms has /// no effect. pub fn attach_kernel_driver(&self, interface: u8) -> Result<(), Error> { @@ -121,7 +129,7 @@ impl Device { self.backend.device_descriptor() } - /// Get device speed. + /// Get the device's connection speed. pub fn speed(&self) -> Option { self.backend.speed() } @@ -156,7 +164,7 @@ impl Device { /// descriptor field from [`ConfigurationDescriptor::configuration_value`] or `0` to /// unconfigure the device. /// - /// ### Platform-specific notes + /// ### Platform-specific details /// * Not supported on Windows pub fn set_configuration( &self, @@ -267,7 +275,7 @@ impl Device { /// This `Device` will no longer be usable, and you should drop it and call /// [`list_devices`][`super::list_devices`] to find and re-open it again. /// - /// ### Platform-specific notes + /// ### Platform-specific details /// * Not supported on Windows pub fn reset(&self) -> impl MaybeFuture> { self.backend.clone().reset() @@ -297,7 +305,7 @@ impl Device { /// # Ok(()) } /// ``` /// - /// ### Platform-specific notes + /// ### Platform-specific details /// /// * Not supported on Windows. You must [claim an interface][`Device::claim_interface`] /// and use the interface handle to submit transfers. @@ -334,7 +342,7 @@ impl Device { /// # Ok(()) } /// ``` /// - /// ### Platform-specific notes + /// ### Platform-specific details /// /// * Not supported on Windows. You must [claim an interface][`Device::claim_interface`] /// and use the interface handle to submit transfers. @@ -376,6 +384,9 @@ impl Interface { /// An alternate setting is a mode of the interface that makes particular endpoints available /// and may enable or disable functionality of the device. The OS resets the device to the default /// alternate setting when the interface is released or the program exits. + /// + /// You must not have any pending transfers or open `Endpoints` on this interface when changing + /// the alternate setting. pub fn set_alt_setting(&self, alt_setting: u8) -> impl MaybeFuture> { self.backend.clone().set_alt_setting(alt_setting) } @@ -410,14 +421,13 @@ impl Interface { /// # Ok(()) } /// ``` /// - /// ### Platform-specific notes - /// * On Windows, if the `recipient` is `Interface`, the WinUSB driver sends - /// the interface number in the least significant byte of `index`, - /// overriding any value passed. A warning is logged if the passed `index` - /// least significant byte differs from the interface number, and this may - /// become an error in the future. - /// * On Windows, the timeout is currently fixed to 5 seconds and the timeout - /// argument is ignored. + /// ### Platform-specific details + /// * On Windows, if the `recipient` is `Interface`, the least significant + /// byte of `index` must match the interface number, or + /// `TransferError::InvalidArgument` will be returned. This is a WinUSB + /// limitation. + /// * On Windows, the timeout is currently fixed to 5 seconds and the + /// timeout argument is ignored. pub fn control_in( &self, data: ControlIn, @@ -452,14 +462,13 @@ impl Interface { /// # Ok(()) } /// ``` /// - /// ### Platform-specific notes - /// * On Windows, if the `recipient` is `Interface`, the WinUSB driver sends - /// the interface number in the least significant byte of `index`, - /// overriding any value passed. A warning is logged if the passed `index` - /// least significant byte differs from the interface number, and this may - /// become an error in the future. - /// * On Windows, the timeout is currently fixed to 5 seconds and the timeout - /// argument is ignored. + /// ### Platform-specific details + /// * On Windows, if the `recipient` is `Interface`, the least significant + /// byte of `index` must match the interface number, or + /// `TransferError::InvalidArgument` will be returned. This is a WinUSB + /// limitation. + /// * On Windows, the timeout is currently fixed to 5 seconds and the + /// timeout argument is ignored. pub fn control_out( &self, data: ControlOut, @@ -498,6 +507,11 @@ impl Interface { } /// Open an endpoint. + /// + /// This claims exclusive access to the endpoint and returns an [`Endpoint`] + /// that can be used to submit transfers. The type-level `EndpointType` and + /// `EndpointDirection` parameters must match the endpoint type and + /// direction. pub fn endpoint( &self, address: u8, @@ -629,6 +643,10 @@ impl Endpoint { /// with the system allocator, it cannot be converted to a [`Vec`] without /// copying. /// + /// This is a somewhat expensive operation, requiring a `mmap` system call, + /// so is likely only beneficial for buffers that will be used repeatedly. + /// Consider using [`Buffer::new`] for one-off transfers. + /// /// This is currently only supported on Linux, falling back to [`Buffer::new`] /// on other platforms, or if the memory allocation fails. pub fn allocate(&self, len: usize) -> Buffer { diff --git a/src/enumeration.rs b/src/enumeration.rs index ab5cda8..fcf1790 100644 --- a/src/enumeration.rs +++ b/src/enumeration.rs @@ -12,12 +12,12 @@ pub struct DeviceId(pub(crate) crate::platform::DeviceId); /// Information about a device that can be obtained without opening it. /// -/// Found in the results of [`crate::list_devices`]. +/// `DeviceInfo` is returned by [`list_devices`][crate::list_devices]. /// /// ### Platform-specific notes /// /// * Some fields are platform-specific -/// * Linux: `sysfs_path` +/// * Linux: `sysfs_path`, `busnum` /// * Windows: `instance_id`, `parent_instance_id`, `port_number`, `driver` /// * macOS: `registry_id`, `location_id` #[derive(Clone)] @@ -149,7 +149,7 @@ impl DeviceInfo { /// Path of port numbers identifying the port where the device is connected. /// /// Together with the bus ID, it identifies a physical port. The path is - /// expected to remain stable across device insertions or reboots. + /// expected to remain stable across device insertions or reboots. /// /// Since USB SuperSpeed is a separate topology from USB 2.0 speeds, a /// physical port may be identified differently depending on speed. @@ -213,10 +213,9 @@ impl DeviceInfo { self.usb_version } - /// Code identifying the standard device class, from the `bDeviceClass` device descriptor field. - /// - /// `0x00`: specified at the interface level.\ - /// `0xFF`: vendor-defined. + /// Code identifying the [standard device + /// class](https://www.usb.org/defined-class-codes), from the `bDeviceClass` + /// device descriptor field. #[doc(alias = "bDeviceClass")] pub fn class(&self) -> u8 { self.class @@ -473,7 +472,7 @@ impl UsbControllerType { /// Information about a system USB bus. /// /// Platform-specific fields: -/// * Linux: `path`, `parent_path`, `busnum`, `root_hub` +/// * Linux: `path`, `busnum`, `root_hub` /// * Windows: `instance_id`, `parent_instance_id`, `location_paths`, `devinst`, `root_hub_description` /// * macOS: `registry_id`, `location_id`, `name`, `provider_class_name`, `class_name` #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] @@ -616,12 +615,6 @@ impl BusInfo { /// Detected USB controller type /// /// None means the controller type could not be determined. - /// - /// ### Platform-specific notes - /// - /// * Linux: Parsed from driver in use. - /// * macOS: The IOService entry matched. - /// * Windows: Parsed from the numbers following ROOT_HUB in the instance_id. pub fn controller_type(&self) -> Option { self.controller_type } diff --git a/src/error.rs b/src/error.rs index 062586b..c299c03 100644 --- a/src/error.rs +++ b/src/error.rs @@ -97,7 +97,7 @@ pub enum ErrorKind { Other, } -/// Error from [`crate::Device::active_configuration`] +/// Error from [`Device::active_configuration`][crate::Device::active_configuration]. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct ActiveConfigurationError { pub(crate) configuration_value: u8, diff --git a/src/io/read.rs b/src/io/read.rs index d9d01d0..a702b80 100644 --- a/src/io/read.rs +++ b/src/io/read.rs @@ -16,8 +16,28 @@ use crate::{ }; /// Wrapper for a Bulk or Interrupt IN [`Endpoint`](crate::Endpoint) that -/// implements [`Read`](std::io::Read) and [`BufRead`](std::io::BufRead) and -/// their `tokio` and `smol` async equivalents. +/// manages transfers to provide a higher-level buffered API. +/// +/// Most of the functionality of this type is provided through standard IO +/// traits; you'll want to use one of the following: +/// +/// * [`std::io::Read`](std::io::Read) and [`BufRead`](std::io::BufRead) for +/// blocking IO. +/// * With the `tokio` cargo feature, +/// [`tokio::io::AsyncRead`](tokio::io::AsyncRead) and +/// [`AsyncBufRead`](tokio::io::AsyncBufRead) for async IO. Tokio also +/// provides `AsyncReadExt` and `AsyncBufReadExt` with additional methods. +/// * With the `smol` cargo feature, +/// [`futures_io::AsyncRead`](futures_io::AsyncRead) and +/// [`AsyncBufRead`](futures_io::AsyncBufRead) for async IO. +/// `futures_lite` provides `AsyncReadExt` and `AsyncBufReadExt` with +/// additional methods. +/// +/// By default, this type ignores USB packet lengths and boundaries. For protocols +/// that use short or zero-length packets as delimiters, you can use the +/// [`until_short_packet()`](Self::until_short_packet) method to get an +/// [`EndpointReadUntilShortPacket`](EndpointReadUntilShortPacket) adapter +/// that observes these delimiters. pub struct EndpointRead { endpoint: Endpoint, reading: Option, @@ -381,6 +401,9 @@ impl futures_io::AsyncBufRead for EndpointRead /// have any state other than that of the underlying [`EndpointRead`], so /// dropping and re-creating with another call to /// [`EndpointRead::until_short_packet()`] has no effect. +/// +/// This implements the same traits as `EndpointRead` but observes packet +/// boundaries instead of ignoring them. pub struct EndpointReadUntilShortPacket<'a, EpType: BulkOrInterrupt> { reader: &'a mut EndpointRead, } @@ -399,10 +422,11 @@ impl std::fmt::Display for ExpectedShortPacket { impl Error for ExpectedShortPacket {} impl EndpointReadUntilShortPacket<'_, EpType> { - /// Check if the underlying endpoint has reached the end of a short packet. + /// Check if the endpoint has reached the end of a short packet. /// - /// Upon reading the end of a short packet, the next `read()` or `fill_buf()` - /// will return 0 bytes (EOF). To read the next message, call `consume_end()`. + /// Upon reading the end of a short packet, the next `read()` or + /// `fill_buf()` will return 0 bytes (EOF) and this method will return + /// `true`. To begin reading the next message, call `consume_end()`. pub fn is_end(&self) -> bool { self.reader .reading diff --git a/src/io/write.rs b/src/io/write.rs index 0925ae1..457310b 100644 --- a/src/io/write.rs +++ b/src/io/write.rs @@ -13,7 +13,22 @@ use std::{ use std::{pin::Pin, task::ready}; /// Wrapper for a Bulk or Interrupt OUT [`Endpoint`](crate::Endpoint) that -/// implements [`Write`](std::io::Write) and its `tokio` and `smol` async equivalents. +/// manages transfers to provide a higher-level buffered API. +/// +/// Most of the functionality of this type is provided through standard IO +/// traits; you'll want to use one of the following: +/// +/// * [`std::io::Write`](std::io::Write) for blocking IO. +/// * With the `tokio` cargo feature, +/// [`tokio::io::AsyncWrite`](tokio::io::AsyncWrite). Tokio also provides +/// `AsyncWriteExt` with additional methods. +/// * With the `smol` cargo feature, +/// [`futures_io::AsyncWrite`](futures_io::AsyncWrite) for async IO. +/// `futures_lite` provides `AsyncWriteExt` with additional methods. +/// +/// Written data is buffered and may not be sent until the buffer is full or +/// [`submit`](Self::submit) / [`submit_end`](Self::submit_end) or +/// [`flush`](Self::flush) / [`flush_end`](Self::flush_end) are called. pub struct EndpointWrite { endpoint: Endpoint, writing: Option, diff --git a/src/lib.rs b/src/lib.rs index af3a67f..9905508 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,13 @@ #![warn(missing_docs)] //! A new library for cross-platform low-level access to USB devices. //! +//! `nusb` supports Windows, macOS, and Linux, and provides both async and +//! blocking APIs for listing and watching USB devices, reading descriptor +//! details, opening and managing devices and interfaces, and performing +//! transfers on control, bulk, and interrupt endpoints. +//! //! `nusb` is comparable to the C library [libusb] and its Rust bindings [rusb], -//! but written in pure Rust. It supports usage from both async and -//! blocking contexts, and transfers are natively async. +//! but written in pure Rust. //! //! [libusb]: https://libusb.info //! [rusb]: https://docs.rs/rusb/ @@ -16,6 +20,41 @@ //! replace the kernel driver and program the device from user-space using this //! library, but you'd have to re-implement the class functionality yourself.) //! +//! ## Example usage +//! +//! ```no_run +//! use nusb::{list_devices, MaybeFuture}; +//! use nusb::transfer::{Bulk, In, Out, ControlOut, ControlType, Recipient}; +//! use std::io::{Read, Write, Error, ErrorKind}; +//! use std::time::Duration; +//! +//! # fn main() -> Result<(), std::io::Error> { +//! let device = list_devices().wait()? +//! .find(|dev| dev.vendor_id() == 0xAAAA && dev.product_id() == 0xBBBB) +//! .ok_or(Error::new(ErrorKind::NotFound, "device not found"))?; +//! +//! let device = device.open().wait()?; +//! let interface = device.claim_interface(0).wait()?; +//! +//! interface.control_out(ControlOut { +//! control_type: ControlType::Vendor, +//! recipient: Recipient::Device, +//! request: 0x10, +//! value: 0x0, +//! index: 0x0, +//! data: &[0x01, 0x02, 0x03, 0x04], +//! }, Duration::from_millis(100)).wait()?; +//! +//! let mut writer = interface.endpoint::(0x01)?.writer(4096); +//! writer.write_all(&[0x00, 0xff])?; +//! writer.flush()?; +//! +//! let mut reader = interface.endpoint::(0x81)?.reader(4096); +//! let mut buf = [0; 64]; +//! reader.read_exact(&mut buf)?; +//! # Ok(()) } +//! ``` +//! //! ## USB and usage overview //! //! When a USB device connects, the OS queries the device descriptor containing @@ -29,10 +68,10 @@ //! Additional information about the device can be queried with //! [`device.active_configuration()`](`Device::active_configuration`). //! -//! USB devices consist of one or more interfaces exposing a group of -//! functionality. A device with multiple interfaces is known as a composite -//! device. To open an interface, call [`Device::claim_interface`]. Only one -//! program (or kernel driver) may claim an interface at a time. +//! USB devices consist of one or more interfaces. A device with multiple +//! interfaces is known as a composite device. To open an interface, call +//! [`Device::claim_interface`]. Only one program (or kernel driver) may claim +//! an interface at a time. //! //! Use the resulting [`Interface`] to perform control transfers or open //! an [`Endpoint`] to perform bulk or interrupt transfers. Submitting a @@ -40,6 +79,11 @@ //! internal queue for the endpoint. Completed transfers can be popped //! from the queue synchronously or asynchronously. //! +//! The [`EndpointRead`][io::EndpointRead] and +//! [`EndpointWrite`][io::EndpointWrite] types wrap the endpoint and +//! manage transfers and buffers to implement the standard `Read` and +//! `Write` traits and their async equivalents. +//! //! *For more details on how USB works, [USB in a //! Nutshell](https://beyondlogic.org/usbnutshell/usb1.shtml) is a good //! overview.* @@ -188,10 +232,11 @@ pub fn list_devices() -> impl MaybeFuture>(); -/// let buses: HashMap)> = nusb::list_buses().wait().unwrap() +/// let buses: HashMap<_, _> = nusb::list_buses().wait().unwrap() /// .map(|bus| { /// let bus_id = bus.bus_id().to_owned(); -/// (bus.bus_id().to_owned(), (bus, devices.clone().into_iter().filter(|dev| dev.bus_id() == bus_id).collect())) +/// let devs: Vec<_> = devices.iter().filter(|dev| dev.bus_id() == bus_id).cloned().collect(); +/// (bus_id, (bus, devs)) /// }) /// .collect(); /// ``` diff --git a/src/maybe_future.rs b/src/maybe_future.rs index 792452a..5c58253 100644 --- a/src/maybe_future.rs +++ b/src/maybe_future.rs @@ -133,7 +133,7 @@ pub mod blocking { } #[cfg(not(any(feature = "smol", feature = "tokio")))] - fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { unreachable!() } }