ds4-rs 0.1.1

Read Dualshock4 controller data by using hidapi
Documentation
use hidapi::{HidApi, HidDevice, HidResult};

mod battery_level;

pub mod headset;
pub use self::headset::Headset;

pub mod buttons;
pub use self::buttons::{Button, Buttons};

pub mod analog_sticks;
pub use self::analog_sticks::{AnalogStick, AnalogSticks};

pub mod touchpad;
pub use self::touchpad::{Touchpad, TouchpadTouch};

pub mod motion;
pub use self::motion::Motion;

const DUALSHOCK4_VENDOR_ID: u16 = 0x54c;

// Dualshock4 product ID changed after playstation update 5.50
const DUALSHOCK4_PRODUCT_ID_NEW: u16 = 0x9cc;
const DUALSHOCK4_PRODUCT_ID_OLD: u16 = 0x5c4;

const DUALSHOCK4_USB_RAW_BUFFER_DATA_LENGTH: usize = 64;
const DUALSHOCK4_BLUETOOTH_RAW_BUFFER_DATA_LENGTH: usize = 10;

#[derive(PartialEq, Debug)]
pub enum ConnectionType {
    Usb,
    Bluetooth,
}

#[derive(PartialEq, Debug)]
pub struct Dualshock4Data {
    pub connection_type: ConnectionType,
    pub battery_level: u8,
    pub headset: Headset,
    pub analog_sticks: AnalogSticks,
    pub buttons: Buttons,
    pub touchpad: Touchpad,
    pub motion: Motion,
}

#[derive(Debug)]
pub enum Dualshock4Error {
    HidError(hidapi::HidError),
    UnexpectedDataLength,
}
pub type Dualshock4Result<T> = Result<T, Dualshock4Error>;

/// Open Dualshock4 device.
pub fn get_device(api: &HidApi) -> HidResult<HidDevice> {
    api.open(DUALSHOCK4_VENDOR_ID, DUALSHOCK4_PRODUCT_ID_NEW)
}

/// Open Dualshock4 device (before playstation 5.50 update).
pub fn get_device_old(api: &HidApi) -> HidResult<HidDevice> {
    api.open(DUALSHOCK4_VENDOR_ID, DUALSHOCK4_PRODUCT_ID_OLD)
}

/// Read and decode dualshock4 device data.
pub fn read(controller: &HidDevice) -> Dualshock4Result<Dualshock4Data> {
    let mut buf = [0; DUALSHOCK4_USB_RAW_BUFFER_DATA_LENGTH];

    match controller.read(&mut buf[..]) {
        Ok(DUALSHOCK4_USB_RAW_BUFFER_DATA_LENGTH) => decode_buf(ConnectionType::Usb, &buf),
        Ok(DUALSHOCK4_BLUETOOTH_RAW_BUFFER_DATA_LENGTH) => {
            decode_buf(ConnectionType::Bluetooth, &buf)
        }
        Ok(_) => Err(Dualshock4Error::UnexpectedDataLength),
        Err(err) => Err(Dualshock4Error::HidError(err)),
    }
}

fn decode_buf(connection_type: ConnectionType, buf: &[u8]) -> Dualshock4Result<Dualshock4Data> {
    let battery_level = battery_level::decode(buf);
    let headset = headset::decode(buf);
    let buttons = buttons::decode(buf);
    let analog_sticks = analog_sticks::decode(buf);
    let touchpad = touchpad::decode(buf);
    let motion = motion::decode(buf);

    Ok(Dualshock4Data {
        connection_type,
        battery_level,
        headset,
        analog_sticks,
        buttons,
        touchpad,
        motion,
    })
}

#[cfg(test)]
mod tests {
    use crate::*; // in-crate imports
    use criterion::{Criterion, criterion_group, criterion_main};
    use dualshock4::*;
    use rand::Rng;
    use scroll::Pwrite;
    use std::hint::black_box;

    // ==========================
    // Random helpers
    // ==========================
    fn rng_u8(range: std::ops::Range<u8>) -> u8 {
        let mut rng = rand::rng();
        rng.random_range(range)
    }

    fn rng_i16() -> i16 {
        let mut rng = rand::rng();
        rng.random()
    }

    fn rng_bool() -> bool {
        let mut rng = rand::rng();
        rng.random()
    }

