feat: add UAC1 loopback test device and fix endpoint attribute dispatch
Add a simulated USB Audio Class 1 loopback device for testing isochronous transfers. Audio sent to the playback OUT endpoint (48kHz/16-bit/stereo) is looped back to the capture IN endpoint. - Add UsbEndpoint::transfer_type() masking bmAttributes to bits 0-1, fixing dispatch for isochronous endpoints with sync-type sub-bits - Update all endpoint attribute dispatch sites across the library - Add UacLoopbackBuffer, UacControlHandler, UacStreamOutHandler, UacStreamInHandler in lib/src/uac.rs - Add build_uac_loopback_device() builder function - Add `test_uac connect` CLI subcommand - Add 10 unit tests covering buffer, descriptors, and handler behavior - Add design spec and implementation plan docs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e4cdc4beec
commit
18a413870a
14 changed files with 1970 additions and 50 deletions
17
NOTES.md
17
NOTES.md
|
|
@ -1,17 +0,0 @@
|
|||
# usbip
|
||||
|
||||
## linux modules
|
||||
|
||||
- vhci-hcd: client side, virtual usb host controller
|
||||
- usbip-host: server side, bound to usb device to make it exportable
|
||||
- usbip-vudc: server side, export a usb gadget
|
||||
|
||||
### usbip-host
|
||||
|
||||
Files:
|
||||
|
||||
- drivers/usb/usbip/stub.h
|
||||
- drivers/usb/usbip/stub_main.c
|
||||
- drivers/usb/usbip/stub_dev.c
|
||||
- drivers/usb/usbip/stub_rx.c
|
||||
- drivers/usb/usbip/stub_tx.c
|
||||
38
README.md
38
README.md
|
|
@ -1,15 +1,18 @@
|
|||
# usbip-rs
|
||||
|
||||
A USB/IP tool and library for Linux. Share physical USB devices over vsock or TCP, or export simulated devices for testing.
|
||||
Implements a userspace server-side server for the usbip protocol, and a tool for the client-side to set up the socket and hand it off to the kernel. In usbip terminology, the "server"/"host" is the side that has the physical device being shared, and the "client" is the side the device is being shared to.
|
||||
|
||||
## Overview
|
||||
This project is based on [https://github.com/jiegec/usbip](jiegec/usbip), with significant changes and additions.
|
||||
|
||||
usbip-rs provides:
|
||||
## Why?
|
||||
|
||||
- **CLI tool** (`usbip-rs`) — pass through real USB devices from a host to a client over vsock or TCP, using Linux's vhci_hcd kernel module
|
||||
- **Rust library** (`usbip-rs` crate) — implement custom USB/IP device handlers, with built-in HID keyboard and CDC ACM serial examples
|
||||
Implementing the server side in userspace instead of the kernel's `usbip_host` driver reduces the attack surface considerably. The kernel is not exposed directly to potentially untrusted network input. The userspace server talks to the USB device via /dev/bus/usb/..., and can validate the client's traffic before passing it on to the real USB device. It also makes it possible to implement strong sandboxing via seccomp and namespaces for the server side. The client side is of course still all in the kernel, since making the USB devices visible to the client kernel's drivers is the whole point. But for my use case, the server side is trusted and client side is untrusted, so securing the host side is critical.
|
||||
|
||||
Supported transfer types: control, bulk and interrupt. Isochronous support is being worked on.
|
||||
There are a few other userspace usbip server implementations around, but as far as I could tell, none of them support isochronous mode. Isochronous is needed for e.g. USB audio devices and webcams. This project does, though my testing so far is limited to USB audio devices which work well.
|
||||
|
||||
This implementation also allows for reversing the connection flow. In standard usbip, the server side listens for connections, and the client side connects to it to access the device. usbip-rs supports having the client listen and the server initiating the connection, which maps much better to my needs.
|
||||
|
||||
usbip-rs also supports using vsock instead of TCP as the transport, making the "ip" in "usbip" not really true. This is useful when sharing a device to a virtual machine on the same hardware.
|
||||
|
||||
## Building
|
||||
|
||||
|
|
@ -31,7 +34,7 @@ The CLI binary is at `target/release/usbip-rs`.
|
|||
|
||||
## Usage
|
||||
|
||||
The CLI has three top-level commands: `client`, `host`, and `test_hid`. Transport addresses use the format `vsock:<port>`, `vsock:<cid>:<port>`, `tcp:<port>`, or `tcp:<host>:<port>`.
|
||||
The CLI has four top-level commands: `client`, `host`, `test_hid`, and `test_uac`. Transport addresses use the format `vsock:<port>`, `vsock:<cid>:<port>`, `tcp:<port>`, `tcp:<host>:<port>`, or `fc-vsock:<path>:<port>`.
|
||||
|
||||
### Pass through a real USB device
|
||||
|
||||
|
|
@ -59,24 +62,17 @@ usbip-rs client listen vsock:5000
|
|||
usbip-rs test_hid connect vsock:5000
|
||||
```
|
||||
|
||||
### Library examples
|
||||
|
||||
The `lib/examples/` directory contains standalone USB/IP servers using the library directly:
|
||||
|
||||
- **hid_keyboard** — simulated HID keyboard
|
||||
- **cdc_acm_serial** — simulated CDC ACM serial device
|
||||
- **host** — physical device passthrough (legacy, single-device)
|
||||
### Test with a simulated UAC1 loopback audio device
|
||||
|
||||
```bash
|
||||
cargo run -p usbip-rs --example hid_keyboard
|
||||
# Client side
|
||||
usbip-rs client listen tcp:3240
|
||||
|
||||
# Host side (simulated USB audio device, no real device needed)
|
||||
usbip-rs test_uac connect tcp:3240
|
||||
```
|
||||
|
||||
These listen on the standard USB/IP port and work with the Linux `usbip` userspace tools:
|
||||
|
||||
```bash
|
||||
usbip list -r <host-ip>
|
||||
usbip attach -r <host-ip> -b <bus-id>
|
||||
```
|
||||
The UAC1 loopback device advertises 48kHz 16-bit stereo playback and capture. Audio sent to the playback endpoint is looped back to the capture endpoint.
|
||||
|
||||
## Project structure
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use clap::{Parser, Subcommand};
|
|||
mod client;
|
||||
mod host;
|
||||
mod test_hid;
|
||||
mod test_uac;
|
||||
mod transport;
|
||||
mod vhci;
|
||||
|
||||
|
|
@ -31,6 +32,12 @@ enum Commands {
|
|||
#[command(subcommand)]
|
||||
action: TestHidAction,
|
||||
},
|
||||
/// Export a simulated UAC1 loopback device for testing
|
||||
#[command(name = "test_uac")]
|
||||
TestUac {
|
||||
#[command(subcommand)]
|
||||
action: TestUacAction,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
|
|
@ -62,6 +69,15 @@ enum TestHidAction {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum TestUacAction {
|
||||
/// Connect to a listening client with a simulated UAC1 loopback device
|
||||
Connect {
|
||||
/// Transport address: vsock:[<cid>:]<port>, tcp:[<host>:]<port>, or fc-vsock:<path>:<port>
|
||||
address: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::Builder::from_default_env()
|
||||
|
|
@ -89,6 +105,12 @@ async fn main() {
|
|||
test_hid::run(addr).await
|
||||
}
|
||||
},
|
||||
Commands::TestUac { action } => match action {
|
||||
TestUacAction::Connect { address } => {
|
||||
let addr = transport::parse_address(&address).expect("Invalid transport address");
|
||||
test_uac::run(addr).await
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
|
|
|
|||
46
cli/src/test_uac.rs
Normal file
46
cli/src/test_uac.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
use log::info;
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
use usbip_rs::{UsbDevice, usbip_protocol::UsbIpResponse};
|
||||
|
||||
use crate::transport;
|
||||
|
||||
/// Send handshake and run URB loop.
|
||||
async fn do_test_uac_session<S: AsyncReadExt + AsyncWriteExt + Unpin + Send + 'static>(
|
||||
mut stream: S,
|
||||
device: UsbDevice,
|
||||
) -> std::io::Result<()> {
|
||||
let handshake = UsbIpResponse::op_rep_import_success(&device).to_bytes()?;
|
||||
stream
|
||||
.write_all(&handshake)
|
||||
.await
|
||||
.map_err(|e| std::io::Error::other(format!("Failed to send handshake: {e}")))?;
|
||||
info!("Handshake sent, entering URB loop");
|
||||
|
||||
usbip_rs::handle_urb_loop(stream, Arc::new(device)).await
|
||||
}
|
||||
|
||||
pub async fn run(addr: transport::TransportAddr) -> std::io::Result<()> {
|
||||
let device = usbip_rs::uac::build_uac_loopback_device()?;
|
||||
|
||||
info!(
|
||||
"Created simulated UAC1 loopback device {:04x}:{:04x}",
|
||||
device.vendor_id, device.product_id
|
||||
);
|
||||
|
||||
match addr {
|
||||
transport::TransportAddr::Vsock(v) => {
|
||||
let stream = transport::connect_vsock(v.cid, v.port).await?;
|
||||
do_test_uac_session(stream, device).await
|
||||
}
|
||||
transport::TransportAddr::Tcp(ref t) => {
|
||||
let stream = transport::connect_tcp(t).await?;
|
||||
do_test_uac_session(stream, device).await
|
||||
}
|
||||
transport::TransportAddr::FcVsock(ref fc) => {
|
||||
let stream = transport::connect_fc_vsock(fc).await?;
|
||||
do_test_uac_session(stream, device).await
|
||||
}
|
||||
}
|
||||
}
|
||||
1063
docs/superpowers/plans/2026-03-25-uac-loopback-device.md
Normal file
1063
docs/superpowers/plans/2026-03-25-uac-loopback-device.md
Normal file
File diff suppressed because it is too large
Load diff
195
docs/superpowers/specs/2026-03-25-uac-loopback-device-design.md
Normal file
195
docs/superpowers/specs/2026-03-25-uac-loopback-device-design.md
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
# UAC1 Loopback Test Device Design
|
||||
|
||||
## Overview
|
||||
|
||||
Add a simulated USB Audio Class 1 (UAC1) loopback device to verify isochronous transfer functionality. The device has one playback output (ISO OUT) and one capture input (ISO IN). Audio sent to the playback endpoint is looped back and returned from the capture endpoint.
|
||||
|
||||
**Audio format:** 48kHz, 16-bit, stereo (192 bytes/frame at 1ms full-speed intervals).
|
||||
|
||||
## Module Structure
|
||||
|
||||
**File: `lib/src/uac.rs`**
|
||||
|
||||
Three handler structs with a shared loopback buffer:
|
||||
|
||||
### UacLoopbackBuffer
|
||||
|
||||
Shared ring buffer connecting playback OUT to capture IN.
|
||||
|
||||
- Inner storage: `Mutex<VecDeque<u8>>`
|
||||
- Capacity: ~960 bytes (5 frames, absorbs jitter without unbounded growth)
|
||||
- `write(data: &[u8])` — appends; drops oldest samples if over capacity
|
||||
- `read(len: usize) -> Vec<u8>` — drains up to `len` bytes, pads with silence (zeros) on underrun
|
||||
|
||||
### UacControlHandler
|
||||
|
||||
Handles the AudioControl interface (interface 0).
|
||||
|
||||
- EP0 class-specific control requests: returns STALL (`status = -32` / `-EPIPE`) for all unknown/unsupported requests. No Feature Unit in the topology means no volume/mute requests to handle.
|
||||
- `get_class_specific_descriptor()` — returns the AC Header + Input/Output Terminal chain.
|
||||
|
||||
### UacStreamOutHandler (playback)
|
||||
|
||||
Handles AudioStreaming interface 1, alt setting 1 (ISO OUT).
|
||||
|
||||
- ISO OUT: extracts audio data from each ISO packet, writes to `UacLoopbackBuffer`. Returns success descriptors (`actual_length = length` per packet).
|
||||
- `get_class_specific_descriptor()` — returns AS General + Format Type I descriptor.
|
||||
- `set_alt_setting()` — clears the loopback buffer on alt switch to prevent stale data.
|
||||
|
||||
### UacStreamInHandler (capture)
|
||||
|
||||
Handles AudioStreaming interface 2, alt setting 1 (ISO IN).
|
||||
|
||||
- ISO IN: reads from `UacLoopbackBuffer` to fill each ISO packet. On underrun, fills with silence (zeros). Returns descriptors with `actual_length` set to packet size.
|
||||
- `get_class_specific_descriptor()` — returns AS General + Format Type I descriptor.
|
||||
|
||||
### Builder Function
|
||||
|
||||
```rust
|
||||
pub fn build_uac_loopback_device() -> std::io::Result<UsbDevice>
|
||||
```
|
||||
|
||||
Constructs the full `UsbDevice` with all three interfaces, alt settings, descriptors, and wired-up handlers. Keeps descriptor complexity out of the CLI.
|
||||
|
||||
## Library Fix: EndpointAttributes transfer type masking
|
||||
|
||||
The `EndpointAttributes` enum (values 0-3) represents the base transfer type, but per the USB spec, `bmAttributes` encodes sync type and usage type in bits 2-5 for isochronous endpoints (e.g., `0x0D` = isochronous adaptive, `0x05` = isochronous async). The library currently compares or parses `ep.attributes` directly against the enum, which fails when sub-bits are set.
|
||||
|
||||
**Fix:** Add a `transfer_type()` method to `UsbEndpoint` that masks to bits 0-1:
|
||||
|
||||
```rust
|
||||
impl UsbEndpoint {
|
||||
pub fn transfer_type(&self) -> Option<EndpointAttributes> {
|
||||
FromPrimitive::from_u8(self.attributes & 0x03)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Update all dispatch sites to use `ep.transfer_type()` instead of `FromPrimitive::from_u8(ep.attributes)` or `ep.attributes == EndpointAttributes::X as u8`:
|
||||
|
||||
- `device.rs:377` — main URB dispatch (`FromPrimitive::from_u8(ep.attributes)`)
|
||||
- `host.rs` — nusb and rusb handler comparisons (`ep.attributes == ...`)
|
||||
- `cdc.rs:59` — CDC handler comparison
|
||||
- `util.rs:123` — ISO loopback test handler comparison
|
||||
|
||||
This allows the UAC device to use spec-compliant attribute values while keeping existing devices (which use base values like `0x01`) working unchanged.
|
||||
|
||||
## USB Descriptor Topology
|
||||
|
||||
### Device Level
|
||||
|
||||
- `device_class: 0x00` (per-interface)
|
||||
- `vendor_id: 0x1234`, `product_id: 0x5678` (arbitrary test values)
|
||||
- `usb_version: 2.0`, `device_bcd: 1.0`
|
||||
- Speed: Full-speed (`UsbSpeed::Full as u32`, UAC1 isochronous at 1ms intervals)
|
||||
|
||||
### Interface 0 — AudioControl
|
||||
|
||||
- Alt 0 only (no alt settings needed)
|
||||
- `class=0x01` (Audio), `subclass=0x01` (AudioControl), `protocol=0x00`
|
||||
- No endpoints (AC interrupt endpoint is optional in UAC1)
|
||||
- Class-specific descriptor chain:
|
||||
- **AC Header** — links to streaming interfaces 1 and 2, `wTotalLength=52` (10+12+9+12+9)
|
||||
- **Input Terminal 1** (USB streaming, ID=1) — receives playback from host
|
||||
- **Output Terminal 2** (speaker, ID=2) — source=IT1
|
||||
- **Input Terminal 3** (microphone, ID=3) — capture source
|
||||
- **Output Terminal 4** (USB streaming, ID=4) — source=IT3
|
||||
|
||||
### Interface 1 — AudioStreaming OUT (playback)
|
||||
|
||||
- **Alt 0:** zero-bandwidth (no endpoints, required by UAC1 spec)
|
||||
- **Alt 1:** active
|
||||
- `class=0x01` (Audio), `subclass=0x02` (AudioStreaming), `protocol=0x00`
|
||||
- Class-specific: AS General (terminal link=IT1) + Format Type I (48kHz, 16-bit, stereo)
|
||||
- Endpoint: `0x01` (OUT), isochronous adaptive, `bmAttributes=0x0D`, `max_packet_size=192`, interval=1
|
||||
|
||||
### Interface 2 — AudioStreaming IN (capture)
|
||||
|
||||
- **Alt 0:** zero-bandwidth (no endpoints)
|
||||
- **Alt 1:** active
|
||||
- Same class/subclass/protocol as interface 1
|
||||
- Class-specific: AS General (terminal link=OT4) + Format Type I (48kHz, 16-bit, stereo)
|
||||
- Endpoint: `0x82` (IN), isochronous async, `bmAttributes=0x05`, `max_packet_size=192`, interval=1
|
||||
|
||||
## Handler Behavior Details
|
||||
|
||||
### Control Request Handling
|
||||
|
||||
The AudioControl interface has no Feature Unit (no volume/mute controls). All class-specific control requests receive a STALL response (`status = -32`). The Linux audio driver probes for optional features and handles STALLs gracefully.
|
||||
|
||||
### Loopback Data Flow
|
||||
|
||||
```
|
||||
Host playback OUT → UacStreamOutHandler → UacLoopbackBuffer → UacStreamInHandler → Host capture IN
|
||||
```
|
||||
|
||||
- OUT handler writes samples into the ring buffer
|
||||
- IN handler reads samples from the ring buffer
|
||||
- Buffer overflow: oldest samples dropped, newest preserved
|
||||
- Buffer underrun: silence (zeros) returned
|
||||
|
||||
### Alt Setting Transitions
|
||||
|
||||
UAC1 requires zero-bandwidth alt setting 0 for streaming interfaces. The host driver activates alt setting 1 to start streaming. On `set_alt_setting()`, the OUT handler clears the loopback buffer.
|
||||
|
||||
Both alt 0 and alt 1 of each streaming interface share the same handler `Arc`. This is required because `set_alt_setting()` is called on the current active handler before the switch — sharing the `Arc` ensures the notification reaches the right handler regardless of which alt setting is active.
|
||||
|
||||
## CLI Subcommand
|
||||
|
||||
**File: `cli/src/test_uac.rs`**
|
||||
|
||||
```
|
||||
usbip-rs test_uac connect <address>
|
||||
```
|
||||
|
||||
Follows the `test_hid` pattern:
|
||||
|
||||
1. Calls `usbip_rs::uac::build_uac_loopback_device()`
|
||||
2. Connects to client via transport (vsock/tcp/fc-vsock)
|
||||
3. Sends OP_REP_IMPORT handshake
|
||||
4. Runs `handle_urb_loop`
|
||||
|
||||
No background task needed — the loopback is entirely driven by the host's URBs.
|
||||
|
||||
**File: `cli/src/main.rs`** — add `TestUac` variant to the clap subcommand enum.
|
||||
|
||||
## Public API Surface
|
||||
|
||||
Exported from `lib/src/lib.rs` via `pub mod uac`:
|
||||
|
||||
- `UacLoopbackBuffer` — for test injection/inspection
|
||||
- `UacControlHandler`
|
||||
- `UacStreamOutHandler`
|
||||
- `UacStreamInHandler`
|
||||
- `build_uac_loopback_device() -> std::io::Result<UsbDevice>`
|
||||
|
||||
All public for test suite reuse. Tests can construct individual handlers with a shared buffer and drive them directly without building the full device.
|
||||
|
||||
## Testing
|
||||
|
||||
Unit tests in `lib/src/uac.rs`:
|
||||
|
||||
- **`desc_verify`** — validates each handler's class-specific descriptor via `verify_descriptor()`
|
||||
- **`loopback_buffer_write_read`** — write data, read back, verify match
|
||||
- **`loopback_buffer_underrun`** — read from empty buffer, verify silence
|
||||
- **`loopback_buffer_overflow`** — write beyond capacity, verify oldest dropped, newest preserved
|
||||
- **`stream_out_handler_iso`** — ISO OUT URB request, verify success response
|
||||
- **`stream_in_handler_iso`** — write to buffer then ISO IN request, verify data returned
|
||||
- **`stream_in_handler_silence`** — ISO IN with empty buffer, verify zero-filled packets
|
||||
- **`control_handler_stall`** — unknown class request, verify STALL status (-32)
|
||||
|
||||
No integration tests in this scope — those will come when the broader test suite is built.
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `lib/src/endpoint.rs` | Add `transfer_type()` method to `UsbEndpoint` |
|
||||
| `lib/src/device.rs` | Use `ep.transfer_type()` in URB dispatch |
|
||||
| `lib/src/host.rs` | Use `ep.transfer_type()` in handler comparisons |
|
||||
| `lib/src/cdc.rs` | Use `ep.transfer_type()` in handler comparison |
|
||||
| `lib/src/util.rs` | Use `ep.transfer_type()` in ISO loopback handler |
|
||||
| `lib/src/uac.rs` | New — handlers, buffer, builder, tests |
|
||||
| `lib/src/lib.rs` | Add `pub mod uac` |
|
||||
| `cli/src/test_uac.rs` | New — CLI subcommand |
|
||||
| `cli/src/main.rs` | Add `TestUac` subcommand variant |
|
||||
|
|
@ -56,7 +56,7 @@ impl UsbInterfaceHandler for UsbCdcAcmHandler {
|
|||
) -> Result<UrbResponse> {
|
||||
let ep = request.ep;
|
||||
let req = &request.data;
|
||||
if ep.attributes == EndpointAttributes::Interrupt as u8 {
|
||||
if ep.transfer_type() == Some(EndpointAttributes::Interrupt) {
|
||||
// interrupt
|
||||
if let Direction::In = ep.direction() {
|
||||
// interrupt in
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ pub enum ClassCode {
|
|||
}
|
||||
|
||||
/// A list of defined USB endpoint attributes
|
||||
#[derive(Copy, Clone, Debug, FromPrimitive)]
|
||||
#[derive(Copy, Clone, Debug, FromPrimitive, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum EndpointAttributes {
|
||||
Control = 0,
|
||||
|
|
|
|||
|
|
@ -374,7 +374,7 @@ impl UsbDevice {
|
|||
let setup_packet = request.setup.clone();
|
||||
let out_data = request.data.clone();
|
||||
|
||||
match (FromPrimitive::from_u8(ep.attributes), ep.direction()) {
|
||||
match (ep.transfer_type(), ep.direction()) {
|
||||
(Some(Control), In) => {
|
||||
// control in
|
||||
debug!("Control IN setup={setup_packet:x?}");
|
||||
|
|
|
|||
|
|
@ -28,4 +28,10 @@ impl UsbEndpoint {
|
|||
pub fn is_ep0(&self) -> bool {
|
||||
self.address & 0x7F == 0
|
||||
}
|
||||
|
||||
/// Get the base transfer type from bmAttributes (bits 0-1).
|
||||
/// This masks off the isochronous sync/usage sub-bits (bits 2-5).
|
||||
pub fn transfer_type(&self) -> Option<EndpointAttributes> {
|
||||
FromPrimitive::from_u8(self.attributes & 0x03)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ impl UsbInterfaceHandler for RusbUsbHostInterfaceHandler {
|
|||
let mut buffer = vec![0u8; transfer_buffer_length as usize];
|
||||
let timeout = std::time::Duration::new(1, 0);
|
||||
let handle = self.handle.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if ep.attributes == EndpointAttributes::Control as u8 {
|
||||
if ep.transfer_type() == Some(EndpointAttributes::Control) {
|
||||
// control
|
||||
if let Direction::In = ep.direction() {
|
||||
// control in
|
||||
|
|
@ -75,7 +75,7 @@ impl UsbInterfaceHandler for RusbUsbHostInterfaceHandler {
|
|||
)
|
||||
})?;
|
||||
}
|
||||
} else if ep.attributes == EndpointAttributes::Interrupt as u8 {
|
||||
} else if ep.transfer_type() == Some(EndpointAttributes::Interrupt) {
|
||||
// interrupt
|
||||
if let Direction::In = ep.direction() {
|
||||
// interrupt in
|
||||
|
|
@ -94,7 +94,7 @@ impl UsbInterfaceHandler for RusbUsbHostInterfaceHandler {
|
|||
)
|
||||
})?;
|
||||
}
|
||||
} else if ep.attributes == EndpointAttributes::Bulk as u8 {
|
||||
} else if ep.transfer_type() == Some(EndpointAttributes::Bulk) {
|
||||
// bulk
|
||||
if let Direction::In = ep.direction() {
|
||||
// bulk in
|
||||
|
|
@ -110,7 +110,7 @@ impl UsbInterfaceHandler for RusbUsbHostInterfaceHandler {
|
|||
)
|
||||
})?;
|
||||
}
|
||||
} else if ep.attributes == EndpointAttributes::Isochronous as u8 {
|
||||
} else if ep.transfer_type() == Some(EndpointAttributes::Isochronous) {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Unsupported,
|
||||
"isochronous transfers not supported on rusb handler",
|
||||
|
|
@ -236,7 +236,7 @@ impl UsbInterfaceHandler for NusbUsbHostInterfaceHandler {
|
|||
_ => std::time::Duration::from_secs(1), // control, bulk
|
||||
};
|
||||
let handle = self.handle.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if ep.attributes == EndpointAttributes::Control as u8 {
|
||||
if ep.transfer_type() == Some(EndpointAttributes::Control) {
|
||||
// control
|
||||
if let Direction::In = ep.direction() {
|
||||
// control in
|
||||
|
|
@ -307,7 +307,7 @@ impl UsbInterfaceHandler for NusbUsbHostInterfaceHandler {
|
|||
.wait()
|
||||
.map_err(std::io::Error::from)?;
|
||||
}
|
||||
} else if ep.attributes == EndpointAttributes::Interrupt as u8 {
|
||||
} else if ep.transfer_type() == Some(EndpointAttributes::Interrupt) {
|
||||
// interrupt
|
||||
// Release Mutex before blocking so other URBs on this interface aren't starved.
|
||||
if let Direction::In = ep.direction() {
|
||||
|
|
@ -359,7 +359,7 @@ impl UsbInterfaceHandler for NusbUsbHostInterfaceHandler {
|
|||
let completion = endpoint.transfer_blocking(buffer, timeout);
|
||||
completion.status.map_err(std::io::Error::from)?;
|
||||
}
|
||||
} else if ep.attributes == EndpointAttributes::Bulk as u8 {
|
||||
} else if ep.transfer_type() == Some(EndpointAttributes::Bulk) {
|
||||
// bulk
|
||||
if let Direction::In = ep.direction() {
|
||||
// bulk in - round up to max_packet_size as required by USB spec
|
||||
|
|
@ -387,7 +387,7 @@ impl UsbInterfaceHandler for NusbUsbHostInterfaceHandler {
|
|||
let completion = endpoint.transfer_blocking(buffer, timeout);
|
||||
completion.status.map_err(std::io::Error::from)?;
|
||||
}
|
||||
} else if ep.attributes == EndpointAttributes::Isochronous as u8 {
|
||||
} else if ep.transfer_type() == Some(EndpointAttributes::Isochronous) {
|
||||
// Isochronous transfer
|
||||
if request.iso_packet_descriptors.is_empty() {
|
||||
warn!("ISO transfer on ep {:02x} but no packet descriptors", ep.address);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ mod consts;
|
|||
mod device;
|
||||
mod endpoint;
|
||||
pub mod hid;
|
||||
pub mod uac;
|
||||
mod host;
|
||||
mod interface;
|
||||
mod setup;
|
||||
|
|
|
|||
608
lib/src/uac.rs
Normal file
608
lib/src/uac.rs
Normal file
|
|
@ -0,0 +1,608 @@
|
|||
//! USB Audio Class 1 (UAC1) loopback device for testing isochronous transfers.
|
||||
use super::*;
|
||||
|
||||
/// Capacity in bytes: 5 frames of 48kHz 16-bit stereo (5 * 192 = 960).
|
||||
const LOOPBACK_BUFFER_CAPACITY: usize = 960;
|
||||
|
||||
/// Shared ring buffer connecting playback OUT to capture IN.
|
||||
#[derive(Debug)]
|
||||
pub struct UacLoopbackBuffer {
|
||||
inner: Mutex<VecDeque<u8>>,
|
||||
}
|
||||
|
||||
impl UacLoopbackBuffer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: Mutex::new(VecDeque::with_capacity(LOOPBACK_BUFFER_CAPACITY)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Write audio data into the buffer. Drops oldest samples if over capacity.
|
||||
pub fn write(&self, data: &[u8]) {
|
||||
let mut buf = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||
buf.extend(data);
|
||||
let excess = buf.len().saturating_sub(LOOPBACK_BUFFER_CAPACITY);
|
||||
if excess > 0 {
|
||||
buf.drain(..excess);
|
||||
}
|
||||
}
|
||||
|
||||
/// Read up to `len` bytes from the buffer. Pads with silence on underrun.
|
||||
pub fn read(&self, len: usize) -> Vec<u8> {
|
||||
let mut buf = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let available = buf.len().min(len);
|
||||
let mut result: Vec<u8> = buf.drain(..available).collect();
|
||||
result.resize(len, 0); // pad with silence
|
||||
result
|
||||
}
|
||||
|
||||
/// Clear all buffered data.
|
||||
pub fn clear(&self) {
|
||||
self.inner.lock().unwrap_or_else(|e| e.into_inner()).clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for the AudioControl interface (interface 0).
|
||||
/// STALLs all class-specific requests — no Feature Unit in this topology.
|
||||
#[derive(Debug)]
|
||||
pub struct UacControlHandler;
|
||||
|
||||
impl UacControlHandler {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl UsbInterfaceHandler for UacControlHandler {
|
||||
fn handle_urb(
|
||||
&self,
|
||||
_interface: &UsbInterface,
|
||||
request: UrbRequest,
|
||||
) -> Result<UrbResponse> {
|
||||
// STALL all class-specific control requests (no Feature Unit)
|
||||
if request.ep.is_ep0() {
|
||||
return Ok(UrbResponse { status: -32, ..Default::default() }); // EPIPE = STALL
|
||||
}
|
||||
Ok(UrbResponse::default())
|
||||
}
|
||||
|
||||
fn get_class_specific_descriptor(&self) -> Vec<u8> {
|
||||
vec![
|
||||
// --- AC Interface Header (UAC1 spec Table 4-2) ---
|
||||
0x0A, // bLength: 10
|
||||
0x24, // bDescriptorType: CS_INTERFACE
|
||||
0x01, // bDescriptorSubtype: HEADER
|
||||
0x00, 0x01, // bcdADC: 1.00
|
||||
0x34, 0x00, // wTotalLength: 52 (10 + 12 + 9 + 12 + 9)
|
||||
0x02, // bInCollection: 2 streaming interfaces
|
||||
0x01, // baInterfaceNr(1): interface 1
|
||||
0x02, // baInterfaceNr(2): interface 2
|
||||
|
||||
// --- Input Terminal 1: USB Streaming (playback source) ---
|
||||
0x0C, // bLength: 12
|
||||
0x24, // bDescriptorType: CS_INTERFACE
|
||||
0x02, // bDescriptorSubtype: INPUT_TERMINAL
|
||||
0x01, // bTerminalID: 1
|
||||
0x01, 0x01, // wTerminalType: USB Streaming (0x0101)
|
||||
0x00, // bAssocTerminal: 0
|
||||
0x02, // bNrChannels: 2 (stereo)
|
||||
0x03, 0x00, // wChannelConfig: left + right
|
||||
0x00, // iChannelNames: 0
|
||||
0x00, // iTerminal: 0
|
||||
|
||||
// --- Output Terminal 2: Speaker (playback sink) ---
|
||||
0x09, // bLength: 9
|
||||
0x24, // bDescriptorType: CS_INTERFACE
|
||||
0x03, // bDescriptorSubtype: OUTPUT_TERMINAL
|
||||
0x02, // bTerminalID: 2
|
||||
0x01, 0x03, // wTerminalType: Speaker (0x0301)
|
||||
0x00, // bAssocTerminal: 0
|
||||
0x01, // bSourceID: 1 (Input Terminal 1)
|
||||
0x00, // iTerminal: 0
|
||||
|
||||
// --- Input Terminal 3: Microphone (capture source) ---
|
||||
0x0C, // bLength: 12
|
||||
0x24, // bDescriptorType: CS_INTERFACE
|
||||
0x02, // bDescriptorSubtype: INPUT_TERMINAL
|
||||
0x03, // bTerminalID: 3
|
||||
0x01, 0x02, // wTerminalType: Microphone (0x0201)
|
||||
0x00, // bAssocTerminal: 0
|
||||
0x02, // bNrChannels: 2 (stereo)
|
||||
0x03, 0x00, // wChannelConfig: left + right
|
||||
0x00, // iChannelNames: 0
|
||||
0x00, // iTerminal: 0
|
||||
|
||||
// --- Output Terminal 4: USB Streaming (capture sink) ---
|
||||
0x09, // bLength: 9
|
||||
0x24, // bDescriptorType: CS_INTERFACE
|
||||
0x03, // bDescriptorSubtype: OUTPUT_TERMINAL
|
||||
0x04, // bTerminalID: 4
|
||||
0x01, 0x01, // wTerminalType: USB Streaming (0x0101)
|
||||
0x00, // bAssocTerminal: 0
|
||||
0x03, // bSourceID: 3 (Input Terminal 3)
|
||||
0x00, // iTerminal: 0
|
||||
]
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for AudioStreaming OUT interface (playback).
|
||||
/// Writes received audio data into the shared loopback buffer.
|
||||
#[derive(Debug)]
|
||||
pub struct UacStreamOutHandler {
|
||||
buffer: Arc<UacLoopbackBuffer>,
|
||||
}
|
||||
|
||||
impl UacStreamOutHandler {
|
||||
pub fn new(buffer: Arc<UacLoopbackBuffer>) -> Self {
|
||||
Self { buffer }
|
||||
}
|
||||
}
|
||||
|
||||
impl UsbInterfaceHandler for UacStreamOutHandler {
|
||||
fn handle_urb(
|
||||
&self,
|
||||
_interface: &UsbInterface,
|
||||
request: UrbRequest,
|
||||
) -> Result<UrbResponse> {
|
||||
if request.ep.is_ep0() {
|
||||
return Ok(UrbResponse { status: -32, ..Default::default() });
|
||||
}
|
||||
|
||||
// ISO OUT: write audio data into loopback buffer
|
||||
if request.ep.transfer_type() == Some(EndpointAttributes::Isochronous) {
|
||||
let descriptors: Vec<IsoPacketDescriptor> = request
|
||||
.iso_packet_descriptors
|
||||
.iter()
|
||||
.map(|d| {
|
||||
let offset = d.offset as usize;
|
||||
let length = d.length as usize;
|
||||
if offset + length <= request.data.len() {
|
||||
self.buffer.write(&request.data[offset..offset + length]);
|
||||
}
|
||||
IsoPacketDescriptor {
|
||||
offset: d.offset,
|
||||
length: d.length,
|
||||
actual_length: d.length,
|
||||
status: 0,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
return Ok(UrbResponse {
|
||||
data: vec![],
|
||||
iso_packet_descriptors: descriptors,
|
||||
status: 0,
|
||||
start_frame: request.start_frame,
|
||||
error_count: 0,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(UrbResponse::default())
|
||||
}
|
||||
|
||||
fn get_class_specific_descriptor(&self) -> Vec<u8> {
|
||||
vec![
|
||||
// --- AS General Descriptor (UAC1 spec Table 4-19) ---
|
||||
0x07, // bLength: 7
|
||||
0x24, // bDescriptorType: CS_INTERFACE
|
||||
0x01, // bDescriptorSubtype: AS_GENERAL
|
||||
0x01, // bTerminalLink: 1 (Input Terminal 1 — playback)
|
||||
0x01, // bDelay: 1 frame
|
||||
0x01, 0x00, // wFormatTag: PCM (0x0001)
|
||||
|
||||
// --- Format Type I Descriptor (UAC1 spec Table 2-1, Format Type I) ---
|
||||
0x0B, // bLength: 11
|
||||
0x24, // bDescriptorType: CS_INTERFACE
|
||||
0x02, // bDescriptorSubtype: FORMAT_TYPE
|
||||
0x01, // bFormatType: FORMAT_TYPE_I
|
||||
0x02, // bNrChannels: 2 (stereo)
|
||||
0x02, // bSubframeSize: 2 bytes (16-bit)
|
||||
0x10, // bBitResolution: 16
|
||||
0x01, // bSamFreqType: 1 (one discrete frequency)
|
||||
0x80, 0xBB, 0x00, // tSamFreq: 48000 Hz (0x00BB80, little-endian)
|
||||
]
|
||||
}
|
||||
|
||||
fn set_alt_setting(&self, _alt: u8) -> Result<()> {
|
||||
self.buffer.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for AudioStreaming IN interface (capture).
|
||||
/// Reads audio data from the shared loopback buffer; returns silence on underrun.
|
||||
#[derive(Debug)]
|
||||
pub struct UacStreamInHandler {
|
||||
buffer: Arc<UacLoopbackBuffer>,
|
||||
}
|
||||
|
||||
impl UacStreamInHandler {
|
||||
pub fn new(buffer: Arc<UacLoopbackBuffer>) -> Self {
|
||||
Self { buffer }
|
||||
}
|
||||
}
|
||||
|
||||
impl UsbInterfaceHandler for UacStreamInHandler {
|
||||
fn handle_urb(
|
||||
&self,
|
||||
_interface: &UsbInterface,
|
||||
request: UrbRequest,
|
||||
) -> Result<UrbResponse> {
|
||||
if request.ep.is_ep0() {
|
||||
return Ok(UrbResponse { status: -32, ..Default::default() });
|
||||
}
|
||||
|
||||
// ISO IN: read audio data from loopback buffer
|
||||
if request.ep.transfer_type() == Some(EndpointAttributes::Isochronous) {
|
||||
let mut data = Vec::new();
|
||||
let descriptors: Vec<IsoPacketDescriptor> = request
|
||||
.iso_packet_descriptors
|
||||
.iter()
|
||||
.map(|d| {
|
||||
let packet_data = self.buffer.read(d.length as usize);
|
||||
data.extend_from_slice(&packet_data);
|
||||
IsoPacketDescriptor {
|
||||
offset: d.offset,
|
||||
length: d.length,
|
||||
actual_length: d.length,
|
||||
status: 0,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
return Ok(UrbResponse {
|
||||
data,
|
||||
iso_packet_descriptors: descriptors,
|
||||
status: 0,
|
||||
start_frame: request.start_frame,
|
||||
error_count: 0,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(UrbResponse::default())
|
||||
}
|
||||
|
||||
fn get_class_specific_descriptor(&self) -> Vec<u8> {
|
||||
vec![
|
||||
// --- AS General Descriptor ---
|
||||
0x07, // bLength: 7
|
||||
0x24, // bDescriptorType: CS_INTERFACE
|
||||
0x01, // bDescriptorSubtype: AS_GENERAL
|
||||
0x04, // bTerminalLink: 4 (Output Terminal 4 — capture)
|
||||
0x01, // bDelay: 1 frame
|
||||
0x01, 0x00, // wFormatTag: PCM (0x0001)
|
||||
|
||||
// --- Format Type I Descriptor ---
|
||||
0x0B, // bLength: 11
|
||||
0x24, // bDescriptorType: CS_INTERFACE
|
||||
0x02, // bDescriptorSubtype: FORMAT_TYPE
|
||||
0x01, // bFormatType: FORMAT_TYPE_I
|
||||
0x02, // bNrChannels: 2 (stereo)
|
||||
0x02, // bSubframeSize: 2 bytes (16-bit)
|
||||
0x10, // bBitResolution: 16
|
||||
0x01, // bSamFreqType: 1 (one discrete frequency)
|
||||
0x80, 0xBB, 0x00, // tSamFreq: 48000 Hz
|
||||
]
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a complete UAC1 loopback device with AudioControl + two AudioStreaming interfaces.
|
||||
pub fn build_uac_loopback_device() -> std::io::Result<UsbDevice> {
|
||||
let buffer = Arc::new(UacLoopbackBuffer::new());
|
||||
|
||||
let control_handler: Arc<dyn UsbInterfaceHandler> = Arc::new(UacControlHandler::new());
|
||||
let out_handler: Arc<dyn UsbInterfaceHandler> = Arc::new(UacStreamOutHandler::new(buffer.clone()));
|
||||
let in_handler: Arc<dyn UsbInterfaceHandler> = Arc::new(UacStreamInHandler::new(buffer));
|
||||
|
||||
let mut device = UsbDevice::new(0)?;
|
||||
device.vendor_id = 0x1234;
|
||||
device.product_id = 0x5678;
|
||||
device.device_class = 0x00; // per-interface
|
||||
device.speed = UsbSpeed::Full as u32;
|
||||
device.usb_version = device::Version { major: 2, minor: 0, patch: 0 };
|
||||
device.device_bcd = device::Version { major: 1, minor: 0, patch: 0 };
|
||||
|
||||
device.string_manufacturer = device.new_string("Test")?;
|
||||
device.string_product = device.new_string("UAC1 Loopback")?;
|
||||
|
||||
// Interface 0: AudioControl (single alt setting, no endpoints)
|
||||
let ac_class_desc = control_handler.get_class_specific_descriptor();
|
||||
device.interface_states.push(InterfaceState::new(UsbInterface {
|
||||
interface_class: ClassCode::Audio as u8,
|
||||
interface_subclass: 0x01, // AudioControl
|
||||
interface_protocol: 0x00,
|
||||
endpoints: vec![],
|
||||
string_interface: 0,
|
||||
class_specific_descriptor: ac_class_desc,
|
||||
handler: control_handler,
|
||||
}));
|
||||
|
||||
// Interface 1: AudioStreaming OUT (playback) — alt 0 (zero-bw) + alt 1 (active)
|
||||
let out_class_desc = out_handler.get_class_specific_descriptor();
|
||||
let out_alt0 = UsbInterface {
|
||||
interface_class: ClassCode::Audio as u8,
|
||||
interface_subclass: 0x02, // AudioStreaming
|
||||
interface_protocol: 0x00,
|
||||
endpoints: vec![], // zero-bandwidth
|
||||
string_interface: 0,
|
||||
class_specific_descriptor: vec![],
|
||||
handler: out_handler.clone(),
|
||||
};
|
||||
let out_alt1 = UsbInterface {
|
||||
interface_class: ClassCode::Audio as u8,
|
||||
interface_subclass: 0x02,
|
||||
interface_protocol: 0x00,
|
||||
endpoints: vec![UsbEndpoint {
|
||||
address: 0x01, // OUT
|
||||
attributes: 0x0D, // isochronous adaptive
|
||||
max_packet_size: 192,
|
||||
interval: 1,
|
||||
}],
|
||||
string_interface: 0,
|
||||
class_specific_descriptor: out_class_desc,
|
||||
handler: out_handler,
|
||||
};
|
||||
device.interface_states.push(InterfaceState::with_alt_settings(vec![out_alt0, out_alt1]));
|
||||
|
||||
// Interface 2: AudioStreaming IN (capture) — alt 0 (zero-bw) + alt 1 (active)
|
||||
let in_class_desc = in_handler.get_class_specific_descriptor();
|
||||
let in_alt0 = UsbInterface {
|
||||
interface_class: ClassCode::Audio as u8,
|
||||
interface_subclass: 0x02,
|
||||
interface_protocol: 0x00,
|
||||
endpoints: vec![],
|
||||
string_interface: 0,
|
||||
class_specific_descriptor: vec![],
|
||||
handler: in_handler.clone(),
|
||||
};
|
||||
let in_alt1 = UsbInterface {
|
||||
interface_class: ClassCode::Audio as u8,
|
||||
interface_subclass: 0x02,
|
||||
interface_protocol: 0x00,
|
||||
endpoints: vec![UsbEndpoint {
|
||||
address: 0x82, // IN
|
||||
attributes: 0x05, // isochronous async
|
||||
max_packet_size: 192,
|
||||
interval: 1,
|
||||
}],
|
||||
string_interface: 0,
|
||||
class_specific_descriptor: in_class_desc,
|
||||
handler: in_handler,
|
||||
};
|
||||
device.interface_states.push(InterfaceState::with_alt_settings(vec![in_alt0, in_alt1]));
|
||||
|
||||
Ok(device)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::util::tests::*;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn loopback_buffer_write_read() {
|
||||
let buf = UacLoopbackBuffer::new();
|
||||
buf.write(&[1, 2, 3, 4]);
|
||||
assert_eq!(buf.read(4), vec![1, 2, 3, 4]);
|
||||
// Buffer should be empty now
|
||||
assert_eq!(buf.read(2), vec![0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loopback_buffer_underrun() {
|
||||
let buf = UacLoopbackBuffer::new();
|
||||
let result = buf.read(4);
|
||||
assert_eq!(result, vec![0, 0, 0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loopback_buffer_overflow() {
|
||||
let buf = UacLoopbackBuffer::new();
|
||||
// Write more than capacity: 100 old bytes + CAPACITY new bytes
|
||||
let mut big_data = vec![0xBB; 100];
|
||||
big_data.extend(vec![0xAA; LOOPBACK_BUFFER_CAPACITY]);
|
||||
buf.write(&big_data);
|
||||
// Should have dropped the oldest 100 bytes (0xBB), kept newest CAPACITY (0xAA)
|
||||
let result = buf.read(LOOPBACK_BUFFER_CAPACITY);
|
||||
assert_eq!(result.len(), LOOPBACK_BUFFER_CAPACITY);
|
||||
assert!(result.iter().all(|&b| b == 0xAA), "oldest bytes should have been dropped");
|
||||
// Buffer is now empty
|
||||
assert_eq!(buf.read(1), vec![0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desc_verify_control() {
|
||||
setup_test_logger();
|
||||
let handler = UacControlHandler::new();
|
||||
verify_descriptor(&handler.get_class_specific_descriptor());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_handler_stall() {
|
||||
setup_test_logger();
|
||||
let handler = UacControlHandler::new();
|
||||
let interface = UsbInterface {
|
||||
interface_class: ClassCode::Audio as u8,
|
||||
interface_subclass: 0x01,
|
||||
interface_protocol: 0x00,
|
||||
endpoints: vec![],
|
||||
string_interface: 0,
|
||||
class_specific_descriptor: handler.get_class_specific_descriptor(),
|
||||
handler: Arc::new(UacControlHandler::new()),
|
||||
};
|
||||
// Send an unknown class request (SET_CUR to some unit)
|
||||
let request = UrbRequest {
|
||||
ep: UsbEndpoint {
|
||||
address: 0x00,
|
||||
attributes: EndpointAttributes::Control as u8,
|
||||
max_packet_size: 64,
|
||||
interval: 0,
|
||||
},
|
||||
setup: SetupPacket {
|
||||
request_type: 0x21, // class, interface, host-to-device
|
||||
request: 0x01, // SET_CUR
|
||||
value: 0x0100,
|
||||
index: 0x0000,
|
||||
length: 0,
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let response = handler.handle_urb(&interface, request).unwrap();
|
||||
assert_eq!(response.status, -32); // EPIPE = STALL
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desc_verify_stream_out() {
|
||||
setup_test_logger();
|
||||
let handler = UacStreamOutHandler::new(Arc::new(UacLoopbackBuffer::new()));
|
||||
verify_descriptor(&handler.get_class_specific_descriptor());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stream_out_handler_iso() {
|
||||
setup_test_logger();
|
||||
let buffer = Arc::new(UacLoopbackBuffer::new());
|
||||
let handler = UacStreamOutHandler::new(buffer.clone());
|
||||
let interface = UsbInterface {
|
||||
interface_class: ClassCode::Audio as u8,
|
||||
interface_subclass: 0x02,
|
||||
interface_protocol: 0x00,
|
||||
endpoints: vec![UsbEndpoint {
|
||||
address: 0x01,
|
||||
attributes: 0x0D, // isochronous adaptive
|
||||
max_packet_size: 192,
|
||||
interval: 1,
|
||||
}],
|
||||
string_interface: 0,
|
||||
class_specific_descriptor: handler.get_class_specific_descriptor(),
|
||||
handler: Arc::new(UacStreamOutHandler::new(Arc::new(UacLoopbackBuffer::new()))),
|
||||
};
|
||||
let audio_data: Vec<u8> = (0..192).map(|i| (i & 0xFF) as u8).collect();
|
||||
let request = UrbRequest {
|
||||
ep: UsbEndpoint {
|
||||
address: 0x01,
|
||||
attributes: 0x0D,
|
||||
max_packet_size: 192,
|
||||
interval: 1,
|
||||
},
|
||||
data: audio_data.clone(),
|
||||
number_of_packets: 1,
|
||||
iso_packet_descriptors: vec![IsoPacketDescriptor {
|
||||
offset: 0,
|
||||
length: 192,
|
||||
actual_length: 0,
|
||||
status: 0,
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
let response = handler.handle_urb(&interface, request).unwrap();
|
||||
assert_eq!(response.status, 0);
|
||||
assert_eq!(response.iso_packet_descriptors.len(), 1);
|
||||
assert_eq!(response.iso_packet_descriptors[0].actual_length, 192);
|
||||
// Verify data was written to loopback buffer
|
||||
assert_eq!(buffer.read(192), audio_data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desc_verify_stream_in() {
|
||||
setup_test_logger();
|
||||
let handler = UacStreamInHandler::new(Arc::new(UacLoopbackBuffer::new()));
|
||||
verify_descriptor(&handler.get_class_specific_descriptor());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stream_in_handler_iso() {
|
||||
setup_test_logger();
|
||||
let buffer = Arc::new(UacLoopbackBuffer::new());
|
||||
let handler = UacStreamInHandler::new(buffer.clone());
|
||||
let interface = UsbInterface {
|
||||
interface_class: ClassCode::Audio as u8,
|
||||
interface_subclass: 0x02,
|
||||
interface_protocol: 0x00,
|
||||
endpoints: vec![UsbEndpoint {
|
||||
address: 0x82,
|
||||
attributes: 0x05, // isochronous async
|
||||
max_packet_size: 192,
|
||||
interval: 1,
|
||||
}],
|
||||
string_interface: 0,
|
||||
class_specific_descriptor: handler.get_class_specific_descriptor(),
|
||||
handler: Arc::new(UacStreamInHandler::new(Arc::new(UacLoopbackBuffer::new()))),
|
||||
};
|
||||
// Pre-fill buffer with known data
|
||||
let audio_data: Vec<u8> = (0..192).map(|i| (i & 0xFF) as u8).collect();
|
||||
buffer.write(&audio_data);
|
||||
|
||||
let request = UrbRequest {
|
||||
ep: UsbEndpoint {
|
||||
address: 0x82,
|
||||
attributes: 0x05,
|
||||
max_packet_size: 192,
|
||||
interval: 1,
|
||||
},
|
||||
transfer_buffer_length: 192,
|
||||
number_of_packets: 1,
|
||||
iso_packet_descriptors: vec![IsoPacketDescriptor {
|
||||
offset: 0,
|
||||
length: 192,
|
||||
actual_length: 0,
|
||||
status: 0,
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
let response = handler.handle_urb(&interface, request).unwrap();
|
||||
assert_eq!(response.status, 0);
|
||||
assert_eq!(response.data, audio_data);
|
||||
assert_eq!(response.iso_packet_descriptors.len(), 1);
|
||||
assert_eq!(response.iso_packet_descriptors[0].actual_length, 192);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stream_in_handler_silence() {
|
||||
setup_test_logger();
|
||||
let buffer = Arc::new(UacLoopbackBuffer::new());
|
||||
let handler = UacStreamInHandler::new(buffer);
|
||||
let interface = UsbInterface {
|
||||
interface_class: ClassCode::Audio as u8,
|
||||
interface_subclass: 0x02,
|
||||
interface_protocol: 0x00,
|
||||
endpoints: vec![],
|
||||
string_interface: 0,
|
||||
class_specific_descriptor: handler.get_class_specific_descriptor(),
|
||||
handler: Arc::new(UacStreamInHandler::new(Arc::new(UacLoopbackBuffer::new()))),
|
||||
};
|
||||
let request = UrbRequest {
|
||||
ep: UsbEndpoint {
|
||||
address: 0x82,
|
||||
attributes: 0x05,
|
||||
max_packet_size: 192,
|
||||
interval: 1,
|
||||
},
|
||||
transfer_buffer_length: 192,
|
||||
number_of_packets: 1,
|
||||
iso_packet_descriptors: vec![IsoPacketDescriptor {
|
||||
offset: 0,
|
||||
length: 192,
|
||||
actual_length: 0,
|
||||
status: 0,
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
let response = handler.handle_urb(&interface, request).unwrap();
|
||||
assert_eq!(response.status, 0);
|
||||
// All silence (zeros) because buffer is empty
|
||||
assert_eq!(response.data, vec![0u8; 192]);
|
||||
}
|
||||
}
|
||||
|
|
@ -120,7 +120,7 @@ pub(crate) mod tests {
|
|||
) -> std::io::Result<UrbResponse> {
|
||||
let ep = request.ep;
|
||||
|
||||
if ep.attributes == EndpointAttributes::Isochronous as u8 {
|
||||
if ep.transfer_type() == Some(EndpointAttributes::Isochronous) {
|
||||
if let crate::Direction::In = ep.direction() {
|
||||
// ISO IN: generate test data
|
||||
let mut data = Vec::new();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue