Skip to content

Commit 1d0e46d

Browse files
committed
feat(Horipad Steam Controller): Add Horipad Steam Controller Support.
- Adds Horipad Steam Controller as a source device. - Adds Horipad Steam Controller as a target device.
1 parent 72bf1f4 commit 1d0e46d

File tree

15 files changed

+1624
-9
lines changed

15 files changed

+1624
-9
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# yaml-language-server: $schema=https://raw.githubusercontent.com/ShadowBlip/InputPlumber/main/rootfs/usr/share/inputplumber/schema/composite_device_v1.json
2+
# Schema version number
3+
version: 1
4+
5+
# The type of configuration schema
6+
kind: CompositeDevice
7+
8+
# Name of the composite device mapping
9+
name: Horipad Steam
10+
11+
# Only use this profile if *any* of the given matches matches. If this list is
12+
# empty,then the source devices will *always* be checked.
13+
# /sys/class/dmi/id/product_name
14+
matches: []
15+
16+
# Only allow a CompositeDevice to manage at most the given number of
17+
# source devices. When this limit is reached, a new CompositeDevice will be
18+
# created for any new matching devices.
19+
maximum_sources: 2
20+
21+
# One or more source devices to combine into a single virtual device. The events
22+
# from these devices will be watched and translated according to the key map.
23+
source_devices:
24+
- group: gamepad
25+
blocked: true
26+
udev:
27+
attributes:
28+
- name: id/vendor
29+
value: "0f0d"
30+
- name: id/product
31+
value: "{0196,01ab}"
32+
sys_name: "event*"
33+
subsystem: input
34+
- group: gamepad
35+
hidraw:
36+
vendor_id: 0x0f0d
37+
product_id: 0x0196
38+
- group: gamepad
39+
hidraw:
40+
vendor_id: 0x0f0d
41+
product_id: 0x01ab
42+
43+
# The target input device(s) to emulate by default
44+
target_devices:
45+
- hori-steam
46+
- mouse
47+
- keyboard

rootfs/usr/share/inputplumber/schema/composite_device_v1.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"mouse",
6060
"keyboard",
6161
"gamepad",
62+
"hori-steam",
6263
"xb360",
6364
"xbox-elite",
6465
"xbox-series",

