#![doc = include_str!("../README.md")]
use std::fmt::Display;
#[cfg(target_os = "linux")]
use std::fs;
#[cfg(target_os = "linux")]
use std::path::Path;
use std::path::PathBuf;
use std::{io, mem::MaybeUninit};
#[repr(C)]
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub struct InputReport {
pub magic: u32,
pub frame_num: u32,
pub buttons: u64,
pub lpad_x: i16,
pub lpad_y: i16,
pub rpad_x: i16,
pub rpad_y: i16,
pub accel_x: i16,
pub accel_y: i16,
pub accel_z: i16,
pub gyro_pitch: i16,
pub gyro_roll: i16,
pub gyro_yaw: i16,
pub pose_quat_w: i16,
pub pose_quat_x: i16,
pub pose_quat_y: i16,
pub pose_quat_z: i16,
pub ltrig: u16,
pub rtrig: u16,
pub lthumb_x: i16,
pub lthumb_y: i16,
pub rthumb_x: i16,
pub rthumb_y: i16,
pub lpad_force: u16,
pub rpad_force: u16,
pub lthumb_cap: i16,
pub rthumb_cap: i16,
}
impl From<[u8; 64]> for InputReport {
fn from(value: [u8; 64]) -> Self {
unsafe {
let mut ret = MaybeUninit::zeroed();
(ret.as_mut_ptr() as *mut u8)
.copy_from_nonoverlapping(value.as_ptr(), std::mem::size_of::<Self>());
ret.assume_init()
}
}
}
impl Display for InputReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InputReport")
.field("magic", &format!("0x{:08x}", self.magic))
.field("frame_num", &self.frame_num)
.field("buttons", &{
let mut buttons = String::with_capacity(64);
macro_rules! btn {
($id:ident, $s:literal) => {
if self.buttons & buttons::$id != 0 {
buttons.push($s);
} else {
buttons.push(' ');
}
};
}
btn!(A, 'a');
btn!(B, 'b');
btn!(X, 'x');
btn!(Y, 'y');
btn!(D_UP, '↑');
btn!(D_DOWN, '↓');
btn!(D_LEFT, '←');
btn!(D_RIGHT, '→');
btn!(VIEW, '⧉');
btn!(STEAM, 's');
btn!(OPTIONS, '≡');
btn!(DOTS, '…');
btn!(L1, 'l');
btn!(L2_FULL, 'L');
btn!(L4, '4');
btn!(L5, '5');
btn!(R1, 'r');
btn!(R2_FULL, 'R');
btn!(R4, '4');
btn!(R5, '5');
btn!(LTHUMB_TOUCH, 't');
btn!(LTHUMB_CLICK, 'c');
btn!(RTHUMB_TOUCH, 'T');
btn!(RTHUMB_CLICK, 'C');
btn!(LPAD_TOUCH, 't');
btn!(LPAD_CLICK, 'c');
btn!(RPAD_TOUCH, 'T');
btn!(RPAD_CLICK, 'C');
buttons
})
.field("left_trigger", &self.ltrig)
.field("right_trigger", &self.rtrig)
.field("left_stick", &(self.lthumb_x, self.lthumb_y))
.field("right_stick", &(self.rthumb_x, self.rthumb_y))
.field("left_pad", &(self.lpad_x, self.lpad_y))
.field("right_pad", &(self.rpad_x, self.rpad_y))
.field("left_thumb_capacitance", &self.lthumb_cap)
.field("right_thumb_capacitance", &self.rthumb_cap)
.field("left_pad_force", &self.lpad_force)
.field("right_pad_force", &self.rpad_force)
.field(
"accel",
&(
self.accel_x as f64 / 16384.0,
self.accel_y as f64 / 16384.0,
self.accel_z as f64 / 16384.0,
),
)
.field(
"gyro",
&(
self.gyro_pitch as f64 / 16.4,
self.gyro_yaw as f64 / 16.4,
self.gyro_roll as f64 / 16.4,
),
)
.field(
"quaternion",
&(
self.pose_quat_w as f64 / 32768.0,
self.pose_quat_x as f64 / 32768.0,
self.pose_quat_y as f64 / 32768.0,
self.pose_quat_z as f64 / 32768.0,
),
)
.finish()
}
}
pub const INPUT_REPORT_MAGIC: u32 = 0x40090001;
pub mod buttons {
pub const R2_FULL: u64 = 1 << 0;
pub const L2_FULL: u64 = 1 << 1;
pub const R1: u64 = 1 << 2;
pub const L1: u64 = 1 << 3;
pub const Y: u64 = 1 << 4;
pub const B: u64 = 1 << 5;
pub const X: u64 = 1 << 6;
pub const A: u64 = 1 << 7;
pub const D_UP: u64 = 1 << 8;
pub const D_RIGHT: u64 = 1 << 9;
pub const D_LEFT: u64 = 1 << 10;
pub const D_DOWN: u64 = 1 << 11;
pub const VIEW: u64 = 1 << 12;
pub const STEAM: u64 = 1 << 13;
pub const OPTIONS: u64 = 1 << 14;
pub const L5: u64 = 1 << 15;
pub const R5: u64 = 1 << 16;
pub const LPAD_CLICK: u64 = 1 << 17;
pub const RPAD_CLICK: u64 = 1 << 18;
pub const LPAD_TOUCH: u64 = 1 << 19;
pub const RPAD_TOUCH: u64 = 1 << 20;
pub const LTHUMB_CLICK: u64 = 1 << 22;
pub const RTHUMB_CLICK: u64 = 1 << 26;
pub const L4: u64 = 1 << 41;
pub const R4: u64 = 1 << 42;
pub const LTHUMB_TOUCH: u64 = 1 << 46;
pub const RTHUMB_TOUCH: u64 = 1 << 47;
pub const DOTS: u64 = 1 << 50;
}
pub mod commands {
pub const PAD_LEFT: u8 = 0;
pub const PAD_RIGHT: u8 = 1;
pub const PAD_BOTH: u8 = 2;
pub const fn turn_on_gyro() -> [u8; 65] {
let mut ret = [0; 65];
ret[1] = 0x87;
ret[2] = 3;
ret[3] = 0x30;
ret[4] = 0x18;
ret[5] = 0x00;
ret
}
pub const fn pulse(pad: u8, on_us: u16, off_us: u16, repeats: u16) -> [u8; 65] {
if repeats > 0x7fff {
panic!("repeats out of range");
}
let mut ret = [0; 65];
ret[1] = 0x8f;
ret[2] = 7;
ret[3] = pad;
ret[4] = on_us as u8;
ret[5] = (on_us >> 8) as u8;
ret[6] = off_us as u8;
ret[7] = (off_us >> 8) as u8;
ret[8] = repeats as u8;
ret[9] = (repeats >> 8) as u8;
ret
}
pub const fn beep(pad: u8, freq: f64, duration: f64) -> [u8; 65] {
let period = (500_000.0 / freq).round();
let repeats = (freq * duration * 0.5).round();
if period < 0.0 || period > 0xffff as f64 {
panic!("frequency out of range");
}
if repeats < 0.0 || repeats > 0x7fff as f64 {
panic!("duration out of range");
}
let period = period as u16;
let repeats = repeats as u16;
pulse(pad, period, period, repeats)
}
pub const fn tick(pad: u8, intensity: u8, gain_db: i8) -> [u8; 65] {
let mut ret = [0; 65];
ret[1] = 0xea;
ret[2] = 4;
ret[3] = pad;
ret[4] = 1;
ret[5] = intensity;
ret[6] = gain_db as u8;
ret
}
pub const fn rumble(
intensity: u16,
left_speed: u16,
right_speed: u16,
left_gain: i8,
right_gain: i8,
) -> [u8; 65] {
let mut ret = [0; 65];
ret[1] = 0xeb;
ret[2] = 9;
ret[3] = 0;
ret[4] = intensity as u8;
ret[5] = (intensity >> 8) as u8;
ret[6] = left_speed as u8;
ret[7] = (left_speed >> 8) as u8;
ret[8] = right_speed as u8;
ret[9] = (right_speed >> 8) as u8;
ret[10] = left_gain as u8;
ret[11] = right_gain as u8;
ret
}
}
#[cfg(target_os = "linux")]
fn is_deck_hidraw<P: AsRef<Path>>(p: P) -> io::Result<bool> {
use std::collections::HashMap;
let device_uevent_path = p.as_ref().join("device/uevent");
let uevent_data = fs::read(device_uevent_path)?;
let mut uevent_map = HashMap::new();
for line in uevent_data.split(|&c| c == b'\n') {
let line = line.trim_ascii();
if line.len() > 0 {
let mut line_split = line.splitn(2, |&c| c == b'=');
let key = line_split.next().unwrap();
let val = line_split.next().unwrap_or_default();
uevent_map.insert(key, val);
}
}
Ok(uevent_map.get(b"MODALIAS".as_slice())
== Some(&b"hid:b0003g0103v000028DEp00001205".as_slice()))
}
#[cfg(target_os = "linux")]
pub fn find_deck_hidraw() -> io::Result<Option<PathBuf>> {
let mut found_deck_hidraw = None;
for hidraw_instance in fs::read_dir("/sys/class/hidraw")? {
let hidraw_instance = hidraw_instance?;
match is_deck_hidraw(hidraw_instance.path()) {
Ok(true) => {
found_deck_hidraw = Some(hidraw_instance);
break;
}
_ => {}
}
}
Ok(found_deck_hidraw.map(|inst| Path::new("/dev").join(inst.file_name())))
}
#[cfg(not(target_os = "linux"))]
pub fn find_deck_hidraw() -> io::Result<Option<PathBuf>> {
Ok(None)
}
#[cfg(test)]
mod tests {
use super::*;
use std::mem;
const _INPUT_CORRECT_SIZE: () = assert!(mem::size_of::<InputReport>() == 64);
}