    // ==========================
    // Generate Dualshock4 USB data
    // ==========================
    fn generate_test_usb_data(buf: &mut [u8]) -> Dualshock4Data {
        let battery_level = generate_battery_level_data(buf);
        let headset = generate_headset_data(buf);
        let analog_sticks = generate_analog_sticks_data(buf);
        let buttons = generate_buttons_data(buf);
        let touchpad = generate_touchpad_data(buf);
        let motion = generate_motion_data(buf);

        Dualshock4Data {
            connection_type: ConnectionType::Usb,
            battery_level,
            headset,
            analog_sticks,
            buttons,
            touchpad,
            motion,
        }
    }

    fn generate_battery_level_data(buf: &mut [u8]) -> u8 {
        let value = rng_u8(0..22);
        buf[battery_level::DATA_BLOCK_BATTERY_LEVEL] = value;
        value
    }

    fn generate_headset_data(buf: &mut [u8]) -> Headset {
        let value = rng_u8(0..3);
        buf[headset::DATA_BLOCK_HEADSET] = match value {
            0 => headset::HEADSET_MASK_NONE,
            1 => headset::HEADSET_MASK_HEADPHONES,
            2 => headset::HEADSET_MASK_HEADSET_WITH_MIC,
            _ => 0,
        };
        match value {
            0 => Headset::None,
            1 => Headset::Headphones,
            2 => Headset::HeadsetWithMic,
            _ => Headset::Unknown,
        }
    }

    fn generate_analog_sticks_data(buf: &mut [u8]) -> AnalogSticks {
        let left = generate_analog_stick_data(&analog_sticks::CONFIG.left, buf);
        let right = generate_analog_stick_data(&analog_sticks::CONFIG.right, buf);
        AnalogSticks { left, right }
    }

    fn generate_analog_stick_data(
        config: &analog_sticks::AnalogStickConfig,
        buf: &mut [u8],
    ) -> AnalogStick {
        let x = rng_u8(0..255);
        let y = rng_u8(0..255);
        buf[config.block_x] = x;
        buf[config.block_y] = y;
        AnalogStick { x, y }
    }

    fn generate_buttons_data(buf: &mut [u8]) -> Buttons {
        Buttons {
            x: generate_button_data(buttons::CONFIG.x, buf),
            square: generate_button_data(buttons::CONFIG.square, buf),
            triangle: generate_button_data(buttons::CONFIG.triangle, buf),
            circle: generate_button_data(buttons::CONFIG.circle, buf),
            dpad_up: generate_button_data(buttons::CONFIG.dpad_up, buf),
            dpad_up_right: generate_button_data(buttons::CONFIG.dpad_up_right, buf),
            dpad_right: generate_button_data(buttons::CONFIG.dpad_right, buf),
            dpad_down_right: generate_button_data(buttons::CONFIG.dpad_down_right, buf),
            dpad_down: generate_button_data(buttons::CONFIG.dpad_down, buf),
            dpad_down_left: generate_button_data(buttons::CONFIG.dpad_down_left, buf),
            dpad_left: generate_button_data(buttons::CONFIG.dpad_left, buf),
            dpad_up_left: generate_button_data(buttons::CONFIG.dpad_up_left, buf),
            share: generate_button_data(buttons::CONFIG.share, buf),
            options: generate_button_data(buttons::CONFIG.options, buf),
            psx: generate_button_data(buttons::CONFIG.psx, buf),
            touchpad: generate_button_data(buttons::CONFIG.touchpad, buf),
            l1: generate_button_data(buttons::CONFIG.l1, buf),
            r1: generate_button_data(buttons::CONFIG.r1, buf),
            left_stick: generate_button_data(buttons::CONFIG.left_stick, buf),
            right_stick: generate_button_data(buttons::CONFIG.right_stick, buf),
            l2: generate_button_data(buttons::CONFIG.l2, buf),
            r2: generate_button_data(buttons::CONFIG.r2, buf),
        }
    }

