-
Notifications
You must be signed in to change notification settings - Fork 58
feat(CapabilityMap): add generic hidraw button driver #563
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
dc2ddc6
0a0613d
3152835
401830a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| version: 2 | ||
| kind: CapabilityMap | ||
| name: GPD Win 5 HID Buttons | ||
| id: gpd_win5_hid | ||
|
|
||
| # GPD Win 5 vendor HID report (VID 0x2f24, PID 0x0137, Usage Page 0xFF00) | ||
| # Idle: 01 a5 00 5a ff 00 01 09 00 00 00 00 | ||
| # BUF[8] = 0x68 mode switch, 0x00 released | ||
| # BUF[9] = 0x69 left back, 0x00 released | ||
| # BUF[10] = 0x6a right back, 0x00 released | ||
| mapping: | ||
| - name: Mode Switch | ||
| source_events: | ||
| - hidraw: | ||
| input_type: button | ||
| byte_start: 8 | ||
| target_event: | ||
| gamepad: | ||
| button: QuickAccess | ||
|
|
||
| - name: Left Back | ||
| source_events: | ||
| - hidraw: | ||
| input_type: button | ||
| byte_start: 9 | ||
| target_event: | ||
| gamepad: | ||
| button: LeftPaddle1 | ||
|
|
||
| - name: Right Back | ||
| source_events: | ||
| - hidraw: | ||
| input_type: button | ||
| byte_start: 10 | ||
| target_event: | ||
| gamepad: | ||
| button: RightPaddle1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| pub mod translator; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| use crate::{ | ||
| config::capability_map::CapabilityMapConfigV2, | ||
| input::{ | ||
| capability::Capability, | ||
| event::{native::NativeEvent, value::InputValue}, | ||
| }, | ||
| }; | ||
|
|
||
| #[derive(Debug, Clone)] | ||
| struct HidrawButtonMapping { | ||
| report_id: Option<u8>, | ||
| byte_index: usize, | ||
| detection: DetectionMode, | ||
| capability: Capability, | ||
| } | ||
|
|
||
| #[derive(Debug, Clone)] | ||
| enum DetectionMode { | ||
| NonZero, | ||
| Value(u8), | ||
| /// Bit position (LSB=0) | ||
| Bit(u8), | ||
| } | ||
|
|
||
| /// Translates raw HID reports into [NativeEvent]s using a capability map. | ||
| #[derive(Debug)] | ||
| pub struct HidrawEventTranslator { | ||
| mappings: Vec<HidrawButtonMapping>, | ||
| state: Vec<bool>, | ||
| } | ||
|
|
||
| impl HidrawEventTranslator { | ||
| /// Create a new translator from a V2 capability map. | ||
| pub fn new(capability_map: &CapabilityMapConfigV2) -> Self { | ||
| let mut mappings = Vec::new(); | ||
|
|
||
| for mapping in capability_map.mapping.iter() { | ||
| for source in mapping.source_events.iter() { | ||
|
pastaq marked this conversation as resolved.
|
||
| let Some(hidraw) = source.hidraw.as_ref() else { | ||
| continue; | ||
| }; | ||
|
|
||
| if hidraw.input_type != "button" { | ||
| log::warn!( | ||
| "Unsupported hidraw input_type '{}' in mapping '{}', skipping", | ||
| hidraw.input_type, | ||
| mapping.name, | ||
| ); | ||
| continue; | ||
| } | ||
|
Comment on lines
+43
to
+50
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this implementation only supports simple button decoding, it would be nice to add a |
||
|
|
||
| let cap: Capability = mapping.target_event.clone().into(); | ||
| if cap == Capability::NotImplemented { | ||
| log::warn!( | ||
| "Unresolved target capability in mapping '{}', skipping", | ||
| mapping.name, | ||
| ); | ||
| continue; | ||
| } | ||
|
|
||
| let detection = if let Some(value) = hidraw.value { | ||
| DetectionMode::Value(value) | ||
| } else if let Some(bit) = hidraw.bit_offset { | ||
| DetectionMode::Bit(bit) | ||
| } else { | ||
| DetectionMode::NonZero | ||
| }; | ||
|
|
||
| mappings.push(HidrawButtonMapping { | ||
| report_id: hidraw.report_id, | ||
| byte_index: hidraw.byte_start as usize, | ||
| detection, | ||
| capability: cap, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| let state = vec![false; mappings.len()]; | ||
| Self { mappings, state } | ||
| } | ||
|
|
||
| pub fn has_mappings(&self) -> bool { | ||
|
pastaq marked this conversation as resolved.
Outdated
|
||
| !self.mappings.is_empty() | ||
| } | ||
|
|
||
| pub fn capabilities(&self) -> Vec<Capability> { | ||
| self.mappings.iter().map(|m| m.capability.clone()).collect() | ||
| } | ||
|
|
||
| /// Translate a raw HID report into [NativeEvent]s. Only emits events on | ||
| /// state changes. | ||
| pub fn translate(&mut self, report: &[u8]) -> Vec<NativeEvent> { | ||
| let mut events = Vec::new(); | ||
|
|
||
| for (idx, mapping) in self.mappings.iter().enumerate() { | ||
| if let Some(expected_id) = mapping.report_id { | ||
| if report.first().copied() != Some(expected_id) { | ||
| continue; | ||
| } | ||
| } | ||
|
|
||
| if mapping.byte_index >= report.len() { | ||
| continue; | ||
|
pastaq marked this conversation as resolved.
|
||
| } | ||
|
|
||
| let byte_val = report[mapping.byte_index]; | ||
| let pressed = match mapping.detection { | ||
| DetectionMode::NonZero => byte_val != 0, | ||
| DetectionMode::Value(expected) => byte_val == expected, | ||
| DetectionMode::Bit(bit) => (byte_val & (1 << bit)) != 0, | ||
| }; | ||
|
|
||
| if pressed != self.state[idx] { | ||
| self.state[idx] = pressed; | ||
| events.push(NativeEvent::new( | ||
| mapping.capability.clone(), | ||
| InputValue::Bool(pressed), | ||
| )); | ||
| } | ||
| } | ||
|
|
||
| events | ||
| } | ||
|
|
||
| /// Returns whether the capability map contains any hidraw source events. | ||
| pub fn has_hidraw_mappings(capability_map: &CapabilityMapConfigV2) -> bool { | ||
| capability_map | ||
| .mapping | ||
| .iter() | ||
| .any(|m| m.source_events.iter().any(|s| s.hidraw.is_some())) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| pub mod context; | ||
| pub mod dbus; | ||
| pub mod evdev; | ||
| pub mod hidraw; | ||
| pub mod native; | ||
| pub mod value; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ pub mod blocked; | |
| pub mod dualsense; | ||
| pub mod flydigi_vader_4_pro; | ||
| pub mod fts3528; | ||
| pub mod generic_buttons; | ||
| pub mod gpd_win_mini_touchpad; | ||
| pub mod gpd_win_mini_macro_keyboard; | ||
| pub mod horipad_steam; | ||
|
|
@@ -20,6 +21,7 @@ use std::{error::Error, time::Duration}; | |
|
|
||
| use blocked::BlockedHidrawDevice; | ||
| use flydigi_vader_4_pro::Vader4Pro; | ||
| use generic_buttons::GenericHidrawButtons; | ||
| use gpd_win_mini_touchpad::GpdWinMiniTouchpad; | ||
| use gpd_win_mini_macro_keyboard::GpdWinMiniMacroKeyboard; | ||
| use horipad_steam::HoripadSteam; | ||
|
|
@@ -31,12 +33,18 @@ use xpad_uhid::XpadUhid; | |
| use zotac_zone::ZotacZone; | ||
|
|
||
| use crate::{ | ||
| config, | ||
| config::{ | ||
| self, | ||
| capability_map::{load_capability_mappings, CapabilityMapConfig, CapabilityMapConfigV2}, | ||
| }, | ||
| constants::BUS_SOURCES_PREFIX, | ||
| drivers, | ||
| input::{ | ||
| capability::Capability, composite_device::client::CompositeDeviceClient, | ||
| info::DeviceInfoRef, output_capability::OutputCapability, | ||
| capability::Capability, | ||
| composite_device::client::CompositeDeviceClient, | ||
| event::hidraw::translator::HidrawEventTranslator, | ||
| info::DeviceInfoRef, | ||
| output_capability::OutputCapability, | ||
| }, | ||
| udev::device::UdevDevice, | ||
| }; | ||
|
|
@@ -76,6 +84,7 @@ pub enum HidRawDevice { | |
| Blocked(SourceDriver<BlockedHidrawDevice>), | ||
| DualSense(SourceDriver<DualSenseController>), | ||
| Fts3528Touchscreen(SourceDriver<Fts3528Touchscreen>), | ||
| GenericButtons(SourceDriver<GenericHidrawButtons>), | ||
| GpdWinMiniTouchpad(SourceDriver<GpdWinMiniTouchpad>), | ||
| GpdWinMiniMacroKeyboard(SourceDriver<GpdWinMiniMacroKeyboard>), | ||
| HoripadSteam(SourceDriver<HoripadSteam>), | ||
|
|
@@ -98,6 +107,7 @@ impl SourceDeviceCompatible for HidRawDevice { | |
| HidRawDevice::Blocked(source_driver) => source_driver.info_ref(), | ||
| HidRawDevice::DualSense(source_driver) => source_driver.info_ref(), | ||
| HidRawDevice::Fts3528Touchscreen(source_driver) => source_driver.info_ref(), | ||
| HidRawDevice::GenericButtons(source_driver) => source_driver.info_ref(), | ||
| HidRawDevice::GpdWinMiniTouchpad(source_driver) => source_driver.info_ref(), | ||
| HidRawDevice::GpdWinMiniMacroKeyboard(source_driver) => source_driver.info_ref(), | ||
| HidRawDevice::HoripadSteam(source_driver) => source_driver.info_ref(), | ||
|
|
@@ -120,6 +130,7 @@ impl SourceDeviceCompatible for HidRawDevice { | |
| HidRawDevice::Blocked(source_driver) => source_driver.get_id(), | ||
| HidRawDevice::DualSense(source_driver) => source_driver.get_id(), | ||
| HidRawDevice::Fts3528Touchscreen(source_driver) => source_driver.get_id(), | ||
| HidRawDevice::GenericButtons(source_driver) => source_driver.get_id(), | ||
| HidRawDevice::GpdWinMiniTouchpad(source_driver) => source_driver.get_id(), | ||
| HidRawDevice::GpdWinMiniMacroKeyboard(source_driver) => source_driver.get_id(), | ||
| HidRawDevice::HoripadSteam(source_driver) => source_driver.get_id(), | ||
|
|
@@ -142,6 +153,7 @@ impl SourceDeviceCompatible for HidRawDevice { | |
| HidRawDevice::Blocked(source_driver) => source_driver.client(), | ||
| HidRawDevice::DualSense(source_driver) => source_driver.client(), | ||
| HidRawDevice::Fts3528Touchscreen(source_driver) => source_driver.client(), | ||
| HidRawDevice::GenericButtons(source_driver) => source_driver.client(), | ||
| HidRawDevice::GpdWinMiniTouchpad(source_driver) => source_driver.client(), | ||
| HidRawDevice::GpdWinMiniMacroKeyboard(source_driver) => source_driver.client(), | ||
| HidRawDevice::HoripadSteam(source_driver) => source_driver.client(), | ||
|
|
@@ -164,6 +176,7 @@ impl SourceDeviceCompatible for HidRawDevice { | |
| HidRawDevice::Blocked(source_driver) => source_driver.run().await, | ||
| HidRawDevice::DualSense(source_driver) => source_driver.run().await, | ||
| HidRawDevice::Fts3528Touchscreen(source_driver) => source_driver.run().await, | ||
| HidRawDevice::GenericButtons(source_driver) => source_driver.run().await, | ||
| HidRawDevice::GpdWinMiniTouchpad(source_driver) => source_driver.run().await, | ||
| HidRawDevice::GpdWinMiniMacroKeyboard(source_driver) => source_driver.run().await, | ||
| HidRawDevice::HoripadSteam(source_driver) => source_driver.run().await, | ||
|
|
@@ -186,6 +199,7 @@ impl SourceDeviceCompatible for HidRawDevice { | |
| HidRawDevice::Blocked(source_driver) => source_driver.get_capabilities(), | ||
| HidRawDevice::DualSense(source_driver) => source_driver.get_capabilities(), | ||
| HidRawDevice::Fts3528Touchscreen(source_driver) => source_driver.get_capabilities(), | ||
| HidRawDevice::GenericButtons(source_driver) => source_driver.get_capabilities(), | ||
| HidRawDevice::GpdWinMiniTouchpad(source_driver) => source_driver.get_capabilities(), | ||
| HidRawDevice::GpdWinMiniMacroKeyboard(source_driver) => source_driver.get_capabilities(), | ||
| HidRawDevice::HoripadSteam(source_driver) => source_driver.get_capabilities(), | ||
|
|
@@ -210,6 +224,9 @@ impl SourceDeviceCompatible for HidRawDevice { | |
| HidRawDevice::Fts3528Touchscreen(source_driver) => { | ||
| source_driver.get_output_capabilities() | ||
| } | ||
| HidRawDevice::GenericButtons(source_driver) => { | ||
| source_driver.get_output_capabilities() | ||
| } | ||
| HidRawDevice::GpdWinMiniTouchpad(source_driver) => { | ||
| source_driver.get_output_capabilities() | ||
| } | ||
|
|
@@ -238,6 +255,7 @@ impl SourceDeviceCompatible for HidRawDevice { | |
| HidRawDevice::Blocked(source_driver) => source_driver.get_device_path(), | ||
| HidRawDevice::DualSense(source_driver) => source_driver.get_device_path(), | ||
| HidRawDevice::Fts3528Touchscreen(source_driver) => source_driver.get_device_path(), | ||
| HidRawDevice::GenericButtons(source_driver) => source_driver.get_device_path(), | ||
| HidRawDevice::GpdWinMiniTouchpad(source_driver) => source_driver.get_device_path(), | ||
| HidRawDevice::GpdWinMiniMacroKeyboard(source_driver) => source_driver.get_device_path(), | ||
| HidRawDevice::HoripadSteam(source_driver) => source_driver.get_device_path(), | ||
|
|
@@ -269,7 +287,25 @@ impl HidRawDevice { | |
| let driver_type = HidRawDevice::get_driver_type(&device_info, is_blocked); | ||
|
|
||
| match driver_type { | ||
| DriverType::Unknown => Err("No driver for hidraw interface found".into()), | ||
| DriverType::Unknown => { | ||
| if let Some(cap_map) = Self::load_hidraw_capability_map(&conf) { | ||
|
pastaq marked this conversation as resolved.
Outdated
|
||
| log::info!( | ||
| "Using generic hidraw button driver with capability map '{}'", | ||
| cap_map.name, | ||
| ); | ||
| let device = GenericHidrawButtons::new(device_info.clone(), cap_map)?; | ||
| let source_device = | ||
| SourceDriver::new(composite_device, device, device_info.into(), conf); | ||
| return Ok(Self::GenericButtons(source_device)); | ||
| } | ||
|
|
||
| let vid = device_info.id_vendor(); | ||
| let pid = device_info.id_product(); | ||
| Err(format!( | ||
| "No driver for hidraw interface found. VID: {vid:#06x}, PID: {pid:#06x}" | ||
| ) | ||
| .into()) | ||
|
Comment on lines
+299
to
+302
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be good to further specify in the error here that no driver or capability map was found for this device. |
||
| } | ||
| DriverType::Blocked => { | ||
| let options = SourceDriverOptions { | ||
| poll_rate: Duration::from_millis(200), | ||
|
|
@@ -456,6 +492,26 @@ impl HidRawDevice { | |
| } | ||
| } | ||
|
|
||
| fn load_hidraw_capability_map( | ||
| conf: &Option<config::SourceDevice>, | ||
| ) -> Option<CapabilityMapConfigV2> { | ||
| let cap_map_id = conf.as_ref()?.capability_map_id.as_ref()?; | ||
| let mappings = load_capability_mappings(); | ||
| let cap_map = match mappings.get(cap_map_id) { | ||
| Some(CapabilityMapConfig::V2(config)) => config.clone(), | ||
| _ => { | ||
| log::warn!("Capability map '{cap_map_id}' not found or not V2"); | ||
| return None; | ||
| } | ||
| }; | ||
|
|
||
| if !HidrawEventTranslator::has_hidraw_mappings(&cap_map) { | ||
| return None; | ||
| } | ||
|
|
||
| Some(cap_map) | ||
| } | ||
|
|
||
| /// Return the driver type for the given vendor and product | ||
| fn get_driver_type(device: &UdevDevice, is_blocked: bool) -> DriverType { | ||
| log::debug!("Finding driver for interface: {:?}", device); | ||
|
|
@@ -593,8 +649,7 @@ impl HidRawDevice { | |
| return DriverType::GpdWinMiniMacroKeyboard; | ||
| } | ||
|
|
||
| // Unknown | ||
| log::warn!("No driver for hidraw interface found. VID: {vid}, PID: {pid}"); | ||
| log::debug!("No specialized hidraw driver for VID: {vid:#06x}, PID: {pid:#06x}"); | ||
| DriverType::Unknown | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the purpose of this field in decoding hidraw data?