//! Direct hidraw access for the beyerdynamic MMX 150 wireless dongle. //! //! The dongle exposes a vendor HID interface (`2e50:0035`) alongside its USB //! audio interfaces. The kernel binds it to `hid-generic`, which shows up as //! `/dev/hidrawN`. We talk to it with the standard `HIDIOCGFEATURE`, //! `HIDIOCSFEATURE`, `HIDIOCGINPUT`, `HIDIOCGSOUTPUT` ioctls plus plain //! `read`/`write` on the interrupt endpoints. use std::cmp::Ordering; use std::fs; use std::io; use std::os::fd::AsRawFd; use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; /// beyerdynamic USB vendor ID. pub const BEYERDYNAMIC_VID: u16 = 0x2e50; /// MMX 150 wireless low-latency dongle product ID. pub const MMX150_DONGLE_PID: u16 = 0x0035; const HIDRAW_SYSFS: &str = "/sys/class/hidraw"; const IOC_NRSHIFT: u32 = 0; const IOC_TYPESHIFT: u32 = 8; const IOC_SIZESHIFT: u32 = 16; const IOC_DIRSHIFT: u32 = 30; const IOC_READ: u32 = 2; const IOC_WRITE: u32 = 1; fn ioc(dir: u32, ty: u8, nr: u32, size: u32) -> libc::c_ulong { u64::from( (dir << IOC_DIRSHIFT) | (u32::from(ty) << IOC_TYPESHIFT) | (nr << IOC_NRSHIFT) | (size << IOC_SIZESHIFT), ) } const HIDIOCGFEATURE_NR: u32 = 0x06; const HIDIOCSFEATURE_NR: u32 = 0x07; const HIDIOCGINPUT_NR: u32 = 0x0a; const HIDIOCGSOUTPUT_NR: u32 = 0x0d; fn hid_get_feature_cmd(size: u32) -> libc::c_ulong { ioc(IOC_READ, b'H', HIDIOCGFEATURE_NR, size) } fn hid_set_feature_cmd(size: u32) -> libc::c_ulong { ioc(IOC_WRITE, b'H', HIDIOCSFEATURE_NR, size) } fn hid_get_input_cmd(size: u32) -> libc::c_ulong { ioc(IOC_READ, b'H', HIDIOCGINPUT_NR, size) } fn hid_get_output_cmd(size: u32) -> libc::c_ulong { ioc(IOC_WRITE, b'H', HIDIOCGSOUTPUT_NR, size) } /// A HID device discovered on the system. #[derive(Debug, Clone)] pub struct HidDevice { pub vid: u16, pub pid: u16, pub name: String, pub devnode: PathBuf, } /// Enumerate hidraw devices matching `vid`/`pid` by reading the sysfs `uevent` /// of each `/sys/class/hidraw/hidrawN`. /// /// # Errors /// /// Returns an error if `/sys/class/hidraw` cannot be listed. pub fn find_devices(vid: u16, pid: u16) -> io::Result> { find_with(|v, p| v == vid && p == pid) } /// Find the first (only) MMX 150 dongle HID device. /// /// # Errors /// /// Propagates errors from [`find_devices`]. pub fn find_mmx150() -> io::Result> { Ok(find_devices(BEYERDYNAMIC_VID, MMX150_DONGLE_PID)? .into_iter() .next()) } /// Enumerate all hidraw devices belonging to a vendor. /// /// # Errors /// /// Returns an error if `/sys/class/hidraw` cannot be listed. pub fn find_devices_by_vendor(vid: u16) -> io::Result> { find_with(|v, _| v == vid) } /// Collect USB hidraw devices whose `HID_ID` vendor/product pair matches /// `keep`. Returns one [`HidDevice`] per match. fn find_with(mut keep: impl FnMut(u16, u16) -> bool) -> io::Result> { let mut out = Vec::new(); for entry in fs::read_dir(HIDRAW_SYSFS)? { let entry = entry?; let name = entry.file_name(); let Some(name) = name.to_str() else { continue; }; if !name.starts_with("hidraw") { continue; } let Some(uevent) = read_sysfs(&entry.path().join("device/uevent")) else { continue; }; let Some((bus, v, p)) = parse_hid_id(&uevent) else { continue; }; if bus != 3 || !keep(v, p) { continue; // USB only, filtered by caller } let dev_name = uevent .lines() .find(|l| l.starts_with("HID_NAME=")) .map(|l| l.trim_start_matches("HID_NAME=").to_string()) .unwrap_or_default(); out.push(HidDevice { vid: v, pid: p, name: dev_name, devnode: PathBuf::from("/dev").join(name), }); } Ok(out) } /// An open hidraw file descriptor. pub struct Hidraw { file: fs::File, pub devnode: PathBuf, } impl Hidraw { /// Open a hidraw device node for reading and writing. /// /// # Errors /// /// Returns an error if the node cannot be opened for read/write access. pub fn open(devnode: impl AsRef) -> io::Result { let file = fs::OpenOptions::new() .read(true) .write(true) .open(devnode.as_ref())?; Ok(Self { file, devnode: devnode.as_ref().to_path_buf(), }) } /// `HIDIOCGFEATURE`: fetch `report_id` into `data` (report length + 1 /// bytes including the report-id byte). Returns bytes received. /// /// # Errors /// /// Returns an error if the ioctl fails. pub fn get_feature(&self, report_id: u8, len: usize) -> io::Result> { let mut buf = vec![0u8; len]; if let Some(first) = buf.first_mut() { *first = report_id; } self.raw_ioctl(hid_get_feature_cmd(report_size(len)), &mut buf)?; Ok(buf) } /// `HIDIOCSFEATURE`: set a feature report (`data[0]` is the report id). /// /// # Errors /// /// Returns an error if the ioctl fails. pub fn set_feature(&self, data: &[u8]) -> io::Result { self.raw_ioctl( hid_set_feature_cmd(report_size(data.len())), &mut data.to_vec(), ) } /// `HIDIOCGINPUT`: fetch an input report by id. /// /// # Errors /// /// Returns an error if the ioctl fails. pub fn get_input(&self, report_id: u8, len: usize) -> io::Result> { let mut buf = vec![0u8; len]; if let Some(first) = buf.first_mut() { *first = report_id; } self.raw_ioctl(hid_get_input_cmd(report_size(len)), &mut buf)?; Ok(buf) } /// `HIDIOCGSOUTPUT`: send an output report (`data[0]` is the report id) /// via a control transfer, without going through the interrupt endpoint. /// /// # Errors /// /// Returns an error if the ioctl fails. pub fn send_output(&self, data: &[u8]) -> io::Result { self.raw_ioctl( hid_get_output_cmd(report_size(data.len())), &mut data.to_vec(), ) } /// Read from the interrupt IN endpoint (asynchronous input reports). /// /// # Errors /// /// Returns an error if the read fails. pub fn read(&mut self, buf: &mut [u8]) -> io::Result { use std::io::Read; self.file.read(buf) } /// Wait up to `timeout` for one input report on the interrupt IN /// endpoint. Returns `Ok(None)` if none arrived in time. /// /// # Errors /// /// Returns an error if `select` or `read` fails with anything other than /// `EINTR`. pub fn read_with_timeout(&mut self, timeout: Duration) -> io::Result>> { let deadline = Instant::now() .checked_add(timeout) .unwrap_or_else(Instant::now); loop { let Some(remaining) = deadline.checked_duration_since(Instant::now()) else { return Ok(None); }; let fd = self.file.as_raw_fd(); let mut set = unsafe { std::mem::zeroed::() }; unsafe { libc::FD_ZERO(std::ptr::addr_of_mut!(set)) }; unsafe { libc::FD_SET(fd, std::ptr::addr_of_mut!(set)) }; let mut tv = libc::timeval { tv_sec: libc::time_t::try_from(remaining.as_secs()).unwrap_or(libc::time_t::MAX), tv_usec: libc::suseconds_t::from(remaining.subsec_micros()), }; let n = unsafe { libc::select( fd.saturating_add(1), std::ptr::addr_of_mut!(set), std::ptr::null_mut(), std::ptr::null_mut(), std::ptr::addr_of_mut!(tv), ) }; match n.cmp(&0) { Ordering::Less => { let err = io::Error::last_os_error(); if err.kind() != io::ErrorKind::Interrupted { return Err(err); } } Ordering::Equal => return Ok(None), Ordering::Greater => { let mut buf = [0u8; 256]; match self.read(&mut buf) { Ok(len) => { return Ok(Some(buf.get(..len).unwrap_or(&[]).to_vec())); } Err(e) if e.kind() == io::ErrorKind::Interrupted => {} Err(e) => return Err(e), } } } } } /// Write to the interrupt OUT endpoint. /// /// # Errors /// /// Returns an error if the write fails. pub fn write(&mut self, buf: &[u8]) -> io::Result { use std::io::Write; self.file.write(buf) } fn raw_ioctl(&self, request: libc::c_ulong, buf: &mut [u8]) -> io::Result { // SAFETY: buf is owned and valid for the duration of the call. let ret = unsafe { libc::ioctl( self.file.as_raw_fd(), request, buf.as_mut_ptr().cast::(), ) }; if ret < 0 { return Err(io::Error::last_os_error()); } Ok(usize::try_from(ret).unwrap_or(0)) } } impl std::os::fd::AsRawFd for Hidraw { fn as_raw_fd(&self) -> std::os::fd::RawFd { self.file.as_raw_fd() } } /// Clamp a report length to what the ioctl size field can hold. fn report_size(len: usize) -> u32 { u32::try_from(len).unwrap_or(u32::MAX) } fn read_sysfs(path: &Path) -> Option { fs::read_to_string(path).ok() } /// Parse the `HID_ID=::` line (hex values). fn parse_hid_id(uevent: &str) -> Option<(u32, u16, u16)> { let line = uevent.lines().find(|l| l.starts_with("HID_ID="))?; let id = line.trim_start_matches("HID_ID="); let mut parts = id.split(':'); let bus = u32::from_str_radix(parts.next()?, 16).ok()?; let vid = u16::from_str_radix(parts.next()?, 16).ok()?; let pid = u16::from_str_radix(parts.next()?, 16).ok()?; Some((bus, vid, pid)) } #[cfg(test)] mod tests { use super::*; #[test] fn parses_hid_id() { let uevent = "MAJOR=240\nMINOR=1\nDEVNAME=hidraw1\nHID_ID=0003:00002E50:00000035\nHID_NAME=beyerdynamic Dongle MMX 150 wireless\n"; let (bus, vid, pid) = parse_hid_id(uevent).unwrap(); assert_eq!(bus, 3); assert_eq!(vid, 0x2e50); assert_eq!(pid, 0x0035); } }