1use hidapi::{HidApi, HidDevice};
4use std::time::Duration;
5
6use crate::devices::FrameDevice;
7use crate::devices::device::Device;
8use crate::devices::util::{
9 is_u32_masked_button_pressed, scale_stick_to_byte, scale_trigger_to_byte,
10};
11use crate::devices::{DeviceButton, DeviceConfig, GyroActivationMode};
12use crate::dsu::DSUFrame;
13use crate::errors::DeviceError;
14
15const VID: u16 = 0x28de;
16const PID_WIRED: u16 = 0x1102;
17const PID_WIRELESS: u16 = 0x1142;
18
19const USAGE_PAGE_VENDOR_MIN: u16 = 0xFF00;
20
21const FEATURE_REPORT_ID: u8 = 0x01;
22const FEATURE_REPORT_SIZE: usize = 64;
23const SEND_FEATURE_REPORT_SLEEP_DURATION: Duration = Duration::from_millis(50);
24const CMD_SET_SETTINGS_VALUES: u8 = 0x87;
25
26const SETTING_LIZARD_MODE: u8 = 9;
27const SETTING_IMU_MODE: u8 = 48;
28
29const LIZARD_MODE_OFF: u16 = 0;
30const LIZARD_MODE_ON: u16 = 1;
31const IMU_MODE_SEND_RAW_ACCEL: u16 = 0x08;
32const IMU_MODE_SEND_RAW_GYRO: u16 = 0x10;
33const IMU_MODE_GYRO_ACCEL: u16 = IMU_MODE_SEND_RAW_ACCEL | IMU_MODE_SEND_RAW_GYRO;
34
35const REPORT_ID_STATE: u8 = 0x01;
36const READ_TIMEOUT_MILLIS: i32 = 100;
37
38const ACCEL_PER_G: f32 = 16384.0;
39const GYRO_PER_DPS: f32 = 16.384;
40const ANALOG_TRIGGER_TO_DIGITAL_THRESHOLD: u8 = 228;
41
42const MASK_A: u64 = 0x0000_0000_0000_0080;
43const MASK_B: u64 = 0x0000_0000_0000_0020;
44const MASK_X: u64 = 0x0000_0000_0000_0040;
45const MASK_Y: u64 = 0x0000_0000_0000_0010;
46const MASK_RIGHT_BUMPER: u64 = 0x0000_0000_0000_0004;
47const MASK_LEFT_BUMPER: u64 = 0x0000_0000_0000_0008;
48const MASK_DPAD_UP: u64 = 0x0000_0000_0000_0100;
49const MASK_DPAD_RIGHT: u64 = 0x0000_0000_0000_0200;
50const MASK_DPAD_LEFT: u64 = 0x0000_0000_0000_0400;
51const MASK_DPAD_DOWN: u64 = 0x0000_0000_0000_0800;
52const MASK_SELECT: u64 = 0x0000_0000_0000_1000;
53const MASK_GUIDE: u64 = 0x0000_0000_0000_2000;
54const MASK_START: u64 = 0x0000_0000_0000_4000;
55const MASK_LEFT_GRIP: u64 = 0x0000_0000_0000_8000;
56const MASK_RIGHT_GRIP: u64 = 0x0000_0000_0001_0000;
57const MASK_RIGHT_PAD_CLICKED: u64 = 0x0000_0000_0004_0000;
58const MASK_LEFT_PAD_TOUCH: u64 = 0x0000_0000_0008_0000;
59const MASK_RIGHT_PAD_TOUCH: u64 = 0x0000_0000_0010_0000;
60const MASK_LEFT_STICK_CLICK: u64 = 0x0000_0000_0040_0000;
61const MASK_LEFT_PAD_AND_JOYSTICK: u64 = 0x0000_0000_0080_0000;
62
63#[derive(Debug, Clone, Copy, PartialEq)]
64struct LegacyFrame {
65 buttons: u64,
66 trigger_left: u8,
67 trigger_right: u8,
68 left_stick_x: i16,
69 left_stick_y: i16,
70 right_pad_x: i16,
71 right_pad_y: i16,
72 imu_timestamp: u32,
73 accel_x: i16,
74 accel_y: i16,
75 accel_z: i16,
76 gyro_x: i16,
77 gyro_y: i16,
78 gyro_z: i16,
79 left_pad_touch: bool,
80 right_pad_touch: bool,
81 left_stick_click: bool,
82 right_pad_click: bool,
83 left_grip: bool,
84 right_grip: bool,
85}
86
87impl LegacyFrame {
88 fn parse(data: &[u8]) -> Option<Self> {
89 if data.is_empty() {
90 return None;
91 }
92
93 let report_id = data[0];
94 if report_id != REPORT_ID_STATE {
95 return None;
96 }
97
98 if data.len() < 64 {
99 return None;
100 }
101
102 let buttons = u64::from_le_bytes([data[8], data[9], data[10], 0, 0, 0, 0, 0]);
103
104 Some(Self {
105 buttons,
106 trigger_left: data[11],
107 trigger_right: data[12],
108 left_stick_x: i16::from_le_bytes([data[54], data[55]]),
109 left_stick_y: i16::from_le_bytes([data[56], data[57]]),
110 right_pad_x: i16::from_le_bytes([data[20], data[21]]),
111 right_pad_y: i16::from_le_bytes([data[22], data[23]]),
112 imu_timestamp: u32::from_le_bytes([data[4], data[5], data[6], data[7]]),
113 accel_x: i16::from_le_bytes([data[28], data[29]]),
114 accel_y: i16::from_le_bytes([data[30], data[31]]),
115 accel_z: i16::from_le_bytes([data[32], data[33]]),
116 gyro_x: i16::from_le_bytes([data[34], data[35]]),
117 gyro_y: i16::from_le_bytes([data[36], data[37]]),
118 gyro_z: i16::from_le_bytes([data[38], data[39]]),
119 left_pad_touch: is_u32_masked_button_pressed(
120 buttons as u32,
121 MASK_LEFT_PAD_TOUCH as u32,
122 ) || is_u32_masked_button_pressed(
123 buttons as u32,
124 MASK_LEFT_PAD_AND_JOYSTICK as u32,
125 ),
126 right_pad_touch: is_u32_masked_button_pressed(
127 buttons as u32,
128 MASK_RIGHT_PAD_TOUCH as u32,
129 ),
130 left_stick_click: is_u32_masked_button_pressed(
131 buttons as u32,
132 MASK_LEFT_STICK_CLICK as u32,
133 ),
134 right_pad_click: is_u32_masked_button_pressed(
135 buttons as u32,
136 MASK_RIGHT_PAD_CLICKED as u32,
137 ),
138 left_grip: is_u32_masked_button_pressed(buttons as u32, MASK_LEFT_GRIP as u32),
139 right_grip: is_u32_masked_button_pressed(buttons as u32, MASK_RIGHT_GRIP as u32),
140 })
141 }
142}
143
144#[derive(Debug, Clone, Copy, PartialEq)]
145pub enum ConnectionMode {
146 Usb,
147 Wireless,
148}
149
150pub struct LegacySteamController {
152 config: DeviceConfig,
153 hid: HidDevice,
154}
155
156impl LegacySteamController {
157 pub fn find(
159 config: DeviceConfig,
160 api: &HidApi,
161 device_path: Option<&str>,
162 ) -> Result<Self, DeviceError> {
163 let candidates: Vec<_> = api
164 .device_list()
165 .filter(|d| {
166 d.vendor_id() == VID
167 && d.usage_page() >= USAGE_PAGE_VENDOR_MIN
168 && (d.product_id() == PID_WIRED || d.product_id() == PID_WIRELESS)
169 && Self::supports_interface(d.interface_number())
170 })
171 .collect();
172
173 if let Some(target) = device_path {
174 let info = candidates
175 .into_iter()
176 .find(|d| d.path().to_str().ok() == Some(target));
177
178 let Some(info) = info else {
179 return Err(DeviceError::NoDeviceFoundAtPath(target.to_string()));
180 };
181
182 let hid = info.open_device(api)?;
183 probe_device(&hid)?;
184 log::info!(
185 "Opened legacy controller on {} ({:?})",
186 target,
187 connection_mode_from_pid(info.product_id())
188 );
189 return Ok(Self { config, hid });
190 }
191
192 for info in candidates {
193 let Ok(path) = info.path().to_str() else {
194 continue;
195 };
196
197 let hid = match info.open_device(api) {
198 Ok(hid) => hid,
199 Err(err) => {
200 log::debug!("Failed to obtain handle to legacy device at {path}: {err:?}");
201 continue;
202 }
203 };
204
205 if let Err(e) = probe_device(&hid) {
206 log::debug!("Probe failed for legacy device at {path}: {e}");
207 continue;
208 }
209
210 log::info!(
211 "Opened legacy controller on {} ({:?})",
212 path,
213 connection_mode_from_pid(info.product_id())
214 );
215 return Ok(Self { config, hid });
216 }
217
218 Err(DeviceError::NoDeviceFound)
219 }
220
221 fn supports_interface(interface_number: i32) -> bool {
222 interface_number == 2 || (1..=4).contains(&interface_number)
223 }
224
225 fn initialize_impl(&self) -> Result<(), DeviceError> {
226 send_setting(&self.hid, SETTING_LIZARD_MODE, LIZARD_MODE_OFF)?;
227 std::thread::sleep(SEND_FEATURE_REPORT_SLEEP_DURATION);
228 send_setting(&self.hid, SETTING_IMU_MODE, IMU_MODE_GYRO_ACCEL)?;
229 Ok(())
230 }
231
232 fn to_dsu_frame_impl(&self, frame: &LegacyFrame, gyro_disabled: bool) -> DSUFrame {
233 let l2 = scale_trigger_to_byte(
234 ((frame.trigger_left as u16) << 7 | frame.trigger_left as u16) as i16,
235 );
236 let r2 = scale_trigger_to_byte(
237 ((frame.trigger_right as u16) << 7 | frame.trigger_right as u16) as i16,
238 );
239
240 let gyro_x_dps = frame.gyro_x as f32 / GYRO_PER_DPS;
241 let gyro_y_dps = -(frame.gyro_z as f32 / GYRO_PER_DPS);
242 let gyro_z_dps = frame.gyro_y as f32 / GYRO_PER_DPS;
243
244 let apply_deadzone = |v: f32| {
245 if v.abs() < self.config.gyro_deadzone {
246 0.0
247 } else {
248 v
249 }
250 };
251
252 let zero_on_gyro_disabled = |v: f32| {
253 if gyro_disabled { 0.0 } else { v }
254 };
255
256 DSUFrame {
257 dpad_left: is_u32_masked_button_pressed(frame.buttons as u32, MASK_DPAD_LEFT as u32),
258 dpad_down: is_u32_masked_button_pressed(frame.buttons as u32, MASK_DPAD_DOWN as u32),
259 dpad_right: is_u32_masked_button_pressed(frame.buttons as u32, MASK_DPAD_RIGHT as u32),
260 dpad_up: is_u32_masked_button_pressed(frame.buttons as u32, MASK_DPAD_UP as u32),
261 options: is_u32_masked_button_pressed(frame.buttons as u32, MASK_START as u32),
262 r3: frame.right_pad_click,
263 l3: frame.left_stick_click,
264 share: is_u32_masked_button_pressed(frame.buttons as u32, MASK_SELECT as u32),
265 y: is_u32_masked_button_pressed(frame.buttons as u32, MASK_Y as u32),
266 b: is_u32_masked_button_pressed(frame.buttons as u32, MASK_B as u32),
267 a: is_u32_masked_button_pressed(frame.buttons as u32, MASK_A as u32),
268 x: is_u32_masked_button_pressed(frame.buttons as u32, MASK_X as u32),
269 r1: is_u32_masked_button_pressed(frame.buttons as u32, MASK_RIGHT_BUMPER as u32),
270 l1: is_u32_masked_button_pressed(frame.buttons as u32, MASK_LEFT_BUMPER as u32),
271 r2: r2 >= ANALOG_TRIGGER_TO_DIGITAL_THRESHOLD,
272 l2: l2 >= ANALOG_TRIGGER_TO_DIGITAL_THRESHOLD,
273 home: is_u32_masked_button_pressed(frame.buttons as u32, MASK_GUIDE as u32),
274 touch: frame.left_pad_touch || frame.right_pad_touch,
275 left_stick_x: scale_stick_to_byte(frame.left_stick_x),
276 left_stick_y: scale_stick_to_byte(frame.left_stick_y),
277 right_stick_x: scale_stick_to_byte(frame.right_pad_x),
278 right_stick_y: scale_stick_to_byte(frame.right_pad_y),
279 analog_r2: r2,
280 analog_l2: l2,
281 raw_accel_x: frame.accel_x as f32,
282 raw_accel_y: frame.accel_y as f32,
283 raw_accel_z: frame.accel_z as f32,
284 raw_gyro_x: frame.gyro_x as f32,
285 raw_gyro_y: frame.gyro_y as f32,
286 raw_gyro_z: frame.gyro_z as f32,
287 accel_x: zero_on_gyro_disabled(-(frame.accel_x as f32 / ACCEL_PER_G)),
288 accel_y: zero_on_gyro_disabled(-(frame.accel_z as f32 / ACCEL_PER_G)),
289 accel_z: zero_on_gyro_disabled(frame.accel_y as f32 / ACCEL_PER_G),
290 gyro_x: zero_on_gyro_disabled(
291 apply_deadzone(gyro_x_dps) * self.config.gyro_pitch_scale,
292 ),
293 gyro_y: zero_on_gyro_disabled(apply_deadzone(gyro_y_dps) * self.config.gyro_yaw_scale),
294 gyro_z: zero_on_gyro_disabled(apply_deadzone(gyro_z_dps) * self.config.gyro_roll_scale),
295 }
296 }
297
298 fn is_device_button_pressed_impl(&self, button: &DeviceButton, frame: &LegacyFrame) -> bool {
299 match button {
300 DeviceButton::DpadLeft => {
301 is_u32_masked_button_pressed(frame.buttons as u32, MASK_DPAD_LEFT as u32)
302 }
303 DeviceButton::DpadDown => {
304 is_u32_masked_button_pressed(frame.buttons as u32, MASK_DPAD_DOWN as u32)
305 }
306 DeviceButton::DpadRight => {
307 is_u32_masked_button_pressed(frame.buttons as u32, MASK_DPAD_RIGHT as u32)
308 }
309 DeviceButton::DpadUp => {
310 is_u32_masked_button_pressed(frame.buttons as u32, MASK_DPAD_UP as u32)
311 }
312 DeviceButton::Start => {
313 is_u32_masked_button_pressed(frame.buttons as u32, MASK_START as u32)
314 }
315 DeviceButton::Select => {
316 is_u32_masked_button_pressed(frame.buttons as u32, MASK_SELECT as u32)
317 }
318 DeviceButton::Guide => {
319 is_u32_masked_button_pressed(frame.buttons as u32, MASK_GUIDE as u32)
320 }
321 DeviceButton::Quaternary => false,
322 DeviceButton::A => is_u32_masked_button_pressed(frame.buttons as u32, MASK_A as u32),
323 DeviceButton::B => is_u32_masked_button_pressed(frame.buttons as u32, MASK_B as u32),
324 DeviceButton::X => is_u32_masked_button_pressed(frame.buttons as u32, MASK_X as u32),
325 DeviceButton::Y => is_u32_masked_button_pressed(frame.buttons as u32, MASK_Y as u32),
326 DeviceButton::L1 => {
327 is_u32_masked_button_pressed(frame.buttons as u32, MASK_LEFT_BUMPER as u32)
328 }
329 DeviceButton::R1 => {
330 is_u32_masked_button_pressed(frame.buttons as u32, MASK_RIGHT_BUMPER as u32)
331 }
332 DeviceButton::L2 => {
333 scale_trigger_to_byte(
334 ((frame.trigger_left as u16) << 7 | frame.trigger_left as u16) as i16,
335 ) >= ANALOG_TRIGGER_TO_DIGITAL_THRESHOLD
336 }
337 DeviceButton::R2 => {
338 scale_trigger_to_byte(
339 ((frame.trigger_right as u16) << 7 | frame.trigger_right as u16) as i16,
340 ) >= ANALOG_TRIGGER_TO_DIGITAL_THRESHOLD
341 }
342 DeviceButton::L3 => frame.left_stick_click,
343 DeviceButton::R3 => frame.right_pad_click,
344 DeviceButton::L4 => false,
345 DeviceButton::L5 => false,
346 DeviceButton::R4 => false,
347 DeviceButton::R5 => false,
348 DeviceButton::LeftStickTouch => false,
349 DeviceButton::RightStickTouch => false,
350 DeviceButton::LeftPadTouch => frame.left_pad_touch,
351 DeviceButton::RightPadTouch => frame.right_pad_touch,
352 DeviceButton::LeftGrip => false,
353 DeviceButton::RightGrip => false,
354 DeviceButton::Unknown => false,
355 }
356 }
357}
358
359impl FrameDevice<LegacyFrame> for LegacySteamController {
360 fn to_dsu_frame(&self, frame: &LegacyFrame, gyro_disabled: bool) -> DSUFrame {
361 self.to_dsu_frame_impl(frame, gyro_disabled)
362 }
363
364 fn is_device_button_pressed(&self, button: &DeviceButton, frame: &LegacyFrame) -> bool {
365 self.is_device_button_pressed_impl(button, frame)
366 }
367}
368
369impl Device for LegacySteamController {
370 fn initialize(&self) -> Result<(), DeviceError> {
371 self.initialize_impl()
372 }
373
374 fn read_frame(&self) -> Result<DSUFrame, DeviceError> {
375 let mut buf = [0u8; 64];
376 let n = self.hid.read_timeout(&mut buf, READ_TIMEOUT_MILLIS)?;
377
378 if n == 0 {
379 return Err(DeviceError::ShortRead(0, 1));
380 }
381
382 let frame = LegacyFrame::parse(&buf[..n]).ok_or(DeviceError::InvalidReport(buf[0]))?;
383
384 let inputs = &self.config.gyro_activation_inputs;
385 let mut enable_gyro = true;
386
387 if !inputs.is_empty() {
388 enable_gyro = match self.config.gyro_activation_mode {
389 GyroActivationMode::Any => inputs
390 .iter()
391 .any(|button| self.is_device_button_pressed_impl(button, &frame)),
392 GyroActivationMode::All => inputs
393 .iter()
394 .all(|button| self.is_device_button_pressed_impl(button, &frame)),
395 };
396 }
397
398 Ok(FrameDevice::to_dsu_frame(self, &frame, !enable_gyro))
399 }
400}
401
402impl Drop for LegacySteamController {
403 fn drop(&mut self) {
404 if !self.config.no_enable_lizard_mode_on_close
405 && send_setting(&self.hid, SETTING_LIZARD_MODE, LIZARD_MODE_ON).is_ok()
406 {
407 log::debug!("Re-enabled lizard mode on legacy controller");
408 }
409 }
410}
411
412fn send_setting(hid: &HidDevice, setting: u8, value: u16) -> Result<(), DeviceError> {
413 let mut buf = [0u8; FEATURE_REPORT_SIZE];
414 buf[0] = FEATURE_REPORT_ID;
415 buf[1] = CMD_SET_SETTINGS_VALUES;
416 buf[2] = 3;
417 buf[3] = setting;
418 buf[4] = (value & 0xFF) as u8;
419 buf[5] = ((value >> 8) & 0xFF) as u8;
420
421 hid.send_feature_report(&buf)?;
422 Ok(())
423}
424
425fn connection_mode_from_pid(pid: u16) -> ConnectionMode {
426 match pid {
427 PID_WIRELESS => ConnectionMode::Wireless,
428 _ => ConnectionMode::Usb,
429 }
430}
431
432fn probe_device(hid: &HidDevice) -> Result<(), DeviceError> {
433 let mut probe = [0u8; FEATURE_REPORT_SIZE];
434 probe[0] = FEATURE_REPORT_ID;
435 probe[1] = CMD_SET_SETTINGS_VALUES;
436 probe[2] = 3;
437 probe[3] = SETTING_LIZARD_MODE;
438 probe[4] = 0;
439 probe[5] = 0;
440 hid.send_feature_report(&probe)?;
441 Ok(())
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447
448 #[test]
449 fn parses_legacy_state_packet() {
450 let mut data = [0u8; 64];
451 data[0] = REPORT_ID_STATE;
452 data[8] = 0x80;
453 data[9] = 0x13;
454 data[10] = 0x08;
455 data[11] = 0x40;
456 data[12] = 0x20;
457 data[20] = 0x34;
458 data[21] = 0x12;
459 data[22] = 0x78;
460 data[23] = 0x56;
461 data[28] = 0x11;
462 data[29] = 0x22;
463 data[30] = 0x33;
464 data[31] = 0x44;
465 data[32] = 0x55;
466 data[33] = 0x66;
467 data[34] = 0x77;
468 data[35] = 0x88;
469 data[36] = 0x99;
470 data[37] = 0xAA;
471 data[38] = 0xBB;
472 data[39] = 0xCC;
473 data[54] = 0x01;
474 data[55] = 0x02;
475 data[56] = 0x03;
476 data[57] = 0x04;
477
478 let frame = LegacyFrame::parse(&data).expect("frame");
479 assert_eq!(frame.trigger_left, 0x40);
480 assert_eq!(frame.trigger_right, 0x20);
481 assert_eq!(frame.left_stick_x, i16::from_le_bytes([0x01, 0x02]));
482 assert_eq!(frame.right_pad_x, i16::from_le_bytes([0x34, 0x12]));
483 assert!(frame.left_pad_touch);
484 }
485}