diff --git a/src/device.rs b/src/device.rs index 461c70f..0f3f935 100644 --- a/src/device.rs +++ b/src/device.rs @@ -1,13 +1,13 @@ use crate::{ descriptors::{ decode_string_descriptor, validate_string_descriptor, ConfigurationDescriptor, - DeviceDescriptor, InterfaceDescriptor, DESCRIPTOR_TYPE_STRING, + DeviceDescriptor, InterfaceDescriptor, TransferType, DESCRIPTOR_TYPE_STRING, }, io::{EndpointRead, EndpointWrite}, platform, transfer::{ Buffer, BulkOrInterrupt, Completion, ControlIn, ControlOut, Direction, EndpointDirection, - EndpointType, In, Out, TransferError, + EndpointType, In, IsoCompletion, Out, TransferError, }, ActiveConfigurationError, DeviceInfo, Error, ErrorKind, GetDescriptorError, MaybeFuture, Speed, }; @@ -541,6 +541,48 @@ impl Interface { ep_dir: PhantomData, }) } + + /// Open an isochronous endpoint. + /// + /// This claims exclusive access to the endpoint and returns an [`IsoEndpoint`] + /// that can be used to submit isochronous transfers. + /// + /// `num_packets` specifies the number of packets per transfer. The buffer + /// submitted to each transfer should be large enough to hold + /// `num_packets * max_packet_size` bytes. + /// + /// ### Platform-specific details + /// * Only supported on Linux + #[cfg(any(target_os = "linux", target_os = "android"))] + pub fn iso_endpoint( + &self, + address: u8, + num_packets: usize, + ) -> Result, Error> { + let intf_desc = self.descriptor(); + let ep_desc = + intf_desc.and_then(|desc| desc.endpoints().find(|ep| ep.address() == address)); + let Some(ep_desc) = ep_desc else { + return Err(Error::new( + ErrorKind::NotFound, + "specified endpoint does not exist on this interface", + )); + }; + + if address & Direction::MASK != Dir::DIR as u8 { + return Err(Error::new(ErrorKind::Other, "incorrect endpoint direction")); + } + + if ep_desc.transfer_type() != TransferType::Isochronous { + return Err(Error::new(ErrorKind::Other, "endpoint is not isochronous")); + } + + let backend = self.backend.iso_endpoint(ep_desc, num_packets)?; + Ok(IsoEndpoint { + backend, + ep_dir: PhantomData, + }) + } } impl Debug for Interface { @@ -809,3 +851,120 @@ fn assert_send_sync() { require_send_sync::>(); require_send_sync::>(); } + +/// Exclusive access to an isochronous endpoint of a USB device. +/// +/// Obtain an `IsoEndpoint` with the [`Interface::iso_endpoint`] method. +/// +/// Isochronous endpoints are used for real-time streaming data like audio +/// and video. Unlike bulk/interrupt endpoints, isochronous transfers: +/// - Have guaranteed bandwidth +/// - Do not retry on error (data is time-sensitive) +/// - Transfer multiple packets per URB +/// - Have per-packet status information +/// +/// When the `IsoEndpoint` is dropped, any pending transfers are cancelled. +/// +/// ### Platform-specific details +/// * Only supported on Linux +#[cfg(any(target_os = "linux", target_os = "android"))] +pub struct IsoEndpoint { + backend: platform::IsoEndpoint, + ep_dir: PhantomData, +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +impl IsoEndpoint { + /// Get the endpoint address. + pub fn endpoint_address(&self) -> u8 { + self.backend.endpoint_address() + } + + /// Get the maximum packet size for this endpoint. + pub fn max_packet_size(&self) -> usize { + self.backend.max_packet_size + } + + /// Get the number of packets per transfer. + pub fn num_packets(&self) -> usize { + self.backend.num_packets() + } + + /// Get the number of transfers that have been submitted with `submit` that + /// have not yet been returned from `next_complete`. + pub fn pending(&self) -> usize { + self.backend.pending() + } + + /// Request cancellation of all pending transfers. + pub fn cancel_all(&mut self) { + self.backend.cancel_all() + } + + /// Allocate a buffer for use on this endpoint, zero-copy if possible. + /// + /// See [`Endpoint::allocate`] for details. + pub fn allocate(&self, len: usize) -> Buffer { + #[cfg(any(target_os = "linux", target_os = "android"))] + { + if let Ok(b) = self.backend.allocate(len) { + return b; + } + } + + Buffer::new(len) + } + + /// Begin an isochronous transfer on the endpoint. + /// + /// `packet_size` is the size of each isochronous packet. The buffer should + /// be large enough to hold `num_packets * packet_size` bytes. + /// + /// Submitted transfers are queued and completed in order. + pub fn submit(&mut self, buf: Buffer, packet_size: usize) { + self.backend.submit(buf, packet_size) + } + + /// Return a `Future` that waits for the next pending transfer to complete. + /// + /// This future is cancel-safe. + /// + /// ## Panics + /// * if there are no transfers pending + pub fn next_complete(&mut self) -> impl Future + Send + Sync + '_ { + poll_fn(|cx| self.poll_next_complete(cx)) + } + + /// Poll for a pending transfer completion. + /// + /// ## Panics + /// * if there are no transfers pending + pub fn poll_next_complete(&mut self, cx: &mut Context<'_>) -> Poll { + self.backend.poll_next_complete(cx) + } + + /// Wait for a pending transfer completion. + /// + /// Blocks for up to `timeout` waiting for a transfer to complete. + /// + /// ## Panics + /// * if there are no transfers pending + pub fn wait_next_complete(&mut self, timeout: Duration) -> Option { + self.backend.wait_next_complete(timeout) + } +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +impl Debug for IsoEndpoint { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("IsoEndpoint") + .field( + "address", + &format_args!("0x{:02x}", self.endpoint_address()), + ) + .field("type", &"Isochronous") + .field("direction", &Dir::DIR) + .field("num_packets", &self.num_packets()) + .finish() + } +} diff --git a/src/lib.rs b/src/lib.rs index 9905508..f5a64e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -188,6 +188,8 @@ pub use enumeration::BusInfo; pub use enumeration::{DeviceId, DeviceInfo, InterfaceInfo, Speed, UsbControllerType}; mod device; +#[cfg(any(target_os = "linux", target_os = "android"))] +pub use device::IsoEndpoint; pub use device::{Device, Endpoint, Interface}; pub mod transfer; diff --git a/src/platform/linux_usbfs/device.rs b/src/platform/linux_usbfs/device.rs index f4edee4..7687ab8 100644 --- a/src/platform/linux_usbfs/device.rs +++ b/src/platform/linux_usbfs/device.rs @@ -25,9 +25,11 @@ use slab::Slab; use super::{ errno_to_transfer_error, events, usbfs::{self, Urb}, - TransferData, + IsoTransferData, TransferData, }; +use crate::transfer::IsoCompletion; + #[cfg(not(target_os = "android"))] use super::SysfsPath; @@ -203,28 +205,47 @@ impl LinuxDevice { debug!("Handling events for device {}", self.events_id); match usbfs::reap_urb_ndelay(&self.fd) { Ok(urb) => { - let transfer_data: *mut TransferData = unsafe { &(*urb) }.usercontext.cast(); + let ep_type = unsafe { (*urb).ep_type }; + + // Check if this is an isochronous transfer + if ep_type == usbfs::USBDEVFS_URB_TYPE_ISO { + // SAFETY: For isochronous transfers, usercontext points to IsoTransferData + let transfer_data: *mut IsoTransferData = unsafe { &(*urb) }.usercontext.cast(); - { - let transfer = unsafe { &*transfer_data }; - debug_assert!(transfer.urb_ptr() == urb); debug!( - "URB {:?} for ep {:x} completed, status={} actual_length={}", - transfer.urb_ptr(), - transfer.urb().endpoint, - transfer.urb().status, - transfer.urb().actual_length + "ISO URB {:?} for ep {:x} completed, status={} actual_length={}", + urb, + unsafe { (*urb).endpoint }, + unsafe { (*urb).status }, + unsafe { (*urb).actual_length } ); - if let Some(deadline) = transfer.deadline { - let mut timeouts = self.timeouts.lock().unwrap(); - timeouts.remove(&TimeoutEntry { deadline, urb }); - self.update_timeouts(timeouts, Instant::now()); - } - }; + // SAFETY: pointer came from submit via kernel and we're now done with it + unsafe { notify_completion::(transfer_data) } + } else { + let transfer_data: *mut TransferData = unsafe { &(*urb) }.usercontext.cast(); + + { + let transfer = unsafe { &*transfer_data }; + debug_assert!(transfer.urb_ptr() == urb); + debug!( + "URB {:?} for ep {:x} completed, status={} actual_length={}", + transfer.urb_ptr(), + transfer.urb().endpoint, + transfer.urb().status, + transfer.urb().actual_length + ); + + if let Some(deadline) = transfer.deadline { + let mut timeouts = self.timeouts.lock().unwrap(); + timeouts.remove(&TimeoutEntry { deadline, urb }); + self.update_timeouts(timeouts, Instant::now()); + } + }; - // SAFETY: pointer came from submit via kernel and we're now done with it - unsafe { notify_completion::(transfer_data) } + // SAFETY: pointer came from submit via kernel and we're now done with it + unsafe { notify_completion::(transfer_data) } + } } Err(Errno::AGAIN) => {} Err(Errno::NODEV) => { @@ -521,6 +542,50 @@ impl LinuxDevice { } } + /// Submit an isochronous transfer. + pub(crate) fn submit_iso(&self, transfer: Idle) -> Pending { + let len = transfer.urb().buffer_length; + let num_packets = transfer.num_packets(); + let pending = transfer.pre_submit(); + let urb = pending.urb_ptr(); + + // SAFETY: We got the urb from `Idle`, which always points to + // a valid URB with valid buffers, which is not already pending + unsafe { + let ep = (*urb).endpoint; + (*urb).usercontext = pending.as_ptr().cast(); + if let Err(e) = usbfs::submit_urb(&self.fd, urb) { + // SAFETY: Transfer was not submitted. We still own the transfer + // and can write to the URB and complete it in place of the handler. + let u = &mut *urb; + debug!( + "Failed to submit ISO URB {urb:?}: {len} bytes, {num_packets} packets on ep {ep:x}: {e} {u:?}" + ); + u.actual_length = 0; + u.status = e.raw_os_error(); + notify_completion::(pending.as_ptr().cast()); + } else { + debug!( + "Submitted ISO URB {urb:?}: {len} bytes, {num_packets} packets on ep {ep:x}" + ); + } + }; + + pending + } + + /// Cancel an isochronous transfer. + pub(crate) fn cancel_iso(&self, transfer: &mut Pending) { + let urb = transfer.urb_ptr(); + unsafe { + if let Err(e) = usbfs::discard_urb(&self.fd, urb) { + debug!("Failed to cancel ISO URB {urb:?}: {e}"); + } else { + debug!("Requested cancellation of ISO URB {urb:?}"); + } + } + } + pub(crate) fn speed(&self) -> Option { usbfs::get_speed(&self.fd) .inspect_err(|e| log::error!("USBDEVFS_GET_SPEED failed: {e}")) @@ -700,6 +765,51 @@ impl LinuxInterface { idle_transfer: None, }) } + + /// Open an isochronous endpoint. + /// + /// `num_packets` specifies the number of packets per transfer. This affects + /// how the buffer is divided among packets. + pub fn iso_endpoint( + self: &Arc, + descriptor: EndpointDescriptor, + num_packets: usize, + ) -> Result { + let address = descriptor.address(); + let ep_type = descriptor.transfer_type(); + let max_packet_size = descriptor.max_packet_size(); + + if ep_type != TransferType::Isochronous { + return Err(Error::new(ErrorKind::Other, "endpoint is not isochronous")); + } + + if num_packets == 0 { + return Err(Error::new( + ErrorKind::Other, + "num_packets must be greater than 0", + )); + } + + let mut state = self.state.lock().unwrap(); + + if state.endpoints.is_set(address) { + return Err(Error::new(ErrorKind::Busy, "endpoint already in use")); + } + state.endpoints.set(address); + + Ok(LinuxIsoEndpoint { + inner: Arc::new(EndpointInner { + address, + ep_type, + interface: self.clone(), + notify: Notify::new(), + }), + max_packet_size, + num_packets, + pending: VecDeque::new(), + idle_transfer: None, + }) + } } impl Drop for LinuxInterface { @@ -847,3 +957,108 @@ impl Drop for EndpointInner { state.endpoints.clear(self.address); } } + +/// Linux-specific isochronous endpoint. +/// +/// Isochronous transfers are handled differently from bulk/interrupt: +/// - Each transfer consists of multiple packets +/// - Packets have individual status and length +/// - The `ISO_ASAP` flag is used for scheduling +pub(crate) struct LinuxIsoEndpoint { + inner: Arc, + + pub(crate) max_packet_size: usize, + + /// Number of packets per transfer + num_packets: usize, + + /// A queue of pending transfers + pending: VecDeque>, + + /// Cached idle transfer for reuse + idle_transfer: Option>, +} + +impl LinuxIsoEndpoint { + pub(crate) fn endpoint_address(&self) -> u8 { + self.inner.address + } + + pub(crate) fn pending(&self) -> usize { + self.pending.len() + } + + /// Get the number of packets per transfer. + pub(crate) fn num_packets(&self) -> usize { + self.num_packets + } + + pub(crate) fn cancel_all(&mut self) { + // Cancel transfers in reverse order + for transfer in self.pending.iter_mut().rev() { + self.inner.interface.device.cancel_iso(transfer); + } + } + + fn get_transfer(&mut self) -> Idle { + self.idle_transfer.take().unwrap_or_else(|| { + Idle::new( + self.inner.clone(), + IsoTransferData::new(self.inner.address, self.num_packets), + ) + }) + } + + /// Submit a buffer for isochronous transfer. + /// + /// The buffer should be large enough to hold `num_packets * packet_size` bytes. + pub(crate) fn submit(&mut self, data: Buffer, packet_size: usize) { + let mut transfer = self.get_transfer(); + transfer.set_buffer(data, packet_size); + self.pending + .push_back(self.inner.interface.device.submit_iso(transfer)); + } + + pub(crate) fn poll_next_complete(&mut self, cx: &mut Context) -> Poll { + self.inner.notify.subscribe(cx); + if let Some(mut transfer) = take_completed_from_queue(&mut self.pending) { + let completion = transfer.take_completion(); + self.idle_transfer = Some(transfer); + Poll::Ready(completion) + } else { + Poll::Pending + } + } + + pub(crate) fn wait_next_complete(&mut self, timeout: Duration) -> Option { + self.inner.notify.wait_timeout(timeout, || { + take_completed_from_queue(&mut self.pending).map(|mut transfer| { + let completion = transfer.take_completion(); + self.idle_transfer = Some(transfer); + completion + }) + }) + } + + pub(crate) fn allocate(&self, len: usize) -> Result { + Buffer::mmap(&self.inner.interface.device.fd, len).inspect_err(|e| { + warn!( + "Failed to allocate zero-copy buffer of length {len} for endpoint {}: {e}", + self.inner.address + ); + }) + } +} + +impl Drop for LinuxIsoEndpoint { + fn drop(&mut self) { + if !self.pending.is_empty() { + debug!( + "Dropping iso endpoint {:02x} with {} pending transfers", + self.inner.address, + self.pending.len() + ); + self.cancel_all(); + } + } +} diff --git a/src/platform/linux_usbfs/mod.rs b/src/platform/linux_usbfs/mod.rs index 2e83c94..91b0511 100644 --- a/src/platform/linux_usbfs/mod.rs +++ b/src/platform/linux_usbfs/mod.rs @@ -3,6 +3,7 @@ use std::io; use std::num::NonZeroU32; use rustix::io::Errno; +pub(crate) use transfer::IsoTransferData; pub(crate) use transfer::TransferData; mod usbfs; @@ -24,6 +25,7 @@ mod device; pub(crate) use device::LinuxDevice as Device; pub(crate) use device::LinuxEndpoint as Endpoint; pub(crate) use device::LinuxInterface as Interface; +pub(crate) use device::LinuxIsoEndpoint as IsoEndpoint; use crate::transfer::TransferError; use crate::ErrorKind; diff --git a/src/platform/linux_usbfs/transfer.rs b/src/platform/linux_usbfs/transfer.rs index 3d1311a..4605f98 100644 --- a/src/platform/linux_usbfs/transfer.rs +++ b/src/platform/linux_usbfs/transfer.rs @@ -11,15 +11,15 @@ use crate::{ descriptors::TransferType, transfer::{ internal::Pending, Allocator, Buffer, Completion, ControlIn, ControlOut, Direction, - TransferError, SETUP_PACKET_SIZE, + IsoCompletion, IsoPacketResult, TransferError, SETUP_PACKET_SIZE, }, }; use super::{ errno_to_transfer_error, usbfs::{ - Urb, USBDEVFS_URB_TYPE_BULK, USBDEVFS_URB_TYPE_CONTROL, USBDEVFS_URB_TYPE_INTERRUPT, - USBDEVFS_URB_TYPE_ISO, + IsoPacketDesc, Urb, USBDEVFS_URB_ISO_ASAP, USBDEVFS_URB_TYPE_BULK, + USBDEVFS_URB_TYPE_CONTROL, USBDEVFS_URB_TYPE_INTERRUPT, USBDEVFS_URB_TYPE_ISO, }, }; @@ -186,3 +186,235 @@ impl Drop for TransferData { } } } + +/// Linux-specific isochronous transfer state. +/// +/// Similar to TransferData but handles the variable-length iso_packet_desc array +/// required for isochronous transfers. +pub struct IsoTransferData { + /// Pointer to the URB allocation (includes iso_packet_desc array). + urb: *mut u8, + /// Total allocated size for the URB + iso_packet_desc array. + urb_alloc_size: usize, + /// Number of isochronous packets. + num_packets: usize, + /// Buffer capacity. + capacity: u32, + /// Buffer allocator. + allocator: Allocator, + /// Transfer deadline for timeout (reserved for future use). + #[allow(dead_code)] + pub(crate) deadline: Option, +} + +unsafe impl Send for IsoTransferData {} +unsafe impl Sync for IsoTransferData {} + +impl IsoTransferData { + /// Create a new isochronous transfer with the specified number of packets. + pub(super) fn new(endpoint: u8, num_packets: usize) -> IsoTransferData { + // Calculate allocation size: Urb + num_packets * IsoPacketDesc + let urb_size = mem::size_of::(); + let iso_desc_size = mem::size_of::() * num_packets; + let total_size = urb_size + iso_desc_size; + + // Allocate with proper alignment + let layout = std::alloc::Layout::from_size_align(total_size, mem::align_of::()) + .expect("Invalid layout for IsoUrb"); + + let urb_ptr = unsafe { std::alloc::alloc_zeroed(layout) }; + if urb_ptr.is_null() { + std::alloc::handle_alloc_error(layout); + } + + let mut empty = ManuallyDrop::new(Vec::new()); + + // Initialize the URB + unsafe { + let urb = urb_ptr as *mut Urb; + (*urb).ep_type = USBDEVFS_URB_TYPE_ISO; + (*urb).endpoint = endpoint; + (*urb).status = 0; + (*urb).flags = USBDEVFS_URB_ISO_ASAP; + (*urb).buffer = empty.as_mut_ptr(); + (*urb).buffer_length = 0; + (*urb).actual_length = 0; + (*urb).start_frame = 0; + (*urb).number_of_packets_or_stream_id = num_packets as u32; + (*urb).error_count = 0; + (*urb).signr = 0; + (*urb).usercontext = null_mut(); + + // Initialize iso_packet_desc array to zeros (already done by alloc_zeroed) + } + + IsoTransferData { + urb: urb_ptr, + urb_alloc_size: total_size, + num_packets, + capacity: 0, + allocator: Allocator::Default, + deadline: None, + } + } + + /// Set the buffer and initialize packet descriptors. + /// + /// `packet_size` is the size of each isochronous packet. + pub fn set_buffer(&mut self, buf: Buffer, packet_size: usize) { + debug_assert!(self.capacity == 0); + let buf = ManuallyDrop::new(buf); + self.capacity = buf.capacity; + + let urb = self.urb_mut(); + urb.buffer = buf.ptr; + urb.actual_length = 0; + + let total_len = match Direction::from_address(urb.endpoint) { + Direction::Out => buf.len as i32, + Direction::In => buf.requested_len as i32, + }; + urb.buffer_length = total_len; + + self.allocator = buf.allocator; + + // Initialize packet descriptors + let mut offset = 0usize; + for i in 0..self.num_packets { + let remaining = (total_len as usize).saturating_sub(offset); + let len = remaining.min(packet_size); + + let desc = self.iso_packet_desc_mut(i); + desc.length = len as u32; + desc.actual_length = 0; + desc.status = 0; + + offset += len; + } + } + + /// Take the completion result, consuming the buffer. + pub fn take_completion(&mut self) -> IsoCompletion { + let status = self.status(); + let urb = self.urb(); + + // Collect packet results + let mut packets = Vec::with_capacity(self.num_packets); + let mut offset = 0usize; + for i in 0..self.num_packets { + let desc = self.iso_packet_desc(i); + packets.push(IsoPacketResult { + offset, + length: desc.length as usize, + actual_length: desc.actual_length as usize, + status: desc.status as i32, + }); + offset += desc.length as usize; + } + + let error_count = urb.error_count as usize; + let requested_len = urb.buffer_length as u32; + + // For isochronous transfers, the buffer length is always the requested length + // because each packet's data is placed at its expected offset (based on desc.length), + // not packed together. This is different from bulk/interrupt where actual_length + // represents contiguous data. + let len = urb.buffer_length as u32; + + let mut empty = ManuallyDrop::new(Vec::new()); + let ptr = mem::replace(&mut self.urb_mut().buffer, empty.as_mut_ptr()); + let capacity = mem::replace(&mut self.capacity, 0); + self.urb_mut().buffer_length = 0; + self.urb_mut().actual_length = 0; + let allocator = mem::replace(&mut self.allocator, Allocator::Default); + + IsoCompletion { + buffer: Buffer { + ptr, + len, + requested_len, + capacity, + allocator, + }, + packets, + error_count, + status, + } + } + + #[inline] + pub(super) fn urb(&self) -> &Urb { + unsafe { &*(self.urb as *const Urb) } + } + + #[inline] + pub(super) fn urb_mut(&mut self) -> &mut Urb { + unsafe { &mut *(self.urb as *mut Urb) } + } + + #[inline] + #[allow(dead_code)] + pub(super) fn urb_ptr(&self) -> *mut Urb { + self.urb as *mut Urb + } + + /// Get a reference to the i-th iso packet descriptor. + #[inline] + fn iso_packet_desc(&self, index: usize) -> &IsoPacketDesc { + debug_assert!(index < self.num_packets); + unsafe { + let base = self.urb.add(mem::size_of::()) as *const IsoPacketDesc; + &*base.add(index) + } + } + + /// Get a mutable reference to the i-th iso packet descriptor. + #[inline] + fn iso_packet_desc_mut(&mut self, index: usize) -> &mut IsoPacketDesc { + debug_assert!(index < self.num_packets); + unsafe { + let base = self.urb.add(mem::size_of::()) as *mut IsoPacketDesc; + &mut *base.add(index) + } + } + + /// Get the number of packets in this transfer. + pub fn num_packets(&self) -> usize { + self.num_packets + } + + #[inline] + pub fn status(&self) -> Result<(), TransferError> { + if self.urb().status == 0 { + return Ok(()); + } + + // It's sometimes positive, sometimes negative, but rustix panics if negative. + Err(errno_to_transfer_error(Errno::from_raw_os_error( + self.urb().status.abs(), + ))) + } +} + +impl Pending { + pub fn urb_ptr(&self) -> *mut Urb { + // Get urb pointer without dereferencing as `IsoTransferData`, because + // it may be mutably aliased. + unsafe { *addr_of_mut!((*self.as_ptr()).urb) as *mut Urb } + } +} + +impl Drop for IsoTransferData { + fn drop(&mut self) { + unsafe { + // Take and drop the completion to free the buffer + drop(self.take_completion()); + + // Free the URB allocation + let layout = + std::alloc::Layout::from_size_align(self.urb_alloc_size, mem::align_of::()) + .expect("Invalid layout for IsoUrb"); + std::alloc::dealloc(self.urb, layout); + } + } +} diff --git a/src/platform/linux_usbfs/usbfs.rs b/src/platform/linux_usbfs/usbfs.rs index e0e6ea1..53a9627 100644 --- a/src/platform/linux_usbfs/usbfs.rs +++ b/src/platform/linux_usbfs/usbfs.rs @@ -179,7 +179,7 @@ pub fn reset(fd: Fd) -> io::Result<()> { } const USBDEVFS_URB_SHORT_NOT_OK: c_uint = 0x01; -const USBDEVFS_URB_ISO_ASAP: c_uint = 0x02; +pub const USBDEVFS_URB_ISO_ASAP: c_uint = 0x02; const USBDEVFS_URB_BULK_CONTINUATION: c_uint = 0x04; const USBDEVFS_URB_ZERO_PACKET: c_uint = 0x40; const USBDEVFS_URB_NO_INTERRUPT: c_uint = 0x80; @@ -189,6 +189,19 @@ pub const USBDEVFS_URB_TYPE_INTERRUPT: c_uchar = 1; pub const USBDEVFS_URB_TYPE_CONTROL: c_uchar = 2; pub const USBDEVFS_URB_TYPE_BULK: c_uchar = 3; +/// ISO packet descriptor for isochronous transfers. +/// Each packet in an isochronous transfer has its own status and length. +#[repr(C)] +#[derive(Debug, Clone, Copy, Default)] +pub struct IsoPacketDesc { + /// Expected packet length (for OUT) or maximum expected length (for IN) + pub length: c_uint, + /// Actual number of bytes transferred + pub actual_length: c_uint, + /// Per-packet status (0 = success, negative = error) + pub status: c_uint, +} + #[repr(C)] #[derive(Debug)] pub struct Urb { diff --git a/src/transfer/mod.rs b/src/transfer/mod.rs index bd144f0..1bf7323 100644 --- a/src/transfer/mod.rs +++ b/src/transfer/mod.rs @@ -132,6 +132,13 @@ impl EndpointType for Interrupt { } impl BulkOrInterrupt for Interrupt {} +/// Type-level endpoint type: Isochronous +pub enum Isochronous {} +impl private::Sealed for Isochronous {} +impl EndpointType for Isochronous { + const TYPE: TransferType = TransferType::Isochronous; +} + /// A completed transfer returned from [`Endpoint::next_complete`][`crate::Endpoint::next_complete`]. /// /// A transfer can partially complete even in the case of failure or @@ -157,3 +164,45 @@ impl Completion { self.status.map(|()| self.buffer) } } + +/// A completed isochronous packet with its status and data range. +#[derive(Debug, Clone)] +pub struct IsoPacketResult { + /// Offset into the buffer where this packet's data starts. + pub offset: usize, + /// Expected packet length. + pub length: usize, + /// Actual number of bytes transferred. + pub actual_length: usize, + /// Per-packet status (0 = success). + pub status: i32, +} + +/// Completion result for an isochronous transfer. +/// +/// Unlike bulk/interrupt transfers, isochronous transfers consist of multiple +/// packets, each with its own status and length. The overall transfer may +/// succeed even if individual packets fail (with `error_count > 0`). +#[derive(Debug)] +pub struct IsoCompletion { + /// The transfer buffer containing all packet data. + pub buffer: Buffer, + /// Per-packet results, in order. + pub packets: Vec, + /// Number of packets with errors. + pub error_count: usize, + /// Overall transfer status. + pub status: Result<(), TransferError>, +} + +impl IsoCompletion { + /// Get an iterator over the packets that completed successfully. + pub fn successful_packets(&self) -> impl Iterator { + self.packets.iter().filter(|p| p.status == 0) + } + + /// Get the total number of bytes actually transferred across all packets. + pub fn total_actual_length(&self) -> usize { + self.packets.iter().map(|p| p.actual_length).sum() + } +}