async_hid/backend/hidraw/
mod.rs

1mod descriptor;
2mod ioctl;
3mod utils;
4
5use std::fs::{OpenOptions, read_dir, read_to_string};
6use std::os::fd::{AsRawFd, OwnedFd};
7use std::os::unix::fs::OpenOptionsExt;
8use std::path::{Path, PathBuf};
9
10use futures_core::Stream;
11use nix::fcntl::OFlag;
12use nix::unistd::{read, write};
13
14use crate::backend::hidraw::descriptor::HidrawReportDescriptor;
15use crate::backend::hidraw::utils::{iter, TryIterExt};
16use crate::{ensure, DeviceInfo, ErrorSource, HidError, HidResult, SerialNumberExt, AccessMode};
17
18use crate::backend::hidraw::async_api::{AsyncFd, read_with, write_with};
19use crate::backend::hidraw::ioctl::hidraw_ioc_grdescsize;
20
21pub async fn enumerate() -> HidResult<impl Stream<Item = DeviceInfo> + Send + Unpin> {
22    let devices = read_dir("/sys/class/hidraw/")?
23        .map(|r| r.map(|e| e.path()))
24        .try_collect_vec()?;
25    let devices = devices
26        .into_iter()
27        .map(get_device_info_raw)
28        .filter_map(|r| {
29            r.map_err(|e| log::trace!("Failed to query device information\n\tbecause {e:?}"))
30                .ok()
31        })
32        .flatten();
33    Ok(iter(devices))
34}
35
36fn get_device_info_raw(path: PathBuf) -> HidResult<Vec<DeviceInfo>> {
37    let properties = read_to_string(path.join("uevent"))?;
38    let id = read_property(&properties, "DEVNAME")
39        .ok_or(HidError::custom("Can't find dev name"))
40        .and_then(mange_dev_name)?;
41
42    let properties = read_to_string(path.join("device/uevent"))?;
43
44    let (_bus, vendor_id, product_id) = read_property(&properties, "HID_ID")
45        .and_then(parse_hid_vid_pid)
46        .ok_or(HidError::custom("Can't find hid ids"))?;
47
48    let name = read_property(&properties, "HID_NAME")
49        .ok_or(HidError::custom("Can't find hid name"))?
50        .to_string();
51
52    let serial_number = read_property(&properties, "HID_UNIQ")
53        .filter(|s| !s.is_empty())
54        .map(str::to_string);
55
56    let info = DeviceInfo {
57        id: id.into(),
58        name,
59        product_id,
60        vendor_id,
61        usage_id: 0,
62        usage_page: 0,
63        private_data: BackendPrivateData { serial_number }
64    };
65
66    let results = HidrawReportDescriptor::from_syspath(&path)
67        .map(|descriptor| {
68            descriptor
69                .usages()
70                .map(|(usage_page, usage_id)| DeviceInfo {
71                    usage_page,
72                    usage_id,
73                    ..info.clone()
74                })
75                .collect()
76        })
77        .unwrap_or_else(|_| vec![info]);
78    Ok(results)
79}
80
81fn read_property<'a>(properties: &'a str, key: &str) -> Option<&'a str> {
82    properties
83        .lines()
84        .filter_map(|l| l.split_once('='))
85        .find_map(|(k, v)| (k == key).then_some(v))
86}
87
88fn mange_dev_name(dev_name: &str) -> HidResult<PathBuf> {
89    let path = Path::new(dev_name);
90    if path.is_absolute() {
91        ensure!(
92            dev_name
93                .strip_prefix("/dev/")
94                .is_some_and(|z| !z.is_empty()),
95            HidError::custom("Absolute device paths must start with /dev/")
96        );
97        Ok(path.to_path_buf())
98    } else {
99        Ok(Path::new("/dev/").join(path))
100    }
101}
102
103fn parse_hid_vid_pid(s: &str) -> Option<(u16, u16, u16)> {
104    let mut elems = s.split(':').filter_map(|s| u16::from_str_radix(s, 16).ok());
105    let devtype = elems.next()?;
106    let vendor = elems.next()?;
107    let product = elems.next()?;
108
109    Some((devtype, vendor, product))
110}
111
112impl SerialNumberExt for DeviceInfo {
113    fn serial_number(&self) -> Option<&str> {
114        self.private_data
115            .serial_number
116            .as_ref()
117            .map(String::as_str)
118    }
119}
120
121
122#[derive(Debug)]
123pub struct BackendDevice {
124    fd: AsyncFd
125}
126
127impl BackendDevice {
128    pub async fn read_input_report(&self, buf: &mut [u8]) -> HidResult<usize> {
129        read_with(&self.fd, |fd| read(fd.as_raw_fd(), buf).map_err(BackendError::from))
130            .await
131            .map_err(HidError::from)
132    }
133
134    pub async fn write_output_report(&self, data: &[u8]) -> HidResult<()> {
135        ensure!(!data.is_empty(), HidError::zero_sized_data());
136        write_with(&self.fd, |fd| write(fd.as_raw_fd(), data).map_err(BackendError::from))
137            .await
138            .map_err(HidError::from)
139            .map(|i| debug_assert_eq!(i, data.len()))
140    }
141}
142
143pub async fn open(id: &BackendDeviceId, mode: AccessMode) -> HidResult<BackendDevice> {
144    let fd: OwnedFd = OpenOptions::new()
145        .read(mode.readable())
146        .write(mode.writeable())
147        .custom_flags((OFlag::O_CLOEXEC | OFlag::O_NONBLOCK).bits())
148        .open(id)?
149        .into();
150
151    let mut size = 0i32;
152    unsafe { hidraw_ioc_grdescsize(fd.as_raw_fd(), &mut size) }
153        .map_err(|e| HidError::custom(format!("ioctl(GRDESCSIZE) error for {:?}, not a HIDRAW device?: {}", id, e)))?;
154
155    Ok(BackendDevice { fd: AsyncFd::new(fd)? })
156}
157
158
159#[derive(Debug, Clone, Eq, PartialEq)]
160pub struct BackendPrivateData {
161    serial_number: Option<String>
162}
163pub type BackendDeviceId = PathBuf;
164pub type BackendError = std::io::Error;
165
166impl From<BackendError> for ErrorSource {
167    fn from(value: BackendError) -> Self {
168        ErrorSource::PlatformSpecific(value)
169    }
170}
171
172#[cfg(all(feature = "async-io", feature = "tokio"))]
173compile_error!("Only tokio or async-io can be active at the same time");
174
175#[cfg(feature = "async-io")]
176mod async_api {
177    use std::os::fd::OwnedFd;
178    use async_io::Async;
179
180    pub type AsyncFd = Async<OwnedFd>;
181
182    pub async fn read_with<R>(inner: &AsyncFd, op: impl FnMut(&OwnedFd) -> std::io::Result<R>) -> std::io::Result<R> {
183        inner.read_with(op).await
184    }
185
186    pub async fn write_with<R>(inner: &AsyncFd, op: impl FnMut(&OwnedFd) -> std::io::Result<R>) -> std::io::Result<R> {
187        inner.write_with(op).await
188    }
189}
190
191#[cfg(feature = "tokio")]
192mod async_api {
193    use std::os::fd::OwnedFd;
194    use tokio::io::Interest;
195
196    pub type AsyncFd = tokio::io::unix::AsyncFd<OwnedFd>;
197
198    pub async fn read_with<R>(inner: &AsyncFd, op: impl FnMut(&OwnedFd) -> std::io::Result<R>) -> std::io::Result<R> {
199        inner.async_io(Interest::READABLE, op).await
200    }
201
202    pub async fn write_with<R>(inner: &AsyncFd, op: impl FnMut(&OwnedFd) -> std::io::Result<R>) -> std::io::Result<R> {
203        inner.async_io(Interest::WRITABLE, op).await
204    }
205}
206
207/*
208udev device searching
209
210pub async fn enumerate() -> HidResult<impl Stream<Item = DeviceInfo>> {
211    let mut enumerator = Enumerator::new()?;
212    enumerator.match_subsystem("hidraw")?;
213    let devices: Vec<Device> = enumerator
214        .scan_devices()?
215        .collect();
216    let devices = devices
217        .into_iter()
218        .map(get_device_info)
219        .filter_map(|r| {
220            r.map_err(|e| log::trace!("Failed to query device information\n\tbecause {e:?}"))
221                .ok()
222        })
223        .flatten();
224    Ok(iter(devices))
225}
226
227fn get_device_info(raw_device: Device) -> HidResult<Vec<DeviceInfo>> {
228  let device = raw_device
229      .parent_with_subsystem("hid")?
230      .ok_or(HidError::custom("Can't find hid interface"))?;
231
232  let (_bus, vendor_id, product_id) = device
233      .property_value("HID_ID")
234      .and_then(|s| s.to_str())
235      .and_then(parse_hid_vid_pid)
236      .ok_or(HidError::custom("Can't find hid ids"))?;
237
238  let id = raw_device
239      .devnode()
240      .ok_or(HidError::custom("Can't find device node"))?
241      .to_path_buf();
242
243  let name = device
244      .property_value("HID_NAME")
245      .ok_or(HidError::custom("Can't find hid name"))?
246      .to_string_lossy()
247      .to_string();
248
249  let info = DeviceInfo {
250      id: id.into(),
251      name,
252      product_id,
253      vendor_id,
254      usage_id: 0,
255      usage_page: 0
256  };
257  let results = HidrawReportDescriptor::from_syspath(raw_device.syspath())
258      .map(|descriptor| {
259          descriptor
260              .usages()
261              .map(|(usage_page, usage_id)| DeviceInfo {
262                  usage_page,
263                  usage_id,
264                  ..info.clone()
265              })
266              .collect()
267      })
268      .unwrap_or_else(|_| vec![info]);
269  Ok(results)
270}
271*/