commit 85551a97793637cc1fcde36fd7f643298247717a Author: Kevin Mehall Date: Sat Sep 30 16:51:13 2023 -0600 Initial commit: bulk transfers on Linux usbfs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3892438 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "nusb" +version = "0.1.0" +description = "Low-level access to USB devices in pure Rust" +authors = ["Kevin Mehall "] +edition = "2021" +license = "Apache-2.0 OR MIT" + +[dependencies] +atomic-waker = "1.1.2" +futures-lite = "1.13.0" +log = "0.4.20" +once_cell = "1.18.0" +slab = "0.4.9" + +[dev-dependencies] +env_logger = "0.10.0" + +[target.'cfg(target_os="linux")'.dependencies] +rustix = { version = "0.38.15", features = ["fs", "event"] } + +[patch.crates-io] +rustix = { path = "../rustix" } + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..55859f2 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +nusb +---- + +A new pure-Rust library for cross-platform low-level access to USB devices. diff --git a/examples/bulk.rs b/examples/bulk.rs new file mode 100644 index 0000000..3a72011 --- /dev/null +++ b/examples/bulk.rs @@ -0,0 +1,23 @@ +use futures_lite::future::block_on; +use nusb::TransferStatus; + +fn main() { + env_logger::init(); + let di = nusb::list_devices() + .unwrap() + .find(|d| d.vendor_id() == 0x59e3 && d.product_id() == 0x0a23) + .expect("device should be connected"); + + println!("Device info: {di:?}"); + + let device = di.open().unwrap(); + let interface = device.claim_interface(0).unwrap(); + + loop { + let result = block_on(interface.bulk_transfer(0x81, Vec::with_capacity(256))); + println!("{result:?}"); + if result.status != TransferStatus::Complete { + break; + } + } +} diff --git a/examples/list.rs b/examples/list.rs new file mode 100644 index 0000000..a84fa56 --- /dev/null +++ b/examples/list.rs @@ -0,0 +1,6 @@ +fn main() { + env_logger::init(); + for dev in nusb::list_devices().unwrap() { + println!("{:#?}", dev); + } +} diff --git a/src/device.rs b/src/device.rs new file mode 100644 index 0000000..1254cfa --- /dev/null +++ b/src/device.rs @@ -0,0 +1,105 @@ +use std::{collections::VecDeque, sync::Arc, time::Duration}; + +use crate::{transfer::EndpointType, Completion, DeviceInfo, Error, Transfer}; + +type TransferError = Error; +type Buffer = Vec; + +#[derive(Clone)] +pub struct Device { + backend: Arc, +} + +impl Device { + pub(crate) fn open(d: &DeviceInfo) -> Result { + let backend = crate::platform::Device::from_device_info(d)?; + Ok(Device { backend }) + } + + pub fn set_configuration(&self, configuration: u8) -> Result<(), Error> { + self.backend.set_configuration(configuration) + } + + pub fn reset(&self) -> Result<(), Error> { + self.backend.reset() + } + + pub fn claim_interface(&self, interface: u8) -> Result { + let backend = self.backend.claim_interface(interface)?; + Ok(Interface { backend }) + } +} + +pub struct Interface { + backend: Arc, +} + +impl Interface { + pub fn set_alt_setting(&self) { + todo!() + } + + pub fn bulk_transfer(&self, endpoint: u8, buf: Vec) -> Transfer { + let mut t = Transfer::new(self.backend.clone(), endpoint, EndpointType::Bulk); + t.submit(buf); + t + } + + pub fn interrupt_transfer(&self, endpoint: u8, buf: Vec) -> Transfer { + let mut t = Transfer::new(self.backend.clone(), endpoint, EndpointType::Interrupt); + t.submit(buf); + t + } +} + +struct Queue { + pending: VecDeque, + cached: Option, +} + +impl Queue { + /// Submit a new transfer on the endpoint. + /// + /// For an IN endpoint, the transfer size is set by the *capacity* of + /// the buffer, and the length and current contents are ignored. The + /// buffer is returned from a later call to `complete` filled with + /// the data read from the endpoint. + /// + /// For an OUT endpoint, the contents of the buffer are written to + /// the endpoint. + pub fn submit(&mut self, buf: Buffer) -> Result<(), TransferError> { + todo!() + } + + /// Block waiting for the next pending transfer to complete, and return + /// its buffer or an error status. + /// + /// For an IN endpoint, the returned buffer contains the data + /// read from the device. + /// + /// For an OUT endpoint, the buffer is unmodified, but can be + /// reused for another transfer. + pub fn complete(&mut self, timeout: Option) -> Option { + todo!() + } + + /// Get the number of transfers that have been submitted with + /// `submit` that have not yet been returned from `complete`. + pub fn pending_transfers(&self) -> usize { + todo!() + } + + /// Get the number of transfers that have completed and are + /// ready to be returned from `complete` without blocking. + pub fn ready_transfers(&self) -> usize { + todo!() + } + + /// Cancel all pending transfers on the endpoint pipe. + /// TODO: maybe this should be on the `Device` or an object separable from the `Pipe` + /// so it can be called from another thread, and cause a blocking `complete` call to + //// immediately return. + fn cancel_all(&mut self) -> Result<(), TransferError> { + todo!() + } +} diff --git a/src/enumeration.rs b/src/enumeration.rs new file mode 100644 index 0000000..73fcf79 --- /dev/null +++ b/src/enumeration.rs @@ -0,0 +1,115 @@ +use std::{fmt::Display, str::FromStr}; + +use crate::{platform, Device, Error}; + +#[derive(Debug, Clone)] +pub struct DeviceInfo { + #[cfg(target_os = "linux")] + pub(crate) path: crate::platform::SysfsPath, + + pub(crate) bus_number: u8, + pub(crate) device_address: u8, + + pub(crate) vendor_id: u16, + pub(crate) product_id: u16, + + pub(crate) device_version: u16, + pub(crate) class: u8, + pub(crate) subclass: u8, + pub(crate) protocol: u8, + + pub(crate) speed: Speed, + + pub(crate) manufacturer_string: Option, + pub(crate) product_string: Option, + pub(crate) serial_number: Option, +} + +impl DeviceInfo { + #[cfg(target_os = "linux")] + pub fn path(&self) -> &platform::SysfsPath { + &self.path + } + + pub fn bus_number(&self) -> u8 { + self.bus_number + } + pub fn device_address(&self) -> u8 { + self.device_address + } + + pub fn vendor_id(&self) -> u16 { + self.vendor_id + } + pub fn product_id(&self) -> u16 { + self.product_id + } + + pub fn device_version(&self) -> u16 { + self.device_version + } + + pub fn class(&self) -> u8 { + self.class + } + pub fn subclass(&self) -> u8 { + self.subclass + } + pub fn protocol(&self) -> u8 { + self.protocol + } + + pub fn speed(&self) -> Speed { + self.speed + } + + pub fn manufacturer_string(&self) -> Option<&str> { + self.manufacturer_string.as_deref() + } + pub fn product_string(&self) -> Option<&str> { + self.product_string.as_deref() + } + pub fn serial_number(&self) -> Option<&str> { + self.serial_number.as_deref() + } + + pub fn open(&self) -> Result { + Device::open(self) + } +} + +#[derive(Copy, Clone, Eq, PartialOrd, Ord, PartialEq, Hash, Debug)] +#[non_exhaustive] +pub enum Speed { + Low, + Full, + High, + Super, + SuperPlus, +} + +#[derive(Copy, Clone, Debug)] +pub struct UnknownValue; + +impl Display for UnknownValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Unknown value") + } +} + +impl std::error::Error for UnknownValue {} + +impl FromStr for Speed { + type Err = UnknownValue; //TODO + + fn from_str(s: &str) -> Result { + match s { + "low" | "1.5" => Ok(Speed::Low), + "full" | "12" => Ok(Speed::Full), + "high" | "480" => Ok(Speed::High), + "super" | "5000" => Ok(Speed::Super), + "super+" | "10000" => Ok(Speed::SuperPlus), + _ => Err(UnknownValue), + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ff31726 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,16 @@ +use std::{fmt::Display, io, str::FromStr}; + +pub mod platform; + +use device::Device; +pub use platform::list_devices; + +mod enumeration; +pub use enumeration::{DeviceInfo, Speed, UnknownValue}; + +mod device; + +mod transfer; +pub use transfer::{Completion, Transfer, TransferStatus}; + +pub type Error = io::Error; diff --git a/src/platform/linux_usbfs/device.rs b/src/platform/linux_usbfs/device.rs new file mode 100644 index 0000000..2549e6e --- /dev/null +++ b/src/platform/linux_usbfs/device.rs @@ -0,0 +1,158 @@ +use std::{path::PathBuf, sync::Arc}; + +use log::{debug, error}; +use rustix::{ + fd::OwnedFd, + fs::{Mode, OFlags}, + io::Errno, +}; + +use super::{events, usbfs}; +use crate::{ + platform::linux_usbfs::transfer::{Transfer, TransferInner}, + DeviceInfo, Error, +}; + +pub(crate) struct LinuxDevice { + fd: OwnedFd, + events_id: usize, +} + +impl LinuxDevice { + pub(crate) fn from_device_info(d: &DeviceInfo) -> Result, Error> { + Self::open(d.bus_number(), d.device_address()) + } + + pub(crate) fn open(busnum: u8, devnum: u8) -> Result, Error> { + let path = PathBuf::from(format!("/dev/bus/usb/{busnum:03}/{devnum:03}")); + debug!("Opening usbfs device {}", path.display()); + let fd = rustix::fs::open(path, OFlags::RDWR | OFlags::CLOEXEC, Mode::empty())?; + + // because there's no Arc::try_new_cyclic + let mut events_err = None; + let arc = Arc::new_cyclic(|weak| { + let res = events::register(&fd, weak.clone()); + let events_id = *res.as_ref().unwrap_or(&usize::MAX); + events_err = res.err(); + LinuxDevice { fd, events_id } + }); + + if let Some(err) = events_err { + error!("Failed to initialize event loop: {err}"); + Err(err) + } else { + debug!( + "Opened device bus={busnum} addr={devnum} with id {}", + arc.events_id + ); + Ok(arc) + } + } + + pub(crate) fn handle_events(&self) { + debug!("Handling events for device {}", self.events_id); + match usbfs::reap_urb_ndelay(&self.fd) { + Ok(urb_ptr) => { + { + let urb = unsafe { &*urb_ptr }; + debug!( + "URB {:?} for ep {:x} completed, status={} actual_length={}", + urb_ptr, urb.endpoint, urb.status, urb.actual_length + ); + } + unsafe { Transfer::notify_completion(urb_ptr as *mut TransferInner) } + } + Err(Errno::AGAIN) => {} + Err(Errno::NODEV) => { + debug!("Device {} disconnected", self.events_id); + + // epoll returns events continuously on a disconnected device, and REAPURB + // only returns ENODEV after all events are received, so unregister to + // keep the event thread from spinning because we won't receive further events. + // The drop impl will try to unregister again, but that's ok. + events::unregister_fd(&self.fd); + } + Err(e) => { + error!("Unexpected error {e} from REAPURBNDELAY"); + } + } + } + + pub(crate) fn set_configuration(&self, configuration: u8) -> Result<(), Error> { + usbfs::set_configuration(&self.fd, configuration)?; + Ok(()) + } + + pub(crate) fn reset(&self) -> Result<(), Error> { + usbfs::reset(&self.fd)?; + Ok(()) + } + + pub(crate) fn claim_interface( + self: &Arc, + interface: u8, + ) -> Result, Error> { + usbfs::claim_interface(&self.fd, interface)?; + debug!( + "Claimed interface {interface} on device id {dev}", + dev = self.events_id + ); + Ok(Arc::new(LinuxInterface { + device: self.clone(), + interface, + })) + } +} + +impl Drop for LinuxDevice { + fn drop(&mut self) { + debug!("Closing device {}", self.events_id); + events::unregister(&self.fd, self.events_id) + } +} + +pub(crate) struct LinuxInterface { + pub(crate) interface: u8, + pub(crate) device: Arc, +} + +impl LinuxInterface { + pub(crate) unsafe fn submit_transfer(&self, transfer: *mut TransferInner) { + let urb = transfer as *mut usbfs::Urb; + let ep = unsafe { (&mut *urb).endpoint }; + if let Err(e) = usbfs::submit_urb(&self.device.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. + unsafe { + { + let u = &mut *urb; + debug!("Failed to submit URB {urb:?} on ep {ep:x}: {e} {u:?}"); + u.actual_length = 0; + u.status = e.raw_os_error(); + } + Transfer::notify_completion(transfer) + } + } else { + debug!("Submitted URB {urb:?} on ep {ep:x}"); + } + } + + pub(crate) unsafe fn cancel_transfer(&self, transfer: *mut TransferInner) { + unsafe { + let urb = transfer as *mut usbfs::Urb; + if let Err(e) = usbfs::discard_urb(&self.device.fd, urb) { + debug!("Failed to cancel URB {urb:?}: {e}"); + } + } + } +} + +impl Drop for LinuxInterface { + fn drop(&mut self) { + let res = usbfs::release_interface(&self.device.fd, self.interface); + debug!( + "Released interface {} on device {}: {res:?}", + self.interface, self.device.events_id + ); + } +} diff --git a/src/platform/linux_usbfs/enumeration.rs b/src/platform/linux_usbfs/enumeration.rs new file mode 100644 index 0000000..d46c2f8 --- /dev/null +++ b/src/platform/linux_usbfs/enumeration.rs @@ -0,0 +1,87 @@ +use std::fs; +use std::io; +use std::num::ParseIntError; +use std::path::PathBuf; +use std::str::FromStr; + +use log::debug; + +use crate::DeviceInfo; +use crate::Error; + +#[derive(Debug, Clone)] +pub struct SysfsPath(PathBuf); + +impl SysfsPath { + fn read_attr(&self, attr: &str) -> Result + where + T: FromStr, + T::Err: std::error::Error + Send + Sync + 'static, + { + let attr_path = self.0.join(attr); + let read_res = fs::read_to_string(&attr_path); + debug!("sysfs read {attr_path:?}: {read_res:?}"); + + read_res? + .trim() + .parse() + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) + } + + fn read_attr_hex(&self, attr: &str) -> Result { + let s = self.read_attr::(attr)?; + T::from_hex_str(&s) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid hex str")) + } +} + +trait FromHexStr: Sized { + fn from_hex_str(s: &str) -> Result; +} + +impl FromHexStr for u8 { + fn from_hex_str(s: &str) -> Result { + u8::from_str_radix(s, 16) + } +} + +impl FromHexStr for u16 { + fn from_hex_str(s: &str) -> Result { + u16::from_str_radix(s, 16) + } +} + +const SYSFS_PREFIX: &'static str = "/sys/bus/usb/devices/"; + +pub fn list_devices() -> Result, Error> { + Ok(fs::read_dir(SYSFS_PREFIX)?.flat_map(|entry| { + let res = probe_device(SysfsPath(entry.ok()?.path())); + if let Err(x) = &res { + debug!("failed to probe, skipping: {x}") + } + res.ok() + })) +} + +pub fn probe_device(path: SysfsPath) -> Result { + debug!("probe device {path:?}"); + Ok(DeviceInfo { + bus_number: path.read_attr("busnum")?, + device_address: path.read_attr("devnum")?, + vendor_id: path.read_attr_hex("idVendor")?, + product_id: path.read_attr_hex("idProduct")?, + device_version: path.read_attr_hex("bcdDevice")?, + class: path.read_attr_hex("bDeviceClass")?, + subclass: path.read_attr_hex("bDeviceSubClass")?, + protocol: path.read_attr_hex("bDeviceProtocol")?, + speed: path.read_attr("speed")?, + manufacturer_string: path.read_attr("manufacturer").ok(), + product_string: path.read_attr("product").ok(), + serial_number: path.read_attr("serial").ok(), + path: path, + }) +} +/// Returns the path of a device in usbfs +fn usb_devfs_path(busnum: u8, devnum: u8) -> PathBuf { + PathBuf::from(format!("/dev/bus/usb/{busnum:03}/{devnum:03}")) +} diff --git a/src/platform/linux_usbfs/events.rs b/src/platform/linux_usbfs/events.rs new file mode 100644 index 0000000..40992c4 --- /dev/null +++ b/src/platform/linux_usbfs/events.rs @@ -0,0 +1,66 @@ +use once_cell::sync::OnceCell; +use rustix::{ + event::epoll::{self, EventData}, + fd::OwnedFd, +}; +use slab::Slab; +use std::{ + sync::{Mutex, Weak}, + thread, +}; + +use crate::Error; + +use super::Device; + +static EPOLL_FD: OnceCell = OnceCell::new(); +static DEVICES: Mutex>> = Mutex::new(Slab::new()); + +pub(super) fn register(usb_fd: &OwnedFd, weak_device: Weak) -> Result { + let mut start_thread = false; + let epoll_fd = EPOLL_FD.get_or_try_init(|| { + start_thread = true; + epoll::create(epoll::CreateFlags::CLOEXEC) + })?; + + let id = { + let mut devices = DEVICES.lock().unwrap(); + devices.insert(weak_device) + }; + + if start_thread { + thread::spawn(event_loop); + } + + let data = EventData::new_u64(id as u64); + epoll::add(epoll_fd, usb_fd, data, epoll::EventFlags::OUT)?; + Ok(id) +} + +pub(super) fn unregister_fd(fd: &OwnedFd) { + let epoll_fd = EPOLL_FD.get().unwrap(); + epoll::delete(epoll_fd, fd).ok(); +} + +pub(super) fn unregister(fd: &OwnedFd, events_id: usize) { + let epoll_fd = EPOLL_FD.get().unwrap(); + epoll::delete(epoll_fd, fd).ok(); + DEVICES.lock().unwrap().remove(events_id); +} + +fn event_loop() { + let epoll_fd = EPOLL_FD.get().unwrap(); + let mut event_list = epoll::EventVec::with_capacity(4); + loop { + epoll::wait(epoll_fd, &mut event_list, -1).unwrap(); + let devices = DEVICES.lock().unwrap(); + for event in &event_list { + let key = event.data.u64() as usize; + let device = devices.get(key).and_then(|w| w.upgrade()); + + if let Some(device) = device { + device.handle_events(); + } + } + } +} diff --git a/src/platform/linux_usbfs/mod.rs b/src/platform/linux_usbfs/mod.rs new file mode 100644 index 0000000..7779628 --- /dev/null +++ b/src/platform/linux_usbfs/mod.rs @@ -0,0 +1,13 @@ +use std::path::PathBuf; + +mod transfer; +mod usbfs; +pub use transfer::Transfer; + +mod enumeration; +mod events; +pub use enumeration::{list_devices, SysfsPath}; + +mod device; +pub(crate) use device::LinuxDevice as Device; +pub(crate) use device::LinuxInterface as Interface; diff --git a/src/platform/linux_usbfs/transfer.rs b/src/platform/linux_usbfs/transfer.rs new file mode 100644 index 0000000..4847ac6 --- /dev/null +++ b/src/platform/linux_usbfs/transfer.rs @@ -0,0 +1,242 @@ +use std::{ + cell::UnsafeCell, + future::Future, + mem::{self, ManuallyDrop}, + ptr::{null_mut, NonNull}, + sync::{ + atomic::{AtomicU8, Ordering}, + Arc, + }, + task::{Context, Poll}, +}; + +use atomic_waker::AtomicWaker; +use rustix::io::Errno; + +use crate::{transfer::EndpointType, Completion, TransferStatus}; + +use super::{ + usbfs::{ + Urb, USBDEVFS_URB_TYPE_BULK, USBDEVFS_URB_TYPE_CONTROL, USBDEVFS_URB_TYPE_INTERRUPT, + USBDEVFS_URB_TYPE_ISO, + }, + Interface, +}; + +#[repr(C)] +pub(crate) struct TransferInner { + urb: UnsafeCell, + state: AtomicU8, + waker: AtomicWaker, + interface: Arc, +} + +impl TransferInner { + /// Transfer ownership of `buf` into the transfer's `urb`. + /// SAFETY: requires that there is no concurrent access to `urb` + unsafe fn put_buffer(&self, buf: Vec) { + unsafe { + let mut buf = ManuallyDrop::new(buf); + let urb = &mut *self.urb.get(); + urb.buffer = buf.as_mut_ptr(); + assert!(buf.len() < i32::MAX as usize, "Buffer too large"); + urb.actual_length = buf.len() as i32; + assert!(buf.capacity() < i32::MAX as usize, "Buffer too large"); + urb.buffer_length = buf.capacity() as i32; + } + } + + /// Transfer ownership of transfer's `urb` buffer back to a `Vec`. + /// SAFETY: requires that the the buffer is present and there is no concurrent + /// access to `urb`. Invalidates the buffer. + unsafe fn take_buffer(&self) -> Vec { + unsafe { + let urb = &mut *self.urb.get(); + Vec::from_raw_parts( + mem::replace(&mut urb.buffer, null_mut()), + urb.actual_length as usize, + urb.buffer_length as usize, + ) + } + } + + /// Get the transfer status + /// SAFETY: requires that there is no concurrent access to `urb` + unsafe fn status(&self) -> TransferStatus { + let status = unsafe { (&*self.urb.get()).status }; + + if status == 0 { + return TransferStatus::Complete; + } + + // It's sometimes positive, sometimes negative, but rustix panics if negative. + match Errno::from_raw_os_error(status.abs()) { + Errno::NODEV | Errno::SHUTDOWN => TransferStatus::Disconnected, + Errno::PIPE => TransferStatus::Stall, + Errno::NOENT | Errno::CONNRESET => TransferStatus::Cancelled, + Errno::PROTO | Errno::ILSEQ | Errno::OVERFLOW | Errno::COMM | Errno::TIME => { + TransferStatus::Fault + } + _ => TransferStatus::UnknownError, + } + } +} + +pub struct Transfer { + ptr: NonNull, +} + +/// The transfer has not been submitted. The buffer is not valid. +const STATE_IDLE: u8 = 0; + +/// The transfer has been submitted to the kernel and completion has not yet +/// been handled. The buffer points to valid memory but cannot be accessed by +/// userspace. There is a future or queue waiting for it completion. +const STATE_PENDING: u8 = 1; + +/// Like PENDING, but there is no one waiting for completion. The completion +/// handler will drop the buffer and transfer. +const STATE_ABANDONED: u8 = 3; + +/// The transfer completion has been handled. The buffer is valid and may +/// be accessed. +const STATE_COMPLETED: u8 = 3; + +impl Transfer { + pub(crate) fn new(interface: Arc, endpoint: u8, ep_type: EndpointType) -> Transfer { + let ep_type = match ep_type { + EndpointType::Control => USBDEVFS_URB_TYPE_CONTROL, + EndpointType::Interrupt => USBDEVFS_URB_TYPE_INTERRUPT, + EndpointType::Bulk => USBDEVFS_URB_TYPE_BULK, + EndpointType::Isochronous => USBDEVFS_URB_TYPE_ISO, + }; + + let b = Box::new(TransferInner { + urb: UnsafeCell::new(Urb { + ep_type, + endpoint, + status: 0, + flags: 0, + buffer: null_mut(), + buffer_length: 0, + actual_length: 0, + start_frame: 0, + number_of_packets_or_stream_id: 0, + error_count: 0, + signr: 0, + usercontext: null_mut(), + }), + state: AtomicU8::new(STATE_IDLE), + waker: AtomicWaker::new(), + interface, + }); + + Transfer { + ptr: Box::leak(b).into(), + } + } + + fn inner(&self) -> &TransferInner { + // Safety: while Transfer is alive, its TransferInner is alive + unsafe { self.ptr.as_ref() } + } + + /// Prepare the transfer for submission by filling the buffer fields + /// and setting the state to PENDING. Returns a `*mut TransferInner` + /// that must later be passed to `complete`. + /// + /// Panics if the transfer has already been submitted. + pub(crate) fn submit(&mut self, data: Vec) { + let inner = self.inner(); + assert_eq!( + inner.state.load(Ordering::Acquire), + STATE_IDLE, + "Transfer should be idle when submitted" + ); + unsafe { + // SAFETY: invariants guaranteed by being in state IDLE + inner.put_buffer(data); + } + inner.state.store(STATE_PENDING, Ordering::Release); + unsafe { + inner.interface.submit_transfer(self.ptr.as_ptr()); + } + } + + pub(crate) fn cancel(&mut self) { + let inner = self.inner(); + unsafe { + inner.interface.cancel_transfer(self.ptr.as_ptr()); + } + } + + pub fn poll_completion(&self, cx: &Context) -> Poll { + let inner = self.inner(); + inner.waker.register(cx.waker()); + match inner.state.load(Ordering::Acquire) { + STATE_PENDING => Poll::Pending, + STATE_COMPLETED => { + // SAFETY: state means we have exclusive access + // and the buffer is valid. + inner.state.store(STATE_IDLE, Ordering::Relaxed); + unsafe { + let data = inner.take_buffer(); + let status = inner.status(); + Poll::Ready(Completion { data, status }) + } + } + s => panic!("Polling transfer in unexpected state {s}"), + } + } + + pub(crate) unsafe fn notify_completion(transfer: *mut TransferInner) { + unsafe { + let waker = (*transfer).waker.take(); + match (*transfer).state.swap(STATE_COMPLETED, Ordering::Release) { + STATE_PENDING => { + if let Some(waker) = waker { + waker.wake() + } + } + STATE_ABANDONED => { + let b = Box::from_raw(transfer); + drop(b.take_buffer()); + drop(b); + } + s => panic!("Completing transfer in unexpected state {s}"), + } + } + } +} + +impl Drop for Transfer { + fn drop(&mut self) { + match self.inner().state.swap(STATE_ABANDONED, Ordering::Acquire) { + STATE_PENDING => { + self.cancel(); + /* handler responsible for dropping */ + } + STATE_IDLE => { + // SAFETY: state means there is no concurrent access + unsafe { drop(Box::from_raw(self.ptr.as_ptr())) } + } + STATE_COMPLETED => { + // SAFETY: state means buffer is valid and there is no concurrent access + unsafe { + let b = Box::from_raw(self.ptr.as_ptr()); + drop(b.take_buffer()); + drop(b); + } + } + s => panic!("Dropping transfer in unexpected state {s}"), + } + } +} + +impl Future for Transfer { + type Output = Completion; + + fn poll(mut self: std::pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.as_mut().poll_completion(cx) + } +} diff --git a/src/platform/linux_usbfs/usbfs.rs b/src/platform/linux_usbfs/usbfs.rs new file mode 100644 index 0000000..8b40e38 --- /dev/null +++ b/src/platform/linux_usbfs/usbfs.rs @@ -0,0 +1,133 @@ +use std::{ + ffi::{c_int, c_uchar, c_uint, c_void}, + marker::PhantomData, +}; + +/// Wrappers for the [usbfs] character device ioctls, translated from the +/// [C structures and ioctl definitions][uapi]. +/// +/// [usbfs]: https://www.kernel.org/doc/html/latest/driver-api/usb/usb.html#the-usb-character-device-nodes +/// [uapi]: https://github.com/torvalds/linux/blob/master/tools/include/uapi/linux/usbdevice_fs.h +use rustix::{ + fd::AsFd, + io, + ioctl::{self, CompileTimeOpcode, Ioctl, IoctlOutput}, +}; + +pub fn set_configuration(fd: Fd, configuration: u8) -> io::Result<()> { + unsafe { + let ctl = + ioctl::Setter::, c_uint>::new(configuration.into()); + ioctl::ioctl(fd, ctl) + } +} + +pub fn claim_interface(fd: Fd, interface: u8) -> io::Result<()> { + unsafe { + let ctl = + ioctl::Setter::, c_uint>::new(interface.into()); + ioctl::ioctl(fd, ctl) + } +} + +pub fn release_interface(fd: Fd, interface: u8) -> io::Result<()> { + unsafe { + let ctl = + ioctl::Setter::, c_uint>::new(interface.into()); + ioctl::ioctl(fd, ctl) + } +} + +pub struct PassPtr { + input: *mut Input, + _opcode: PhantomData, +} + +impl PassPtr { + /// Create a new pointer setter-style `ioctl` object. + /// + /// # Safety + /// + /// - `Opcode` must provide a valid opcode. + /// - For this opcode, `Input` must be the type that the kernel expects to + /// get. + #[inline] + pub unsafe fn new(input: *mut Input) -> Self { + Self { + input, + _opcode: PhantomData, + } + } +} + +unsafe impl Ioctl for PassPtr { + type Output = (); + + const IS_MUTATING: bool = false; + const OPCODE: rustix::ioctl::Opcode = Opcode::OPCODE; + + fn as_ptr(&mut self) -> *mut c_void { + self.input as *mut c_void + } + + unsafe fn output_from_ptr(_: IoctlOutput, _: *mut c_void) -> rustix::io::Result { + Ok(()) + } +} + +pub unsafe fn submit_urb(fd: Fd, urb: *mut Urb) -> io::Result<()> { + unsafe { + let ctl = PassPtr::, Urb>::new(urb); + ioctl::ioctl(fd, ctl) + } +} + +pub fn reap_urb_ndelay(fd: Fd) -> io::Result<*mut Urb> { + unsafe { + let ctl = ioctl::Getter::, *mut Urb>::new(); + ioctl::ioctl(fd, ctl) + } +} + +pub unsafe fn discard_urb(fd: Fd, urb: *mut Urb) -> io::Result<()> { + unsafe { + let ctl = PassPtr::, Urb>::new(urb); + ioctl::ioctl(fd, ctl) + } +} + +pub fn reset(fd: Fd) -> io::Result<()> { + unsafe { + let ctl = ioctl::NoArg::>::new(); + ioctl::ioctl(fd, ctl) + } +} + +const USBDEVFS_URB_SHORT_NOT_OK: c_uint = 0x01; +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; + +pub const USBDEVFS_URB_TYPE_ISO: c_uchar = 0; +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; + +#[repr(C)] +#[derive(Debug)] +pub struct Urb { + pub ep_type: c_uchar, + pub endpoint: c_uchar, + pub status: c_int, + pub flags: c_uint, + pub buffer: *mut u8, + pub buffer_length: c_int, + pub actual_length: c_int, + pub start_frame: c_int, + pub number_of_packets_or_stream_id: c_uint, // a union in C + pub error_count: c_int, + pub signr: c_uint, + pub usercontext: *mut c_void, + // + variable size array of iso_packet_desc +} diff --git a/src/platform/mod.rs b/src/platform/mod.rs new file mode 100644 index 0000000..e1c7023 --- /dev/null +++ b/src/platform/mod.rs @@ -0,0 +1,5 @@ +#[cfg(target_os = "linux")] +mod linux_usbfs; + +#[cfg(target_os = "linux")] +pub use linux_usbfs::*; diff --git a/src/transfer.rs b/src/transfer.rs new file mode 100644 index 0000000..e7ab4c8 --- /dev/null +++ b/src/transfer.rs @@ -0,0 +1,25 @@ +pub use crate::platform::Transfer; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum EndpointType { + Control, + Interrupt, + Bulk, + Isochronous, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum TransferStatus { + Complete, + Cancelled, + Stall, + Disconnected, + Fault, + UnknownError, +} + +#[derive(Debug, Clone)] +pub struct Completion { + pub data: Vec, + pub status: TransferStatus, +}