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
|
|
@ -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