    fn generate_button_data(config: buttons::ButtonConfig, buf: &mut [u8]) -> Button {
        static mut IS_DPAD_PRESSED: bool = false;
        let is_dpad_up_config = config.value == buttons::CONFIG.dpad_up.value;
        let mut is_pressed = rng_bool();

        if config.block == 0x05 && config.value < 0x08 {
            unsafe {
                if IS_DPAD_PRESSED {
                    is_pressed = false;
                } else if is_pressed {
                    IS_DPAD_PRESSED = true;
                }
            }
        }

        if !is_pressed && is_dpad_up_config {
            buf[config.block] += 0x08;
        }

        if is_pressed && !is_dpad_up_config {
            buf[config.block] += config.value;
        }

        let analog_value = config.analog_block.map(|block| {
            let val = rng_u8(0..255);
            buf[block] += val;
            val
        });

        Button {
            pressed: is_pressed,
            analog_value,
        }
    }

    fn generate_touchpad_data(buf: &mut [u8]) -> Touchpad {
        Touchpad {
            touch_1: generate_touchpad_touch_data(touchpad::CONFIG.touch_1, buf),
            touch_2: generate_touchpad_touch_data(touchpad::CONFIG.touch_2, buf),
        }
    }

    fn generate_touchpad_touch_data(
        config: touchpad::TouchpadTouchConfig,
        buf: &mut [u8],
    ) -> TouchpadTouch {
        const TOUCHPAD_RESOLUTION_WIDTH: u16 = 943;
        const TOUCHPAD_RESOLUTION_HEIGHT: u16 = 1920;

        let active = rng_bool();
        let mut x = None;
        let mut y = None;

        if active {
            let temp_x = rng_u8(0..TOUCHPAD_RESOLUTION_WIDTH as u8) as u16;
            let temp_y = rng_u8(0..TOUCHPAD_RESOLUTION_HEIGHT as u8) as u16;

            buf[config.data_block_a] = (temp_x & 0xff) as u8;
            buf[config.data_block_b] = (((temp_y | 0xf0) << 4) ^ ((temp_x | 0x0f) >> 8)) as u8;
            buf[config.data_block_c] = (temp_y >> 4) as u8;

            x = Some(temp_x);
            y = Some(temp_y);
        }

        buf[config.active_block] = if active { 0x00 } else { 0xff };

        TouchpadTouch { active, x, y }
    }

    fn generate_motion_data(buf: &mut [u8]) -> Motion {
        let x = rng_i16();
        let y = rng_i16();
        let z = rng_i16();
        let roll = rng_i16();
        let yaw = rng_i16();
        let pitch = rng_i16();

        buf.pwrite_with::<i16>(x, motion::CONFIG.motion_x, scroll::BE)
            .unwrap();
        buf.pwrite_with::<i16>(y, motion::CONFIG.motion_y, scroll::BE)
            .unwrap();
        buf.pwrite_with::<i16>(z, motion::CONFIG.motion_z, scroll::BE)
            .unwrap();
        buf.pwrite_with::<i16>(roll, motion::CONFIG.gyro_x, scroll::BE)
            .unwrap();
        buf.pwrite_with::<i16>(yaw, motion::CONFIG.gyro_y, scroll::BE)
            .unwrap();
        buf.pwrite_with::<i16>(pitch, motion::CONFIG.gyro_z, scroll::BE)
            .unwrap();

        Motion {
            x,
            y,
            z,
            roll,
            yaw,
            pitch,
        }
    }

    // ==========================
    // Unit test
    // ==========================
    #[test]
    fn test_decode_usb_buf_unit() {
        let mut buf = [0u8; DUALSHOCK4_USB_RAW_BUFFER_DATA_LENGTH];
        let data = generate_test_usb_data(&mut buf);
        assert!(data.battery_level <= 21);
        assert!(matches!(
            data.headset,
            Headset::None | Headset::Headphones | Headset::HeadsetWithMic | Headset::Unknown
        ));
    }

    // ==========================
    // Benchmark with Criterion
    // ==========================
    fn bench_decode_buf(c: &mut Criterion) {
        c.bench_function("decode_usb_buf", |b| {
            b.iter(|| {
                for _ in 0..1000 {
                    let mut buf = [0u8; DUALSHOCK4_USB_RAW_BUFFER_DATA_LENGTH];
                    black_box(generate_test_usb_data(&mut buf));
                }
            })
        });
    }

    criterion_group!(benches, bench_decode_buf);
    criterion_main!(benches);
}