Phase 3: Device backend trait¶
Parent plan: PLAN-usb-redir.md
Goal¶
Define the UsbDeviceBackend trait and supporting types
that abstract over real and virtual USB devices. After this
phase, the trait exists with full documentation, the
DeviceSource and UsbDeviceInfo enums are defined, and
a combined enumeration function can list both physical and
virtual devices. No concrete backend implementations yet —
those come in phases 4 (real devices) and 7 (virtual mass
storage).
This phase is purely types, traits, and enums. It compiles and has unit tests for the info types, but nothing connects to real hardware or files.
Background¶
Why a trait¶
The master plan calls for two kinds of USB device:
- Physical — a real USB device on the host, accessed
via
nusb(phase 4). - Virtual — a software-emulated device backed by a local resource, e.g. a RAW disk image presented as a USB mass storage device (phase 7).
From the usbredir channel handler's perspective, both must
be interchangeable. The server sends set_configuration,
control_packet, bulk_packet, etc. and the handler
delegates to whichever backend is active. The trait is the
boundary that makes this possible.
Trait method design¶
The trait methods map directly to what the channel handler needs from phases 5 and 6:
-
Descriptor queries (
device_info,endpoint_info,interface_info): called once at device attach time to build the usbredir messages that describe the device to the server. These return the phase 2 message structs directly (DeviceConnect,EpInfo,InterfaceInfo). -
Configuration management (
set_configuration,get_configuration,set_alt_setting,get_alt_setting,reset): called when the server configures the device. For real devices these forward to the USB stack. For virtual devices they update internal state. -
Data transfers (
control_transfer,bulk_in,bulk_out): the core I/O operations. The channel handler dispatches usbredir data packets to these methods and sends the results back. -
Interrupt transfers (
start_interrupt_in,stop_interrupt_in): for phase 9. Defined in the trait now so implementations know the full interface, but the default implementation returnsStall.
All async methods use &mut self — the channel handler
owns one backend at a time and calls it sequentially (no
concurrent transfers within a single channel).
Return types¶
Transfer methods return TransferResult rather than raw
Result<Vec<u8>> so that USB-level status (stall, timeout,
babble) can be communicated separately from Rust errors.
A Rust Err means the backend itself failed (e.g. file
I/O error); a TransferResult with status Stall means
the USB transfer was rejected normally.
Detailed steps¶
Step 1: Create module structure¶
Create src/usb/mod.rs with the trait and type
definitions. Add mod usb; to src/main.rs.
The module will contain:
src/usb/
└── mod.rs — UsbDeviceBackend trait, UsbDeviceInfo,
DeviceSource, TransferResult, ControlSetup
Later phases add files here:
Step 2: Define TransferResult¶
Data transfers can succeed with data, succeed with no data, or fail with a USB status code. This needs its own type:
use crate::usbredir::constants::Status;
/// Result of a USB data transfer.
#[derive(Debug, Clone)]
pub struct TransferResult {
pub status: Status,
pub data: Vec<u8>,
}
impl TransferResult {
pub fn success(data: Vec<u8>) -> Self {
TransferResult {
status: Status::Success,
data,
}
}
pub fn success_empty() -> Self {
TransferResult {
status: Status::Success,
data: Vec::new(),
}
}
pub fn stall() -> Self {
TransferResult {
status: Status::Stall,
data: Vec::new(),
}
}
pub fn error(status: Status) -> Self {
TransferResult {
status,
data: Vec::new(),
}
}
}
Step 3: Define ControlSetup¶
The control transfer method needs the USB setup packet
fields. Rather than passing 6 separate arguments, bundle
them into a struct that mirrors the ControlPacketHeader
from phase 2 (minus status, which is a response field):
/// USB control transfer setup packet fields.
#[derive(Debug, Clone)]
pub struct ControlSetup {
pub endpoint: u8,
pub request_type: u8,
pub request: u8,
pub value: u16,
pub index: u16,
pub length: u16,
}
This maps 1:1 to the incoming ControlPacketHeader fields.
The channel handler constructs it from the parsed usbredir
message.
Step 4: Define the UsbDeviceBackend trait¶
use anyhow::Result;
use crate::usbredir::messages::{
DeviceConnect, EpInfo, InterfaceInfo,
};
pub trait UsbDeviceBackend: Send {
// ── Descriptor queries ──────────────────────
/// USB device descriptor info for usb_redir_device_connect.
fn device_info(&self) -> DeviceConnect;
/// Endpoint descriptor info for usb_redir_ep_info.
fn endpoint_info(&self) -> EpInfo;
/// Interface descriptor info for usb_redir_interface_info.
fn interface_info(&self) -> InterfaceInfo;
// ── Configuration management ────────────────
/// Set USB configuration. Returns status.
async fn set_configuration(
&mut self,
configuration: u8,
) -> Result<Status>;
/// Get current USB configuration number.
async fn get_configuration(
&mut self,
) -> Result<u8>;
/// Set alternate setting for an interface.
async fn set_alt_setting(
&mut self,
interface: u8,
alt_setting: u8,
) -> Result<Status>;
/// Get current alternate setting for an interface.
async fn get_alt_setting(
&mut self,
interface: u8,
) -> Result<u8>;
/// Reset the USB device.
async fn reset(&mut self) -> Result<()>;
// ── Data transfers ──────────────────────────
/// Execute a USB control transfer.
///
/// For IN transfers (device→host), the returned
/// TransferResult contains the response data.
/// For OUT transfers (host→device), `data` carries
/// the payload and the result data is empty.
async fn control_transfer(
&mut self,
setup: &ControlSetup,
data: &[u8],
) -> Result<TransferResult>;
/// Bulk IN transfer: read up to `max_len` bytes from
/// the given endpoint.
async fn bulk_in(
&mut self,
endpoint: u8,
max_len: usize,
) -> Result<TransferResult>;
/// Bulk OUT transfer: write `data` to the given
/// endpoint.
async fn bulk_out(
&mut self,
endpoint: u8,
data: &[u8],
) -> Result<TransferResult>;
// ── Interrupt transfers ─────────────────────
// Defined now for interface completeness; default
// implementations return Stall. Phase 9 will
// implement these for real devices.
/// Start receiving interrupt IN transfers on the
/// given endpoint. The backend should begin polling
/// and return data via interrupt_in().
async fn start_interrupt_in(
&mut self,
_endpoint: u8,
) -> Result<Status> {
Ok(Status::Stall)
}
/// Stop receiving interrupt IN transfers.
async fn stop_interrupt_in(
&mut self,
_endpoint: u8,
) -> Result<Status> {
Ok(Status::Stall)
}
/// Read pending interrupt IN data. Returns empty
/// data if nothing is available.
async fn interrupt_in(
&mut self,
_endpoint: u8,
_max_len: usize,
) -> Result<TransferResult> {
Ok(TransferResult::stall())
}
/// Write interrupt OUT data.
async fn interrupt_out(
&mut self,
_endpoint: u8,
_data: &[u8],
) -> Result<TransferResult> {
Ok(TransferResult::stall())
}
// ── Metadata ────────────────────────────────
/// Whether this is a virtual (emulated) device.
fn is_virtual(&self) -> bool;
/// Human-readable description for logging and UI.
fn description(&self) -> String;
}
Design notes:
Sendbound: the backend is owned by a tokio task, which requiresSend.- No
Syncbound: single-owner, no shared references. - Async methods use the native Rust async-in-trait
(stable since 1.75). No
async-traitcrate needed. - Interrupt methods have default implementations so that phase 4 (real devices) and phase 7 (virtual MSC) don't have to stub them.
description()provides a UI label ("SanDisk Ultra USB 3.0" for real devices, "RAW Disk: /path/to/image.raw" for virtual).
Step 5: Define DeviceSource and UsbDeviceInfo¶
These types describe available devices before they're opened. Used for enumeration and UI display.
use std::path::PathBuf;
/// Where a USB device comes from.
#[derive(Debug, Clone)]
pub enum DeviceSource {
/// A physical USB device on the host.
Physical {
bus: u8,
address: u8,
},
/// A virtual mass storage device backed by a RAW
/// disk image file.
VirtualDisk {
path: PathBuf,
read_only: bool,
},
}
/// Information about an available USB device (real or
/// virtual) for display in the UI and device selection.
#[derive(Debug, Clone)]
pub struct UsbDeviceInfo {
pub source: DeviceSource,
pub vendor_id: u16,
pub product_id: u16,
pub name: String,
pub speed: u8,
pub device_class: u8,
pub device_subclass: u8,
pub device_protocol: u8,
}
impl UsbDeviceInfo {
/// Short label for UI display.
pub fn label(&self) -> String {
match &self.source {
DeviceSource::Physical { bus, address } => {
format!(
"{} [{:04x}:{:04x}] (bus {} addr {})",
self.name,
self.vendor_id,
self.product_id,
bus,
address,
)
}
DeviceSource::VirtualDisk { path, read_only }
=> {
let ro = if *read_only { " [RO]" } else { "" };
format!(
"RAW Disk: {}{}",
path.display(),
ro,
)
}
}
}
}
Step 6: Define enumeration function¶
A function that combines real and virtual device lists:
/// Enumerate all available USB devices (physical +
/// configured virtual devices).
///
/// `virtual_disks` comes from CLI flags (--usb-disk).
/// Physical device enumeration is stubbed until phase 4.
pub fn enumerate_devices(
virtual_disks: &[(PathBuf, bool)],
) -> Vec<UsbDeviceInfo> {
let mut devices = Vec::new();
// Physical devices (phase 4 will fill this in)
// devices.extend(enumerate_physical());
// Virtual disk devices
for (path, read_only) in virtual_disks {
devices.push(UsbDeviceInfo {
source: DeviceSource::VirtualDisk {
path: path.clone(),
read_only: *read_only,
},
vendor_id: 0x1d6b,
product_id: 0x0104,
name: format!(
"Virtual Disk ({})",
path.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| path.display().to_string()),
),
speed: 3, // High Speed
device_class: 0x00,
device_subclass: 0x00,
device_protocol: 0x00,
});
}
devices
}
Phase 4 will add enumerate_physical() using nusb.
Step 7: Add ChannelEvent for device list¶
In src/channels/mod.rs, add:
use crate::usb::UsbDeviceInfo;
/// Available USB devices changed (enumeration result).
UsbDevicesChanged(Vec<UsbDeviceInfo>),
This event will be sent by the usbredir channel when devices are enumerated. For phase 3 this is just the type definition — nothing sends it yet.
Step 8: Unit tests¶
Tests in src/usb/mod.rs using #[cfg(test)]:
-
TransferResult constructors: verify
success(),success_empty(),stall(),error()set the correct status and data. -
UsbDeviceInfo::label() for physical device: verify format includes name, VID:PID, bus, address.
-
UsbDeviceInfo::label() for virtual disk: verify format includes path and read-only marker.
-
enumerate_devices() with no virtual disks: returns empty list (no physical enumeration yet).
-
enumerate_devices() with virtual disks: returns one
UsbDeviceInfoper entry, correct vendor/product IDs, name includes filename. -
DeviceSource clone and debug: verify the enum derives work correctly.
Files changed¶
| File | Change |
|---|---|
src/usb/mod.rs |
New — UsbDeviceBackend trait, TransferResult, ControlSetup, DeviceSource, UsbDeviceInfo, enumerate_devices(), unit tests |
src/main.rs |
Add mod usb; |
src/channels/mod.rs |
Add UsbDevicesChanged event variant (type only, not sent yet) |
What is NOT in scope¶
- Concrete backend implementations (phases 4 and 7).
- Wiring the backend into the channel handler (phase 5).
- CLI flags for
--usb-disk(phase 7 adds the flag, phase 8 wires up the UI). - Physical device enumeration via
nusb(phase 4). - Sending
UsbDevicesChangedevents (phases 4/7). - Interrupt transfer implementations (phase 9) — the trait defines the methods with default stubs.
Dependencies on phase 2¶
This phase imports types from the src/usbredir/ module:
usbredir::constants::Status— forTransferResultand configuration method return types.usbredir::messages::DeviceConnect— returned bydevice_info().usbredir::messages::EpInfo— returned byendpoint_info().usbredir::messages::InterfaceInfo— returned byinterface_info().
Phase 2 must be implemented before this phase. The trait methods return phase 2 structs directly so that the channel handler can serialise them into usbredir messages without intermediate conversion.
Testing¶
Build and lint¶
Unit tests¶
Verification¶
The trait compiles and is documented. No integration test
is possible until a concrete implementation exists (phase 4
or 7), but the types are exercised by the unit tests for
TransferResult, UsbDeviceInfo, and
enumerate_devices().
Back brief¶
Before starting this phase, confirm understanding: we are
defining the abstraction layer only. The UsbDeviceBackend
trait specifies the contract between the usbredir channel
handler and any USB device (real or virtual). No concrete
implementations exist yet. The trait methods return
phase 2 usbredir message types directly. Supporting types
(TransferResult, ControlSetup, DeviceSource,
UsbDeviceInfo) and an enumeration stub are included.
The UsbDevicesChanged channel event type is added but
not emitted.