diff --git a/rootfs/usr/share/inputplumber/devices/60-horipad_steam.yaml b/rootfs/usr/share/inputplumber/devices/60-horipad_steam.yaml new file mode 100644 index 00000000..44a77834 --- /dev/null +++ b/rootfs/usr/share/inputplumber/devices/60-horipad_steam.yaml @@ -0,0 +1,47 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/ShadowBlip/InputPlumber/main/rootfs/usr/share/inputplumber/schema/composite_device_v1.json +# Schema version number +version: 1 + +# The type of configuration schema +kind: CompositeDevice + +# Name of the composite device mapping +name: Horipad Steam + +# Only use this profile if *any* of the given matches matches. If this list is +# empty,then the source devices will *always* be checked. +# /sys/class/dmi/id/product_name +matches: [] + +# Only allow a CompositeDevice to manage at most the given number of +# source devices. When this limit is reached, a new CompositeDevice will be +# created for any new matching devices. +maximum_sources: 2 + +# One or more source devices to combine into a single virtual device. The events +# from these devices will be watched and translated according to the key map. +source_devices: + - group: gamepad + blocked: true + udev: + attributes: + - name: id/vendor + value: "0f0d" + - name: id/product + value: "{0196,01ab}" + sys_name: "event*" + subsystem: input + - group: gamepad + hidraw: + vendor_id: 0x0f0d + product_id: 0x0196 + - group: gamepad + hidraw: + vendor_id: 0x0f0d + product_id: 0x01ab + +# The target input device(s) to emulate by default +target_devices: + - hori-steam + - mouse + - keyboard diff --git a/rootfs/usr/share/inputplumber/schema/composite_device_v1.json b/rootfs/usr/share/inputplumber/schema/composite_device_v1.json index db0b4493..5f046adc 100644 --- a/rootfs/usr/share/inputplumber/schema/composite_device_v1.json +++ b/rootfs/usr/share/inputplumber/schema/composite_device_v1.json @@ -59,6 +59,7 @@ "mouse", "keyboard", "gamepad", + "hori-steam", "xb360", "xbox-elite", "xbox-series", diff --git a/rootfs/usr/share/inputplumber/schema/device_profile_v1.json b/rootfs/usr/share/inputplumber/schema/device_profile_v1.json index 4ab81d3d..053cea13 100644 --- a/rootfs/usr/share/inputplumber/schema/device_profile_v1.json +++ b/rootfs/usr/share/inputplumber/schema/device_profile_v1.json @@ -31,6 +31,7 @@ "ds5", "ds5-edge", "gamepad", + "hori-steam", "keyboard", "mouse", "touchpad", diff --git a/src/drivers/horipad_steam/driver.rs b/src/drivers/horipad_steam/driver.rs new file mode 100644 index 00000000..b8f4272f --- /dev/null +++ b/src/drivers/horipad_steam/driver.rs @@ -0,0 +1,310 @@ +use std::{error::Error, ffi::CString}; + +use hidapi::HidDevice; +use packed_struct::{types::SizedInteger, PackedStruct}; + +use crate::{drivers::horipad_steam::hid_report::Direction, udev::device::UdevDevice}; + +use super::{ + event::{ + AccelerometerEvent, AccelerometerInput, AxisEvent, BinaryInput, ButtonEvent, Event, + JoyAxisInput, TriggerEvent, TriggerInput, + }, + hid_report::PackedInputDataReport, +}; + +// Report ID +pub const REPORT_ID: u8 = 0x07; + +// Input report size +const PACKET_SIZE: usize = 287; + +// HID buffer read timeout +const HID_TIMEOUT: i32 = 10; + +// Input report axis ranges +pub const JOY_AXIS_MAX: f64 = 255.0; +pub const JOY_AXIS_MIN: f64 = 0.0; +pub const TRIGGER_AXIS_MAX: f64 = 255.0; + +pub const VID: u16 = 0x0F0D; +pub const PIDS: [u16; 2] = [0x0196, 0x01AB]; + +#[derive(Debug, Clone, Default)] +struct DPadState { + up: bool, + down: bool, + left: bool, + right: bool, +} + +pub struct Driver { + /// HIDRAW device instance + device: HidDevice, + /// State for the device + state: Option, + /// Last DPad state + dpad: DPadState, +} + +impl Driver { + pub fn new(udevice: UdevDevice) -> Result> { + let path = udevice.devnode(); + + let cs_path = CString::new(path.clone())?; + let api = hidapi::HidApi::new()?; + let device = api.open_path(&cs_path)?; + + let info = device.get_device_info()?; + if info.vendor_id() != VID || !PIDS.contains(&info.product_id()) { + return Err(format!("Device '{path}' is not a Horipad Steam Controller").into()); + } + + Ok(Self { + device, + state: None, + dpad: Default::default(), + }) + } + + /// Poll the device and read input reports + pub fn poll(&mut self) -> Result, Box> { + // Read data from the device into a buffer + let mut buf = [0; PACKET_SIZE]; + let _bytes_read = self.device.read_timeout(&mut buf[..], HID_TIMEOUT)?; + + let report_id = buf[0]; + if report_id != REPORT_ID { + log::debug!("Got unhandled report_id {report_id}, someone should look into that..."); + return Ok(vec![]); + } + + let input_report = PackedInputDataReport::unpack(&buf)?; + + // Print input report for debugging + //log::trace!("--- Input report ---"); + //log::trace!("{input_report}"); + //log::trace!("---- End Report ----"); + + // Update the state + let old_dinput_state = self.update_state(input_report); + + // Translate the state into a stream of input events + let events = self.translate_events(old_dinput_state); + + Ok(events) + } + + /// Update touchinput state + fn update_state( + &mut self, + input_report: PackedInputDataReport, + ) -> Option { + let old_state = self.state; + self.state = Some(input_report); + old_state + } + + /// Translate the state into individual events + fn translate_events(&mut self, old_state: Option) -> Vec { + let mut events = Vec::new(); + let Some(state) = self.state else { + return events; + }; + + // Translate state changes into events if they have changed + let Some(old_state) = old_state else { + return events; + }; + + // Binary Events + if state.a != old_state.a { + events.push(Event::Button(ButtonEvent::A(BinaryInput { + pressed: state.a, + }))); + } + if state.b != old_state.b { + events.push(Event::Button(ButtonEvent::B(BinaryInput { + pressed: state.b, + }))); + } + if state.x != old_state.x { + events.push(Event::Button(ButtonEvent::X(BinaryInput { + pressed: state.x, + }))); + } + if state.y != old_state.y { + events.push(Event::Button(ButtonEvent::Y(BinaryInput { + pressed: state.y, + }))); + } + if state.rb != old_state.rb { + events.push(Event::Button(ButtonEvent::RB(BinaryInput { + pressed: state.rb, + }))); + } + if state.lb != old_state.lb { + events.push(Event::Button(ButtonEvent::LB(BinaryInput { + pressed: state.lb, + }))); + } + if state.view != old_state.view { + events.push(Event::Button(ButtonEvent::View(BinaryInput { + pressed: state.view, + }))); + } + if state.menu != old_state.menu { + events.push(Event::Button(ButtonEvent::Menu(BinaryInput { + pressed: state.menu, + }))); + } + if state.steam != old_state.steam { + events.push(Event::Button(ButtonEvent::Steam(BinaryInput { + pressed: state.steam, + }))); + } + if state.quick != old_state.quick { + events.push(Event::Button(ButtonEvent::Quick(BinaryInput { + pressed: state.quick, + }))); + } + if state.ls_click != old_state.ls_click { + events.push(Event::Button(ButtonEvent::LSClick(BinaryInput { + pressed: state.ls_click, + }))); + } + if state.rs_click != old_state.rs_click { + events.push(Event::Button(ButtonEvent::RSClick(BinaryInput { + pressed: state.rs_click, + }))); + } + if state.ls_touch != old_state.ls_touch { + events.push(Event::Button(ButtonEvent::LSTouch(BinaryInput { + pressed: state.ls_touch, + }))); + } + if state.rs_touch != old_state.rs_touch { + events.push(Event::Button(ButtonEvent::RSTouch(BinaryInput { + pressed: state.rs_touch, + }))); + } + if state.lt_digital != old_state.lt_digital { + events.push(Event::Button(ButtonEvent::LTDigital(BinaryInput { + pressed: state.ls_touch, + }))); + } + if state.rt_digital != old_state.rt_digital { + events.push(Event::Button(ButtonEvent::RTDigital(BinaryInput { + pressed: state.rs_touch, + }))); + } + if state.l4 != old_state.l4 { + events.push(Event::Button(ButtonEvent::L4(BinaryInput { + pressed: state.l4, + }))); + } + if state.r4 != old_state.r4 { + events.push(Event::Button(ButtonEvent::R4(BinaryInput { + pressed: state.r4, + }))); + } + if state.m1 != old_state.m1 { + events.push(Event::Button(ButtonEvent::M1(BinaryInput { + pressed: state.m1, + }))); + } + if state.m2 != old_state.m2 { + events.push(Event::Button(ButtonEvent::M2(BinaryInput { + pressed: state.m2, + }))); + } + if state.dpad != old_state.dpad { + log::debug!("New Dpad State: {:?}", state.dpad); + let up = [Direction::Up, Direction::UpRight, Direction::UpLeft].contains(&state.dpad); + let down = + [Direction::Down, Direction::DownRight, Direction::DownLeft].contains(&state.dpad); + let left = + [Direction::Left, Direction::DownLeft, Direction::UpLeft].contains(&state.dpad); + let right = + [Direction::Right, Direction::DownRight, Direction::UpRight].contains(&state.dpad); + let dpad_state = DPadState { + up, + down, + left, + right, + }; + + if up != self.dpad.up { + events.push(Event::Button(ButtonEvent::DPadUp(BinaryInput { + pressed: up, + }))); + } + if down != self.dpad.down { + events.push(Event::Button(ButtonEvent::DPadDown(BinaryInput { + pressed: down, + }))); + } + if left != self.dpad.left { + events.push(Event::Button(ButtonEvent::DPadLeft(BinaryInput { + pressed: left, + }))); + } + if right != self.dpad.right { + events.push(Event::Button(ButtonEvent::DPadRight(BinaryInput { + pressed: right, + }))); + } + + self.dpad = dpad_state; + } + + // Axis events + if state.joystick_l_x != old_state.joystick_l_x + || state.joystick_l_y != old_state.joystick_l_y + { + events.push(Event::Axis(AxisEvent::LStick(JoyAxisInput { + x: state.joystick_l_x, + y: state.joystick_l_y, + }))); + } + if state.joystick_r_x != old_state.joystick_r_x + || state.joystick_r_y != old_state.joystick_r_y + { + events.push(Event::Axis(AxisEvent::RStick(JoyAxisInput { + x: state.joystick_r_x, + y: state.joystick_r_y, + }))); + } + + if state.lt_analog != old_state.lt_analog { + events.push(Event::Trigger(TriggerEvent::LTAnalog(TriggerInput { + value: state.lt_analog, + }))); + } + if state.rt_analog != old_state.rt_analog { + events.push(Event::Trigger(TriggerEvent::RTAnalog(TriggerInput { + value: state.rt_analog, + }))); + } + + // Accelerometer events + events.push(Event::Accelerometer(AccelerometerEvent::Accelerometer( + AccelerometerInput { + x: -state.accel_x.to_primitive(), + y: state.accel_y.to_primitive(), + z: -state.accel_z.to_primitive(), + }, + ))); + events.push(Event::Accelerometer(AccelerometerEvent::Gyro( + AccelerometerInput { + x: -state.gyro_x.to_primitive(), + y: state.gyro_y.to_primitive(), + z: -state.gyro_z.to_primitive(), + }, + ))); + + log::trace!("Got events: {events:?}"); + + events + } +} diff --git a/src/drivers/horipad_steam/event.rs b/src/drivers/horipad_steam/event.rs new file mode 100644 index 00000000..3313ea41 --- /dev/null +++ b/src/drivers/horipad_steam/event.rs @@ -0,0 +1,110 @@ +/// Events that can be emitted by the controller +#[derive(Clone, Debug)] +pub enum Event { + Accelerometer(AccelerometerEvent), + Axis(AxisEvent), + Button(ButtonEvent), + Trigger(TriggerEvent), +} + +/// Binary input contain either pressed or unpressed +#[derive(Clone, Debug)] +pub struct BinaryInput { + pub pressed: bool, +} + +/// Axis input contain (x, y) coordinates +#[derive(Clone, Debug)] +pub struct JoyAxisInput { + pub x: u8, + pub y: u8, +} + +/// Trigger input contains non-negative integars +#[derive(Clone, Debug)] +pub struct TriggerInput { + pub value: u8, +} + +/// Button events represend binary inputs +#[derive(Clone, Debug)] +pub enum ButtonEvent { + /// A Button + A(BinaryInput), + /// X Button + X(BinaryInput), + /// B Button + B(BinaryInput), + /// Y Button + Y(BinaryInput), + /// Right shoulder button + RB(BinaryInput), + /// Left shoulder button + LB(BinaryInput), + /// View ⧉ button + View(BinaryInput), + /// Menu (☰) button + Menu(BinaryInput), + /// Steam button + Steam(BinaryInput), + /// ... button + Quick(BinaryInput), + // M1 below the d-pad + M1(BinaryInput), + // M2 below the right stick + M2(BinaryInput), + // L4 behind + L4(BinaryInput), + // R4 behind + R4(BinaryInput), + /// Z-axis button on the left stick + LSClick(BinaryInput), + /// Z-axis button on the right stick + RSClick(BinaryInput), + // Capacitive touch on the left stick + LSTouch(BinaryInput), + // Capacitive touch on the right stick + RSTouch(BinaryInput), + // Digital TriggerEvent Left + LTDigital(BinaryInput), + // Digital TriggerEvent Right + RTDigital(BinaryInput), + /// DPad up + DPadUp(BinaryInput), + /// DPad right + DPadRight(BinaryInput), + /// DPad down + DPadDown(BinaryInput), + /// DPad left + DPadLeft(BinaryInput), +} + +/// Axis events are events that have (x, y) values +#[derive(Clone, Debug)] +pub enum AxisEvent { + LStick(JoyAxisInput), + RStick(JoyAxisInput), +} + +/// Trigger events contain values indicating how far a trigger is pulled +#[derive(Clone, Debug)] +pub enum TriggerEvent { + LTAnalog(TriggerInput), + RTAnalog(TriggerInput), +} + +/// AccelerometerInput represents the state of the accelerometer (x, y, z) values +#[derive(Clone, Debug)] +pub struct AccelerometerInput { + pub x: i16, + pub y: i16, + pub z: i16, +} + +/// AccelerometerEvent has data from the accelerometer +#[derive(Clone, Debug)] +pub enum AccelerometerEvent { + Accelerometer(AccelerometerInput), + /// Pitch, yaw, roll + Gyro(AccelerometerInput), +} diff --git a/src/drivers/horipad_steam/hid_report.rs b/src/drivers/horipad_steam/hid_report.rs new file mode 100644 index 00000000..5107ddb7 --- /dev/null +++ b/src/drivers/horipad_steam/hid_report.rs @@ -0,0 +1,210 @@ +use std::{error::Error, fmt::Display}; + +use packed_struct::prelude::*; + +use super::driver::REPORT_ID; + +//use super::driver::*; + +#[derive(PrimitiveEnum_u8, Clone, Copy, PartialEq, Debug, Default)] +pub enum Direction { + Up = 0, + UpRight = 1, + Right = 2, + DownRight = 3, + Down = 4, + DownLeft = 5, + Left = 6, + UpLeft = 7, + #[default] + None = 15, +} + +impl Direction { + pub fn as_bitflag(&self) -> u8 { + match *self { + Self::Up => 0b00000001, + Self::UpRight => 0b00000011, + Self::Right => 0b00000010, + Self::DownRight => 0b00000110, + Self::Down => 0b00000100, + Self::DownLeft => 0b00001100, + Self::Left => 0b00001000, + Self::UpLeft => 0b00001001, + Self::None => 0b00000000, + } + } + + pub fn from_bitflag(bits: u8) -> Self { + match bits { + 0b00000001 => Self::Up, + 0b00000011 => Self::UpRight, + 0b00000010 => Self::Right, + 0b00000110 => Self::DownRight, + 0b00000100 => Self::Down, + 0b00001100 => Self::DownLeft, + 0b00001000 => Self::Left, + 0b00001001 => Self::UpLeft, + 0b00000000 => Self::None, + _ => Self::None, + } + } + + pub fn change(&self, direction: Direction, pressed: bool) -> Direction { + let change = direction.as_bitflag(); + let old_direction = self.as_bitflag(); + let new_direction = if pressed { + log::debug!("Pressed"); + old_direction | direction.as_bitflag() + } else { + log::debug!("NOT pressed"); + old_direction ^ direction.as_bitflag() + }; + log::debug!( + "Change: {change} Old direction: {old_direction}, New direction: {new_direction}" + ); + Direction::from_bitflag(new_direction) + } +} + +/// Horipad Steam input report for Bluetooth +#[derive(PackedStruct, Debug, Copy, Clone, PartialEq)] +#[packed_struct(bit_numbering = "msb0", size_bytes = "287")] +pub struct PackedInputDataReport { + // byte 0 + #[packed_field(bytes = "0")] + pub report_id: u8, // Report ID (always 0x07) + + // byte 1-4 + #[packed_field(bytes = "1")] + pub joystick_l_x: u8, // left stick X axis + #[packed_field(bytes = "2")] + pub joystick_l_y: u8, // left stick Y axis + #[packed_field(bytes = "3")] + pub joystick_r_x: u8, // right stick X axis + #[packed_field(bytes = "4")] + pub joystick_r_y: u8, // right stick Y axis + + // byte 5 + #[packed_field(bits = "40")] + pub x: bool, + #[packed_field(bits = "41")] + pub quick: bool, // Quick Access ... Button + #[packed_field(bits = "42")] + pub b: bool, + #[packed_field(bits = "43")] + pub a: bool, + #[packed_field(bits = "44..=47", ty = "enum")] + pub dpad: Direction, // Directional buttons + + // byte 6 + #[packed_field(bits = "48")] + pub menu: bool, // ☰ Button + #[packed_field(bits = "49")] + pub view: bool, // ⧉ Button + #[packed_field(bits = "50")] + pub rt_digital: bool, + #[packed_field(bits = "51")] + pub lt_digital: bool, + #[packed_field(bits = "52")] + pub rb: bool, + #[packed_field(bits = "53")] + pub lb: bool, + #[packed_field(bits = "54")] + pub m1: bool, + #[packed_field(bits = "55")] + pub y: bool, // Triggers + + // byte 7 + #[packed_field(bits = "56")] + pub r4: bool, + #[packed_field(bits = "57")] + pub l4: bool, + #[packed_field(bits = "58")] + pub rs_touch: bool, + #[packed_field(bits = "59")] + pub ls_touch: bool, + #[packed_field(bits = "60")] + pub m2: bool, + #[packed_field(bits = "61")] + pub rs_click: bool, + #[packed_field(bits = "62")] + pub ls_click: bool, + #[packed_field(bits = "63")] + pub steam: bool, // Steam Button + + // byte 8-9 + #[packed_field(bytes = "8")] + pub rt_analog: u8, // L2 trigger axis + #[packed_field(bytes = "9")] + pub lt_analog: u8, // R2 trigger axis + + // byte 10-11 + #[packed_field(bytes = "10..=11", endian = "lsb")] + pub tick: Integer>, + + // bytes 12-17 // Gyro + #[packed_field(bytes = "12..=13", endian = "lsb")] + pub gyro_z: Integer>, + #[packed_field(bytes = "14..=15", endian = "lsb")] + pub gyro_y: Integer>, + #[packed_field(bytes = "16..=17", endian = "lsb")] + pub gyro_x: Integer>, + // bytes 18-23 // Accelerometer + #[packed_field(bytes = "18..=19", endian = "lsb")] + pub accel_z: Integer>, + #[packed_field(bytes = "20..=21", endian = "lsb")] + pub accel_y: Integer>, + #[packed_field(bytes = "22..=23", endian = "lsb")] + pub accel_x: Integer>, + + // byte 24 + #[packed_field(bits = "195", endian = "lsb")] + pub charging: bool, + #[packed_field(bits = "196..=199", endian = "lsb")] + pub charge_percent: Integer>, +} + +impl Default for PackedInputDataReport { + fn default() -> Self { + Self { + report_id: REPORT_ID, + joystick_l_x: 128, + joystick_l_y: 128, + joystick_r_x: 128, + joystick_r_y: 128, + x: false, + quick: false, + b: false, + a: false, + dpad: Direction::None, + menu: false, + view: false, + rt_digital: false, + lt_digital: false, + rb: false, + lb: false, + m1: false, + y: false, + r4: false, + l4: false, + rs_touch: false, + ls_touch: false, + m2: false, + rs_click: false, + ls_click: false, + steam: false, + rt_analog: 0, + lt_analog: 0, + tick: Integer::from_primitive(0), + gyro_z: Integer::from_primitive(0), + gyro_y: Integer::from_primitive(0), + gyro_x: Integer::from_primitive(0), + accel_z: Integer::from_primitive(0), + accel_y: Integer::from_primitive(0), + accel_x: Integer::from_primitive(0), + charging: false, + charge_percent: Integer::from_primitive(0), + } + } +} diff --git a/src/drivers/horipad_steam/hid_report_test.rs b/src/drivers/horipad_steam/hid_report_test.rs new file mode 100644 index 00000000..cdbfa864 --- /dev/null +++ b/src/drivers/horipad_steam/hid_report_test.rs @@ -0,0 +1,35 @@ +use std::error::Error; + +use packed_struct::PackedStructSlice; + +use super::hid_report::PackedInputDataReport; + +#[tokio::test] +async fn test_horipad_steam() -> Result<(), Box> { + let report = PackedInputDataReport::unpack_from_slice(&DATA_A).unwrap(); + println!("{report}"); + assert_eq!(report.a, true); + + Ok(()) +} + +const DATA_A: [u8; 287] = [ + 0x07, 0x80, 0x80, 0x80, 0x80, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x3d, 0x8b, 0xfb, 0xff, 0x21, 0x00, + 0x20, 0x00, 0x34, 0x03, 0x70, 0xf0, 0x20, 0x00, 0x19, 0x01, 0x80, 0x00, 0x00, 0x02, 0x80, 0x80, + 0x80, 0x80, 0x1f, 0x00, 0x00, 0x00, 0x00, 0xcd, 0x8c, 0xfb, 0xff, 0x21, 0x00, 0x20, 0x00, 0x34, + 0x03, 0x70, 0xf0, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa9, 0x45, 0xe9, 0xbf, +]; diff --git a/src/drivers/horipad_steam/mod.rs b/src/drivers/horipad_steam/mod.rs new file mode 100644 index 00000000..f665f434 --- /dev/null +++ b/src/drivers/horipad_steam/mod.rs @@ -0,0 +1,5 @@ +pub mod driver; +pub mod event; +pub mod hid_report; +pub mod hid_report_test; +pub mod report_descriptor; diff --git a/src/drivers/horipad_steam/report_descriptor.rs b/src/drivers/horipad_steam/report_descriptor.rs new file mode 100644 index 00000000..4b55a4c8 --- /dev/null +++ b/src/drivers/horipad_steam/report_descriptor.rs @@ -0,0 +1,71 @@ +pub const REPORT_DESCRIPTOR: [u8; 140] = [ + 0x05, 0x01, // Usage Page (Generic Desktop) 0 + 0x09, 0x05, // Usage (Game Pad) 2 + 0xa1, 0x01, // Collection (Application) 4 + 0x85, 0x07, // Report ID (7) 6 + 0xa1, 0x00, // Collection (Physical) 8 + 0x09, 0x30, // Usage (X) 10 + 0x09, 0x31, // Usage (Y) 12 + 0x09, 0x32, // Usage (Z) 14 + 0x09, 0x35, // Usage (Rz) 16 + 0x15, 0x00, // Logical Minimum (0) 18 + 0x26, 0xff, 0x00, // Logical Maximum (255) 20 + 0x75, 0x08, // Report Size (8) 23 + 0x95, 0x04, // Report Count (4) 25 + 0x81, 0x02, // Input (Data,Var,Abs) 27 + 0xc0, // End Collection 29 + 0x09, 0x39, // Usage (Hat switch) 30 + 0x15, 0x00, // Logical Minimum (0) 32 + 0x25, 0x07, // Logical Maximum (7) 34 + 0x35, 0x00, // Physical Minimum (0) 36 + 0x46, 0x3b, 0x01, // Physical Maximum (315) 38 + 0x65, 0x14, // Unit (EnglishRotation: deg) 41 + 0x75, 0x04, // Report Size (4) 43 + 0x95, 0x01, // Report Count (1) 45 + 0x81, 0x42, // Input (Data,Var,Abs,Null) 47 + 0x05, 0x09, // Usage Page (Button) 49 + 0x19, 0x01, // Usage Minimum (1) 51 + 0x29, 0x14, // Usage Maximum (20) 53 + 0x15, 0x00, // Logical Minimum (0) 55 + 0x25, 0x01, // Logical Maximum (1) 57 + 0x75, 0x01, // Report Size (1) 59 + 0x95, 0x14, // Report Count (20) 61 + 0x81, 0x02, // Input (Data,Var,Abs) 63 + 0x05, 0x02, // Usage Page (Simulation Controls) 65 + 0x15, 0x00, // Logical Minimum (0) 67 + 0x26, 0xff, 0x00, // Logical Maximum (255) 69 + 0x09, 0xc4, // Usage (Accelerator) 72 + 0x09, 0xc5, // Usage (Brake) 74 + 0x95, 0x02, // Report Count (2) 76 + 0x75, 0x08, // Report Size (8) 78 + 0x81, 0x02, // Input (Data,Var,Abs) 80 + 0x06, 0x00, 0xff, // Usage Page (Vendor Defined) 82 + 0x09, 0x20, // Usage (Vendor Usage 0x20) 85 + 0x95, 0x26, // Report Count (38) 87 + 0x81, 0x02, // Input (Data,Var,Abs) 89 + 0x85, 0x05, // Report ID (5) 91 + 0x09, 0x21, // Usage (Vendor Usage 0x21) 93 + 0x95, 0x20, // Report Count (32) 95 + 0x91, 0x02, // Output (Data,Var,Abs) 97 + 0x85, 0x12, // Report ID (18) 99 + 0x09, 0x22, // Usage (Vendor Usage 0x22) 101 + 0x95, 0x3f, // Report Count (63) 103 + 0x81, 0x02, // Input (Data,Var,Abs) 105 + 0x09, 0x23, // Usage (Vendor Usage 0x23) 107 + 0x91, 0x02, // Output (Data,Var,Abs) 109 + 0x85, 0x14, // Report ID (20) 111 + 0x09, 0x26, // Usage (Vendor Usage 0x26) 113 + 0x95, 0x3f, // Report Count (63) 115 + 0x81, 0x02, // Input (Data,Var,Abs) 117 + 0x09, 0x27, // Usage (Vendor Usage 0x27) 119 + 0x91, 0x02, // Output (Data,Var,Abs) 121 + 0x85, 0x10, // Report ID (16) 123 + 0x09, 0x24, // Usage (Vendor Usage 0x24) 125 + 0x95, 0x3f, // Report Count (63) 127 + 0x81, 0x02, // Input (Data,Var,Abs) 129 + 0x85, 0x0f, // Report ID (15) 131 + 0x09, 0x28, // Usage (Vendor Usage 0x28) 133 + 0x95, 0x3f, // Report Count (63) 135 + 0x91, 0x02, // Output (Data,Var,Abs) 137 + 0xc0, // End Collection 139 +]; diff --git a/src/drivers/mod.rs b/src/drivers/mod.rs index 5653a42e..3dde1eb5 100644 --- a/src/drivers/mod.rs +++ b/src/drivers/mod.rs @@ -1,5 +1,6 @@ pub mod dualsense; pub mod fts3528; +pub mod horipad_steam; pub mod iio_imu; pub mod lego; pub mod opineo; diff --git a/src/input/source/hidraw.rs b/src/input/source/hidraw.rs index 9998569d..25d49a31 100644 --- a/src/input/source/hidraw.rs +++ b/src/input/source/hidraw.rs @@ -1,5 +1,6 @@ pub mod dualsense; pub mod fts3528; +pub mod horipad_steam; pub mod lego_dinput_combined; pub mod lego_dinput_split; pub mod lego_fps_mode; @@ -11,12 +12,15 @@ pub mod xpad_uhid; use std::{error::Error, time::Duration}; +use horipad_steam::HoripadSteam; use rog_ally::RogAlly; use xpad_uhid::XpadUhid; use crate::{ - constants::BUS_SOURCES_PREFIX, drivers, input::composite_device::client::CompositeDeviceClient, - udev::device::UdevDevice, + constants::BUS_SOURCES_PREFIX, + drivers, + input::composite_device::client::CompositeDeviceClient, + udev::device::{self, UdevDevice}, }; use self::{ @@ -32,30 +36,32 @@ use super::{SourceDriver, SourceDriverOptions}; enum DriverType { Unknown, DualSense, - SteamDeck, + Fts3528Touchscreen, + HoripadSteam, LegionGoDCombined, LegionGoDSplit, LegionGoFPS, LegionGoX, OrangePiNeo, - Fts3528Touchscreen, - XpadUhid, RogAlly, + SteamDeck, + XpadUhid, } /// [HidRawDevice] represents an input device using the hidraw subsystem. #[derive(Debug)] pub enum HidRawDevice { DualSense(SourceDriver), - SteamDeck(SourceDriver), + Fts3528Touchscreen(SourceDriver), + HoripadSteam(SourceDriver), LegionGoDCombined(SourceDriver), LegionGoDSplit(SourceDriver), LegionGoFPS(SourceDriver), LegionGoX(SourceDriver), OrangePiNeo(SourceDriver), - Fts3528Touchscreen(SourceDriver), - XpadUhid(SourceDriver), RogAlly(SourceDriver), + SteamDeck(SourceDriver), + XpadUhid(SourceDriver), } impl HidRawDevice { @@ -135,6 +141,11 @@ impl HidRawDevice { SourceDriver::new_with_options(composite_device, device, device_info, options); Ok(Self::RogAlly(source_device)) } + DriverType::HoripadSteam => { + let device = HoripadSteam::new(device_info.clone())?; + let source_device = SourceDriver::new(composite_device, device, device_info); + Ok(Self::HoripadSteam(source_device)) + } } } @@ -214,6 +225,14 @@ impl HidRawDevice { } } + // Horipad Steam Controller + if vid == drivers::horipad_steam::driver::VID + && drivers::horipad_steam::driver::PIDS.contains(&pid) + { + log::info!("Detected Horipad Steam Controller"); + return DriverType::HoripadSteam; + } + // Unknown log::warn!("No driver for hidraw interface found. VID: {vid}, PID: {pid}"); DriverType::Unknown diff --git a/src/input/source/hidraw/horipad_steam.rs b/src/input/source/hidraw/horipad_steam.rs new file mode 100644 index 00000000..27cfecb4 --- /dev/null +++ b/src/input/source/hidraw/horipad_steam.rs @@ -0,0 +1,303 @@ +use std::{error::Error, fmt::Debug}; + +use crate::{ + drivers::horipad_steam::{ + driver::{Driver, JOY_AXIS_MAX, JOY_AXIS_MIN, TRIGGER_AXIS_MAX}, + event, + }, + input::{ + capability::{Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger}, + event::{native::NativeEvent, value::InputValue}, + source::{InputError, SourceInputDevice, SourceOutputDevice}, + }, + udev::device::UdevDevice, +}; + +/// XpadUhid source device implementation +pub struct HoripadSteam { + driver: Driver, +} + +impl HoripadSteam { + /// Create a new source device with the given udev + /// device information + pub fn new(device_info: UdevDevice) -> Result> { + let driver = Driver::new(device_info)?; + Ok(Self { driver }) + } +} + +impl SourceOutputDevice for HoripadSteam {} + +impl SourceInputDevice for HoripadSteam { + /// Poll the given input device for input events + fn poll(&mut self) -> Result, InputError> { + let events = self.driver.poll()?; + let native_events = translate_events(events); + Ok(native_events) + } + + /// Returns the possible input events this device is capable of emitting + fn get_capabilities(&self) -> Result, InputError> { + Ok(CAPABILITIES.into()) + } +} + +impl Debug for HoripadSteam { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("XpadUhid").finish() + } +} + +/// Returns a value between -1.0 and 1.0 based on the given value with its +/// minimum and maximum values. +fn normalize_signed_value(raw_value: f64, min: f64, max: f64) -> f64 { + let mid = (max + min) / 2.0; + let event_value = raw_value - mid; + + // Normalize the value + if event_value >= 0.0 { + let maximum = max - mid; + event_value / maximum + } else { + let minimum = min - mid; + let value = event_value / minimum; + -value + } +} + +// Returns a value between 0.0 and 1.0 based on the given value with its +// maximum. +fn normalize_unsigned_value(raw_value: f64, max: f64) -> f64 { + raw_value / max +} + +/// Normalize the value to something between -1.0 and 1.0 based on the +/// minimum and maximum axis ranges. +fn normalize_axis_value(event: event::AxisEvent) -> InputValue { + let min = JOY_AXIS_MIN; + let max = JOY_AXIS_MAX; + match event { + event::AxisEvent::LStick(value) => { + let x = normalize_signed_value(value.x as f64, min, max); + let x = Some(x); + + let y = normalize_signed_value(value.y as f64, min, max); + let y = Some(y); + + InputValue::Vector2 { x, y } + } + event::AxisEvent::RStick(value) => { + let x = normalize_signed_value(value.x as f64, min, max); + let x = Some(x); + + let y = normalize_signed_value(value.y as f64, min, max); + let y = Some(y); + + InputValue::Vector2 { x, y } + } + } +} + +/// Normalize the trigger value to something between 0.0 and 1.0 based on the +/// maximum axis range. +fn normalize_trigger_value(event: event::TriggerEvent) -> InputValue { + let max = TRIGGER_AXIS_MAX; + match event { + event::TriggerEvent::LTAnalog(value) => { + InputValue::Float(normalize_unsigned_value(value.value as f64, max)) + } + event::TriggerEvent::RTAnalog(value) => { + InputValue::Float(normalize_unsigned_value(value.value as f64, max)) + } + } +} + +/// Translate the given events into native events +fn translate_events(events: Vec) -> Vec { + let mut translated = Vec::with_capacity(events.len()); + for event in events.into_iter() { + translated.push(translate_event(event)); + } + if !translated.is_empty() { + log::trace!("Translated events: {translated:?}"); + }; + translated +} + +/// Translate the given event into a native event +fn translate_event(event: event::Event) -> NativeEvent { + log::trace!("Got event {event:?}"); + match event { + event::Event::Button(button) => match button { + event::ButtonEvent::A(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::South)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::X(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::North)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::B(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::East)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::Y(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::West)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::Menu(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::Start)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::View(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::Select)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::Steam(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::Guide)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::DPadDown(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::DPadDown)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::DPadUp(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::DPadUp)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::DPadLeft(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::DPadLeft)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::DPadRight(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::DPadRight)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::LB(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftBumper)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::LSClick(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftStick)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::RB(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::RightBumper)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::RSClick(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::RightStick)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::Quick(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::QuickAccess)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::M1(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftPaddle2)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::M2(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::RightPaddle2)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::L4(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftPaddle1)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::R4(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::RightPaddle1)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::LSTouch(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftStickTouch)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::RSTouch(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::RightStickTouch)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::LTDigital(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftTrigger)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::RTDigital(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::RightTrigger)), + InputValue::Bool(value.pressed), + ), + }, + event::Event::Axis(axis) => match axis.clone() { + event::AxisEvent::LStick(_) => NativeEvent::new( + Capability::Gamepad(Gamepad::Axis(GamepadAxis::LeftStick)), + normalize_axis_value(axis), + ), + event::AxisEvent::RStick(_) => NativeEvent::new( + Capability::Gamepad(Gamepad::Axis(GamepadAxis::RightStick)), + normalize_axis_value(axis), + ), + }, + event::Event::Trigger(trigg) => match trigg.clone() { + event::TriggerEvent::LTAnalog(_) => NativeEvent::new( + Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftTrigger)), + normalize_trigger_value(trigg), + ), + event::TriggerEvent::RTAnalog(_) => NativeEvent::new( + Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightTrigger)), + normalize_trigger_value(trigg), + ), + }, + event::Event::Accelerometer(accel_event) => match accel_event { + event::AccelerometerEvent::Accelerometer(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Accelerometer), + InputValue::Vector3 { + x: Some(value.x as f64), + y: Some(value.y as f64), + z: Some(value.z as f64), + }, + ), + event::AccelerometerEvent::Gyro(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Gyro), + InputValue::Vector3 { + x: Some(value.x as f64), + y: Some(value.y as f64), + z: Some(value.z as f64), + }, + ), + }, + } +} + +/// List of all capabilities that the driver implements +pub const CAPABILITIES: &[Capability] = &[ + Capability::Gamepad(Gamepad::Accelerometer), + Capability::Gamepad(Gamepad::Axis(GamepadAxis::LeftStick)), + Capability::Gamepad(Gamepad::Axis(GamepadAxis::RightStick)), + Capability::Gamepad(Gamepad::Button(GamepadButton::DPadDown)), + Capability::Gamepad(Gamepad::Button(GamepadButton::DPadLeft)), + Capability::Gamepad(Gamepad::Button(GamepadButton::DPadRight)), + Capability::Gamepad(Gamepad::Button(GamepadButton::DPadUp)), + Capability::Gamepad(Gamepad::Button(GamepadButton::East)), + Capability::Gamepad(Gamepad::Button(GamepadButton::Guide)), + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftBumper)), + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftPaddle1)), + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftPaddle2)), + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftStick)), + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftStickTouch)), + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftTrigger)), + Capability::Gamepad(Gamepad::Button(GamepadButton::North)), + Capability::Gamepad(Gamepad::Button(GamepadButton::QuickAccess)), + Capability::Gamepad(Gamepad::Button(GamepadButton::RightBumper)), + Capability::Gamepad(Gamepad::Button(GamepadButton::RightPaddle1)), + Capability::Gamepad(Gamepad::Button(GamepadButton::RightPaddle2)), + Capability::Gamepad(Gamepad::Button(GamepadButton::RightStick)), + Capability::Gamepad(Gamepad::Button(GamepadButton::RightStickTouch)), + Capability::Gamepad(Gamepad::Button(GamepadButton::RightTrigger)), + Capability::Gamepad(Gamepad::Button(GamepadButton::Select)), + Capability::Gamepad(Gamepad::Button(GamepadButton::South)), + Capability::Gamepad(Gamepad::Button(GamepadButton::Start)), + Capability::Gamepad(Gamepad::Button(GamepadButton::West)), + Capability::Gamepad(Gamepad::Gyro), + Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftTrigger)), + Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightTrigger)), +]; diff --git a/src/input/source/mod.rs b/src/input/source/mod.rs index 08e69a4f..1525b88c 100644 --- a/src/input/source/mod.rs +++ b/src/input/source/mod.rs @@ -9,7 +9,7 @@ use ::evdev::FFEffectData; use thiserror::Error; use tokio::sync::mpsc::{self, error::TryRecvError}; -use crate::udev::device::UdevDevice; +use crate::udev::device::{self, UdevDevice}; use self::{ client::SourceDeviceClient, command::SourceCommand, evdev::EventDevice, hidraw::HidRawDevice, @@ -379,6 +379,7 @@ impl SourceDevice { HidRawDevice::Fts3528Touchscreen(device) => device.info(), HidRawDevice::XpadUhid(device) => device.info(), HidRawDevice::RogAlly(device) => device.info(), + HidRawDevice::HoripadSteam(device) => device.info(), }, SourceDevice::Iio(device) => match device { IioDevice::BmiImu(device) => device.info(), @@ -405,6 +406,7 @@ impl SourceDevice { HidRawDevice::Fts3528Touchscreen(device) => device.info_ref(), HidRawDevice::XpadUhid(device) => device.info_ref(), HidRawDevice::RogAlly(device) => device.info_ref(), + HidRawDevice::HoripadSteam(device) => device.info_ref(), }, SourceDevice::Iio(device) => match device { IioDevice::BmiImu(device) => device.info_ref(), @@ -431,6 +433,7 @@ impl SourceDevice { HidRawDevice::Fts3528Touchscreen(device) => device.get_id(), HidRawDevice::XpadUhid(device) => device.get_id(), HidRawDevice::RogAlly(device) => device.get_id(), + HidRawDevice::HoripadSteam(device) => device.get_id(), }, SourceDevice::Iio(device) => match device { IioDevice::BmiImu(device) => device.get_id(), @@ -457,6 +460,7 @@ impl SourceDevice { HidRawDevice::Fts3528Touchscreen(device) => device.client(), HidRawDevice::XpadUhid(device) => device.client(), HidRawDevice::RogAlly(device) => device.client(), + HidRawDevice::HoripadSteam(device) => device.client(), }, SourceDevice::Iio(device) => match device { IioDevice::BmiImu(device) => device.client(), @@ -483,6 +487,7 @@ impl SourceDevice { HidRawDevice::Fts3528Touchscreen(device) => device.run().await, HidRawDevice::XpadUhid(device) => device.run().await, HidRawDevice::RogAlly(device) => device.run().await, + HidRawDevice::HoripadSteam(device) => device.run().await, }, SourceDevice::Iio(device) => match device { IioDevice::BmiImu(device) => device.run().await, @@ -509,6 +514,7 @@ impl SourceDevice { HidRawDevice::Fts3528Touchscreen(device) => device.get_capabilities(), HidRawDevice::XpadUhid(device) => device.get_capabilities(), HidRawDevice::RogAlly(device) => device.get_capabilities(), + HidRawDevice::HoripadSteam(device) => device.get_capabilities(), }, SourceDevice::Iio(device) => match device { IioDevice::BmiImu(device) => device.get_capabilities(), @@ -535,6 +541,7 @@ impl SourceDevice { HidRawDevice::Fts3528Touchscreen(device) => device.get_device_path(), HidRawDevice::XpadUhid(device) => device.get_device_path(), HidRawDevice::RogAlly(device) => device.get_device_path(), + HidRawDevice::HoripadSteam(device) => device.get_device_path(), }, SourceDevice::Iio(device) => match device { IioDevice::BmiImu(device) => device.get_device_path(), diff --git a/src/input/target/horipad_steam.rs b/src/input/target/horipad_steam.rs new file mode 100644 index 00000000..fb9c108c --- /dev/null +++ b/src/input/target/horipad_steam.rs @@ -0,0 +1,497 @@ +//! Emulates a Horipad Steam Controller as a target input device. +use std::{cmp::Ordering, error::Error, fmt::Debug, fs::File}; + +use packed_struct::prelude::*; +use uhid_virt::{Bus, CreateParams, StreamError, UHIDDevice}; + +use crate::{ + drivers::horipad_steam::{ + driver::{JOY_AXIS_MAX, JOY_AXIS_MIN, PIDS, TRIGGER_AXIS_MAX, VID}, + hid_report::{Direction, PackedInputDataReport}, + report_descriptor::REPORT_DESCRIPTOR, + }, + input::{ + capability::{Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger}, + composite_device::client::CompositeDeviceClient, + event::{ + native::{NativeEvent, ScheduledNativeEvent}, + value::InputValue, + }, + output_capability::OutputCapability, + output_event::OutputEvent, + }, +}; + +use super::{InputError, OutputError, TargetInputDevice, TargetOutputDevice}; + +/// The [HoripadSteamDevice] is a target input device implementation that emulates +/// a Playstation DualSense controller using uhid. +pub struct HoripadSteamDevice { + device: UHIDDevice, + state: PackedInputDataReport, + timestamp: u8, + queued_events: Vec, +} + +impl HoripadSteamDevice { + pub fn new() -> Result> { + let device = HoripadSteamDevice::create_virtual_device()?; + Ok(Self { + device, + state: PackedInputDataReport::default(), + timestamp: 0, + queued_events: Vec::new(), + }) + } + + /// Create the virtual device to emulate + fn create_virtual_device() -> Result, Box> { + let device = UHIDDevice::create(CreateParams { + name: String::from("HORI CO.,LTD. HORIPAD STEAM"), + phys: String::from(""), + uniq: String::from(""), + bus: Bus::USB, + vendor: VID as u32, + product: PIDS[1] as u32, + version: 0x111, + country: 0, + rd_data: REPORT_DESCRIPTOR.to_vec(), + })?; + + Ok(device) + } + + /// Write the current device state to the device + fn write_state(&mut self) -> Result<(), Box> { + let data = self.state.pack()?; + + // Write the state to the virtual HID + if let Err(e) = self.device.write(&data) { + let err = format!("Failed to write input data report: {:?}", e); + return Err(err.into()); + } + + Ok(()) + } + + /// Update the internal controller state when events are emitted. + fn update_state(&mut self, event: NativeEvent) { + let value = event.get_value(); + let capability = event.as_capability(); + match capability { + Capability::None => (), + Capability::NotImplemented => (), + Capability::Sync => (), + Capability::Gamepad(gamepad) => match gamepad { + Gamepad::Button(btn) => match btn { + GamepadButton::South => self.state.a = event.pressed(), + GamepadButton::East => self.state.b = event.pressed(), + GamepadButton::North => self.state.x = event.pressed(), + GamepadButton::West => self.state.y = event.pressed(), + GamepadButton::Start => self.state.menu = event.pressed(), + GamepadButton::Select => self.state.view = event.pressed(), + GamepadButton::Guide => self.state.steam = event.pressed(), + GamepadButton::QuickAccess => self.state.quick = event.pressed(), + GamepadButton::DPadUp => { + log::debug!("Got DpadUP event to translate!"); + self.state.dpad = self.state.dpad.change(Direction::Up, event.pressed()) + } + GamepadButton::DPadDown => { + log::debug!("Got DPadDown event to translate!"); + self.state.dpad = self.state.dpad.change(Direction::Down, event.pressed()) + } + GamepadButton::DPadLeft => { + log::debug!("Got DPadLeft event to translate!"); + self.state.dpad = self.state.dpad.change(Direction::Left, event.pressed()) + } + GamepadButton::DPadRight => { + log::debug!("Got DPadRight event to translate!"); + self.state.dpad = self.state.dpad.change(Direction::Right, event.pressed()) + } + GamepadButton::LeftBumper => self.state.lb = event.pressed(), + GamepadButton::LeftTrigger => self.state.lt_digital = event.pressed(), + GamepadButton::LeftPaddle1 => self.state.l4 = event.pressed(), + GamepadButton::LeftPaddle2 => self.state.m1 = event.pressed(), + GamepadButton::LeftStick => self.state.ls_click = event.pressed(), + GamepadButton::LeftStickTouch => self.state.ls_touch = event.pressed(), + GamepadButton::RightBumper => self.state.rb = event.pressed(), + GamepadButton::RightTrigger => self.state.rt_digital = event.pressed(), + GamepadButton::RightPaddle1 => self.state.r4 = event.pressed(), + GamepadButton::RightPaddle2 => self.state.m2 = event.pressed(), + GamepadButton::RightStick => self.state.rs_click = event.pressed(), + GamepadButton::RightStickTouch => self.state.rs_touch = event.pressed(), + GamepadButton::LeftPaddle3 => (), + GamepadButton::RightPaddle3 => (), + GamepadButton::Screenshot => (), + _ => (), + }, + Gamepad::Axis(axis) => match axis { + GamepadAxis::LeftStick => { + if let InputValue::Vector2 { x, y } = value { + if let Some(x) = x { + let value = denormalize_signed_value(x, JOY_AXIS_MIN, JOY_AXIS_MAX); + self.state.joystick_l_x = value + } + if let Some(y) = y { + let value = denormalize_signed_value(y, JOY_AXIS_MIN, JOY_AXIS_MAX); + self.state.joystick_l_y = value + } + } + } + GamepadAxis::RightStick => { + if let InputValue::Vector2 { x, y } = value { + if let Some(x) = x { + let value = denormalize_signed_value(x, JOY_AXIS_MIN, JOY_AXIS_MAX); + self.state.joystick_r_x = value + } + if let Some(y) = y { + let value = denormalize_signed_value(y, JOY_AXIS_MIN, JOY_AXIS_MAX); + self.state.joystick_r_y = value + } + } + } + GamepadAxis::Hat0 => { + log::debug!("Got HAT event to translate!"); + if let InputValue::Vector2 { x, y } = value { + if let Some(x) = x { + let value = denormalize_signed_value(x, -1.0, 1.0); + match value.cmp(&0) { + Ordering::Less => { + self.state.dpad = + self.state.dpad.change(Direction::Left, true) + } + Ordering::Equal => { + self.state.dpad = + self.state.dpad.change(Direction::Left, false); + self.state.dpad = + self.state.dpad.change(Direction::Right, false); + } + Ordering::Greater => { + self.state.dpad = + self.state.dpad.change(Direction::Right, true) + } + } + } + if let Some(y) = y { + let value = denormalize_signed_value(y, -1.0, 1.0); + match value.cmp(&0) { + Ordering::Less => { + self.state.dpad = + self.state.dpad.change(Direction::Up, true) + } + Ordering::Equal => { + self.state.dpad = + self.state.dpad.change(Direction::Up, false); + self.state.dpad = + self.state.dpad.change(Direction::Down, false); + } + Ordering::Greater => { + self.state.dpad = + self.state.dpad.change(Direction::Down, true) + } + } + } + } + } + GamepadAxis::Hat1 => (), + GamepadAxis::Hat2 => (), + GamepadAxis::Hat3 => (), + }, + Gamepad::Trigger(trigger) => match trigger { + GamepadTrigger::LeftTrigger => { + if let InputValue::Float(normal_value) = value { + let value = denormalize_unsigned_value(normal_value, TRIGGER_AXIS_MAX); + self.state.lt_analog = value + } + } + GamepadTrigger::LeftTouchpadForce => (), + GamepadTrigger::LeftStickForce => (), + GamepadTrigger::RightTrigger => { + if let InputValue::Float(normal_value) = value { + let value = denormalize_unsigned_value(normal_value, TRIGGER_AXIS_MAX); + self.state.rt_analog = value + } + } + GamepadTrigger::RightTouchpadForce => (), + GamepadTrigger::RightStickForce => (), + }, + Gamepad::Accelerometer => { + if let InputValue::Vector3 { x, y, z } = value { + if let Some(x) = x { + self.state.accel_x = Integer::from_primitive(denormalize_accel_value(x)) + } + if let Some(y) = y { + self.state.accel_y = Integer::from_primitive(denormalize_accel_value(y)) + } + if let Some(z) = z { + self.state.accel_z = Integer::from_primitive(denormalize_accel_value(z)) + } + } + } + Gamepad::Gyro => { + if let InputValue::Vector3 { x, y, z } = value { + if let Some(x) = x { + self.state.gyro_x = Integer::from_primitive(denormalize_gyro_value(x)); + } + if let Some(y) = y { + self.state.gyro_y = Integer::from_primitive(denormalize_gyro_value(y)) + } + if let Some(z) = z { + self.state.gyro_z = Integer::from_primitive(denormalize_gyro_value(z)) + } + } + } + }, + Capability::DBus(_) => (), + Capability::Mouse(_) => (), + Capability::Keyboard(_) => (), + Capability::Touchpad(_) => (), + Capability::Touchscreen(_) => (), + }; + } + + /// Handle [OutputEvent::Output] events from the HIDRAW device. These are + /// events which should be forwarded back to source devices. + fn handle_output(&mut self, _data: Vec) -> Result, Box> { + // Validate the output report size + Ok(vec![]) + } + + /// Handle [OutputEvent::GetReport] events from the HIDRAW device + fn handle_get_report( + &mut self, + _id: u32, + _report_number: u8, + _report_type: uhid_virt::ReportType, + ) -> Result<(), Box> { + Ok(()) + } +} + +impl TargetInputDevice for HoripadSteamDevice { + fn write_event(&mut self, event: NativeEvent) -> Result<(), InputError> { + log::trace!("Received event: {event:?}"); + self.update_state(event); + Ok(()) + } + + fn get_capabilities(&self) -> Result, InputError> { + Ok(vec![ + Capability::Gamepad(Gamepad::Accelerometer), + Capability::Gamepad(Gamepad::Axis(GamepadAxis::LeftStick)), + Capability::Gamepad(Gamepad::Axis(GamepadAxis::RightStick)), + Capability::Gamepad(Gamepad::Button(GamepadButton::DPadDown)), + Capability::Gamepad(Gamepad::Button(GamepadButton::DPadLeft)), + Capability::Gamepad(Gamepad::Button(GamepadButton::DPadRight)), + Capability::Gamepad(Gamepad::Button(GamepadButton::DPadUp)), + Capability::Gamepad(Gamepad::Button(GamepadButton::East)), + Capability::Gamepad(Gamepad::Button(GamepadButton::Guide)), + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftBumper)), + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftPaddle1)), + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftPaddle2)), + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftStick)), + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftStickTouch)), + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftTrigger)), + Capability::Gamepad(Gamepad::Button(GamepadButton::North)), + Capability::Gamepad(Gamepad::Button(GamepadButton::QuickAccess)), + Capability::Gamepad(Gamepad::Button(GamepadButton::RightBumper)), + Capability::Gamepad(Gamepad::Button(GamepadButton::RightPaddle1)), + Capability::Gamepad(Gamepad::Button(GamepadButton::RightPaddle2)), + Capability::Gamepad(Gamepad::Button(GamepadButton::RightStick)), + Capability::Gamepad(Gamepad::Button(GamepadButton::RightStickTouch)), + Capability::Gamepad(Gamepad::Button(GamepadButton::RightTrigger)), + // Capability::Gamepad(Gamepad::Button(GamepadButton::Screenshot)), + Capability::Gamepad(Gamepad::Button(GamepadButton::Select)), + Capability::Gamepad(Gamepad::Button(GamepadButton::South)), + Capability::Gamepad(Gamepad::Button(GamepadButton::Start)), + Capability::Gamepad(Gamepad::Button(GamepadButton::West)), + Capability::Gamepad(Gamepad::Gyro), + Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftTrigger)), + Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightTrigger)), + ]) + } + + /// Returns any events in the queue up to the [TargetDriver] + fn scheduled_events(&mut self) -> Option> { + if self.queued_events.is_empty() { + return None; + } + Some(self.queued_events.drain(..).collect()) + } + + fn stop(&mut self) -> Result<(), InputError> { + let _ = self.device.destroy(); + Ok(()) + } +} + +impl TargetOutputDevice for HoripadSteamDevice { + /// Handle reading from the device and processing input events from source + /// devices. + /// https://www.kernel.org/doc/html/latest/hid/uhid.html#read + fn poll(&mut self, _: &Option) -> Result, OutputError> { + // Read output events + let event = match self.device.read() { + Ok(event) => event, + Err(err) => match err { + StreamError::Io(_e) => { + //log::error!("Error reading from UHID device: {e:?}"); + // Write the current state + self.write_state()?; + return Ok(vec![]); + } + StreamError::UnknownEventType(e) => { + log::debug!("Unknown event type: {:?}", e); + // Write the current state + self.write_state()?; + return Ok(vec![]); + } + }, + }; + + // Match the type of UHID output event + let output_events = match event { + // This is sent when the HID device is started. Consider this as an answer to + // UHID_CREATE. This is always the first event that is sent. + uhid_virt::OutputEvent::Start { dev_flags: _ } => { + log::debug!("Start event received"); + Ok(vec![]) + } + // This is sent when the HID device is stopped. Consider this as an answer to + // UHID_DESTROY. + uhid_virt::OutputEvent::Stop => { + log::debug!("Stop event received"); + Ok(vec![]) + } + // This is sent when the HID device is opened. That is, the data that the HID + // device provides is read by some other process. You may ignore this event but + // it is useful for power-management. As long as you haven't received this event + // there is actually no other process that reads your data so there is no need to + // send UHID_INPUT events to the kernel. + uhid_virt::OutputEvent::Open => { + log::debug!("Open event received"); + Ok(vec![]) + } + // This is sent when there are no more processes which read the HID data. It is + // the counterpart of UHID_OPEN and you may as well ignore this event. + uhid_virt::OutputEvent::Close => { + log::debug!("Close event received"); + Ok(vec![]) + } + // This is sent if the HID device driver wants to send raw data to the I/O + // device. You should read the payload and forward it to the device. + uhid_virt::OutputEvent::Output { data } => { + log::trace!("Got output data: {:?}", data); + let result = self.handle_output(data); + match result { + Ok(events) => Ok(events), + Err(e) => { + let err = format!("Failed process output event: {:?}", e); + Err(err.into()) + } + } + } + // This event is sent if the kernel driver wants to perform a GET_REPORT request + // on the control channel as described in the HID specs. The report-type and + // report-number are available in the payload. + // The kernel serializes GET_REPORT requests so there will never be two in + // parallel. However, if you fail to respond with a UHID_GET_REPORT_REPLY, the + // request might silently time out. + // Once you read a GET_REPORT request, you shall forward it to the HID device and + // remember the "id" field in the payload. Once your HID device responds to the + // GET_REPORT (or if it fails), you must send a UHID_GET_REPORT_REPLY to the + // kernel with the exact same "id" as in the request. If the request already + // timed out, the kernel will ignore the response silently. The "id" field is + // never re-used, so conflicts cannot happen. + uhid_virt::OutputEvent::GetReport { + id, + report_number, + report_type, + } => { + log::trace!( + "Received GetReport event: id: {id}, num: {report_number}, type: {:?}", + report_type + ); + let result = self.handle_get_report(id, report_number, report_type); + if let Err(e) = result { + let err = format!("Failed to process GetReport event: {:?}", e); + return Err(err.into()); + } + Ok(vec![]) + } + // This is the SET_REPORT equivalent of UHID_GET_REPORT. On receipt, you shall + // send a SET_REPORT request to your HID device. Once it replies, you must tell + // the kernel about it via UHID_SET_REPORT_REPLY. + // The same restrictions as for UHID_GET_REPORT apply. + uhid_virt::OutputEvent::SetReport { + id, + report_number, + report_type, + data, + } => { + log::debug!("Received SetReport event: id: {id}, num: {report_number}, type: {:?}, data: {:?}", report_type, data); + Ok(vec![]) + } + }; + + // Write the current state + self.write_state()?; + + output_events + } + + fn get_output_capabilities(&self) -> Result, OutputError> { + Ok(vec![OutputCapability::ForceFeedback]) + } +} + +impl Debug for HoripadSteamDevice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("HoripadSteamDevice") + .field("state", &self.state) + .field("timestamp", &self.timestamp) + .finish() + } +} + +/// Convert the given normalized value between -1.0 - 1.0 to the real value +/// based on the given minimum and maximum axis range. Playstation gamepads +/// use a range from 0-255, with 127 being the "nuetral" point. +fn denormalize_signed_value(normal_value: f64, min: f64, max: f64) -> u8 { + let mid = (max + min) / 2.0; + let normal_value_abs = normal_value.abs(); + if normal_value >= 0.0 { + let maximum = max - mid; + let value = normal_value * maximum + mid; + value as u8 + } else { + let minimum = min - mid; + let value = normal_value_abs * minimum + mid; + value as u8 + } +} + +/// De-normalizes the given value from 0.0 - 1.0 into a real value based on +/// the maximum axis range. +fn denormalize_unsigned_value(normal_value: f64, max: f64) -> u8 { + (normal_value * max).round() as u8 +} + +/// De-normalizes the given value in meters per second into a real value that +/// the controller understands. +/// Accelerometer values are measured in [] +/// units of G acceleration (1G == 9.8m/s). InputPlumber accelerometer +/// values are measured in units of meters per second. To denormalize +/// the value, it needs to be converted into G units (by dividing by 9.8), +/// then multiplying that value by the []. +fn denormalize_accel_value(value_meters_sec: f64) -> i16 { + let value = value_meters_sec; + value as i16 +} + +/// DualSense gyro values are measured in units of degrees per second. +/// InputPlumber gyro values are also measured in degrees per second. +fn denormalize_gyro_value(value_degrees_sec: f64) -> i16 { + let value = value_degrees_sec; + value as i16 +} diff --git a/src/input/target/mod.rs b/src/input/target/mod.rs index 8b9aefdb..a0300182 100644 --- a/src/input/target/mod.rs +++ b/src/input/target/mod.rs @@ -6,6 +6,7 @@ use std::{ time::Duration, }; +use horipad_steam::HoripadSteamDevice; use thiserror::Error; use tokio::sync::mpsc::{self, error::TryRecvError}; @@ -41,6 +42,7 @@ pub mod client; pub mod command; pub mod dbus; pub mod dualsense; +pub mod horipad_steam; pub mod keyboard; pub mod mouse; pub mod steam_deck; @@ -160,6 +162,10 @@ impl TargetDeviceTypeId { id: "ds5-edge", name: "Sony Interactive Entertainment DualSense Edge Wireless Controller", }, + TargetDeviceTypeId { + id: "hori-steam", + name: "HORI CO.,LTD. HORIPAD STEAM", + }, TargetDeviceTypeId { id: "keyboard", name: "InputPlumber Keyboard", @@ -527,6 +533,7 @@ pub enum TargetDevice { Null, DBus(TargetDriver), DualSense(TargetDriver), + HoripadSteam(TargetDriver), Keyboard(TargetDriver), Mouse(TargetDriver), SteamDeck(TargetDriver), @@ -582,6 +589,11 @@ impl TargetDevice { let driver = TargetDriver::new_with_options(id, device, dbus, options); Ok(Self::DualSense(driver)) } + "hori-steam" => { + let device = HoripadSteamDevice::new()?; + let driver = TargetDriver::new(id, device, dbus); + Ok(Self::HoripadSteam(driver)) + } "keyboard" => { let device = KeyboardDevice::new()?; let driver = TargetDriver::new(id, device, dbus); @@ -649,6 +661,7 @@ impl TargetDevice { "ds5-edge-usb".try_into().unwrap(), "ds5-edge-bt".try_into().unwrap(), ], + TargetDevice::HoripadSteam(_) => vec!["hori-steam".try_into().unwrap()], TargetDevice::Keyboard(_) => vec!["keyboard".try_into().unwrap()], TargetDevice::Mouse(_) => vec!["mouse".try_into().unwrap()], TargetDevice::SteamDeck(_) => vec!["deck".try_into().unwrap()], @@ -670,6 +683,7 @@ impl TargetDevice { TargetDevice::Null => "null", TargetDevice::DBus(_) => "dbus", TargetDevice::DualSense(_) => "gamepad", + TargetDevice::HoripadSteam(_) => "gamepad", TargetDevice::Keyboard(_) => "keyboard", TargetDevice::Mouse(_) => "mouse", TargetDevice::SteamDeck(_) => "gamepad", @@ -687,6 +701,7 @@ impl TargetDevice { TargetDevice::Null => None, TargetDevice::DBus(device) => Some(device.client()), TargetDevice::DualSense(device) => Some(device.client()), + TargetDevice::HoripadSteam(device) => Some(device.client()), TargetDevice::Keyboard(device) => Some(device.client()), TargetDevice::Mouse(device) => Some(device.client()), TargetDevice::SteamDeck(device) => Some(device.client()), @@ -704,6 +719,7 @@ impl TargetDevice { TargetDevice::Null => Ok(()), TargetDevice::DBus(device) => device.run(dbus_path).await, TargetDevice::DualSense(device) => device.run(dbus_path).await, + TargetDevice::HoripadSteam(device) => device.run(dbus_path).await, TargetDevice::Keyboard(device) => device.run(dbus_path).await, TargetDevice::Mouse(device) => device.run(dbus_path).await, TargetDevice::SteamDeck(device) => device.run(dbus_path).await,