rootfs/usr/share/inputplumber/schema/device_profile_v1.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"ds5",
3232
"ds5-edge",
3333
"gamepad",
34+
"hori-steam",
3435
"keyboard",
3536
"mouse",
3637
"touchpad",
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
use std::{error::Error, ffi::CString};
2+
3+
use hidapi::HidDevice;
4+
use packed_struct::{types::SizedInteger, PackedStruct};
5+
6+
use crate::{drivers::horipad_steam::hid_report::Direction, udev::device::UdevDevice};
7+
8+
use super::{
9+
event::{
10+
BinaryInput, ButtonEvent, Event, InertialEvent, InertialInput, JoystickEvent,
11+
JoystickInput, TriggerEvent, TriggerInput,
12+
},
13+
hid_report::PackedInputDataReport,
14+
};
15+
16+
// Report ID
17+
pub const REPORT_ID: u8 = 0x07;
18+
19+
// Input report size
20+
const PACKET_SIZE: usize = 287;
21+
22+
// HID buffer read timeout
23+
const HID_TIMEOUT: i32 = 10;
24+
25+
// Input report axis ranges
26+
pub const JOY_AXIS_MAX: f64 = 255.0;
27+
pub const JOY_AXIS_MIN: f64 = 0.0;
28+
pub const TRIGGER_AXIS_MAX: f64 = 255.0;
29+
30+
pub const VID: u16 = 0x0F0D;
31+
pub const PIDS: [u16; 2] = [0x0196, 0x01AB];
32+
33+
#[derive(Debug, Clone, Default)]
34+
struct DPadState {
35+
up: bool,
36+
down: bool,
37+
left: bool,
38+
right: bool,
39+
}
40+
41+
pub struct Driver {
42+
/// HIDRAW device instance
43+
device: HidDevice,
44+
/// State for the device
45+
state: Option<PackedInputDataReport>,
46+
/// Last DPad state
47+
dpad: DPadState,
48+
}
49+
50+
impl Driver {
51+
pub fn new(udevice: UdevDevice) -> Result<Self, Box<dyn Error + Send + Sync>> {
52+
let path = udevice.devnode();
53+
54+
let cs_path = CString::new(path.clone())?;
55+
let api = hidapi::HidApi::new()?;
56+
let device = api.open_path(&cs_path)?;
57+
58+
let info = device.get_device_info()?;
59+
if info.vendor_id() != VID || !PIDS.contains(&info.product_id()) {
60+
return Err(format!("Device '{path}' is not a Horipad Steam Controller").into());
61+
}
62+
63+
Ok(Self {
64+
device,
65+
state: None,
66+
dpad: Default::default(),
67+
})
68+
}
69+
70+
/// Poll the device and read input reports
71+
pub fn poll(&mut self) -> Result<Vec<Event>, Box<dyn Error + Send + Sync>> {
72+
// Read data from the device into a buffer
73+
let mut buf = [0; PACKET_SIZE];
74+
let _bytes_read = self.device.read_timeout(&mut buf[..], HID_TIMEOUT)?;
75+
76+
let report_id = buf[0];
77+
if report_id != REPORT_ID {
78+
log::warn!("Got unhandled report_id {report_id}, someone should look into that...");
79+
return Ok(vec![]);
80+
}
81+
82+
let input_report = PackedInputDataReport::unpack(&buf)?;
83+
84+
// Print input report for debugging
85+
//log::trace!("--- Input report ---");
86+
//log::trace!("{input_report}");
87+
//log::trace!("---- End Report ----");
88+
89+
// Update the state
90+
let old_dinput_state = self.update_state(input_report);
91+
92+
// Translate the state into a stream of input events
93+
let events = self.translate_events(old_dinput_state);
94+
95+
Ok(events)
96+
}
97+
98+
/// Update touchinput state
99+
fn update_state(
100+
&mut self,
101+
input_report: PackedInputDataReport,
102+
) -> Option<PackedInputDataReport> {
103+
let old_state = self.state;
104+
self.state = Some(input_report);
105+
old_state
106+
}
107+
108+
/// Translate the state into individual events
109+
fn translate_events(&mut self, old_state: Option<PackedInputDataReport>) -> Vec<Event> {
110+
let mut events = Vec::new();
111+
let Some(state) = self.state else {
112+
return events;
113+
};
114+
115+
// Translate state changes into events if they have changed
116+
let Some(old_state) = old_state else {
117+
return events;
118+
};
119+
120+
// Binary Events
121+
if state.a != old_state.a {
122+
events.push(Event::Button(ButtonEvent::A(BinaryInput {
123+
pressed: state.a,
124+
})));
125+
}
126+
if state.b != old_state.b {
127+
events.push(Event::Button(ButtonEvent::B(BinaryInput {
128+
pressed: state.b,
129+
})));
130+
}
131+
if state.x != old_state.x {
132+
events.push(Event::Button(ButtonEvent::X(BinaryInput {
133+
pressed: state.x,
134+
})));
135+
}
136+
if state.y != old_state.y {
137+
events.push(Event::Button(ButtonEvent::Y(BinaryInput {
138+
pressed: state.y,
139+
})));
140+
}
141+
if state.rb != old_state.rb {
142+
events.push(Event::Button(ButtonEvent::RB(BinaryInput {
143+
pressed: state.rb,
144+
})));
145+
}
146+
if state.lb != old_state.lb {
147+
events.push(Event::Button(ButtonEvent::LB(BinaryInput {
148+
pressed: state.lb,
149+
})));
150+
}
151+
if state.view != old_state.view {
152+
events.push(Event::Button(ButtonEvent::View(BinaryInput {
153+
pressed: state.view,
154+
})));
155+
}
156+
if state.menu != old_state.menu {
157+
events.push(Event::Button(ButtonEvent::Menu(BinaryInput {
158+
pressed: state.menu,
159+
})));
160+
}
161+
if state.steam != old_state.steam {
162+
events.push(Event::Button(ButtonEvent::Steam(BinaryInput {
163+
pressed: state.steam,
164+
})));
165+
}
166+
if state.quick != old_state.quick {
167+
events.push(Event::Button(ButtonEvent::Quick(BinaryInput {
168+
pressed: state.quick,
169+
})));
170+
}
171+
if state.ls_click != old_state.ls_click {
172+
events.push(Event::Button(ButtonEvent::LSClick(BinaryInput {
173+
pressed: state.ls_click,
174+
})));
175+
}
176+
if state.rs_click != old_state.rs_click {
177+
events.push(Event::Button(ButtonEvent::RSClick(BinaryInput {
178+
pressed: state.rs_click,
179+
})));
180+
}
181+
if state.ls_touch != old_state.ls_touch {
182+
events.push(Event::Button(ButtonEvent::LSTouch(BinaryInput {
183+
pressed: state.ls_touch,
184+
})));
185+
}
186+
if state.rs_touch != old_state.rs_touch {
187+
events.push(Event::Button(ButtonEvent::RSTouch(BinaryInput {
188+
pressed: state.rs_touch,
189+
})));
190+
}
191+
if state.lt_digital != old_state.lt_digital {
192+
events.push(Event::Button(ButtonEvent::LTDigital(BinaryInput {
193+
pressed: state.ls_touch,
194+
})));
195+
}
196+
if state.rt_digital != old_state.rt_digital {
197+
events.push(Event::Button(ButtonEvent::RTDigital(BinaryInput {
198+
pressed: state.rs_touch,
199+
})));
200+
}
201+
if state.l4 != old_state.l4 {
202+
events.push(Event::Button(ButtonEvent::L4(BinaryInput {
203+
pressed: state.l4,
204+
})));
205+
}
206+
if state.r4 != old_state.r4 {
207+
events.push(Event::Button(ButtonEvent::R4(BinaryInput {
208+
pressed: state.r4,
209+
})));
210+
}
211+
if state.m1 != old_state.m1 {
212+
events.push(Event::Button(ButtonEvent::M1(BinaryInput {
213+
pressed: state.m1,
214+
})));
215+
}
216+
if state.m2 != old_state.m2 {
217+
events.push(Event::Button(ButtonEvent::M2(BinaryInput {
218+
pressed: state.m2,
219+
})));
220+
}
221+
if state.dpad != old_state.dpad {
222+
let up = [Direction::Up, Direction::UpRight, Direction::UpLeft].contains(&state.dpad);
223+
let down =
224+
[Direction::Down, Direction::DownRight, Direction::DownLeft].contains(&state.dpad);
225+
let left =
226+
[Direction::Left, Direction::DownLeft, Direction::UpLeft].contains(&state.dpad);
227+
let right =
228+
[Direction::Right, Direction::DownRight, Direction::UpRight].contains(&state.dpad);
229+
let dpad_state = DPadState {
230+
up,
231+
down,
232+
left,
233+
right,
234+
};
235+
236+
if up != self.dpad.up {
237+
events.push(Event::Button(ButtonEvent::DPadUp(BinaryInput {
238+
pressed: up,
239+
})));
240+
}
241+
if down != self.dpad.down {
242+
events.push(Event::Button(ButtonEvent::DPadDown(BinaryInput {
243+
pressed: down,
244+
})));
245+
}
246+
if left != self.dpad.left {
247+
events.push(Event::Button(ButtonEvent::DPadLeft(BinaryInput {
248+
pressed: left,
249+
})));
250+
}
251+
if right != self.dpad.right {
252+
events.push(Event::Button(ButtonEvent::DPadRight(BinaryInput {
253+
pressed: right,
254+
})));
255+
}
256+
257+
self.dpad = dpad_state;
258+
}
259+
260+
// Axis events
261+
if state.joystick_l_x != old_state.joystick_l_x
262+
|| state.joystick_l_y != old_state.joystick_l_y
263+
{
264+
events.push(Event::Joystick(JoystickEvent::LStick(JoystickInput {
265+
x: state.joystick_l_x,
266+
y: state.joystick_l_y,
267+
})));
268+
}
269+
if state.joystick_r_x != old_state.joystick_r_x
270+
|| state.joystick_r_y != old_state.joystick_r_y
271+
{
272+
events.push(Event::Joystick(JoystickEvent::RStick(JoystickInput {
273+
x: state.joystick_r_x,
274+
y: state.joystick_r_y,
275+
})));
276+
}
277+
278+
if state.lt_analog != old_state.lt_analog {
279+
events.push(Event::Trigger(TriggerEvent::LTAnalog(TriggerInput {
280+
value: state.lt_analog,
281+
})));
282+
}
283+
if state.rt_analog != old_state.rt_analog {
284+
events.push(Event::Trigger(TriggerEvent::RTAnalog(TriggerInput {
285+
value: state.rt_analog,
286+
})));
287+
}
288+
289+
// Accelerometer events
290+
events.push(Event::Inertia(InertialEvent::Accelerometer(
291+
InertialInput {
292+
x: -state.accel_x.to_primitive(),
293+
y: state.accel_y.to_primitive(),
294+
z: -state.accel_z.to_primitive(),
295+
},
296+
)));
297+
events.push(Event::Inertia(InertialEvent::Gyro(InertialInput {
298+
x: -state.gyro_x.to_primitive(),
299+
y: state.gyro_y.to_primitive(),
300+
z: -state.gyro_z.to_primitive(),
301+
})));
302+
303+
log::trace!("Got events: {events:?}");
304+
305+
events
306+
}
307+
}

0 commit comments

Comments
 (0)