Initial commit: bulk transfers on Linux usbfs
This commit is contained in:
commit
85551a9779
16 changed files with 1025 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
Cargo.lock
|
||||
25
Cargo.toml
Normal file
25
Cargo.toml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
[package]
|
||||
name = "nusb"
|
||||
version = "0.1.0"
|
||||
description = "Low-level access to USB devices in pure Rust"
|
||||
authors = ["Kevin Mehall <km@kevinmehall.net>"]
|
||||
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" }
|
||||
|
||||
|
||||
4
README.md
Normal file
4
README.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
nusb
|
||||
----
|
||||
|
||||
A new pure-Rust library for cross-platform low-level access to USB devices.
|
||||
23
examples/bulk.rs
Normal file
23
examples/bulk.rs
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
6
examples/list.rs
Normal file
6
examples/list.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
fn main() {
|
||||
env_logger::init();
|
||||
for dev in nusb::list_devices().unwrap() {
|
||||
println!("{:#?}", dev);
|
||||
}
|
||||
}
|
||||
105
src/device.rs
Normal file
105
src/device.rs
Normal file
|
|
@ -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<u8>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Device {
|
||||
backend: Arc<crate::platform::Device>,
|
||||
}
|
||||
|
||||
impl Device {
|
||||
pub(crate) fn open(d: &DeviceInfo) -> Result<Device, std::io::Error> {
|
||||
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<Interface, Error> {
|
||||
let backend = self.backend.claim_interface(interface)?;
|
||||
Ok(Interface { backend })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Interface {
|
||||
backend: Arc<crate::platform::Interface>,
|
||||
}
|
||||
|
||||
impl Interface {
|
||||
pub fn set_alt_setting(&self) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn bulk_transfer(&self, endpoint: u8, buf: Vec<u8>) -> 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<u8>) -> Transfer {
|
||||
let mut t = Transfer::new(self.backend.clone(), endpoint, EndpointType::Interrupt);
|
||||
t.submit(buf);
|
||||
t
|
||||
}
|
||||
}
|
||||
|
||||
struct Queue {
|
||||
pending: VecDeque<Transfer>,
|
||||
cached: Option<Transfer>,
|
||||
}
|
||||
|
||||
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<Duration>) -> Option<Completion> {
|
||||
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!()
|
||||
}
|
||||
}
|
||||
115
src/enumeration.rs
Normal file
115
src/enumeration.rs
Normal file
|
|
@ -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<String>,
|
||||
pub(crate) product_string: Option<String>,
|
||||
pub(crate) serial_number: Option<String>,
|
||||
}
|
||||
|
||||
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, Error> {
|
||||
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<Self, Self::Err> {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/lib.rs
Normal file
16
src/lib.rs
Normal file
|
|
@ -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;
|
||||
158
src/platform/linux_usbfs/device.rs
Normal file
158
src/platform/linux_usbfs/device.rs
Normal file
|
|
@ -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<Arc<LinuxDevice>, Error> {
|
||||
Self::open(d.bus_number(), d.device_address())
|
||||
}
|
||||
|
||||
pub(crate) fn open(busnum: u8, devnum: u8) -> Result<Arc<LinuxDevice>, 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<Self>,
|
||||
interface: u8,
|
||||
) -> Result<Arc<LinuxInterface>, 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<LinuxDevice>,
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
87
src/platform/linux_usbfs/enumeration.rs
Normal file
87
src/platform/linux_usbfs/enumeration.rs
Normal file
|
|
@ -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<T: FromStr>(&self, attr: &str) -> Result<T, io::Error>
|
||||
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<T: FromHexStr>(&self, attr: &str) -> Result<T, io::Error> {
|
||||
let s = self.read_attr::<String>(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<Self, ParseIntError>;
|
||||
}
|
||||
|
||||
impl FromHexStr for u8 {
|
||||
fn from_hex_str(s: &str) -> Result<Self, ParseIntError> {
|
||||
u8::from_str_radix(s, 16)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromHexStr for u16 {
|
||||
fn from_hex_str(s: &str) -> Result<Self, ParseIntError> {
|
||||
u16::from_str_radix(s, 16)
|
||||
}
|
||||
}
|
||||
|
||||
const SYSFS_PREFIX: &'static str = "/sys/bus/usb/devices/";
|
||||
|
||||
pub fn list_devices() -> Result<impl Iterator<Item = DeviceInfo>, 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<DeviceInfo, Error> {
|
||||
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}"))
|
||||
}
|
||||
66
src/platform/linux_usbfs/events.rs
Normal file
66
src/platform/linux_usbfs/events.rs
Normal file
|
|
@ -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<OwnedFd> = OnceCell::new();
|
||||
static DEVICES: Mutex<Slab<Weak<Device>>> = Mutex::new(Slab::new());
|
||||
|
||||
pub(super) fn register(usb_fd: &OwnedFd, weak_device: Weak<Device>) -> Result<usize, Error> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/platform/linux_usbfs/mod.rs
Normal file
13
src/platform/linux_usbfs/mod.rs
Normal file
|
|
@ -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;
|
||||
242
src/platform/linux_usbfs/transfer.rs
Normal file
242
src/platform/linux_usbfs/transfer.rs
Normal file
|
|
@ -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<Urb>,
|
||||
state: AtomicU8,
|
||||
waker: AtomicWaker,
|
||||
interface: Arc<Interface>,
|
||||
}
|
||||
|
||||
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<u8>) {
|
||||
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<u8> {
|
||||
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<TransferInner>,
|
||||
}
|
||||
|
||||
/// 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<Interface>, 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<u8>) {
|
||||
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<Completion> {
|
||||
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::Output> {
|
||||
self.as_mut().poll_completion(cx)
|
||||
}
|
||||
}
|
||||
133
src/platform/linux_usbfs/usbfs.rs
Normal file
133
src/platform/linux_usbfs/usbfs.rs
Normal file
|
|
@ -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: AsFd>(fd: Fd, configuration: u8) -> io::Result<()> {
|
||||
unsafe {
|
||||
let ctl =
|
||||
ioctl::Setter::<ioctl::ReadOpcode<b'U', 5, c_uint>, c_uint>::new(configuration.into());
|
||||
ioctl::ioctl(fd, ctl)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn claim_interface<Fd: AsFd>(fd: Fd, interface: u8) -> io::Result<()> {
|
||||
unsafe {
|
||||
let ctl =
|
||||
ioctl::Setter::<ioctl::ReadOpcode<b'U', 15, c_uint>, c_uint>::new(interface.into());
|
||||
ioctl::ioctl(fd, ctl)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn release_interface<Fd: AsFd>(fd: Fd, interface: u8) -> io::Result<()> {
|
||||
unsafe {
|
||||
let ctl =
|
||||
ioctl::Setter::<ioctl::ReadOpcode<b'U', 16, c_uint>, c_uint>::new(interface.into());
|
||||
ioctl::ioctl(fd, ctl)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PassPtr<Opcode, Input> {
|
||||
input: *mut Input,
|
||||
_opcode: PhantomData<Opcode>,
|
||||
}
|
||||
|
||||
impl<Opcode: CompileTimeOpcode, Input> PassPtr<Opcode, Input> {
|
||||
/// 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<Opcode: CompileTimeOpcode, Input> Ioctl for PassPtr<Opcode, Input> {
|
||||
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<Self::Output> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn submit_urb<Fd: AsFd>(fd: Fd, urb: *mut Urb) -> io::Result<()> {
|
||||
unsafe {
|
||||
let ctl = PassPtr::<ioctl::ReadOpcode<b'U', 10, Urb>, Urb>::new(urb);
|
||||
ioctl::ioctl(fd, ctl)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reap_urb_ndelay<Fd: AsFd>(fd: Fd) -> io::Result<*mut Urb> {
|
||||
unsafe {
|
||||
let ctl = ioctl::Getter::<ioctl::WriteOpcode<b'U', 13, *mut Urb>, *mut Urb>::new();
|
||||
ioctl::ioctl(fd, ctl)
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn discard_urb<Fd: AsFd>(fd: Fd, urb: *mut Urb) -> io::Result<()> {
|
||||
unsafe {
|
||||
let ctl = PassPtr::<ioctl::NoneOpcode<b'U', 11, ()>, Urb>::new(urb);
|
||||
ioctl::ioctl(fd, ctl)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset<Fd: AsFd>(fd: Fd) -> io::Result<()> {
|
||||
unsafe {
|
||||
let ctl = ioctl::NoArg::<ioctl::NoneOpcode<b'U', 20, ()>>::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
|
||||
}
|
||||
5
src/platform/mod.rs
Normal file
5
src/platform/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#[cfg(target_os = "linux")]
|
||||
mod linux_usbfs;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use linux_usbfs::*;
|
||||
25
src/transfer.rs
Normal file
25
src/transfer.rs
Normal file
|
|
@ -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<u8>,
|
||||
pub status: TransferStatus,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue