use glam::Vec2;
use std::ffi::{CStr, CString};
use std::sync::mpsc::{Receiver, Sender, channel};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use hidapi::{BusType, HidApi, HidDevice};
use super::output::{self, Bus, Feedback};
use crate::audio::PadKey;
const VID_SONY: u16 = 0x054C;
const PID_DUALSENSE: u16 = 0x0CE6;
const PID_DUALSENSE_EDGE: u16 = 0x0DF2;
const PID_DUALSHOCK4: u16 = 0x05C4;
const PID_DUALSHOCK4_V2: u16 = 0x09CC;
const DEADZONE: f32 = 0.12;
const TRIGGER_DEADZONE: f32 = 0.06;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Model {
DualSense,
DualShock4,
}
impl Model {
fn from_pid(pid: u16) -> Option<Model> {
match pid {
PID_DUALSENSE | PID_DUALSENSE_EDGE => Some(Model::DualSense),
PID_DUALSHOCK4 | PID_DUALSHOCK4_V2 => Some(Model::DualShock4),
_ => None,
}
}
pub fn name(self) -> &'static str {
match self {
Model::DualSense => "DUALSENSE",
Model::DualShock4 => "DUALSHOCK 4",
}
}
pub fn touch_resolution(self) -> Vec2 {
match self {
Model::DualSense => Vec2::new(1920.0, 1080.0),
Model::DualShock4 => Vec2::new(1920.0, 942.0),
}
}
fn touch_offset(self) -> usize {
match self {
Model::DualSense => 33,
Model::DualShock4 => 35,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Touch {
pub id: u8,
pub x: u16,
pub y: u16,
}
impl Touch {
fn parse(bytes: &[u8]) -> Option<Touch> {
let [contact, x_lo, split, y_hi] = bytes else {
return None;
};
if contact & 0x80 != 0 {
return None;
}
Some(Touch {
id: contact & 0x7F,
x: u16::from(*x_lo) | (u16::from(split & 0x0F) << 8),
y: u16::from(split >> 4) | (u16::from(*y_hi) << 4),
})
}
}
fn touch_points(model: Model, buf: &[u8]) -> [Option<Touch>; 2] {
let at = model.touch_offset();
[
buf.get(at..at + 4).and_then(Touch::parse),
buf.get(at + 4..at + 8).and_then(Touch::parse),
]
}
pub mod button {
pub const SQUARE: u32 = 1 << 0;
pub const CROSS: u32 = 1 << 1;
pub const CIRCLE: u32 = 1 << 2;
pub const TRIANGLE: u32 = 1 << 3;
pub const L1: u32 = 1 << 4;
pub const R1: u32 = 1 << 5;
pub const L2: u32 = 1 << 6;
pub const R2: u32 = 1 << 7;
pub const CREATE: u32 = 1 << 8;
pub const OPTIONS: u32 = 1 << 9;
pub const L3: u32 = 1 << 10;
pub const R3: u32 = 1 << 11;
pub const PS: u32 = 1 << 12;
pub const TOUCHPAD: u32 = 1 << 13;
pub const MUTE: u32 = 1 << 14;
pub const NAMES: [(u32, &str); 15] = [
(SQUARE, "SQR"),
(CROSS, "X"),
(CIRCLE, "CIRC"),
(TRIANGLE, "TRI"),
(L1, "L1"),
(R1, "R1"),
(L2, "L2"),
(R2, "R2"),
(CREATE, "CREATE"),
(OPTIONS, "OPTIONS"),
(L3, "L3"),
(R3, "R3"),
(PS, "PS"),
(TOUCHPAD, "PAD"),
(MUTE, "MUTE"),
];
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Charge {
Discharging,
Charging,
Full,
Error,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Battery {
pub percent: u8,
pub charge: Charge,
}
impl Battery {
fn parse(model: Model, buf: &[u8]) -> Option<Battery> {
let pct = |level: u8| (level.saturating_mul(10) + 5).min(100);
match model {
Model::DualSense => {
let b = *buf.get(53)?;
let (level, status) = (b & 0x0F, b >> 4);
Some(match status {
0x0 => Battery {
percent: pct(level),
charge: Charge::Discharging,
},
0x1 => Battery {
percent: pct(level),
charge: Charge::Charging,
},
0x2 => Battery {
percent: 100,
charge: Charge::Full,
},
_ => Battery {
percent: 0,
charge: Charge::Error,
},
})
}
Model::DualShock4 => {
let b = *buf.get(30)?;
let (level, cabled) = (b & 0x0F, b & 0x10 != 0);
Some(match (cabled, level) {
(true, 11..) => Battery {
percent: 100,
charge: Charge::Full,
},
(true, _) => Battery {
percent: pct(level),
charge: Charge::Charging,
},
(false, _) => Battery {
percent: pct(level),
charge: Charge::Discharging,
},
})
}
}
}
}
#[derive(Clone, Copy, Default, Debug)]
pub struct State {
pub lx: u8,
pub ly: u8,
pub rx: u8,
pub ry: u8,
pub l2: u8,
pub r2: u8,
pub dpad: u8,
pub buttons: u32,
pub battery: Option<Battery>,
pub touch: [Option<Touch>; 2],
}
fn axis(v: u8) -> f32 {
let n = ((v as f32 - 128.0) / 127.0).clamp(-1.0, 1.0);
let m = n.abs();
if m < DEADZONE {
return 0.0;
}
n.signum() * (m - DEADZONE) / (1.0 - DEADZONE)
}
fn trigger(v: u8) -> f32 {
let n = v as f32 / 255.0;
if n < TRIGGER_DEADZONE {
return 0.0;
}
(n - TRIGGER_DEADZONE) / (1.0 - TRIGGER_DEADZONE)
}
impl State {
fn parse(model: Model, buf: &[u8]) -> Option<State> {
let buf = match (model, buf.first()) {
(_, Some(0x01)) => buf,
(Model::DualSense, Some(0x31)) if buf.len() > 1 => &buf[1..],
_ => return None,
};
let (sticks, l2, r2, b0, b1, b2) = match model {
Model::DualSense if buf.len() >= 11 => (
[buf[1], buf[2], buf[3], buf[4]],
buf[5],
buf[6],
buf[8],
buf[9],
buf[10],
),
Model::DualShock4 if buf.len() >= 10 => (
[buf[1], buf[2], buf[3], buf[4]],
buf[8],
buf[9],
buf[5],
buf[6],
buf[7],
),
_ => return None,
};
let mut buttons = 0u32;
for (mask, bit) in [
(0x10, button::SQUARE),
(0x20, button::CROSS),
(0x40, button::CIRCLE),
(0x80, button::TRIANGLE),
] {
if b0 & mask != 0 {
buttons |= bit;
}
}
for (mask, bit) in [
(0x01, button::L1),
(0x02, button::R1),
(0x04, button::L2),
(0x08, button::R2),
(0x10, button::CREATE),
(0x20, button::OPTIONS),
(0x40, button::L3),
(0x80, button::R3),
] {
if b1 & mask != 0 {
buttons |= bit;
}
}
for (mask, bit) in [
(0x01, button::PS),
(0x02, button::TOUCHPAD),
(0x04, button::MUTE),
] {
if b2 & mask != 0 {
buttons |= bit;
}
}
Some(State {
lx: sticks[0],
ly: sticks[1],
rx: sticks[2],
ry: sticks[3],
l2,
r2,
dpad: b0 & 0x0F,
buttons,
battery: Battery::parse(model, buf),
touch: touch_points(model, buf),
})
}
pub fn held(&self, mask: u32) -> bool {
self.buttons & mask != 0
}
pub fn move_axis(&self) -> Vec2 {
Vec2::new(axis(self.lx), -axis(self.ly))
}
pub fn look_axis(&self) -> Vec2 {
Vec2::new(axis(self.rx), -axis(self.ry))
}
pub fn lift(&self) -> f32 {
trigger(self.r2) - trigger(self.l2)
}
pub fn touch(&self) -> Option<Touch> {
self.touch[0].or(self.touch[1])
}
pub fn dpad_name(&self) -> &'static str {
match self.dpad {
0 => "N",
1 => "NE",
2 => "E",
3 => "SE",
4 => "S",
5 => "SW",
6 => "W",
7 => "NW",
_ => "-",
}
}
pub fn render(&self) -> String {
let mut s = format!(
"L({:3},{:3}) R({:3},{:3}) L2:{:3} R2:{:3} DPAD:{:2}",
self.lx,
self.ly,
self.rx,
self.ry,
self.l2,
self.r2,
self.dpad_name()
);
if let Some(b) = self.battery {
let tag = match b.charge {
Charge::Discharging => "",
Charge::Charging => "+",
Charge::Full => " FULL",
Charge::Error => " ERR",
};
s.push_str(&format!(" BATT:{}%{}", b.percent, tag));
}
for (slot, touch) in self.touch.iter().enumerate() {
if let Some(touch) = touch {
s.push_str(&format!(" T{}:{},{}", slot + 1, touch.x, touch.y));
}
}
for (mask, name) in button::NAMES {
if self.held(mask) {
s.push(' ');
s.push_str(name);
}
}
s
}
}
pub struct Gamepad {
device: HidDevice,
path: CString,
model: Model,
state: State,
buf: [u8; 64],
bus: Bus,
seq: u8,
felt: Option<Feedback>,
deaf: bool,
key: Option<PadKey>,
}
impl Gamepad {
pub fn model(&self) -> Model {
self.model
}
pub fn state(&self) -> &State {
&self.state
}
pub fn path(&self) -> &CStr {
&self.path
}
pub fn poll(&mut self) -> Option<&State> {
loop {
match self.device.read(&mut self.buf) {
Ok(0) => break,
Ok(n) => {
if let Some(s) = State::parse(self.model, &self.buf[..n]) {
self.state = s;
}
}
Err(e) => {
log::info!("{} unplugged: {e}", self.model.name());
return None;
}
}
}
Some(&self.state)
}
}
impl Gamepad {
pub fn bus(&self) -> Bus {
self.bus
}
pub fn key(&self) -> Option<PadKey> {
self.key
}
pub fn feel(&mut self, feedback: &Feedback) {
if self.deaf || self.felt.as_ref() == Some(feedback) {
return;
}
let report = match self.model {
Model::DualSense => output::dualsense(feedback, self.bus, self.seq),
Model::DualShock4 => match self.bus {
Bus::Usb => output::dualshock4(feedback),
Bus::Bluetooth => return,
},
};
self.seq = self.seq.wrapping_add(1) & 0x0F;
match self.device.write(&report) {
Ok(_) => self.felt = Some(*feedback),
Err(e) => {
log::warn!("{} will not take feedback: {e}", self.model.name());
self.deaf = true;
}
}
}
}
impl Drop for Gamepad {
fn drop(&mut self) {
if self.felt.is_some() {
self.felt = None;
self.feel(&Feedback::default());
}
}
}
const RESCAN: Duration = Duration::from_millis(500);
struct Found {
device: HidDevice,
path: CString,
model: Model,
bus: Bus,
key: Option<PadKey>,
}
pub struct Hub {
pads: Vec<Gamepad>,
arrivals: Receiver<Found>,
open: Arc<Mutex<Vec<CString>>>,
}
impl Default for Hub {
fn default() -> Self {
Self::new()
}
}
impl Hub {
pub fn new() -> Self {
let (send, arrivals) = channel();
let open = Arc::new(Mutex::new(Vec::new()));
let known = open.clone();
let spawned = std::thread::Builder::new()
.name("gamepad scan".into())
.spawn(move || scan_loop(send, known));
if let Err(e) = &spawned {
log::warn!("could not start the controller scan, no pads: {e}");
}
Self {
pads: Vec::new(),
arrivals,
open,
}
}
pub fn len(&self) -> usize {
self.pads.len()
}
pub fn is_empty(&self) -> bool {
self.pads.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &Gamepad> {
self.pads.iter()
}
pub fn feel(&mut self, index: usize, feedback: &Feedback) {
if let Some(pad) = self.pads.get_mut(index) {
pad.feel(feedback);
}
}
pub fn key(&self, index: usize) -> Option<PadKey> {
self.pads.get(index).and_then(Gamepad::key)
}
pub fn poll(&mut self) -> &[Gamepad] {
let mut lost = Vec::new();
self.pads.retain_mut(|pad| match pad.poll().is_some() {
true => true,
false => {
lost.push(pad.path().to_owned());
false
}
});
for found in self.arrivals.try_iter() {
log::info!("controller {}: {}", self.pads.len() + 1, found.model.name());
self.pads.push(Gamepad {
device: found.device,
path: found.path,
model: found.model,
state: State::default(),
buf: [0u8; 64],
bus: found.bus,
seq: 0,
felt: None,
deaf: false,
key: found.key,
});
}
if !lost.is_empty() || !self.pads.is_empty() {
if let Ok(mut open) = self.open.lock() {
*open = self.pads.iter().map(|pad| pad.path().to_owned()).collect();
}
}
&self.pads
}
}
fn scan_loop(send: Sender<Found>, open: Arc<Mutex<Vec<CString>>>) {
let mut api = match HidApi::new() {
Ok(api) => api,
Err(e) => {
log::warn!("hidapi init failed, no controller input: {e}");
return;
}
};
loop {
if let Err(e) = api.refresh_devices() {
log::warn!("could not rescan for controllers: {e}");
} else {
let known = open.lock().map(|open| open.clone()).unwrap_or_default();
for found in look(&api, &known) {
if send.send(found).is_err() {
return;
}
}
}
std::thread::sleep(RESCAN);
}
}
fn look(api: &HidApi, known: &[CString]) -> Vec<Found> {
let mut found = Vec::new();
for info in api.device_list() {
if info.vendor_id() != VID_SONY {
continue;
}
let Some(model) = Model::from_pid(info.product_id()) else {
continue;
};
if known.iter().any(|path| path.as_c_str() == info.path())
|| found
.iter()
.any(|f: &Found| f.path.as_c_str() == info.path())
{
continue;
}
let device = match info.open_device(api) {
Ok(device) => device,
Err(e) => {
log::debug!(" skipping interface: {e}");
continue;
}
};
if let Err(e) = device.set_blocking_mode(false) {
log::warn!("could not set the controller non-blocking: {e}");
continue;
}
log::info!(
"found {} ({:04X}:{:04X}) {}",
model.name(),
info.vendor_id(),
info.product_id(),
info.product_string().unwrap_or("unnamed"),
);
found.push(Found {
device,
path: info.path().to_owned(),
model,
bus: match info.bus_type() {
BusType::Bluetooth => Bus::Bluetooth,
_ => Bus::Usb,
},
key: PadKey::of_hid_path(info.path()),
});
}
found
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_deadzone_starts_from_zero_and_still_reaches_full() {
assert_eq!(axis(128), 0.0, "centred");
assert_eq!(axis(131), 0.0, "idle noise stays inside the deadzone");
let edge = (128.0 + DEADZONE * 127.0).ceil() as u8;
assert!(axis(edge) < 0.02, "no step as the deadzone lets go");
assert!(
(axis(255) - 1.0).abs() < 1e-3,
"full deflection still reaches 1"
);
assert!((axis(0) + 1.0).abs() < 1e-3);
}
#[test]
fn sticks_map_to_screen_directions() {
let s = State {
lx: 128,
ly: 0,
rx: 128,
ry: 255,
..State::default()
};
assert!(s.move_axis().y > 0.9, "stick away from you is forward");
assert!(s.look_axis().y < -0.9, "stick pulled back looks down");
}
#[test]
fn triggers_lift_and_face_buttons_decode() {
let s = State {
r2: 255,
..State::default()
};
assert!((s.lift() - 1.0).abs() < 1e-3);
let mut buf = [0u8; 64];
buf[0] = 0x01;
buf[8] = 0x20 | 0x03; let parsed = State::parse(Model::DualSense, &buf).expect("report 0x01 parses");
assert!(parsed.held(button::CROSS));
assert!(!parsed.held(button::SQUARE));
assert_eq!(parsed.dpad_name(), "SE");
}
fn put_touch(buf: &mut [u8], at: usize, id: u8, x: u16, y: u16) {
buf[at] = id & 0x7F;
buf[at + 1] = (x & 0xFF) as u8;
buf[at + 2] = ((x >> 8) as u8 & 0x0F) | (((y & 0x0F) as u8) << 4);
buf[at + 3] = (y >> 4) as u8;
}
#[test]
fn a_finger_comes_back_where_it_was_put() {
let mut buf = [0u8; 64];
buf[0] = 0x01;
for slot in [33, 37] {
buf[slot] = 0x80;
}
put_touch(&mut buf, 33, 3, 1919, 1079);
let state = State::parse(Model::DualSense, &buf).expect("a long report");
assert_eq!(
state.touch[0],
Some(Touch {
id: 3,
x: 1919,
y: 1079
}),
"the far corner survives the nibble it is split across",
);
assert_eq!(state.touch[1], None, "the second slot is still empty");
assert_eq!(state.touch(), state.touch[0]);
}
#[test]
fn a_lifted_finger_hands_the_drag_to_the_other_one() {
let mut buf = [0u8; 64];
buf[0] = 0x01;
buf[33] = 0x80;
put_touch(&mut buf, 37, 9, 400, 300);
let state = State::parse(Model::DualSense, &buf).expect("a long report");
assert_eq!(state.touch[0], None);
let touch = state.touch().expect("the second finger is still down");
assert_eq!((touch.id, touch.x, touch.y), (9, 400, 300));
}
#[test]
fn the_two_pads_read_their_touchpads_from_different_places() {
fn report(at: usize) -> [u8; 64] {
let mut buf = [0u8; 64];
buf[0] = 0x01;
buf[at] = 0x80;
buf[at + 4] = 0x80;
put_touch(&mut buf, at, 1, 640, 480);
buf
}
fn found(model: Model, buf: &[u8]) -> Option<(u16, u16)> {
State::parse(model, buf)
.expect("a long report")
.touch()
.map(|touch| (touch.x, touch.y))
}
let (dualsense, dualshock) = (report(33), report(35));
assert_eq!(found(Model::DualSense, &dualsense), Some((640, 480)));
assert_eq!(found(Model::DualShock4, &dualshock), Some((640, 480)));
assert_ne!(found(Model::DualSense, &dualshock), Some((640, 480)));
assert_ne!(found(Model::DualShock4, &dualsense), Some((640, 480)));
}
#[test]
fn a_report_too_short_to_hold_the_touchpad_simply_has_none() {
let mut buf = [0u8; 11];
buf[0] = 0x01;
let state = State::parse(Model::DualSense, &buf).expect("the short report still parses");
assert_eq!(state.touch, [None, None]);
}
#[test]
fn a_report_that_is_not_input_is_rejected() {
let buf = [0x02u8; 64];
assert!(State::parse(Model::DualSense, &buf).is_none());
assert!(State::parse(Model::DualSense, &[0x01]).is_none());
assert!(State::parse(Model::DualSense, &[0x31]).is_none());
}
#[test]
fn the_long_bluetooth_report_reads_like_the_usb_one() {
let mut usb = [0u8; 64];
usb[0] = 0x01;
usb[1] = 200;
usb[2] = 60;
usb[5] = 90;
usb[6] = 255;
usb[8] = 0x20 | 0x02;
let mut bt = [0u8; 65];
bt[0] = 0x31;
bt[1] = 0x17;
bt[2..].copy_from_slice(&usb[1..]);
let from_usb = State::parse(Model::DualSense, &usb).unwrap();
let from_bt = State::parse(Model::DualSense, &bt).unwrap();
assert_eq!(
(from_bt.lx, from_bt.ly, from_bt.l2, from_bt.r2),
(200, 60, 90, 255)
);
assert_eq!(from_bt.buttons, from_usb.buttons);
assert_eq!(from_bt.dpad, from_usb.dpad);
assert!(
State::parse(Model::DualShock4, &bt).is_none(),
"the DualShock 4 has a long report of its own that this is not",
);
}
}