use std::{
collections::HashSet,
ops::{Deref, DerefMut},
time::Duration,
};
use derive_more::{Display, Error, From};
use hidapi::DeviceInfo;
use smol_str::{SmolStr, ToSmolStr as _};
pub mod report;
pub mod thread;
pub use self::thread::HidThread;
#[derive(Debug, Display, Error)]
pub enum HidDeviceError {
#[display("not connected")]
NotConnected,
#[display("not supported")]
NotSupported,
}
#[derive(Debug, Display, Error, From)]
pub enum HidError {
#[from]
Device(HidDeviceError),
#[from]
Api(hidapi::HidError),
#[from]
Anyhow(anyhow::Error),
}
pub type HidResult<T> = std::result::Result<T, HidError>;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[repr(u16)]
pub enum HidUsagePage {
Undefined,
GenericDesktop,
SimulationControls,
VRControls,
SportControls,
GameControls,
GenericDeviceControls,
Keyboard,
LED,
Button,
Ordinal,
Telephony,
Consumer,
Digitizer,
Haptics,
PhysicalInput,
Unicode,
EyeAndHeadTracker,
AuxiliaryDisplay,
Sensors,
MedicalInstrument,
BrailleDisplay,
Light,
Monitor,
MonitorEnumerated,
VESAVirtualControls,
Power,
BatterySystem,
BarcodeScanner,
Scale,
MagneticStripeReader,
CameraControl,
Arcade,
GamingDevice,
FIDO,
Reserved(u16),
VendorDefined(u16),
}
impl From<u16> for HidUsagePage {
fn from(number: u16) -> Self {
#[expect(clippy::match_same_arms)]
match number {
0x00 => Self::Undefined,
0x01 => Self::GenericDesktop,
0x02 => Self::SimulationControls,
0x03 => Self::VRControls,
0x04 => Self::SportControls,
0x05 => Self::GameControls,
0x06 => Self::GenericDeviceControls,
0x07 => Self::Keyboard,
0x08 => Self::LED,
0x09 => Self::Button,
0x0a => Self::Ordinal,
0x0b => Self::Telephony,
0x0c => Self::Consumer,
0x0d => Self::Digitizer,
0x0e => Self::Haptics,
0x0f => Self::PhysicalInput,
0x10 => Self::Unicode,
0x11 => Self::Reserved(number),
0x12 => Self::EyeAndHeadTracker,
0x13 => Self::Reserved(number),
0x14 => Self::AuxiliaryDisplay,
0x15..=0x1f => Self::Reserved(number),
0x20 => Self::Sensors,
0x21..=0x3f => Self::Reserved(number),
0x40 => Self::MedicalInstrument,
0x41 => Self::BrailleDisplay,
0x42..=0x58 => Self::Reserved(number),
0x59 => Self::Light,
0x5a..=0x7f => Self::Reserved(number),
0x80 => Self::Monitor,
0x81 => Self::MonitorEnumerated,
0x82 => Self::VESAVirtualControls,
0x83 => Self::Reserved(number),
0x84 => Self::Power,
0x85 => Self::BatterySystem,
0x86..=0x8b => Self::Reserved(number),
0x8c => Self::BarcodeScanner,
0x8d => Self::Scale,
0x8e => Self::MagneticStripeReader,
0x8f => Self::Reserved(number),
0x90 => Self::CameraControl,
0x91 => Self::Arcade,
0x92 => Self::GamingDevice,
0x93..=0xf1cf => Self::Reserved(number),
0xf1d0 => Self::FIDO,
0xf1d1..=0xfeff => Self::Reserved(number),
0xff00..=0xffff => Self::VendorDefined(number),
}
}
}
#[expect(missing_debug_implementations)]
pub struct HidApi(hidapi::HidApi);
impl AsRef<hidapi::HidApi> for HidApi {
fn as_ref(&self) -> &hidapi::HidApi {
let Self(inner) = self;
inner
}
}
impl AsMut<hidapi::HidApi> for HidApi {
fn as_mut(&mut self) -> &mut hidapi::HidApi {
let Self(inner) = self;
inner
}
}
impl Deref for HidApi {
type Target = hidapi::HidApi;
fn deref(&self) -> &Self::Target {
self.as_ref()
}
}
impl DerefMut for HidApi {
fn deref_mut(&mut self) -> &mut Self::Target {
self.as_mut()
}
}
impl HidApi {
pub fn new() -> HidResult<Self> {
let inner = hidapi::HidApi::new_without_enumerate()?;
Ok(Self(inner))
}
pub fn query_devices(&mut self) -> HidResult<impl Iterator<Item = &DeviceInfo>> {
self.refresh_devices()?;
Ok(self.device_list())
}
pub fn query_devices_dedup(&mut self) -> HidResult<Vec<HidDevice>> {
let mut visited_paths = HashSet::new();
Ok(self
.query_devices()?
.filter(|&info| visited_paths.insert(info.path()))
.map(|info| HidDevice::new(info.clone()))
.collect())
}
pub fn query_device_by_id(&mut self, id: &DeviceId) -> HidResult<Option<HidDevice>> {
Ok(self.query_devices()?.find_map(|info| {
let found_id = DeviceId::try_from(info).ok();
if Some(id) == found_id.as_ref() {
return Some(HidDevice::new(info.clone()));
}
None
}))
}
pub fn connect_device(&self, info: DeviceInfo) -> HidResult<HidDevice> {
let mut device = HidDevice::new(info);
device.connect(self)?;
Ok(device)
}
}
#[expect(missing_debug_implementations)]
pub struct HidDevice {
info: DeviceInfo,
connected: Option<hidapi::HidDevice>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DeviceId {
pub vid: u16,
pub pid: u16,
pub sn: SmolStr,
}
impl TryFrom<&DeviceInfo> for DeviceId {
type Error = ();
fn try_from(from: &DeviceInfo) -> std::result::Result<Self, Self::Error> {
if let Some(sn) = from.serial_number() {
let sn = sn.trim();
if !sn.is_empty() {
let vid = from.vendor_id();
let pid = from.product_id();
let sn = sn.to_smolstr();
return Ok(Self { vid, pid, sn });
}
}
Err(())
}
}
impl HidDevice {
#[must_use]
pub const fn new(info: DeviceInfo) -> Self {
Self {
info,
connected: None,
}
}
#[must_use]
pub const fn info(&self) -> &DeviceInfo {
&self.info
}
#[must_use]
pub const fn is_connected(&self) -> bool {
self.connected.is_some()
}
pub fn connect(&mut self, api: &HidApi) -> HidResult<()> {
if self.is_connected() {
return Ok(());
}
let connected = api.0.open_path(self.info.path())?;
connected.set_blocking_mode(true)?;
self.connected = Some(connected);
debug_assert!(self.is_connected());
Ok(())
}
pub fn disconnect(&mut self) {
self.connected = None;
debug_assert!(!self.is_connected());
}
fn connected(&self) -> HidResult<&hidapi::HidDevice> {
self.connected
.as_ref()
.ok_or(HidDeviceError::NotConnected.into())
}
pub fn get_feature_report(&self, buffer: &mut [u8]) -> HidResult<usize> {
Ok(self.connected()?.get_feature_report(buffer)?)
}
pub fn send_feature_report(&self, data: &[u8]) -> HidResult<()> {
Ok(self.connected()?.send_feature_report(data)?)
}
pub fn read(&self, buffer: &mut [u8], timeout: Option<Duration>) -> HidResult<usize> {
let timeout_millis = timeout_millis(timeout);
Ok(self.connected()?.read_timeout(buffer, timeout_millis)?)
}
pub fn write(&self, data: &[u8]) -> HidResult<usize> {
Ok(self.connected()?.write(data)?)
}
}
const INF_TIMEOUT_MILLIS: i32 = -1;
const MAX_TIMEOUT_MILLIS: i32 = i32::MAX;
#[expect(clippy::cast_possible_truncation)]
fn timeout_millis(timeout: Option<Duration>) -> i32 {
debug_assert_eq!(0, timeout.unwrap_or_default().subsec_nanos() % 1_000_000);
timeout
.as_ref()
.map(Duration::as_millis)
.map_or(INF_TIMEOUT_MILLIS, |millis| {
millis.min(MAX_TIMEOUT_MILLIS as _) as _
})
}