Skip to main content

scdsu_core/devices/
legacy.rs

1//! Adapter for the original Steam Controller family.
2
3use 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
150/// Original Steam Controller family.
151pub struct LegacySteamController {
152    config: DeviceConfig,
153    hid: HidDevice,
154}
155
156impl LegacySteamController {
157    /// Enumerate legacy Steam Controller interfaces and return the first supported device.
158    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}