diff --git a/rootfs/usr/share/inputplumber/capability_maps/gpd_v2_hid1.yaml b/rootfs/usr/share/inputplumber/capability_maps/gpd_v2_hid1.yaml new file mode 100644 index 00000000..28d0a674 --- /dev/null +++ b/rootfs/usr/share/inputplumber/capability_maps/gpd_v2_hid1.yaml @@ -0,0 +1,44 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/ShadowBlip/InputPlumber/main/rootfs/usr/share/inputplumber/schema/capability_map_v2.json +# Schema version number +version: 2 + +# The type of configuration schema +kind: CapabilityMap + +# Name for the device event map +name: GPD HID Type 1 + +id: gpd_v2_hid1 + +# 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 diff --git a/rootfs/usr/share/inputplumber/devices/50-gpd_win5.yaml b/rootfs/usr/share/inputplumber/devices/50-gpd_win5.yaml index e5949af3..8bd064d5 100644 --- a/rootfs/usr/share/inputplumber/devices/50-gpd_win5.yaml +++ b/rootfs/usr/share/inputplumber/devices/50-gpd_win5.yaml @@ -38,6 +38,13 @@ source_devices: name: " Keyboard for Windows" handler: event* phys_path: usb-0000:66:00.0-5.3/input0 + # New firmware: back buttons via vendor HID report + - group: keyboard + hidraw: + vendor_id: 0x2f24 + product_id: 0x0137 + interface_num: 0 + capability_map_id: gpd_v2_hid1 - group: keyboard evdev: name: AT Translated Set 2 keyboard diff --git a/rootfs/usr/share/inputplumber/schema/capability_map_v2.json b/rootfs/usr/share/inputplumber/schema/capability_map_v2.json index 38d21393..0c2b7959 100644 --- a/rootfs/usr/share/inputplumber/schema/capability_map_v2.json +++ b/rootfs/usr/share/inputplumber/schema/capability_map_v2.json @@ -1191,7 +1191,11 @@ "type": "object", "properties": { "bit_offset": { - "type": "integer", + "description": "Bit position within the byte (LSB=0).", + "type": [ + "integer", + "null" + ], "format": "uint8", "maximum": 255, "minimum": 0 @@ -1205,16 +1209,27 @@ "type": "string" }, "report_id": { - "type": "integer", - "format": "uint32", + "type": [ + "integer", + "null" + ], + "format": "uint8", + "maximum": 255, + "minimum": 0 + }, + "value": { + "type": [ + "integer", + "null" + ], + "format": "uint8", + "maximum": 255, "minimum": 0 } }, "required": [ - "report_id", "input_type", - "byte_start", - "bit_offset" + "byte_start" ] }, "MappingType": { @@ -1446,4 +1461,4 @@ ] } } -} +} \ No newline at end of file diff --git a/src/config/capability_map/hidraw.rs b/src/config/capability_map/hidraw.rs index 3d680b30..748b6ba2 100644 --- a/src/config/capability_map/hidraw.rs +++ b/src/config/capability_map/hidraw.rs @@ -6,8 +6,13 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize, Clone, JsonSchema, PartialEq)] #[serde(rename_all = "snake_case")] pub struct HidrawConfig { - pub report_id: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub report_id: Option, pub input_type: String, pub byte_start: u64, - pub bit_offset: u8, + /// Bit position within the byte (LSB=0). + #[serde(skip_serializing_if = "Option::is_none")] + pub bit_offset: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, } diff --git a/src/input/event/hidraw.rs b/src/input/event/hidraw.rs new file mode 100644 index 00000000..a73a1679 --- /dev/null +++ b/src/input/event/hidraw.rs @@ -0,0 +1 @@ +pub mod translator; diff --git a/src/input/event/hidraw/translator.rs b/src/input/event/hidraw/translator.rs new file mode 100644 index 00000000..3ea6e76c --- /dev/null +++ b/src/input/event/hidraw/translator.rs @@ -0,0 +1,129 @@ +use crate::{ + config::capability_map::CapabilityMapConfigV2, + input::{ + capability::Capability, + event::{native::NativeEvent, value::InputValue}, + }, +}; + +#[derive(Debug, Clone)] +struct HidrawButtonMapping { + report_id: Option, + 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 { + source_events: Vec, + state: Vec, +} + +impl HidrawEventTranslator { + /// Create a new translator from a V2 capability map. + pub fn new(capability_map: &CapabilityMapConfigV2) -> Self { + let mut source_events = Vec::new(); + + for mapping in capability_map.mapping.iter() { + for source in mapping.source_events.iter() { + 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; + } + + 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 + }; + + source_events.push(HidrawButtonMapping { + report_id: hidraw.report_id, + byte_index: hidraw.byte_start as usize, + detection, + capability: cap, + }); + } + } + + let state = vec![false; source_events.len()]; + Self { source_events, state } + } + + pub fn has_hid_translation(&self) -> bool { + !self.source_events.is_empty() + } + + pub fn capabilities(&self) -> Vec { + self.source_events.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 { + let mut events = Vec::new(); + + for (idx, mapping) in self.source_events.iter().enumerate() { + if let Some(expected_id) = mapping.report_id { + if report.first().copied() != Some(expected_id) { + continue; + } + } + + if mapping.byte_index >= report.len() { + log::warn!( + "HID report too short for mapping at byte {}: got {} bytes", + mapping.byte_index, + report.len(), + ); + continue; + } + + 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 + } +} diff --git a/src/input/event/mod.rs b/src/input/event/mod.rs index 93b11064..8e237a1f 100644 --- a/src/input/event/mod.rs +++ b/src/input/event/mod.rs @@ -1,6 +1,7 @@ pub mod context; pub mod dbus; pub mod evdev; +pub mod hidraw; pub mod native; pub mod value; diff --git a/src/input/source/hidraw.rs b/src/input/source/hidraw.rs index 0358a375..a26231c9 100644 --- a/src/input/source/hidraw.rs +++ b/src/input/source/hidraw.rs @@ -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,17 @@ 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, + info::DeviceInfoRef, + output_capability::OutputCapability, }, udev::device::UdevDevice, }; @@ -76,6 +83,7 @@ pub enum HidRawDevice { Blocked(SourceDriver), DualSense(SourceDriver), Fts3528Touchscreen(SourceDriver), + GenericButtons(SourceDriver), GpdWinMiniTouchpad(SourceDriver), GpdWinMiniMacroKeyboard(SourceDriver), HoripadSteam(SourceDriver), @@ -98,6 +106,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 +129,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 +152,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 +175,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 +198,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 +223,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 +254,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 +286,21 @@ 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_capability_map_v2(&conf) { + 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()) + } DriverType::Blocked => { let options = SourceDriverOptions { poll_rate: Duration::from_millis(200), @@ -456,6 +487,20 @@ impl HidRawDevice { } } + fn load_capability_map_v2( + conf: &Option, + ) -> Option { + let cap_map_id = conf.as_ref()?.capability_map_id.as_ref()?; + let mappings = load_capability_mappings(); + match mappings.get(cap_map_id) { + Some(CapabilityMapConfig::V2(config)) => Some(config.clone()), + _ => { + log::warn!("Capability map '{cap_map_id}' not found or not V2"); + None + } + } + } + /// 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 +638,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 } } diff --git a/src/input/source/hidraw/generic_buttons.rs b/src/input/source/hidraw/generic_buttons.rs new file mode 100644 index 00000000..d3f16e9e --- /dev/null +++ b/src/input/source/hidraw/generic_buttons.rs @@ -0,0 +1,80 @@ +use std::{error::Error, ffi::CString, fmt::Debug}; + +use hidapi::HidDevice; + +use crate::{ + config::capability_map::CapabilityMapConfigV2, + input::{ + capability::Capability, + event::{hidraw::translator::HidrawEventTranslator, native::NativeEvent}, + source::{InputError, SourceInputDevice, SourceOutputDevice}, + }, + udev::device::UdevDevice, +}; + +const HID_TIMEOUT: i32 = 10; +const READ_BUF_SIZE: usize = 64; + +/// Generic hidraw button source device driven by a capability map. +pub struct GenericHidrawButtons { + device: HidDevice, + translator: HidrawEventTranslator, +} + +impl GenericHidrawButtons { + pub fn new( + device_info: UdevDevice, + capability_map: CapabilityMapConfigV2, + ) -> Result> { + let path = device_info.devnode(); + let cs_path = CString::new(path.clone())?; + let api = hidapi::HidApi::new()?; + let device = api.open_path(&cs_path)?; + device.set_blocking_mode(false)?; + + let translator = HidrawEventTranslator::new(&capability_map); + if !translator.has_hid_translation() { + return Err(format!( + "Capability map '{}' has no hidraw button mappings", + capability_map.name + ) + .into()); + } + + log::info!( + "Opened generic hidraw button device at {path} with {} mapping(s)", + translator.capabilities().len(), + ); + + Ok(Self { device, translator }) + } +} + +impl SourceInputDevice for GenericHidrawButtons { + fn poll(&mut self) -> Result, InputError> { + let mut buf = [0u8; READ_BUF_SIZE]; + let bytes_read = self + .device + .read_timeout(&mut buf[..], HID_TIMEOUT) + .map_err(|e| InputError::DeviceError(e.to_string()))?; + + if bytes_read == 0 { + return Ok(vec![]); + } + + let events = self.translator.translate(&buf[..bytes_read]); + Ok(events) + } + + fn get_capabilities(&self) -> Result, InputError> { + Ok(self.translator.capabilities()) + } +} + +impl SourceOutputDevice for GenericHidrawButtons {} + +impl Debug for GenericHidrawButtons { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GenericHidrawButtons").finish() + } +}