1use hidapi::{HidApi, HidDevice};
4use std::fs::OpenOptions;
5use std::os::unix::io::AsRawFd;
6use std::time::Duration;
7
8use crate::devices::device::Device;
9use crate::devices::util::{
10 is_u32_masked_button_pressed, scale_stick_to_byte, scale_trigger_to_byte,
11};
12use crate::dsu::DSUFrame;
13use crate::errors::DeviceError;
14
15const VID: u16 = 0x28de;
17const PID: u16 = 0x1304;
18
19const USAGE_PAGE_VENDOR: u16 = 0xFF00;
21
22const CMD_CLEAR_DIGITAL_MAPPINGS: u8 = 0x81;
24const CMD_LOAD_DEFAULT_SETTINGS: u8 = 0x8E;
25const CMD_SET_SETTINGS_VALUES: u8 = 0x87;
26
27const SETTING_LEFT_TRACKPAD_MODE: u8 = 0x07;
29const SETTING_RIGHT_TRACKPAD_MODE: u8 = 0x08;
30const SETTING_IMU_MODE: u8 = 0x30;
31
32const MODE_NONE: u16 = 0x07;
34
35const IMU_MODE_SEND_RAW_ACCEL: u16 = 0x08;
37const IMU_MODE_SEND_RAW_GYRO: u16 = 0x10;
38
39const FEATURE_REPORT_SLEEP_MILLIS: u64 = 50;
40const READ_TIMOUT_MILLIS: i32 = 100;
41
42const REPORT_ID_TRITON_FULL: u8 = 0x42;
44
45const REPORT_SIZE: usize = 54;
47
48const ACCEL_PER_G: f32 = 16384.0;
50const GYRO_PER_DPS: f32 = 16.384;
51
52const ANALOG_TRIGGER_TO_DIGITAL_THRESHOLD: u8 = 228; const MASK_A: u32 = 0x0000_0001;
55const MASK_B: u32 = 0x0000_0002;
56const MASK_X: u32 = 0x0000_0004;
57const MASK_Y: u32 = 0x0000_0008;
58const MASK_QAM: u32 = 0x0000_0010;
59const MASK_R3: u32 = 0x0000_0020;
60const MASK_VIEW: u32 = 0x0000_0040;
61const MASK_R: u32 = 0x0000_0200;
64const MASK_DPAD_DOWN: u32 = 0x0000_0400;
65const MASK_DPAD_RIGHT: u32 = 0x0000_0800;
66const MASK_DPAD_LEFT: u32 = 0x0000_1000;
67const MASK_DPAD_UP: u32 = 0x0000_2000;
68const MASK_MENU: u32 = 0x0000_4000;
69const MASK_L3: u32 = 0x0000_8000;
70const MASK_STEAM: u32 = 0x0001_0000;
71const MASK_L: u32 = 0x0008_0000;
74
75#[derive(Debug, Clone, Copy, PartialEq)]
77pub struct TritonFrame {
78 pub seq_num: u8,
79 pub buttons: u32,
80 pub trigger_left: i16,
81 pub trigger_right: i16,
82 pub left_stick_x: i16,
83 pub left_stick_y: i16,
84 pub right_stick_x: i16,
85 pub right_stick_y: i16,
86 pub left_pad_x: i16,
87 pub left_pad_y: i16,
88 pub pressure_left: u16,
89 pub right_pad_x: i16,
90 pub right_pad_y: i16,
91 pub pressure_right: u16,
92 pub imu_timestamp: u32,
93 pub accel_x: i16,
94 pub accel_y: i16,
95 pub accel_z: i16,
96 pub gyro_x: i16,
97 pub gyro_y: i16,
98 pub gyro_z: i16,
99 pub quat_w: i16,
100 pub quat_x: i16,
101 pub quat_y: i16,
102 pub quat_z: i16,
103}
104
105impl TritonFrame {
106 pub fn parse(data: &[u8]) -> Option<Self> {
108 if data.len() < REPORT_SIZE || data[0] != REPORT_ID_TRITON_FULL {
109 return None;
110 }
111 let p = &data[1..];
112 Some(Self {
113 seq_num: p[0],
114 buttons: u32::from_le_bytes([p[1], p[2], p[3], p[4]]),
115 trigger_left: i16::from_le_bytes([p[5], p[6]]),
116 trigger_right: i16::from_le_bytes([p[7], p[8]]),
117 left_stick_x: i16::from_le_bytes([p[9], p[10]]),
118 left_stick_y: i16::from_le_bytes([p[11], p[12]]),
119 right_stick_x: i16::from_le_bytes([p[13], p[14]]),
120 right_stick_y: i16::from_le_bytes([p[15], p[16]]),
121 left_pad_x: i16::from_le_bytes([p[17], p[18]]),
122 left_pad_y: i16::from_le_bytes([p[19], p[20]]),
123 pressure_left: u16::from_le_bytes([p[21], p[22]]),
124 right_pad_x: i16::from_le_bytes([p[23], p[24]]),
125 right_pad_y: i16::from_le_bytes([p[25], p[26]]),
126 pressure_right: u16::from_le_bytes([p[27], p[28]]),
127 imu_timestamp: u32::from_le_bytes([p[29], p[30], p[31], p[32]]),
128 accel_x: i16::from_le_bytes([p[33], p[34]]),
129 accel_y: i16::from_le_bytes([p[35], p[36]]),
130 accel_z: i16::from_le_bytes([p[37], p[38]]),
131 gyro_x: i16::from_le_bytes([p[39], p[40]]),
132 gyro_y: i16::from_le_bytes([p[41], p[42]]),
133 gyro_z: i16::from_le_bytes([p[43], p[44]]),
134 quat_w: i16::from_le_bytes([p[45], p[46]]),
135 quat_x: i16::from_le_bytes([p[47], p[48]]),
136 quat_y: i16::from_le_bytes([p[49], p[50]]),
137 quat_z: i16::from_le_bytes([p[51], p[52]]),
138 })
139 }
140}
141
142impl From<TritonFrame> for DSUFrame {
143 fn from(value: TritonFrame) -> Self {
144 let l2 = scale_trigger_to_byte(value.trigger_left);
145 let r2 = scale_trigger_to_byte(value.trigger_right);
146
147 DSUFrame {
148 dpad_left: is_u32_masked_button_pressed(value.buttons, MASK_DPAD_LEFT),
149 dpad_down: is_u32_masked_button_pressed(value.buttons, MASK_DPAD_DOWN),
150 dpad_right: is_u32_masked_button_pressed(value.buttons, MASK_DPAD_RIGHT),
151 dpad_up: is_u32_masked_button_pressed(value.buttons, MASK_DPAD_UP),
152 options: is_u32_masked_button_pressed(value.buttons, MASK_VIEW),
153 r3: is_u32_masked_button_pressed(value.buttons, MASK_R3),
154 l3: is_u32_masked_button_pressed(value.buttons, MASK_L3),
155 share: is_u32_masked_button_pressed(value.buttons, MASK_MENU),
156 y: is_u32_masked_button_pressed(value.buttons, MASK_Y),
157 b: is_u32_masked_button_pressed(value.buttons, MASK_B),
158 a: is_u32_masked_button_pressed(value.buttons, MASK_A),
159 x: is_u32_masked_button_pressed(value.buttons, MASK_X),
160 r1: is_u32_masked_button_pressed(value.buttons, MASK_R),
161 l1: is_u32_masked_button_pressed(value.buttons, MASK_L),
162 r2: r2 >= ANALOG_TRIGGER_TO_DIGITAL_THRESHOLD,
163 l2: l2 >= ANALOG_TRIGGER_TO_DIGITAL_THRESHOLD,
164 home: is_u32_masked_button_pressed(value.buttons, MASK_STEAM),
165 touch: is_u32_masked_button_pressed(value.buttons, MASK_QAM),
166 left_stick_x: scale_stick_to_byte(value.left_stick_x),
167 left_stick_y: scale_stick_to_byte(value.left_stick_y),
168 right_stick_x: scale_stick_to_byte(value.right_stick_x),
169 right_stick_y: scale_stick_to_byte(value.right_stick_y),
170 analog_r2: r2,
171 analog_l2: l2,
172 accel_x: -(value.accel_x as f32 / ACCEL_PER_G),
173 accel_y: -(value.accel_z as f32 / ACCEL_PER_G),
174 accel_z: (value.accel_y as f32 / ACCEL_PER_G),
175 gyro_x: (value.gyro_x as f32 / GYRO_PER_DPS),
176 gyro_y: -(value.gyro_z as f32 / GYRO_PER_DPS),
177 gyro_z: (value.gyro_y as f32 / GYRO_PER_DPS),
178 }
179 }
180}
181
182pub struct Triton {
184 hid: HidDevice,
185 path: String,
186}
187
188impl Device for Triton {
189 fn initialize(&self) -> Result<(), DeviceError> {
191 let raw = OpenOptions::new().read(true).write(true).open(&self.path)?;
192 enable_imu_on_file(&raw)?;
193 Ok(())
194 }
195
196 fn read_frame(&self) -> Result<DSUFrame, DeviceError> {
198 let mut buf = [0u8; 64];
199 let n = self.hid.read_timeout(&mut buf, READ_TIMOUT_MILLIS)?;
200 if n < REPORT_SIZE {
201 return Err(DeviceError::ShortRead(n, REPORT_SIZE));
202 }
203
204 let frame = TritonFrame::parse(&buf[..n]).ok_or(DeviceError::InvalidReport(buf[0]))?;
205
206 Ok(frame.into())
207 }
208}
209
210impl Drop for Triton {
211 fn drop(&mut self) {
212 let Ok(raw) = OpenOptions::new().read(true).write(true).open(&self.path) else {
214 return;
215 };
216 let mut cmd = [0u8; 64];
217 cmd[0] = 0x01;
218 cmd[1] = CMD_LOAD_DEFAULT_SETTINGS;
219 cmd[2] = 0;
220 if send_feature_report_via_ioctl(&raw, &cmd).is_ok() {
221 log::debug!("IMU disable sequence complete");
222 }
223 }
224}
225
226fn hidiocsfeature(len: usize) -> libc::c_ulong {
230 let dir = 3u32; ((dir << 30) | ((len as u32) << 16) | ((b'H' as u32) << 8) | 6u32) as libc::c_ulong
232}
233
234fn send_feature_report_via_ioctl(file: &std::fs::File, data: &[u8]) -> Result<(), std::io::Error> {
235 let ret = unsafe { libc::ioctl(file.as_raw_fd(), hidiocsfeature(data.len()), data.as_ptr()) };
237 if ret < 0 {
238 Err(std::io::Error::last_os_error())
239 } else {
240 Ok(())
241 }
242}
243
244pub fn find(api: &HidApi, device_path: Option<&str>) -> Result<Triton, DeviceError> {
250 let candidates: Vec<_> = api
251 .device_list()
252 .filter(|d| {
253 d.vendor_id() == VID && d.product_id() == PID && d.usage_page() == USAGE_PAGE_VENDOR
254 })
255 .collect();
256
257 log::debug!("Found {} candidate vendor interfaces", candidates.len());
258
259 if let Some(target) = device_path {
260 let info = candidates
262 .into_iter()
263 .find(|d| d.path().to_str().ok() == Some(target));
264
265 let Some(info) = info else {
266 return Err(DeviceError::NoDeviceFoundAtPath(target.to_string()));
267 };
268
269 let hid = info.open_device(api)?;
270 let device = open_and_probe(target, hid)?;
271
272 return Ok(device);
273 }
274
275 for info in candidates {
277 let Ok(path) = info.path().to_str() else {
278 log::debug!("Skipping device, could not get a path: {info:?}");
279 continue;
280 };
281
282 log::debug!("Trying interface at {}", path);
283
284 let hid = match info.open_device(api) {
285 Ok(hid) => hid,
286 Err(err) => {
287 log::debug!("Failed to obtain handle to device at {path}: {err:?}");
288 continue;
289 }
290 };
291
292 let device = match open_and_probe(path, hid) {
293 Ok(device) => device,
294 Err(err) => {
295 log::debug!("Device at {path} failed probe: {err:?}");
296 continue;
297 }
298 };
299
300 return Ok(device);
301 }
302
303 Err(DeviceError::NoDeviceFound)
304}
305
306fn open_and_probe(path: &str, hid: hidapi::HidDevice) -> Result<Triton, DeviceError> {
307 log::debug!("Trying interface at {}", path);
308
309 let raw = OpenOptions::new().read(true).write(true).open(path)?;
313
314 let mut probe = [0u8; 64];
315 probe[0] = 0x01;
316 probe[1] = CMD_CLEAR_DIGITAL_MAPPINGS;
317
318 send_feature_report_via_ioctl(&raw, &probe)?;
319 log::info!("Opened controller on {}", path);
320 Ok(Triton {
321 hid,
322 path: path.to_string(),
323 })
324}
325
326fn enable_imu_on_file(file: &std::fs::File) -> Result<(), DeviceError> {
327 log::debug!("Sending IMU enable sequence...");
328
329 let mut cmd = [0u8; 64];
331 cmd[0] = 0x01;
332 cmd[1] = CMD_CLEAR_DIGITAL_MAPPINGS;
333 send_feature_report_via_ioctl(file, &cmd)?;
334 log::trace!("Sent CLEAR_DIGITAL_MAPPINGS");
335
336 std::thread::sleep(Duration::from_millis(FEATURE_REPORT_SLEEP_MILLIS));
337
338 let mut cmd = [0u8; 64];
340 cmd[0] = 0x01;
341 cmd[1] = CMD_LOAD_DEFAULT_SETTINGS;
342 cmd[2] = 0;
343 send_feature_report_via_ioctl(file, &cmd)?;
344 log::trace!("Sent LOAD_DEFAULT_SETTINGS");
345
346 std::thread::sleep(Duration::from_millis(FEATURE_REPORT_SLEEP_MILLIS));
347
348 let mut cmd = [0u8; 64];
351 cmd[0] = 0x01;
352 cmd[1] = CMD_SET_SETTINGS_VALUES;
353 cmd[2] = 9; cmd[3] = SETTING_LEFT_TRACKPAD_MODE;
356 cmd[4] = (MODE_NONE & 0xFF) as u8;
357 cmd[5] = (MODE_NONE >> 8) as u8;
358
359 cmd[6] = SETTING_RIGHT_TRACKPAD_MODE;
360 cmd[7] = (MODE_NONE & 0xFF) as u8;
361 cmd[8] = (MODE_NONE >> 8) as u8;
362
363 let imu_mode = IMU_MODE_SEND_RAW_ACCEL | IMU_MODE_SEND_RAW_GYRO;
364 cmd[9] = SETTING_IMU_MODE;
365 cmd[10] = (imu_mode & 0xFF) as u8;
366 cmd[11] = (imu_mode >> 8) as u8;
367
368 send_feature_report_via_ioctl(file, &cmd)?;
369 log::debug!("IMU enable sequence complete");
370
371 Ok(())
372}