use hidapi::{HidApi, HidDevice};
use std::fs::OpenOptions;
use std::os::unix::io::AsRawFd;
use std::time::Duration;
use crate::devices::device::Device;
use crate::devices::util::{
is_u32_masked_button_pressed, scale_stick_to_byte, scale_trigger_to_byte,
};
use crate::dsu::DSUFrame;
use crate::errors::DeviceError;
pub const VID: u16 = 0x28de;
pub const PID: u16 = 0x1304;
pub const USAGE_PAGE_VENDOR: u16 = 0xFF00;
pub const CMD_CLEAR_DIGITAL_MAPPINGS: u8 = 0x81;
pub const CMD_LOAD_DEFAULT_SETTINGS: u8 = 0x8E;
pub const CMD_SET_SETTINGS_VALUES: u8 = 0x87;
pub const SETTING_LEFT_TRACKPAD_MODE: u8 = 0x07;
pub const SETTING_RIGHT_TRACKPAD_MODE: u8 = 0x08;
pub const SETTING_IMU_MODE: u8 = 0x30;
pub const MODE_NONE: u16 = 0x07;
pub const IMU_MODE_SEND_RAW_ACCEL: u16 = 0x08;
pub const IMU_MODE_SEND_RAW_GYRO: u16 = 0x10;
const FEATURE_REPORT_SLEEP_MILLIS: u64 = 50;
const READ_TIMOUT_MILLIS: i32 = 100;
pub const REPORT_ID_TRITON_FULL: u8 = 0x42;
pub const REPORT_SIZE: usize = 54;
pub const ACCEL_PER_G: f32 = 16384.0;
pub const GYRO_PER_DPS: f32 = 16.384;
const ANALOG_TRIGGER_TO_DIGITAL_THRESHOLD: u8 = 228;
pub const MASK_A: u32 = 0x0000_0001;
pub const MASK_B: u32 = 0x0000_0002;
pub const MASK_X: u32 = 0x0000_0004;
pub const MASK_Y: u32 = 0x0000_0008;
pub const MASK_QAM: u32 = 0x0000_0010;
pub const MASK_R3: u32 = 0x0000_0020;
pub const MASK_VIEW: u32 = 0x0000_0040;
pub const MASK_R: u32 = 0x0000_0200;
pub const MASK_DPAD_DOWN: u32 = 0x0000_0400;
pub const MASK_DPAD_RIGHT: u32 = 0x0000_0800;
pub const MASK_DPAD_LEFT: u32 = 0x0000_1000;
pub const MASK_DPAD_UP: u32 = 0x0000_2000;
pub const MASK_MENU: u32 = 0x0000_4000;
pub const MASK_L3: u32 = 0x0000_8000;
pub const MASK_STEAM: u32 = 0x0001_0000;
pub const MASK_L: u32 = 0x0008_0000;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TritonFrame {
pub seq_num: u8,
pub buttons: u32,
pub trigger_left: i16,
pub trigger_right: i16,
pub left_stick_x: i16,
pub left_stick_y: i16,
pub right_stick_x: i16,
pub right_stick_y: i16,
pub left_pad_x: i16,
pub left_pad_y: i16,
pub pressure_left: u16,
pub right_pad_x: i16,
pub right_pad_y: i16,
pub pressure_right: u16,
pub imu_timestamp: u32,
pub accel_x: i16,
pub accel_y: i16,
pub accel_z: i16,
pub gyro_x: i16,
pub gyro_y: i16,
pub gyro_z: i16,
pub quat_w: i16,
pub quat_x: i16,
pub quat_y: i16,
pub quat_z: i16,
}
impl TritonFrame {
pub const REPORT_ID: u8 = REPORT_ID_TRITON_FULL;
pub const REPORT_SIZE: usize = REPORT_SIZE;
pub fn parse(data: &[u8]) -> Option<Self> {
if data.len() < Self::REPORT_SIZE || data[0] != Self::REPORT_ID {
return None;
}
let p = &data[1..];
Some(Self {
seq_num: p[0],
buttons: u32::from_le_bytes([p[1], p[2], p[3], p[4]]),
trigger_left: i16::from_le_bytes([p[5], p[6]]),
trigger_right: i16::from_le_bytes([p[7], p[8]]),
left_stick_x: i16::from_le_bytes([p[9], p[10]]),
left_stick_y: i16::from_le_bytes([p[11], p[12]]),
right_stick_x: i16::from_le_bytes([p[13], p[14]]),
right_stick_y: i16::from_le_bytes([p[15], p[16]]),
left_pad_x: i16::from_le_bytes([p[17], p[18]]),
left_pad_y: i16::from_le_bytes([p[19], p[20]]),
pressure_left: u16::from_le_bytes([p[21], p[22]]),
right_pad_x: i16::from_le_bytes([p[23], p[24]]),
right_pad_y: i16::from_le_bytes([p[25], p[26]]),
pressure_right: u16::from_le_bytes([p[27], p[28]]),
imu_timestamp: u32::from_le_bytes([p[29], p[30], p[31], p[32]]),
accel_x: i16::from_le_bytes([p[33], p[34]]),
accel_y: i16::from_le_bytes([p[35], p[36]]),
accel_z: i16::from_le_bytes([p[37], p[38]]),
gyro_x: i16::from_le_bytes([p[39], p[40]]),
gyro_y: i16::from_le_bytes([p[41], p[42]]),
gyro_z: i16::from_le_bytes([p[43], p[44]]),
quat_w: i16::from_le_bytes([p[45], p[46]]),
quat_x: i16::from_le_bytes([p[47], p[48]]),
quat_y: i16::from_le_bytes([p[49], p[50]]),
quat_z: i16::from_le_bytes([p[51], p[52]]),
})
}
}
impl From<TritonFrame> for DSUFrame {
fn from(value: TritonFrame) -> Self {
let l2 = scale_trigger_to_byte(value.trigger_left);
let r2 = scale_trigger_to_byte(value.trigger_right);
DSUFrame {
dpad_left: is_u32_masked_button_pressed(value.buttons, MASK_DPAD_LEFT),
dpad_down: is_u32_masked_button_pressed(value.buttons, MASK_DPAD_DOWN),
dpad_right: is_u32_masked_button_pressed(value.buttons, MASK_DPAD_RIGHT),
dpad_up: is_u32_masked_button_pressed(value.buttons, MASK_DPAD_UP),
options: is_u32_masked_button_pressed(value.buttons, MASK_VIEW),
r3: is_u32_masked_button_pressed(value.buttons, MASK_R3),
l3: is_u32_masked_button_pressed(value.buttons, MASK_L3),
share: is_u32_masked_button_pressed(value.buttons, MASK_MENU),
y: is_u32_masked_button_pressed(value.buttons, MASK_Y),
b: is_u32_masked_button_pressed(value.buttons, MASK_B),
a: is_u32_masked_button_pressed(value.buttons, MASK_A),
x: is_u32_masked_button_pressed(value.buttons, MASK_X),
r1: is_u32_masked_button_pressed(value.buttons, MASK_R),
l1: is_u32_masked_button_pressed(value.buttons, MASK_L),
r2: r2 >= ANALOG_TRIGGER_TO_DIGITAL_THRESHOLD,
l2: l2 >= ANALOG_TRIGGER_TO_DIGITAL_THRESHOLD,
home: is_u32_masked_button_pressed(value.buttons, MASK_STEAM),
touch: is_u32_masked_button_pressed(value.buttons, MASK_QAM),
left_stick_x: scale_stick_to_byte(value.left_stick_x),
left_stick_y: scale_stick_to_byte(value.left_stick_y),
right_stick_x: scale_stick_to_byte(value.right_stick_x),
right_stick_y: scale_stick_to_byte(value.right_stick_y),
analog_r2: r2,
analog_l2: l2,
accel_x: -(value.accel_x as f32 / ACCEL_PER_G),
accel_y: -(value.accel_z as f32 / ACCEL_PER_G),
accel_z: (value.accel_y as f32 / ACCEL_PER_G),
gyro_x: (value.gyro_x as f32 / GYRO_PER_DPS),
gyro_y: -(value.gyro_z as f32 / GYRO_PER_DPS),
gyro_z: (value.gyro_y as f32 / GYRO_PER_DPS),
}
}
}
pub struct LinuxTriton {
hid: HidDevice,
path: String,
}
impl Device for LinuxTriton {
fn initialize(&self) -> Result<(), DeviceError> {
let raw = OpenOptions::new().read(true).write(true).open(&self.path)?;
enable_imu_on_file(&raw)?;
Ok(())
}
fn read_frame(&self) -> Result<DSUFrame, DeviceError> {
let mut buf = [0u8; 64];
let n = self.hid.read_timeout(&mut buf, READ_TIMOUT_MILLIS)?;
if n < TritonFrame::REPORT_SIZE {
return Err(DeviceError::ShortRead(n, TritonFrame::REPORT_SIZE));
}
let frame = TritonFrame::parse(&buf[..n]).ok_or(DeviceError::InvalidReport(buf[0]))?;
Ok(frame.into())
}
}
impl Drop for LinuxTriton {
fn drop(&mut self) {
let Ok(raw) = OpenOptions::new().read(true).write(true).open(&self.path) else {
return;
};
let mut cmd = [0u8; 64];
cmd[0] = 0x01;
cmd[1] = CMD_LOAD_DEFAULT_SETTINGS;
cmd[2] = 0;
if send_feature_report_via_ioctl(&raw, &cmd).is_ok() {
log::debug!("IMU disable sequence complete");
}
}
}
fn hidiocsfeature(len: usize) -> libc::c_ulong {
let dir = 3u32; ((dir << 30) | ((len as u32) << 16) | ((b'H' as u32) << 8) | 6u32) as libc::c_ulong
}
fn send_feature_report_via_ioctl(file: &std::fs::File, data: &[u8]) -> Result<(), std::io::Error> {
let ret = unsafe { libc::ioctl(file.as_raw_fd(), hidiocsfeature(data.len()), data.as_ptr()) };
if ret < 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(())
}
}
pub fn linux_find_and_open(api: &HidApi) -> Result<LinuxTriton, DeviceError> {
let candidates: Vec<_> = api
.device_list()
.filter(|d| {
d.vendor_id() == VID && d.product_id() == PID && d.usage_page() == USAGE_PAGE_VENDOR
})
.collect();
log::debug!("Found {} candidate vendor interfaces", candidates.len());
for info in candidates {
let Ok(path) = info.path().to_str() else {
log::debug!("Skipping device, could not get a path: {info:?}");
continue;
};
log::debug!("Trying interface at {}", path);
let hid = info.open_device(api)?;
let Ok(raw) = OpenOptions::new().read(true).write(true).open(path) else {
log::debug!("Could not open raw hidraw at {}", path);
continue;
};
let mut probe = [0u8; 64];
probe[0] = 0x01;
probe[1] = CMD_CLEAR_DIGITAL_MAPPINGS;
if send_feature_report_via_ioctl(&raw, &probe).is_ok() {
log::info!("Opened controller on {}", path);
return Ok(LinuxTriton {
hid,
path: path.to_string(),
});
}
log::debug!("Interface at {} rejected feature report probe", path);
}
Err(DeviceError::NoDeviceFound)
}
fn enable_imu_on_file(file: &std::fs::File) -> Result<(), DeviceError> {
log::debug!("Sending IMU enable sequence...");
let mut cmd = [0u8; 64];
cmd[0] = 0x01;
cmd[1] = CMD_CLEAR_DIGITAL_MAPPINGS;
send_feature_report_via_ioctl(file, &cmd)?;
log::trace!("Sent CLEAR_DIGITAL_MAPPINGS");
std::thread::sleep(Duration::from_millis(FEATURE_REPORT_SLEEP_MILLIS));
let mut cmd = [0u8; 64];
cmd[0] = 0x01;
cmd[1] = CMD_LOAD_DEFAULT_SETTINGS;
cmd[2] = 0;
send_feature_report_via_ioctl(file, &cmd)?;
log::trace!("Sent LOAD_DEFAULT_SETTINGS");
std::thread::sleep(Duration::from_millis(FEATURE_REPORT_SLEEP_MILLIS));
let mut cmd = [0u8; 64];
cmd[0] = 0x01;
cmd[1] = CMD_SET_SETTINGS_VALUES;
cmd[2] = 9;
cmd[3] = SETTING_LEFT_TRACKPAD_MODE;
cmd[4] = (MODE_NONE & 0xFF) as u8;
cmd[5] = (MODE_NONE >> 8) as u8;
cmd[6] = SETTING_RIGHT_TRACKPAD_MODE;
cmd[7] = (MODE_NONE & 0xFF) as u8;
cmd[8] = (MODE_NONE >> 8) as u8;
let imu_mode = IMU_MODE_SEND_RAW_ACCEL | IMU_MODE_SEND_RAW_GYRO;
cmd[9] = SETTING_IMU_MODE;
cmd[10] = (imu_mode & 0xFF) as u8;
cmd[11] = (imu_mode >> 8) as u8;
send_feature_report_via_ioctl(file, &cmd)?;
log::debug!("IMU enable sequence complete");
Ok(())
}