use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DeviceKind {
Mouse,
Keyboard,
Numpad,
Presenter,
Remote,
Trackball,
Touchpad,
Tablet,
Gamepad,
Joystick,
Headset,
Unknown,
}
impl DeviceKind {
#[must_use]
pub fn from_registry_type(raw: &str) -> Self {
match raw.trim().to_ascii_lowercase().as_str() {
"mouse" => Self::Mouse,
"keyboard" => Self::Keyboard,
"numpad" => Self::Numpad,
"presenter" => Self::Presenter,
"remote" | "remotecontrol" => Self::Remote,
"trackball" => Self::Trackball,
"touchpad" | "trackpad" => Self::Touchpad,
"tablet" => Self::Tablet,
"gamepad" => Self::Gamepad,
"joystick" => Self::Joystick,
"headset" => Self::Headset,
_ => Self::Unknown,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[allow(
clippy::struct_excessive_bools,
reason = "capabilities is a serialized feature-bit DTO; independent booleans keep the IPC/config shape explicit"
)]
pub struct Capabilities {
pub buttons: bool,
pub pointer: bool,
pub lighting: bool,
pub scroll_inversion: bool,
}
impl Capabilities {
#[must_use]
pub fn from_feature_ids(ids: &[u16]) -> Self {
const BUTTONS: [u16; 5] = [0x1b00, 0x1b01, 0x1b02, 0x1b03, 0x1b04];
const POINTER: [u16; 2] = [0x2201, 0x2202];
const LIGHTING: [u16; 2] = [0x8080, 0x8070];
let has = |family: &[u16]| ids.iter().any(|id| family.contains(id));
Self {
buttons: has(&BUTTONS),
pointer: has(&POINTER),
lighting: has(&LIGHTING),
scroll_inversion: false,
}
}
#[must_use]
pub fn presumed_from_kind(kind: DeviceKind) -> Self {
match kind {
DeviceKind::Mouse | DeviceKind::Trackball => Self {
buttons: true,
pointer: true,
lighting: false,
scroll_inversion: false,
},
DeviceKind::Keyboard => Self {
lighting: true,
..Self::default()
},
_ => Self::default(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum BatteryLevel {
Critical,
Low,
Good,
Full,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BatteryStatus {
Discharging,
Charging,
ChargingSlow,
Full,
Error,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatteryInfo {
pub percentage: u8,
pub level: BatteryLevel,
pub status: BatteryStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReceiverInfo {
pub name: String,
pub vendor_id: u16,
pub product_id: u16,
pub unique_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeviceModelInfo {
pub entity_count: u8,
pub serial_number: Option<String>,
pub unit_id: [u8; 4],
pub transports: DeviceTransports,
pub model_ids: [u16; 3],
pub extended_model_id: u8,
}
impl DeviceModelInfo {
#[must_use]
pub fn config_key(&self) -> String {
format!("{:x}{:04x}", self.extended_model_id, self.model_ids[0])
}
}
#[allow(
clippy::struct_excessive_bools,
reason = "bitfield mirroring HID++ DeviceInformation; transports are independent flags"
)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeviceTransports {
pub usb: bool,
pub equad: bool,
pub btle: bool,
pub bluetooth: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PairedDevice {
pub slot: u8,
pub codename: Option<String>,
pub wpid: Option<u16>,
pub kind: DeviceKind,
pub online: bool,
pub battery: Option<BatteryInfo>,
pub model_info: Option<DeviceModelInfo>,
pub capabilities: Option<Capabilities>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceInventory {
pub receiver: ReceiverInfo,
pub paired: Vec<PairedDevice>,
}
#[cfg(test)]
mod tests {
use super::DeviceKind;
#[test]
fn registry_type_is_case_folded() {
assert_eq!(DeviceKind::from_registry_type("mouse"), DeviceKind::Mouse);
assert_eq!(DeviceKind::from_registry_type("MOUSE"), DeviceKind::Mouse);
assert_eq!(
DeviceKind::from_registry_type(" Keyboard "),
DeviceKind::Keyboard
);
}
#[test]
fn unknown_registry_type_defers_to_the_caller() {
assert_eq!(
DeviceKind::from_registry_type("webcam"),
DeviceKind::Unknown
);
assert_eq!(DeviceKind::from_registry_type(""), DeviceKind::Unknown);
}
#[test]
fn capabilities_track_the_driving_feature_ids() {
use super::Capabilities;
let mouse = Capabilities::from_feature_ids(&[0x0003, 0x1b04, 0x2202, 0x2110]);
assert_eq!(
mouse,
Capabilities {
buttons: true,
pointer: true,
lighting: false,
scroll_inversion: false,
}
);
let keyboard = Capabilities::from_feature_ids(&[0x0001, 0x8080]);
assert_eq!(
keyboard,
Capabilities {
buttons: false,
pointer: false,
lighting: true,
scroll_inversion: false,
}
);
assert_eq!(
Capabilities::from_feature_ids(&[0x0000, 0x0003]),
Capabilities::default()
);
}
#[test]
fn presumed_capabilities_keep_an_unprobed_mouse_configurable() {
use super::Capabilities;
let mouse = Capabilities::presumed_from_kind(DeviceKind::Mouse);
assert!(mouse.buttons && mouse.pointer && !mouse.lighting);
assert!(Capabilities::presumed_from_kind(DeviceKind::Keyboard).lighting);
assert_eq!(
Capabilities::presumed_from_kind(DeviceKind::Unknown),
Capabilities::default()
);
}
}