From ce9a3f0bdcdaf40be81ecd60ffadc79e4076bfd6 Mon Sep 17 00:00:00 2001 From: William Edwards Date: Sun, 26 Jul 2026 23:06:59 -0700 Subject: [PATCH 1/2] feat(CapabilityMap): add generic hidraw driver --- .../schema/capability_map_v2.json | 172 ++++- src/config/capability_map/hidraw.rs | 103 ++- src/input/event/hidraw.rs | 3 + src/input/event/hidraw/translator.rs | 646 ++++++++++++++++++ src/input/event/hidraw/translator_test.rs | 47 ++ src/input/event/mod.rs | 1 + src/input/event/value.rs | 2 +- src/input/source/hidraw.rs | 42 +- src/input/source/hidraw/generic.rs | 75 ++ 9 files changed, 1070 insertions(+), 21 deletions(-) create mode 100644 src/input/event/hidraw.rs create mode 100644 src/input/event/hidraw/translator.rs create mode 100644 src/input/event/hidraw/translator_test.rs create mode 100644 src/input/source/hidraw/generic.rs diff --git a/rootfs/usr/share/inputplumber/schema/capability_map_v2.json b/rootfs/usr/share/inputplumber/schema/capability_map_v2.json index 38d21393..e9488984 100644 --- a/rootfs/usr/share/inputplumber/schema/capability_map_v2.json +++ b/rootfs/usr/share/inputplumber/schema/capability_map_v2.json @@ -239,6 +239,21 @@ "name" ] }, + "Endianness": { + "description": "Endianness is the order in which a multi-byte number is represented.", + "oneOf": [ + { + "description": "Least significant byte ordering", + "type": "string", + "const": "lsb" + }, + { + "description": "Most significant byte ordering", + "type": "string", + "const": "msb" + } + ] + }, "EvdevConfig": { "description": "An [EvdevConfig] defines a matching evdev input event", "type": "object", @@ -1191,30 +1206,66 @@ "type": "object", "properties": { "bit_offset": { - "type": "integer", + "description": "Optional bit offset to start reading from", + "type": [ + "integer", + "null" + ], "format": "uint8", "maximum": 255, "minimum": 0 }, "byte_start": { + "description": "The byte where the data begins", "type": "integer", - "format": "uint64", + "format": "uint", "minimum": 0 }, - "input_type": { - "type": "string" + "endian": { + "description": "Optional endianness of the value being decoded. Defaults to LSB.", + "anyOf": [ + { + "$ref": "#/$defs/Endianness" + }, + { + "type": "null" + } + ] + }, + "max_value": { + "description": "Optional maximum value used for normalizing the value. InputPlumber\ntypically normalizes input values from 0.0 - 1.0 or from -1.0 - 1.0.", + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "min_value": { + "description": "Optional minimum value used for normalizing the value. InputPlumber\ntypically normalizes input values from 0.0 - 1.0 or from -1.0 - 1.0.", + "type": [ + "integer", + "null" + ], + "format": "int64" }, "report_id": { - "type": "integer", - "format": "uint32", + "description": "Optional report ID of the input report. This is typically the first byte\nof the input report.", + "type": [ + "integer", + "null" + ], + "format": "uint8", + "maximum": 255, "minimum": 0 + }, + "value_type": { + "description": "Data type of the input. This is used to decode the value of the input\nreport.", + "$ref": "#/$defs/ValueType2" } }, "required": [ - "report_id", - "input_type", - "byte_start", - "bit_offset" + "value_type", + "byte_start" ] }, "MappingType": { @@ -1444,6 +1495,105 @@ "imu_y", "imu_z" ] + }, + "ValueType2": { + "oneOf": [ + { + "description": "Bool values take up 1 bit in the input report", + "type": "string", + "const": "bool" + }, + { + "description": "Uint8 values take up 1 byte in the input report", + "type": "string", + "const": "uint8" + }, + { + "description": "Uint16 values take up 2 bytes in the input report", + "type": "string", + "const": "uint16" + }, + { + "description": "Uint32 values take up 4 bytes in the input report", + "type": "string", + "const": "uint32" + }, + { + "description": "Int8 values take up 1 byte in the input report", + "type": "string", + "const": "int8" + }, + { + "description": "Int16 values take up 2 bytes in the input report", + "type": "string", + "const": "int16" + }, + { + "description": "Int32 values take up 4 bytes in the input report", + "type": "string", + "const": "int32" + }, + { + "description": "UInt8Vector2 values take up 2 bytes in the input report", + "type": "string", + "const": "vector2_uint8" + }, + { + "description": "UInt16Vector2 values take up 4 bytes in the input report", + "type": "string", + "const": "vector2_uint16" + }, + { + "description": "UInt32Vector2 values take up 8 bytes in the input report", + "type": "string", + "const": "vector2_uint32" + }, + { + "description": "Int8Vector2 values take up 2 bytes in the input report", + "type": "string", + "const": "vector2_int8" + }, + { + "description": "Int16Vector2 values take up 4 bytes in the input report", + "type": "string", + "const": "vector2_int16" + }, + { + "description": "Int32Vector2 values take up 8 bytes in the input report", + "type": "string", + "const": "vector2_int32" + }, + { + "description": "UInt8Vector3 values take up 3 bytes in the input report", + "type": "string", + "const": "vector3_uint8" + }, + { + "description": "UInt16Vector3 values take up 6 bytes in the input report", + "type": "string", + "const": "vector3_uint16" + }, + { + "description": "UInt32Vector3 values take up 12 bytes in the input report", + "type": "string", + "const": "vector3_uint32" + }, + { + "description": "Int8Vector3 values take up 3 bytes in the input report", + "type": "string", + "const": "vector3_int8" + }, + { + "description": "Int16Vector3 values take up 6 bytes in the input report", + "type": "string", + "const": "vector3_int16" + }, + { + "description": "Int32Vector3 values take up 12 bytes in the input report", + "type": "string", + "const": "vector3_int32" + } + ] } } -} +} \ No newline at end of file diff --git a/src/config/capability_map/hidraw.rs b/src/config/capability_map/hidraw.rs index 3d680b30..f4208ccf 100644 --- a/src/config/capability_map/hidraw.rs +++ b/src/config/capability_map/hidraw.rs @@ -6,8 +6,103 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize, Clone, JsonSchema, PartialEq)] #[serde(rename_all = "snake_case")] pub struct HidrawConfig { - pub report_id: u32, - pub input_type: String, - pub byte_start: u64, - pub bit_offset: u8, + /// Optional report ID of the input report. This is typically the first byte + /// of the input report. + #[serde(skip_serializing_if = "Option::is_none")] + pub report_id: Option, + /// Data type of the input. This is used to decode the value of the input + /// report. + pub value_type: ValueType, + /// The byte where the data begins + pub byte_start: usize, + /// Optional maximum value used for normalizing the value. InputPlumber + /// typically normalizes input values from 0.0 - 1.0 or from -1.0 - 1.0. + #[serde(skip_serializing_if = "Option::is_none")] + pub max_value: Option, + /// Optional minimum value used for normalizing the value. InputPlumber + /// typically normalizes input values from 0.0 - 1.0 or from -1.0 - 1.0. + #[serde(skip_serializing_if = "Option::is_none")] + pub min_value: Option, + /// Optional bit offset to start reading from + #[serde(skip_serializing_if = "Option::is_none")] + pub bit_offset: Option, + /// Optional endianness of the value being decoded. Defaults to LSB. + #[serde(skip_serializing_if = "Option::is_none")] + pub endian: Option, +} + +/// Endianness is the order in which a multi-byte number is represented. +#[derive(Default, Debug, Deserialize, Serialize, Clone, JsonSchema, PartialEq)] +pub enum Endianness { + /// Least significant byte ordering + #[default] + #[serde(rename = "lsb")] + Lsb, + /// Most significant byte ordering + #[serde(rename = "msb")] + Msb, +} + +#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema, PartialEq)] +pub enum ValueType { + /// Bool values take up 1 bit in the input report + #[serde(rename = "bool")] + Bool, + + /// Uint8 values take up 1 byte in the input report + #[serde(rename = "uint8")] + UInt8, + /// Uint16 values take up 2 bytes in the input report + #[serde(rename = "uint16")] + UInt16, + /// Uint32 values take up 4 bytes in the input report + #[serde(rename = "uint32")] + UInt32, + /// Int8 values take up 1 byte in the input report + #[serde(rename = "int8")] + Int8, + /// Int16 values take up 2 bytes in the input report + #[serde(rename = "int16")] + Int16, + /// Int32 values take up 4 bytes in the input report + #[serde(rename = "int32")] + Int32, + + /// UInt8Vector2 values take up 2 bytes in the input report + #[serde(rename = "vector2_uint8")] + UInt8Vector2, + /// UInt16Vector2 values take up 4 bytes in the input report + #[serde(rename = "vector2_uint16")] + UInt16Vector2, + /// UInt32Vector2 values take up 8 bytes in the input report + #[serde(rename = "vector2_uint32")] + UInt32Vector2, + /// Int8Vector2 values take up 2 bytes in the input report + #[serde(rename = "vector2_int8")] + Int8Vector2, + /// Int16Vector2 values take up 4 bytes in the input report + #[serde(rename = "vector2_int16")] + Int16Vector2, + /// Int32Vector2 values take up 8 bytes in the input report + #[serde(rename = "vector2_int32")] + Int32Vector2, + + /// UInt8Vector3 values take up 3 bytes in the input report + #[serde(rename = "vector3_uint8")] + UInt8Vector3, + /// UInt16Vector3 values take up 6 bytes in the input report + #[serde(rename = "vector3_uint16")] + UInt16Vector3, + /// UInt32Vector3 values take up 12 bytes in the input report + #[serde(rename = "vector3_uint32")] + UInt32Vector3, + /// Int8Vector3 values take up 3 bytes in the input report + #[serde(rename = "vector3_int8")] + Int8Vector3, + /// Int16Vector3 values take up 6 bytes in the input report + #[serde(rename = "vector3_int16")] + Int16Vector3, + /// Int32Vector3 values take up 12 bytes in the input report + #[serde(rename = "vector3_int32")] + Int32Vector3, } diff --git a/src/input/event/hidraw.rs b/src/input/event/hidraw.rs new file mode 100644 index 00000000..b64a64d6 --- /dev/null +++ b/src/input/event/hidraw.rs @@ -0,0 +1,3 @@ +pub mod translator; +#[cfg(test)] +pub mod translator_test; diff --git a/src/input/event/hidraw/translator.rs b/src/input/event/hidraw/translator.rs new file mode 100644 index 00000000..fd3686d1 --- /dev/null +++ b/src/input/event/hidraw/translator.rs @@ -0,0 +1,646 @@ +use thiserror::Error; + +use crate::{ + config::capability_map::{ + hidraw::{Endianness, HidrawConfig, ValueType}, + CapabilityMapConfigV2, + }, + input::{ + capability::Capability, + event::{ + native::NativeEvent, + value::{normalize_signed_value, normalize_unsigned_value, InputValue}, + }, + }, +}; + +#[derive(Error, Debug, Clone)] +pub enum DecodeError { + #[error("Read zero bytes from input report")] + EmptyInputReport, + #[error("Input report id {0} does not match expected report id: {1}")] + UnexpectedReportId(u8, u8), + #[error("Tried to read byte {0} from input report, but report is only {1} bytes")] + StartByteExceedsReportSize(usize, usize), + #[error("Tried to read a {0} sized value from byte {1} in input report, but report is only {2} bytes")] + ValueExceedsReportSize(usize, usize, usize), +} + +/// Used to translate hidraw input reports into native inputplumber events using a +/// capability map. +#[derive(Debug)] +pub struct HidrawEventTranslator { + mappings: Vec<(Capability, HidrawConfig)>, + last_state: Option>, +} + +impl HidrawEventTranslator { + pub fn new(capability_map: &CapabilityMapConfigV2) -> Self { + // Build a list of hidraw mappings + let mut mappings = vec![]; + for mapping in capability_map.mapping.iter() { + for source_event in mapping.source_events.iter() { + let Some(hidraw_mapping) = source_event.hidraw.as_ref() else { + continue; + }; + let capability: Capability = mapping.target_event.clone().into(); + mappings.push((capability, hidraw_mapping.clone())); + } + } + + Self { + mappings, + last_state: None, + } + } + + /// Translates hidraw input reports into native inputplumber events. + pub fn translate(&mut self, report: &[u8]) -> Vec { + // We should only emit events on state change. If no last state exists, + // then wait until the next translation cycle. + let Some(last_state) = self.last_state.as_ref() else { + self.last_state = Some(report.to_vec()); + return vec![]; + }; + + // Decode the input report according to the mappings + let mut events = vec![]; + for (target_capability, mapping) in self.mappings.iter() { + let value = match Self::decode_value(report, mapping) { + Ok(value) => value, + Err(e) => match e { + DecodeError::EmptyInputReport => { + log::trace!("{e}"); + continue; + } + DecodeError::UnexpectedReportId(..) => { + log::trace!("{e}"); + continue; + } + DecodeError::StartByteExceedsReportSize(..) => { + log::warn!("{e}"); + continue; + } + DecodeError::ValueExceedsReportSize(..) => { + log::warn!("{e}"); + continue; + } + }, + }; + let Ok(last_value) = Self::decode_value(last_state, mapping) else { + continue; + }; + + // Only emit events on state change + if value == last_value { + continue; + } + + let event = NativeEvent::new(target_capability.clone(), value); + events.push(event); + } + + // Keep a copy of the last state to determine if an event needs to be + // emitted. + // TODO: What about multiple input reports? + self.last_state = Some(report.to_vec()); + + events + } + + /// Return the decoded value for the given input report and mapping + fn decode_value(report: &[u8], mapping: &HidrawConfig) -> Result { + // Check if the input report id matches + if let Some(expected_report_id) = mapping.report_id { + let Some(report_id) = report.first() else { + return Err(DecodeError::EmptyInputReport); + }; + if *report_id == expected_report_id { + return Err(DecodeError::UnexpectedReportId( + *report_id, + expected_report_id, + )); + } + } + + // Ensure that the input report is in range of the value + if mapping.byte_start >= report.len() { + return Err(DecodeError::StartByteExceedsReportSize( + mapping.byte_start, + report.len(), + )); + } + + // Translate the event based on the value type + let value = match mapping.value_type { + ValueType::Bool => { + let value = Self::decode_bool(report, mapping); + InputValue::Bool(value) + } + ValueType::UInt8 => { + let value = Self::decode_u8(report, mapping.byte_start, mapping.max_value); + InputValue::Float(value) + } + ValueType::UInt16 => { + let value = Self::decode_u16( + report, + mapping.byte_start, + mapping.max_value, + mapping.endian.as_ref(), + )?; + InputValue::Float(value) + } + ValueType::UInt32 => { + let value = Self::decode_u32( + report, + mapping.byte_start, + mapping.max_value, + mapping.endian.as_ref(), + )?; + InputValue::Float(value) + } + ValueType::Int8 => { + let value = Self::decode_i8( + report, + mapping.byte_start, + mapping.min_value, + mapping.max_value, + ); + InputValue::Float(value) + } + ValueType::Int16 => { + let value = Self::decode_i16( + report, + mapping.byte_start, + mapping.min_value, + mapping.max_value, + mapping.endian.as_ref(), + )?; + InputValue::Float(value) + } + ValueType::Int32 => { + let value = Self::decode_i32( + report, + mapping.byte_start, + mapping.min_value, + mapping.max_value, + mapping.endian.as_ref(), + )?; + InputValue::Float(value) + } + ValueType::UInt8Vector2 => { + const SIZE: usize = 1; + let value_x = Self::decode_u8(report, mapping.byte_start, mapping.max_value); + let value_y = Self::decode_u8(report, mapping.byte_start + SIZE, mapping.max_value); + + InputValue::Vector2 { + x: Some(value_x), + y: Some(value_y), + } + } + ValueType::UInt16Vector2 => { + const SIZE: usize = (u16::BITS / 8) as usize; + let value_x = Self::decode_u16( + report, + mapping.byte_start, + mapping.max_value, + mapping.endian.as_ref(), + )?; + let value_y = Self::decode_u16( + report, + mapping.byte_start + SIZE, + mapping.max_value, + mapping.endian.as_ref(), + )?; + + InputValue::Vector2 { + x: Some(value_x), + y: Some(value_y), + } + } + ValueType::UInt32Vector2 => { + const SIZE: usize = (u32::BITS / 8) as usize; + let value_x = Self::decode_u32( + report, + mapping.byte_start, + mapping.max_value, + mapping.endian.as_ref(), + )?; + let value_y = Self::decode_u16( + report, + mapping.byte_start + SIZE, + mapping.max_value, + mapping.endian.as_ref(), + )?; + + InputValue::Vector2 { + x: Some(value_x), + y: Some(value_y), + } + } + ValueType::Int8Vector2 => { + const SIZE: usize = 1; + let value_x = Self::decode_i8( + report, + mapping.byte_start, + mapping.min_value, + mapping.max_value, + ); + let value_y = Self::decode_i8( + report, + mapping.byte_start + SIZE, + mapping.min_value, + mapping.max_value, + ); + + InputValue::Vector2 { + x: Some(value_x), + y: Some(value_y), + } + } + ValueType::Int16Vector2 => { + const SIZE: usize = (i16::BITS / 8) as usize; + let value_x = Self::decode_i16( + report, + mapping.byte_start, + mapping.min_value, + mapping.max_value, + mapping.endian.as_ref(), + )?; + let value_y = Self::decode_i16( + report, + mapping.byte_start + SIZE, + mapping.min_value, + mapping.max_value, + mapping.endian.as_ref(), + )?; + + InputValue::Vector2 { + x: Some(value_x), + y: Some(value_y), + } + } + ValueType::Int32Vector2 => { + const SIZE: usize = (i32::BITS / 8) as usize; + let value_x = Self::decode_i32( + report, + mapping.byte_start, + mapping.min_value, + mapping.max_value, + mapping.endian.as_ref(), + )?; + let value_y = Self::decode_i32( + report, + mapping.byte_start + SIZE, + mapping.min_value, + mapping.max_value, + mapping.endian.as_ref(), + )?; + + InputValue::Vector2 { + x: Some(value_x), + y: Some(value_y), + } + } + ValueType::UInt8Vector3 => { + const SIZE: usize = 1; + let value_x = Self::decode_u8(report, mapping.byte_start, mapping.max_value); + let value_y = Self::decode_u8(report, mapping.byte_start + SIZE, mapping.max_value); + let value_z = + Self::decode_u8(report, mapping.byte_start + (SIZE * 2), mapping.max_value); + + InputValue::Vector3 { + x: Some(value_x), + y: Some(value_y), + z: Some(value_z), + } + } + ValueType::UInt16Vector3 => { + const SIZE: usize = (u16::BITS / 8) as usize; + let value_x = Self::decode_u16( + report, + mapping.byte_start, + mapping.max_value, + mapping.endian.as_ref(), + )?; + let value_y = Self::decode_u16( + report, + mapping.byte_start + SIZE, + mapping.max_value, + mapping.endian.as_ref(), + )?; + let value_z = Self::decode_u16( + report, + mapping.byte_start + (SIZE * 2), + mapping.max_value, + mapping.endian.as_ref(), + )?; + + InputValue::Vector3 { + x: Some(value_x), + y: Some(value_y), + z: Some(value_z), + } + } + ValueType::UInt32Vector3 => { + const SIZE: usize = (u32::BITS / 8) as usize; + let value_x = Self::decode_u32( + report, + mapping.byte_start, + mapping.max_value, + mapping.endian.as_ref(), + )?; + let value_y = Self::decode_u32( + report, + mapping.byte_start + SIZE, + mapping.max_value, + mapping.endian.as_ref(), + )?; + let value_z = Self::decode_u32( + report, + mapping.byte_start + (SIZE * 2), + mapping.max_value, + mapping.endian.as_ref(), + )?; + + InputValue::Vector3 { + x: Some(value_x), + y: Some(value_y), + z: Some(value_z), + } + } + ValueType::Int8Vector3 => { + const SIZE: usize = 1; + let value_x = Self::decode_i8( + report, + mapping.byte_start, + mapping.min_value, + mapping.max_value, + ); + let value_y = Self::decode_i8( + report, + mapping.byte_start + SIZE, + mapping.min_value, + mapping.max_value, + ); + let value_z = Self::decode_i8( + report, + mapping.byte_start + (SIZE * 2), + mapping.min_value, + mapping.max_value, + ); + + InputValue::Vector3 { + x: Some(value_x), + y: Some(value_y), + z: Some(value_z), + } + } + ValueType::Int16Vector3 => { + const SIZE: usize = (i16::BITS / 8) as usize; + let value_x = Self::decode_i16( + report, + mapping.byte_start, + mapping.min_value, + mapping.max_value, + mapping.endian.as_ref(), + )?; + let value_y = Self::decode_i16( + report, + mapping.byte_start + SIZE, + mapping.min_value, + mapping.max_value, + mapping.endian.as_ref(), + )?; + let value_z = Self::decode_i16( + report, + mapping.byte_start + (SIZE * 2), + mapping.min_value, + mapping.max_value, + mapping.endian.as_ref(), + )?; + + InputValue::Vector3 { + x: Some(value_x), + y: Some(value_y), + z: Some(value_z), + } + } + ValueType::Int32Vector3 => { + const SIZE: usize = (i32::BITS / 8) as usize; + let value_x = Self::decode_i32( + report, + mapping.byte_start, + mapping.min_value, + mapping.max_value, + mapping.endian.as_ref(), + )?; + let value_y = Self::decode_i32( + report, + mapping.byte_start + SIZE, + mapping.min_value, + mapping.max_value, + mapping.endian.as_ref(), + )?; + let value_z = Self::decode_i32( + report, + mapping.byte_start + (SIZE * 2), + mapping.min_value, + mapping.max_value, + mapping.endian.as_ref(), + )?; + + InputValue::Vector3 { + x: Some(value_x), + y: Some(value_y), + z: Some(value_z), + } + } + }; + + Ok(value) + } + + fn decode_bool(report: &[u8], mapping: &HidrawConfig) -> bool { + let byte_value = report[mapping.byte_start]; + if let Some(bit_offset) = mapping.bit_offset { + (byte_value & (1 << bit_offset)) != 0 + } else { + byte_value != 0 + } + } + + fn decode_u8(report: &[u8], byte_start: usize, max_value: Option) -> f64 { + let byte_value = report[byte_start] as f64; + let max = max_value.unwrap_or(u8::MAX as i64) as f64; + normalize_unsigned_value(byte_value, max) + } + + fn decode_i8( + report: &[u8], + byte_start: usize, + min_value: Option, + max_value: Option, + ) -> f64 { + let byte_value = report[byte_start].cast_signed() as f64; + let min = min_value.unwrap_or(i8::MIN as i64) as f64; + let max = max_value.unwrap_or(i8::MAX as i64) as f64; + normalize_signed_value(byte_value, min, max) + } + + fn decode_u16( + report: &[u8], + byte_start: usize, + max_value: Option, + endian: Option<&Endianness>, + ) -> Result { + // Calculate the byte start and end + const SIZE: usize = (u16::BITS / 8) as usize; + let start = byte_start; + let end = start + (SIZE - 1); + + // Ensure end doesn't exceed report size + if end >= report.len() { + return Err(DecodeError::ValueExceedsReportSize( + SIZE, + start, + report.len(), + )); + } + + // Copy the bytes from the report to decode based on endianness + let mut value_bytes = [0u8; SIZE]; + for (i, j) in (start..end).enumerate() { + value_bytes[i] = report[j]; + } + let raw_value = match endian { + Some(Endianness::Lsb) => u16::from_le_bytes(value_bytes), + Some(Endianness::Msb) => u16::from_be_bytes(value_bytes), + None => u16::from_le_bytes(value_bytes), + }; + + // Normalize the value + let max = max_value.unwrap_or(u16::MAX as i64) as f64; + let value = normalize_unsigned_value(raw_value as f64, max); + + Ok(value) + } + + fn decode_i16( + report: &[u8], + byte_start: usize, + min_value: Option, + max_value: Option, + endian: Option<&Endianness>, + ) -> Result { + // Calculate the byte start and end + const SIZE: usize = (i16::BITS / 8) as usize; + let start = byte_start; + let end = start + (SIZE - 1); + + // Ensure end doesn't exceed report size + if end >= report.len() { + return Err(DecodeError::ValueExceedsReportSize( + SIZE, + start, + report.len(), + )); + } + + // Copy the bytes from the report to decode based on endianness + let mut value_bytes = [0u8; SIZE]; + for (i, j) in (start..end).enumerate() { + value_bytes[i] = report[j]; + } + let raw_value = match endian { + Some(Endianness::Lsb) => i16::from_le_bytes(value_bytes), + Some(Endianness::Msb) => i16::from_be_bytes(value_bytes), + None => i16::from_le_bytes(value_bytes), + }; + + // Normalize the value + let min = min_value.unwrap_or(i16::MIN as i64) as f64; + let max = max_value.unwrap_or(i16::MAX as i64) as f64; + let value = normalize_signed_value(raw_value as f64, min, max); + + Ok(value) + } + + fn decode_u32( + report: &[u8], + byte_start: usize, + max_value: Option, + endian: Option<&Endianness>, + ) -> Result { + // Calculate the byte start and end + const SIZE: usize = (u32::BITS / 8) as usize; + let start = byte_start; + let end = start + (SIZE - 1); + + // Ensure end doesn't exceed report size + if end >= report.len() { + return Err(DecodeError::ValueExceedsReportSize( + SIZE, + start, + report.len(), + )); + } + + // Copy the bytes from the report to decode based on endianness + let mut value_bytes = [0u8; SIZE]; + for (i, j) in (start..end).enumerate() { + value_bytes[i] = report[j]; + } + let raw_value = match endian { + Some(Endianness::Lsb) => u32::from_le_bytes(value_bytes), + Some(Endianness::Msb) => u32::from_be_bytes(value_bytes), + None => u32::from_le_bytes(value_bytes), + }; + + // Normalize the value + let max = max_value.unwrap_or(u32::MAX as i64) as f64; + let value = normalize_unsigned_value(raw_value as f64, max); + + Ok(value) + } + + fn decode_i32( + report: &[u8], + byte_start: usize, + min_value: Option, + max_value: Option, + endian: Option<&Endianness>, + ) -> Result { + // Calculate the byte start and end + const SIZE: usize = (i32::BITS / 8) as usize; + let start = byte_start; + let end = start + (SIZE - 1); + + // Ensure end doesn't exceed report size + if end >= report.len() { + return Err(DecodeError::ValueExceedsReportSize( + SIZE, + start, + report.len(), + )); + } + + // Copy the bytes from the report to decode based on endianness + let mut value_bytes = [0u8; SIZE]; + for (i, j) in (start..end).enumerate() { + value_bytes[i] = report[j]; + } + let raw_value = match endian { + Some(Endianness::Lsb) => i32::from_le_bytes(value_bytes), + Some(Endianness::Msb) => i32::from_be_bytes(value_bytes), + None => i32::from_le_bytes(value_bytes), + }; + + // Normalize the value + let min = min_value.unwrap_or(i32::MIN as i64) as f64; + let max = max_value.unwrap_or(i32::MAX as i64) as f64; + let value = normalize_signed_value(raw_value as f64, min, max); + + Ok(value) + } +} diff --git a/src/input/event/hidraw/translator_test.rs b/src/input/event/hidraw/translator_test.rs new file mode 100644 index 00000000..da8ed849 --- /dev/null +++ b/src/input/event/hidraw/translator_test.rs @@ -0,0 +1,47 @@ +use std::error::Error; + +use packed_struct::PackedStructSlice; + +use crate::{ + config::capability_map::CapabilityMapConfig, drivers::dualsense::hid_report::InputState, + input::event::hidraw::translator::HidrawEventTranslator, +}; + +#[tokio::test] +async fn test_ds_translation() -> Result<(), Box> { + let capability_map_str = r#" +version: 2 +kind: CapabilityMap +name: GPD HID Type 1 +id: gpd_v2_hid1 +mapping: + - name: Cross + source_events: + - hidraw: + value_type: bool + byte_start: 7 + bit_offset: 5 + target_event: + gamepad: + button: South +"#; + let capability_map = CapabilityMapConfig::from_yaml(capability_map_str.into()).unwrap(); + let CapabilityMapConfig::V2(capability_map) = capability_map else { + panic!("A v2 capability map was not used"); + }; + + let mut translator = HidrawEventTranslator::new(&capability_map); + let mut report = InputState::default(); + + let report_bytes = report.pack_to_vec().unwrap(); + let events = translator.translate(&report_bytes); + assert_eq!(events.len(), 0, "No events should be emitted"); + + // Press the X button + report.cross = true; + let report_bytes = report.pack_to_vec().unwrap(); + let events = translator.translate(&report_bytes); + assert_eq!(events.len(), 1, "A button down event should be emitted"); + + Ok(()) +} 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/event/value.rs b/src/input/event/value.rs index be091606..50830d03 100644 --- a/src/input/event/value.rs +++ b/src/input/event/value.rs @@ -21,7 +21,7 @@ pub enum TranslationError { } /// InputValue represents different ways to represent a value from an input event. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum InputValue { None, /// Bool values are typically used by button input. diff --git a/src/input/source/hidraw.rs b/src/input/source/hidraw.rs index 92a7b003..909ccd79 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; pub mod gpd_win_mini_macro_keyboard; pub mod gpd_win_mini_touchpad; pub mod horipad_steam; @@ -21,13 +22,18 @@ pub mod zotac_zone; use std::{error::Error, time::Duration}; use crate::{ - config, + config::{ + self, + capability_map::{load_capability_mappings, CapabilityMapConfig}, + }, constants::BUS_SOURCES_PREFIX, drivers, input::{ - capability::Capability, composite_device::client::CompositeDeviceClient, - info::DeviceInfoRef, output_capability::OutputCapability, - source::hidraw::ultimate_2::Ultimate2, + capability::Capability, + composite_device::client::CompositeDeviceClient, + info::DeviceInfoRef, + output_capability::OutputCapability, + source::hidraw::{generic::GenericDevice, ultimate_2::Ultimate2}, }, udev::device::UdevDevice, }; @@ -73,6 +79,7 @@ pub enum HidRawDevice { Blocked(SourceDriver), DualSense(SourceDriver), Fts3528Touchscreen(SourceDriver), + GenericDevice(SourceDriver), GpdWinMiniMacroKeyboard(SourceDriver), GpdWinMiniTouchpad(SourceDriver), HoripadSteam(SourceDriver), @@ -97,6 +104,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::GenericDevice(source_driver) => source_driver.info_ref(), HidRawDevice::GpdWinMiniMacroKeyboard(source_driver) => source_driver.info_ref(), HidRawDevice::GpdWinMiniTouchpad(source_driver) => source_driver.info_ref(), HidRawDevice::HoripadSteam(source_driver) => source_driver.info_ref(), @@ -121,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::GenericDevice(source_driver) => source_driver.get_id(), HidRawDevice::GpdWinMiniMacroKeyboard(source_driver) => source_driver.get_id(), HidRawDevice::GpdWinMiniTouchpad(source_driver) => source_driver.get_id(), HidRawDevice::HoripadSteam(source_driver) => source_driver.get_id(), @@ -145,6 +154,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::GenericDevice(source_driver) => source_driver.client(), HidRawDevice::GpdWinMiniMacroKeyboard(source_driver) => source_driver.client(), HidRawDevice::GpdWinMiniTouchpad(source_driver) => source_driver.client(), HidRawDevice::HoripadSteam(source_driver) => source_driver.client(), @@ -169,6 +179,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::GenericDevice(source_driver) => source_driver.run().await, HidRawDevice::GpdWinMiniMacroKeyboard(source_driver) => source_driver.run().await, HidRawDevice::GpdWinMiniTouchpad(source_driver) => source_driver.run().await, HidRawDevice::HoripadSteam(source_driver) => source_driver.run().await, @@ -193,6 +204,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::GenericDevice(source_driver) => source_driver.get_capabilities(), HidRawDevice::GpdWinMiniMacroKeyboard(source_driver) => { source_driver.get_capabilities() } @@ -221,6 +233,7 @@ impl SourceDeviceCompatible for HidRawDevice { HidRawDevice::Fts3528Touchscreen(source_driver) => { source_driver.get_output_capabilities() } + HidRawDevice::GenericDevice(source_driver) => source_driver.get_output_capabilities(), HidRawDevice::GpdWinMiniMacroKeyboard(source_driver) => { source_driver.get_output_capabilities() } @@ -251,6 +264,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::GenericDevice(source_driver) => source_driver.get_device_path(), HidRawDevice::GpdWinMiniMacroKeyboard(source_driver) => source_driver.get_device_path(), HidRawDevice::GpdWinMiniTouchpad(source_driver) => source_driver.get_device_path(), HidRawDevice::HoripadSteam(source_driver) => source_driver.get_device_path(), @@ -284,7 +298,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 => { + // A capability map is required to use the generic driver + let Some(source_conf) = conf.as_ref() else { + return Err( + "No driver or source device config for hidraw interface found".into(), + ); + }; + let Some(map_id) = source_conf.capability_map_id.as_ref() else { + return Err("No driver or capability map id for hidraw interface found".into()); + }; + let mappings = load_capability_mappings(); + let Some(CapabilityMapConfig::V2(capability_map)) = mappings.get(map_id) else { + return Err("No driver or capability map v2 for hidraw interface found".into()); + }; + let device = GenericDevice::new(device_info.clone(), capability_map)?; + let source_device = + SourceDriver::new(composite_device, device, device_info.into(), conf); + Ok(Self::GenericDevice(source_device)) + } DriverType::Blocked => { let options = SourceDriverOptions { poll_rate: Duration::from_millis(200), diff --git a/src/input/source/hidraw/generic.rs b/src/input/source/hidraw/generic.rs new file mode 100644 index 00000000..7a626890 --- /dev/null +++ b/src/input/source/hidraw/generic.rs @@ -0,0 +1,75 @@ +use std::{error::Error, ffi::CString}; + +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 READ_BUFFER_SIZE: usize = 256; + +#[derive(Debug)] +pub struct GenericDevice { + device: HidDevice, + translator: HidrawEventTranslator, + capabilities: Vec, +} + +impl GenericDevice { + pub fn new( + device_info: UdevDevice, + capability_map: &CapabilityMapConfigV2, + ) -> Result> { + // Open a handle to the hidraw device + let path = device_info.devnode(); + let c_path = CString::new(path)?; + let api = hidapi::HidApi::new()?; + let device = api.open_path(&c_path)?; + + // Generate the capabilities based on the capability map + let mut capabilities = vec![]; + for mapping in capability_map.mapping.iter() { + let capability = mapping.target_event.clone().into(); + capabilities.push(capability); + } + + // Create a translator instance which will translate hidraw input reports + // into inputplumber events + let translator = HidrawEventTranslator::new(capability_map); + + Ok(Self { + device, + translator, + capabilities, + }) + } +} + +impl SourceInputDevice for GenericDevice { + fn poll(&mut self) -> Result, InputError> { + let mut buf = [0u8; READ_BUFFER_SIZE]; + let bytes_read = self + .device + .read(&mut buf[..]) + .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.capabilities.clone()) + } +} + +impl SourceOutputDevice for GenericDevice {} From 2ff946854f7f6aff4be199c0a21273a904a2c398 Mon Sep 17 00:00:00 2001 From: William Edwards Date: Mon, 27 Jul 2026 01:58:19 -0700 Subject: [PATCH 2/2] fix(GPD): add capability map for back buttons from honjow --- .../capability_maps/gpd_hid_type1.yaml | 44 +++++++++++++++++++ .../inputplumber/devices/50-gpd_win5.yaml | 6 +++ 2 files changed, 50 insertions(+) create mode 100644 rootfs/usr/share/inputplumber/capability_maps/gpd_hid_type1.yaml diff --git a/rootfs/usr/share/inputplumber/capability_maps/gpd_hid_type1.yaml b/rootfs/usr/share/inputplumber/capability_maps/gpd_hid_type1.yaml new file mode 100644 index 00000000..badeb88b --- /dev/null +++ b/rootfs/usr/share/inputplumber/capability_maps/gpd_hid_type1.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_hid_type1 + +# 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: + value_type: bool + byte_start: 8 + target_event: + gamepad: + button: QuickAccess + + - name: Left Back + source_events: + - hidraw: + value_type: bool + byte_start: 9 + target_event: + gamepad: + button: LeftPaddle1 + + - name: Right Back + source_events: + - hidraw: + value_type: bool + 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..ff727cf6 100644 --- a/rootfs/usr/share/inputplumber/devices/50-gpd_win5.yaml +++ b/rootfs/usr/share/inputplumber/devices/50-gpd_win5.yaml @@ -38,6 +38,12 @@ source_devices: name: " Keyboard for Windows" handler: event* phys_path: usb-0000:66:00.0-5.3/input0 + - group: keyboard + hidraw: + vendor_id: 0x2f24 + product_id: 0x0137 + interface_num: 0 + capability_map_id: gpd_hid_type1 - group: keyboard evdev: name: AT Translated Set 2 keyboard