ds4_rs/dualshock4/
mod.rs

1use hidapi::{HidApi, HidDevice, HidResult};
2
3mod battery_level;
4
5pub mod headset;
6pub use self::headset::Headset;
7
8pub mod buttons;
9pub use self::buttons::{Button, Buttons};
10
11pub mod analog_sticks;
12pub use self::analog_sticks::{AnalogStick, AnalogSticks};
13
14pub mod touchpad;
15pub use self::touchpad::{Touchpad, TouchpadTouch};
16
17pub mod motion;
18pub use self::motion::Motion;
19
20const DUALSHOCK4_VENDOR_ID: u16 = 0x54c;
21
22// Dualshock4 product ID changed after playstation update 5.50
23const DUALSHOCK4_PRODUCT_ID_NEW: u16 = 0x9cc;
24const DUALSHOCK4_PRODUCT_ID_OLD: u16 = 0x5c4;
25
26const DUALSHOCK4_USB_RAW_BUFFER_DATA_LENGTH: usize = 64;
27const DUALSHOCK4_BLUETOOTH_RAW_BUFFER_DATA_LENGTH: usize = 10;
28
29#[derive(PartialEq, Debug)]
30pub enum ConnectionType {
31    Usb,
32    Bluetooth,
33}
34
35#[derive(PartialEq, Debug)]
36pub struct Dualshock4Data {
37    pub connection_type: ConnectionType,
38    pub battery_level: u8,
39    pub headset: Headset,
40    pub analog_sticks: AnalogSticks,
41    pub buttons: Buttons,
42    pub touchpad: Touchpad,
43    pub motion: Motion,
44}
45
46#[derive(Debug)]
47pub enum Dualshock4Error {
48    HidError(hidapi::HidError),
49    UnexpectedDataLength,
50}
51pub type Dualshock4Result<T> = Result<T, Dualshock4Error>;
52
53/// Open Dualshock4 device.
54pub fn get_device(api: &HidApi) -> HidResult<HidDevice> {
55    api.open(DUALSHOCK4_VENDOR_ID, DUALSHOCK4_PRODUCT_ID_NEW)
56}
57
58/// Open Dualshock4 device (before playstation 5.50 update).
59pub fn get_device_old(api: &HidApi) -> HidResult<HidDevice> {
60    api.open(DUALSHOCK4_VENDOR_ID, DUALSHOCK4_PRODUCT_ID_OLD)
61}
62
63/// Read and decode dualshock4 device data.
64pub fn read(controller: &HidDevice) -> Dualshock4Result<Dualshock4Data> {
65    let mut buf = [0; DUALSHOCK4_USB_RAW_BUFFER_DATA_LENGTH];
66
67    match controller.read(&mut buf[..]) {
68        Ok(DUALSHOCK4_USB_RAW_BUFFER_DATA_LENGTH) => decode_buf(ConnectionType::Usb, &buf),
69        Ok(DUALSHOCK4_BLUETOOTH_RAW_BUFFER_DATA_LENGTH) => {
70            decode_buf(ConnectionType::Bluetooth, &buf)
71        }
72        Ok(_) => Err(Dualshock4Error::UnexpectedDataLength),
73        Err(err) => Err(Dualshock4Error::HidError(err)),
74    }
75}
76
77fn decode_buf(connection_type: ConnectionType, buf: &[u8]) -> Dualshock4Result<Dualshock4Data> {
78    let battery_level = battery_level::decode(buf);
79    let headset = headset::decode(buf);
80    let buttons = buttons::decode(buf);
81    let analog_sticks = analog_sticks::decode(buf);
82    let touchpad = touchpad::decode(buf);
83    let motion = motion::decode(buf);
84
85    Ok(Dualshock4Data {
86        connection_type,
87        battery_level,
88        headset,
89        analog_sticks,
90        buttons,
91        touchpad,
92        motion,
93    })
94}
95
96#[cfg(test)]
97mod tests {
98    use crate::*; // in-crate imports
99    use criterion::{Criterion, criterion_group, criterion_main};
100    use dualshock4::*;
101    use rand::Rng;
102    use scroll::Pwrite;
103    use std::hint::black_box;
104
105    // ==========================
106    // Random helpers
107    // ==========================
108    fn rng_u8(range: std::ops::Range<u8>) -> u8 {
109        let mut rng = rand::rng();
110        rng.random_range(range)
111    }
112
113    fn rng_i16() -> i16 {
114        let mut rng = rand::rng();
115        rng.random()
116    }
117
118    fn rng_bool() -> bool {
119        let mut rng = rand::rng();
120        rng.random()
121    }
122
123    // ==========================
124    // Generate Dualshock4 USB data
125    // ==========================
126    fn generate_test_usb_data(buf: &mut [u8]) -> Dualshock4Data {
127        let battery_level = generate_battery_level_data(buf);
128        let headset = generate_headset_data(buf);
129        let analog_sticks = generate_analog_sticks_data(buf);
130        let buttons = generate_buttons_data(buf);
131        let touchpad = generate_touchpad_data(buf);
132        let motion = generate_motion_data(buf);
133
134        Dualshock4Data {
135            connection_type: ConnectionType::Usb,
136            battery_level,
137            headset,
138            analog_sticks,
139            buttons,
140            touchpad,
141            motion,
142        }
143    }
144
145    fn generate_battery_level_data(buf: &mut [u8]) -> u8 {
146        let value = rng_u8(0..22);
147        buf[battery_level::DATA_BLOCK_BATTERY_LEVEL] = value;
148        value
149    }
150
151    fn generate_headset_data(buf: &mut [u8]) -> Headset {
152        let value = rng_u8(0..3);
153        buf[headset::DATA_BLOCK_HEADSET] = match value {
154            0 => headset::HEADSET_MASK_NONE,
155            1 => headset::HEADSET_MASK_HEADPHONES,
156            2 => headset::HEADSET_MASK_HEADSET_WITH_MIC,
157            _ => 0,
158        };
159        match value {
160            0 => Headset::None,
161            1 => Headset::Headphones,
162            2 => Headset::HeadsetWithMic,
163            _ => Headset::Unknown,
164        }
165    }
166
167    fn generate_analog_sticks_data(buf: &mut [u8]) -> AnalogSticks {
168        let left = generate_analog_stick_data(&analog_sticks::CONFIG.left, buf);
169        let right = generate_analog_stick_data(&analog_sticks::CONFIG.right, buf);
170        AnalogSticks { left, right }
171    }
172
173    fn generate_analog_stick_data(
174        config: &analog_sticks::AnalogStickConfig,
175        buf: &mut [u8],
176    ) -> AnalogStick {
177        let x = rng_u8(0..255);
178        let y = rng_u8(0..255);
179        buf[config.block_x] = x;
180        buf[config.block_y] = y;
181        AnalogStick { x, y }
182    }
183
184    fn generate_buttons_data(buf: &mut [u8]) -> Buttons {
185        Buttons {
186            x: generate_button_data(buttons::CONFIG.x, buf),
187            square: generate_button_data(buttons::CONFIG.square, buf),
188            triangle: generate_button_data(buttons::CONFIG.triangle, buf),
189            circle: generate_button_data(buttons::CONFIG.circle, buf),
190            dpad_up: generate_button_data(buttons::CONFIG.dpad_up, buf),
191            dpad_up_right: generate_button_data(buttons::CONFIG.dpad_up_right, buf),
192            dpad_right: generate_button_data(buttons::CONFIG.dpad_right, buf),
193            dpad_down_right: generate_button_data(buttons::CONFIG.dpad_down_right, buf),
194            dpad_down: generate_button_data(buttons::CONFIG.dpad_down, buf),
195            dpad_down_left: generate_button_data(buttons::CONFIG.dpad_down_left, buf),
196            dpad_left: generate_button_data(buttons::CONFIG.dpad_left, buf),
197            dpad_up_left: generate_button_data(buttons::CONFIG.dpad_up_left, buf),
198            share: generate_button_data(buttons::CONFIG.share, buf),
199            options: generate_button_data(buttons::CONFIG.options, buf),
200            psx: generate_button_data(buttons::CONFIG.psx, buf),
201            touchpad: generate_button_data(buttons::CONFIG.touchpad, buf),
202            l1: generate_button_data(buttons::CONFIG.l1, buf),
203            r1: generate_button_data(buttons::CONFIG.r1, buf),
204            left_stick: generate_button_data(buttons::CONFIG.left_stick, buf),
205            right_stick: generate_button_data(buttons::CONFIG.right_stick, buf),
206            l2: generate_button_data(buttons::CONFIG.l2, buf),
207            r2: generate_button_data(buttons::CONFIG.r2, buf),
208        }
209    }
210
211    fn generate_button_data(config: buttons::ButtonConfig, buf: &mut [u8]) -> Button {
212        static mut IS_DPAD_PRESSED: bool = false;
213        let is_dpad_up_config = config.value == buttons::CONFIG.dpad_up.value;
214        let mut is_pressed = rng_bool();
215
216        if config.block == 0x05 && config.value < 0x08 {
217            unsafe {
218                if IS_DPAD_PRESSED {
219                    is_pressed = false;
220                } else if is_pressed {
221                    IS_DPAD_PRESSED = true;
222                }
223            }
224        }
225
226        if !is_pressed && is_dpad_up_config {
227            buf[config.block] += 0x08;
228        }
229
230        if is_pressed && !is_dpad_up_config {
231            buf[config.block] += config.value;
232        }
233
234        let analog_value = config.analog_block.map(|block| {
235            let val = rng_u8(0..255);
236            buf[block] += val;
237            val
238        });
239
240        Button {
241            pressed: is_pressed,
242            analog_value,
243        }
244    }
245
246    fn generate_touchpad_data(buf: &mut [u8]) -> Touchpad {
247        Touchpad {
248            touch_1: generate_touchpad_touch_data(touchpad::CONFIG.touch_1, buf),
249            touch_2: generate_touchpad_touch_data(touchpad::CONFIG.touch_2, buf),
250        }
251    }
252
253    fn generate_touchpad_touch_data(
254        config: touchpad::TouchpadTouchConfig,
255        buf: &mut [u8],
256    ) -> TouchpadTouch {
257        const TOUCHPAD_RESOLUTION_WIDTH: u16 = 943;
258        const TOUCHPAD_RESOLUTION_HEIGHT: u16 = 1920;
259
260        let active = rng_bool();
261        let mut x = None;
262        let mut y = None;
263
264        if active {
265            let temp_x = rng_u8(0..TOUCHPAD_RESOLUTION_WIDTH as u8) as u16;
266            let temp_y = rng_u8(0..TOUCHPAD_RESOLUTION_HEIGHT as u8) as u16;
267
268            buf[config.data_block_a] = (temp_x & 0xff) as u8;
269            buf[config.data_block_b] = (((temp_y | 0xf0) << 4) ^ ((temp_x | 0x0f) >> 8)) as u8;
270            buf[config.data_block_c] = (temp_y >> 4) as u8;
271
272            x = Some(temp_x);
273            y = Some(temp_y);
274        }
275
276        buf[config.active_block] = if active { 0x00 } else { 0xff };
277
278        TouchpadTouch { active, x, y }
279    }
280
281    fn generate_motion_data(buf: &mut [u8]) -> Motion {
282        let x = rng_i16();
283        let y = rng_i16();
284        let z = rng_i16();
285        let roll = rng_i16();
286        let yaw = rng_i16();
287        let pitch = rng_i16();
288
289        buf.pwrite_with::<i16>(x, motion::CONFIG.motion_x, scroll::BE)
290            .unwrap();
291        buf.pwrite_with::<i16>(y, motion::CONFIG.motion_y, scroll::BE)
292            .unwrap();
293        buf.pwrite_with::<i16>(z, motion::CONFIG.motion_z, scroll::BE)
294            .unwrap();
295        buf.pwrite_with::<i16>(roll, motion::CONFIG.gyro_x, scroll::BE)
296            .unwrap();
297        buf.pwrite_with::<i16>(yaw, motion::CONFIG.gyro_y, scroll::BE)
298            .unwrap();
299        buf.pwrite_with::<i16>(pitch, motion::CONFIG.gyro_z, scroll::BE)
300            .unwrap();
301
302        Motion {
303            x,
304            y,
305            z,
306            roll,
307            yaw,
308            pitch,
309        }
310    }
311
312    // ==========================
313    // Unit test
314    // ==========================
315    #[test]
316    fn test_decode_usb_buf_unit() {
317        let mut buf = [0u8; DUALSHOCK4_USB_RAW_BUFFER_DATA_LENGTH];
318        let data = generate_test_usb_data(&mut buf);
319        assert!(data.battery_level <= 21);
320        assert!(matches!(
321            data.headset,
322            Headset::None | Headset::Headphones | Headset::HeadsetWithMic | Headset::Unknown
323        ));
324    }
325
326    // ==========================
327    // Benchmark with Criterion
328    // ==========================
329    fn bench_decode_buf(c: &mut Criterion) {
330        c.bench_function("decode_usb_buf", |b| {
331            b.iter(|| {
332                for _ in 0..1000 {
333                    let mut buf = [0u8; DUALSHOCK4_USB_RAW_BUFFER_DATA_LENGTH];
334                    black_box(generate_test_usb_data(&mut buf));
335                }
336            })
337        });
338    }
339
340    criterion_group!(benches, bench_decode_buf);
341    criterion_main!(benches);
342}