nusb

repository·main·Indexed 19 days ago

https://github.com/kevinmehall/nusb

A pure-Rust, cross-platform library for low-level USB device access supporting Windows, macOS, Linux, and WebUSB. It provides async and blocking APIs for listing and watching devices, reading descriptors, managing interfaces, and performing transfers on control, bulk, and interrupt endpoints without depending on libusb or other C libraries.

Tokens
10.6K
Snippets
33
Records
46
Agent score
66%

What's inside nusb

  1. Overview of nusb

    main

    nusb is a pure-Rust library designed for cross-platform, low-level access to USB devices. It supports Windows, macOS, Linux, and WebUSB. The library provides both async and blocking APIs for several core USB operations:

    • Listing and watching USB devices
    • Reading descriptor details
    • Opening and managing devices and interfaces
    • Performing transfers on control, bulk, and interrupt endpoints
  2. Key differences between nusb, rusb, and libusb

    main

    When choosing between nusb and existing libraries like rusb or libusb, consider these architectural differences:

    • Pure Rust: nusb has no dependency on libusb or any other C library.
    • Async-first: It is designed with an async-first approach, though it does not strictly require an async runtime to function.
    • No Context Object: Unlike many USB libraries, you do not need to manage a context object. You can open a device directly. A global event loop thread is automatically started when the first device is opened.
    • Low Overhead: It acts as a thinner layer over OS APIs with less internal state management.
  3. Use DeviceSelector to filter USB devices

    main

    The DeviceSelector struct is used as a filter when calling request_device. You can build a selector using a builder-like pattern to match specific hardware criteria.

    Available Filters

    • all(): Matches any device.
    • with_vid(u16): Filters by Vendor ID.
    • with_vid_pid(u16, u16): Filters by both Vendor and Product ID.
    • with_class(u8): Filters by USB device class.
    • with_class_subclass(u8, u8): Filters by class and subclass.
    • with_class_subclass_protocol(u8, u8, u8): Filters by class, subclass, and protocol.
    • with_serial_number(String): Filters by the device's serial number string.
    use nusb::{DeviceSelector, MaybeFuture};
    
    let devices = nusb::request_device(&[
        DeviceSelector::all().with_vid_pid(0x1234, 0x5678),
        DeviceSelector::all().with_vid_pid(0x1111, 0x2222),
    ]).wait().unwrap();
  4. Use EndpointWrite for buffered USB writes

    main

    The EndpointWrite struct wraps a Bulk or Interrupt OUT Endpoint to provide a high-level buffered API. It manages data transfers to the OS, allowing you to write data to a buffer that is only submitted to the USB controller when the buffer is full or when explicitly triggered.

    Depending on your enabled cargo features, EndpointWrite implements several standard IO traits:

    • Blocking IO: Implements std::io::Write.
    • Tokio Async IO: Implements tokio::io::AsyncWrite (requires tokio feature).
    • Smol Async IO: Implements futures_io::AsyncWrite (requires smol feature).

    Important Behavior:

    • Data is buffered and may not be sent until the buffer is full or submit(), submit_end(), flush(), or flush_end() are called.
    • transfer_size determines the size of the buffer passed to the OS for each transfer. It is rounded up to the next multiple of the endpoint's max packet size.
    • Backpressure is applied based on num_transfers. If the number of pending transfers reaches this limit, write calls will block (or async methods will return Pending) until a transfer completes.
    // Example initialization
    let endpoint_write = EndpointWrite::new(endpoint, 4096)
        .with_num_transfers(4)
        .with_write_timeout(Duration::from_secs(5));
  5. Use DeviceInfo to inspect USB devices

    main

    The DeviceInfo struct provides metadata about a USB device that can be retrieved without opening it. This is typically returned by list_devices. It contains standard USB descriptor information (vendor ID, product ID, class, etc.) and platform-specific identifiers.

    Platform-Specific Fields

    • Linux: sysfs_path, busnum
    • Windows: instance_id, parent_instance_id, port_number, driver
    • macOS: registry_id, location_id

    Common Methods

    • id(): Returns an opaque DeviceId used to uniquely identify the device.
    • vendor_id() / product_id(): Returns the 16-bit IDs from the device descriptor.
    • interfaces(): Returns an iterator over InterfaceInfo objects representing the device's active interfaces.
    • open(): Returns a MaybeFuture that, when awaited, attempts to open the device as a Device.
    // Example of opening a device from its info
    let device = device_info.open().await?;
  6. Use the Buffer struct for USB transfers

    main

    The Buffer struct is used for bulk and interrupt transfers. It can be backed by the system allocator (default) or a platform-specific allocator for zero-copy transfers (e.g., Mmap on Linux/Android).

    Usage Patterns

    • OUT Transfers (Device Write): Fill the buffer with data before submitting. The len() method represents the number of initialized bytes that will be sent. Use extend_from_slice or extend_fill to populate it.
    • IN Transfers (Device Read): Set the amount of data you want to receive using set_requested_len(len). The len and current contents are ignored during submission. After the transfer completes, len() will be updated to the actual number of bytes received from the device.

    Key Methods

    • new(requested_len: usize): Allocates a new buffer using the default allocator. The requested_len is used as the initial requested_len and the capacity will be at least that large.
    • set_requested_len(&mut self, len: usize): Sets the number of bytes to request for an IN transfer. Panics if len exceeds capacity.
    • clear(&mut self): Resets len to 0 for buffer reuse without changing capacity or requested_len.
    use nusb::transfer::Buffer;
    
    // For an OUT transfer
    let mut buf = Buffer::new(64);
    buf.extend_from_slice(b"hello");
    // Submit buf for OUT transfer...
    
    // For an IN transfer
    let mut buf = Buffer::new(64);
    buf.set_requested_len(64);
    // Submit buf for IN transfer...
    // After completion, buf.len() returns actual bytes received.
  7. Understand endpoint directions and types

    main

    The library uses type-level markers to define the direction and transfer type of an endpoint. This ensures type safety when calling transfer methods on an Endpoint.

    Directions

    • In: Device-to-host direction.
    • Out: Host-to-device direction.

    Transfer Types

    • Bulk: Bulk transfer type.
    • Interrupt: Interrupt transfer type.
    • BulkOrInterrupt: A trait implemented by both Bulk and Interrupt types, useful for generic code handling these two types.
  8. How `MaybeFuture` works in `nusb`

    main

    Many nusb methods return a MaybeFuture. This type allows the library to provide both asynchronous and blocking APIs:

    1. Asynchronous: Use .await (via IntoFuture) in an async context.
      • Note: For operations that require blocking system calls (like list_devices, open, claim_interface, etc.), you must enable the tokio or smol cargo features to run these on an IO thread. If neither is enabled, .await will panic.
    2. Blocking: Use .wait() to block the current thread until the operation completes. This is suitable for non-async contexts and does not require an async runtime.

    Transfer operations (Bulk, Interrupt, etc.) are implemented on top of natively-async OS APIs and do not require the tokio/smol features for async usage.

  9. Use EndpointRead for buffered USB reads

    main

    The EndpointRead struct wraps a Bulk or Interrupt IN Endpoint to provide a high-level buffered reading API. It manages multiple concurrent transfers to maximize throughput.

    Depending on your enabled cargo features, you can use it with standard synchronous IO traits or asynchronous IO traits:

    • Blocking IO: Implements std::io::Read and std::io::BufRead.
    • Async IO (Tokio): If the tokio feature is enabled, it implements tokio::io::AsyncRead and tokio::io::AsyncBufRead.
    • Async IO (Smol): If the smol feature is enabled, it implements futures_io::AsyncRead and futures_io::AsyncBufRead.

    By default, EndpointRead ignores USB packet boundaries. To observe short or zero-length packets as delimiters, use the until_short_packet() method to get an EndpointReadUntilShortPacket adapter.

    // Example initialization (assuming endpoint is already obtained)
    let mut reader = EndpointRead::new(endpoint, 4096);
    
    // For blocking IO
    let mut buffer = [0u8; 1024];
    let n = reader.read(&mut buffer)?; 
  10. Inspect BusInfo for system USB buses

    main

    The BusInfo struct provides information about a system USB bus, including its ID and the detected controller type.

    Common Methods

    • bus_id(): Returns the system identifier for the bus.
    • controller_type(): Returns the detected UsbControllerType (if available).
    • system_name(): Returns a human-readable name for the bus (e.g., the root hub product string on Linux).
    • driver(): Returns the driver associated with the bus.

    Platform-Specific Fields

    • Linux: sysfs_path, busnum, root_hub (as DeviceInfo)
    • Windows: instance_id, location_paths, devinst, root_hub_description
    • macOS: registry_id, location_id, provider_class_name, class_name
  11. How to achieve optimized streaming with Endpoints

    main

    To maximize throughput and minimize latency (e.g., for high-speed data streams), you should ensure the host controller always has a transfer request pending. This is achieved by submitting multiple requests and re-submitting them as they complete.

    Streaming Pattern:

    1. Maintain a pool of pending transfers (e.g., check ep.pending() < threshold).
    2. Allocate and submit new buffers to fill the pool.
    3. In a loop, wait_next_complete for the next finished transfer.
    4. Process the data and immediately submit the same buffer back to the endpoint to keep the queue full.
    // Optimized Streaming Example
    let mut ep_in = interface.endpoint::<Bulk, In>(0x82).unwrap();
    
    // Keep 8 transfers pending to saturate the bus
    while ep_in.pending() < 8 {
        let buffer = ep_in.allocate(16384);
        ep_in.submit(buffer);
    }
    
    loop {
        // Wait for any transfer to complete
        let completion = ep_in.wait_next_complete(Duration::MAX).unwrap();
    
        handle_transfer(&completion.buffer[..]);
        completion.status?; // Check for errors
    
        // Re-submit the buffer to maintain the pipeline
        ep_in.submit(completion.buffer);
    }
  12. Define USB control transfer types and directions

    main

    When performing USB control transfers, you must specify the direction of data flow, the type of request, and the intended recipient.

    • Direction: Determines if data flows from Host to Device (Direction::Out) or Device to Host (Direction::In).
    • ControlType: Defines the request specification: Standard (USB standard), Class (USB class specification), or Vendor (non-standard).
    • Recipient: Specifies the target of the request: Device, Interface, Endpoint, or Other.
    use nusb::transfer::control::{Direction, ControlType, Recipient};
    
    let direction = Direction::Out;
    let control_type = ControlType::Vendor;
    let recipient = Recipient::Device;