Skip to content

Commit

Permalink
feat(Horipad Steam Controller): Add Horipad Steam Controller Support.
Browse files Browse the repository at this point in the history
- Adds Horipad Steam Controller as a source device.
- Adds Horipad Steam Controller as a target device.
  • Loading branch information
pastaq committed Jan 21, 2025
1 parent 72bf1f4 commit aab9739
Show file tree
Hide file tree
Showing 15 changed files with 1,642 additions and 9 deletions.
47 changes: 47 additions & 0 deletions rootfs/usr/share/inputplumber/devices/60-horipad_steam.yaml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"mouse",
"keyboard",
"gamepad",
"hori-steam",
"xb360",
"xbox-elite",
"xbox-series",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"ds5",
"ds5-edge",
"gamepad",
"hori-steam",
"keyboard",
"mouse",
"touchpad",
Expand Down
310 changes: 310 additions & 0 deletions src/drivers/horipad_steam/driver.rs
Original file line number Diff line number Diff line change
@@ -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<PackedInputDataReport>,
/// Last DPad state
dpad: DPadState,
}

impl Driver {
pub fn new(udevice: UdevDevice) -> Result<Self, Box<dyn Error + Send + Sync>> {
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<Vec<Event>, Box<dyn Error + Send + Sync>> {
// 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<PackedInputDataReport> {
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<PackedInputDataReport>) -> Vec<Event> {
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
}
}
Loading

0 comments on commit aab9739

Please sign in to comment.