1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
|
//! 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::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 {
((dir << IOC_DIRSHIFT)
| ((ty as u32) << IOC_TYPESHIFT)
| (nr << IOC_NRSHIFT)
| (size << IOC_SIZESHIFT)) as libc::c_ulong
}
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`.
pub fn find_devices(vid: u16, pid: u16) -> io::Result<Vec<HidDevice>> {
find_with(|v, p| v == vid && p == pid)
}
/// Find the first (only) MMX 150 dongle HID device.
pub fn find_mmx150() -> io::Result<Option<HidDevice>> {
Ok(find_devices(BEYERDYNAMIC_VID, MMX150_DONGLE_PID)?
.into_iter()
.next())
}
/// Enumerate all hidraw devices belonging to a vendor.
pub fn find_devices_by_vendor(vid: u16) -> io::Result<Vec<HidDevice>> {
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<Vec<HidDevice>> {
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 {
pub fn open(devnode: impl AsRef<Path>) -> io::Result<Hidraw> {
let file = fs::OpenOptions::new()
.read(true)
.write(true)
.open(devnode.as_ref())?;
Ok(Hidraw {
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.
pub fn get_feature(&self, report_id: u8, len: usize) -> io::Result<Vec<u8>> {
let mut buf = vec![0u8; len];
buf[0] = report_id;
self.raw_ioctl(hid_get_feature_cmd(len as u32), &mut buf)?;
Ok(buf)
}
/// `HIDIOCSFEATURE`: set a feature report (`data[0]` is the report id).
pub fn set_feature(&self, data: &[u8]) -> io::Result<usize> {
self.raw_ioctl(hid_set_feature_cmd(data.len() as u32), &mut data.to_vec())
}
/// `HIDIOCGINPUT`: fetch an input report by id.
pub fn get_input(&self, report_id: u8, len: usize) -> io::Result<Vec<u8>> {
let mut buf = vec![0u8; len];
buf[0] = report_id;
self.raw_ioctl(hid_get_input_cmd(len as u32), &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.
pub fn send_output(&self, data: &[u8]) -> io::Result<usize> {
self.raw_ioctl(hid_get_output_cmd(data.len() as u32), &mut data.to_vec())
}
/// Read from the interrupt IN endpoint (asynchronous input reports).
pub fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
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.
pub fn read_with_timeout(&mut self, timeout: Duration) -> io::Result<Option<Vec<u8>>> {
let deadline = Instant::now() + timeout;
loop {
let now = Instant::now();
if now >= deadline {
return Ok(None);
}
let mut set = unsafe { std::mem::zeroed::<libc::fd_set>() };
unsafe { libc::FD_ZERO(&mut set) };
unsafe { libc::FD_SET(self.file.as_raw_fd(), &mut set) };
let remaining = deadline.saturating_duration_since(now);
let mut tv = libc::timeval {
tv_sec: remaining.as_secs() as libc::time_t,
tv_usec: remaining.subsec_micros() as libc::suseconds_t,
};
let n = unsafe {
libc::select(
self.file.as_raw_fd() + 1,
&mut set,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut tv,
)
};
if n < 0 {
let err = io::Error::last_os_error();
if err.kind() == io::ErrorKind::Interrupted {
continue;
}
return Err(err);
}
if n == 0 {
return Ok(None);
}
let mut buf = [0u8; 256];
match self.read(&mut buf) {
Ok(len) => return Ok(Some(buf[..len].to_vec())),
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
}
/// Write to the interrupt OUT endpoint.
pub fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
use std::io::Write;
self.file.write(buf)
}
fn raw_ioctl(&self, request: libc::c_ulong, buf: &mut [u8]) -> io::Result<usize> {
// 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() as *mut libc::c_void,
)
};
if ret < 0 {
return Err(io::Error::last_os_error());
}
Ok(ret as usize)
}
}
impl std::os::fd::AsRawFd for Hidraw {
fn as_raw_fd(&self) -> std::os::fd::RawFd {
self.file.as_raw_fd()
}
}
fn read_sysfs(path: &Path) -> Option<String> {
fs::read_to_string(path).ok()
}
/// Parse the `HID_ID=<bus>:<vendor>:<product>` 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);
}
}
|