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;
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>;
pub fn get_device(api: &HidApi) -> HidResult<HidDevice> {
api.open(DUALSHOCK4_VENDOR_ID, DUALSHOCK4_PRODUCT_ID_NEW)
}
pub fn get_device_old(api: &HidApi) -> HidResult<HidDevice> {
api.open(DUALSHOCK4_VENDOR_ID, DUALSHOCK4_PRODUCT_ID_OLD)
}
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::*; use criterion::{Criterion, criterion_group, criterion_main};
use dualshock4::*;
use rand::Rng;
use scroll::Pwrite;
use std::hint::black_box;
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()
}
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,
}
}
#[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
));
}
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);
}