1use hidapi::{HidApi, HidDevice};
4use std::time::Duration;
5
6use crate::devices::device::Device;
7use crate::devices::util::{
8 is_u32_masked_button_pressed, scale_stick_to_byte, scale_trigger_to_byte,
9};
10use crate::dsu::DSUFrame;
11use crate::errors::DeviceError;
12
13const VID: u16 = 0x28de;
15const PID_WIRED: u16 = 0x1302;
16const PID_BT: u16 = 0x1303;
17const PID_PUCK: u16 = 0x1304;
18
19const USAGE_PAGE_VENDOR_MIN: u16 = 0xFF00;
21
22const FEATURE_REPORT_ID: u8 = 0x01;
24const FEATURE_REPORT_SIZE: usize = 64;
25const SEND_FEATURE_REPORT_SLEEP_DURATION: Duration = Duration::from_millis(50);
26const CMD_SET_SETTINGS_VALUES: u8 = 0x87;
27
28const SETTING_LIZARD_MODE: u8 = 9;
30const SETTING_IMU_MODE: u8 = 48;
31
32const LIZARD_MODE_OFF: u16 = 0;
34const IMU_MODE_SEND_RAW_ACCEL: u16 = 0x08;
35const IMU_MODE_SEND_RAW_GYRO: u16 = 0x10;
36const IMU_MODE_GYRO_ACCEL: u16 = IMU_MODE_SEND_RAW_ACCEL | IMU_MODE_SEND_RAW_GYRO;
37
38const REPORT_ID_STATE_USB: u8 = 0x42;
40const REPORT_ID_STATE_BLE: u8 = 0x45;
41
42const IMU_OFFSET_START: usize = 29;
44const IMU_OFFSET_ACCEL_X: usize = IMU_OFFSET_START + 4;
45const IMU_OFFSET_ACCEL_Y: usize = IMU_OFFSET_START + 6;
46const IMU_OFFSET_ACCEL_Z: usize = IMU_OFFSET_START + 8;
47const IMU_OFFSET_GYRO_X: usize = IMU_OFFSET_START + 10;
48const IMU_OFFSET_GYRO_Y: usize = IMU_OFFSET_START + 12;
49const IMU_OFFSET_GYRO_Z: usize = IMU_OFFSET_START + 14;
50
51const ACCEL_PER_G: f32 = 16384.0;
53const GYRO_PER_DPS: f32 = 16.384;
54
55const ANALOG_TRIGGER_TO_DIGITAL_THRESHOLD: u8 = 228;
56
57const MASK_A: u32 = 0x0000_0001;
59const MASK_B: u32 = 0x0000_0002;
60const MASK_X: u32 = 0x0000_0004;
61const MASK_Y: u32 = 0x0000_0008;
62const MASK_QAM: u32 = 0x0000_0010;
63const MASK_R3: u32 = 0x0000_0020;
64const MASK_VIEW: u32 = 0x0000_0040;
65const MASK_R: u32 = 0x0000_0200;
66const MASK_DPAD_DOWN: u32 = 0x0000_0400;
67const MASK_DPAD_RIGHT: u32 = 0x0000_0800;
68const MASK_DPAD_LEFT: u32 = 0x0000_1000;
69const MASK_DPAD_UP: u32 = 0x0000_2000;
70const MASK_MENU: u32 = 0x0000_4000;
71const MASK_L3: u32 = 0x0000_8000;
72const MASK_STEAM: u32 = 0x0001_0000;
73const MASK_L: u32 = 0x0008_0000;
74
75const READ_TIMEOUT_MILLIS: i32 = 100;
76
77#[derive(Debug, Clone, Copy, PartialEq)]
79pub struct TritonFrame {
80 pub buttons: u32,
81 pub trigger_left: u16,
82 pub trigger_right: u16,
83 pub left_stick_x: i16,
84 pub left_stick_y: i16,
85 pub right_stick_x: i16,
86 pub right_stick_y: i16,
87 pub imu_timestamp: u32,
88 pub accel_x: i16,
89 pub accel_y: i16,
90 pub accel_z: i16,
91 pub gyro_x: i16,
92 pub gyro_y: i16,
93 pub gyro_z: i16,
94}
95
96impl TritonFrame {
97 pub fn parse(data: &[u8]) -> Option<Self> {
99 if data.is_empty() {
100 return None;
101 }
102
103 let report_id = data[0];
104 if report_id != REPORT_ID_STATE_USB && report_id != REPORT_ID_STATE_BLE {
105 return None;
106 }
107
108 if data.len() < 1 + IMU_OFFSET_START + 16 {
110 return None;
111 }
112
113 let p = &data[1..];
114
115 Some(Self {
116 buttons: u32::from_le_bytes([p[1], p[2], p[3], p[4]]),
117 trigger_left: u16::from_le_bytes([p[5], p[6]]),
118 trigger_right: u16::from_le_bytes([p[7], p[8]]),
119 left_stick_x: i16::from_le_bytes([p[9], p[10]]),
120 left_stick_y: i16::from_le_bytes([p[11], p[12]]),
121 right_stick_x: i16::from_le_bytes([p[13], p[14]]),
122 right_stick_y: i16::from_le_bytes([p[15], p[16]]),
123 imu_timestamp: u32::from_le_bytes([
124 p[IMU_OFFSET_START],
125 p[IMU_OFFSET_START + 1],
126 p[IMU_OFFSET_START + 2],
127 p[IMU_OFFSET_START + 3],
128 ]),
129 accel_x: i16::from_le_bytes([p[IMU_OFFSET_ACCEL_X], p[IMU_OFFSET_ACCEL_X + 1]]),
130 accel_y: i16::from_le_bytes([p[IMU_OFFSET_ACCEL_Y], p[IMU_OFFSET_ACCEL_Y + 1]]),
131 accel_z: i16::from_le_bytes([p[IMU_OFFSET_ACCEL_Z], p[IMU_OFFSET_ACCEL_Z + 1]]),
132 gyro_x: i16::from_le_bytes([p[IMU_OFFSET_GYRO_X], p[IMU_OFFSET_GYRO_X + 1]]),
133 gyro_y: i16::from_le_bytes([p[IMU_OFFSET_GYRO_Y], p[IMU_OFFSET_GYRO_Y + 1]]),
134 gyro_z: i16::from_le_bytes([p[IMU_OFFSET_GYRO_Z], p[IMU_OFFSET_GYRO_Z + 1]]),
135 })
136 }
137}
138
139impl From<TritonFrame> for DSUFrame {
140 fn from(value: TritonFrame) -> Self {
141 let l2 = scale_trigger_to_byte(value.trigger_left as i16);
142 let r2 = scale_trigger_to_byte(value.trigger_right as i16);
143
144 DSUFrame {
145 dpad_left: is_u32_masked_button_pressed(value.buttons, MASK_DPAD_LEFT),
146 dpad_down: is_u32_masked_button_pressed(value.buttons, MASK_DPAD_DOWN),
147 dpad_right: is_u32_masked_button_pressed(value.buttons, MASK_DPAD_RIGHT),
148 dpad_up: is_u32_masked_button_pressed(value.buttons, MASK_DPAD_UP),
149 options: is_u32_masked_button_pressed(value.buttons, MASK_VIEW),
150 r3: is_u32_masked_button_pressed(value.buttons, MASK_R3),
151 l3: is_u32_masked_button_pressed(value.buttons, MASK_L3),
152 share: is_u32_masked_button_pressed(value.buttons, MASK_MENU),
153 y: is_u32_masked_button_pressed(value.buttons, MASK_Y),
154 b: is_u32_masked_button_pressed(value.buttons, MASK_B),
155 a: is_u32_masked_button_pressed(value.buttons, MASK_A),
156 x: is_u32_masked_button_pressed(value.buttons, MASK_X),
157 r1: is_u32_masked_button_pressed(value.buttons, MASK_R),
158 l1: is_u32_masked_button_pressed(value.buttons, MASK_L),
159 r2: r2 >= ANALOG_TRIGGER_TO_DIGITAL_THRESHOLD,
160 l2: l2 >= ANALOG_TRIGGER_TO_DIGITAL_THRESHOLD,
161 home: is_u32_masked_button_pressed(value.buttons, MASK_STEAM),
162 touch: is_u32_masked_button_pressed(value.buttons, MASK_QAM),
163 left_stick_x: scale_stick_to_byte(value.left_stick_x),
164 left_stick_y: scale_stick_to_byte(value.left_stick_y),
165 right_stick_x: scale_stick_to_byte(value.right_stick_x),
166 right_stick_y: scale_stick_to_byte(value.right_stick_y),
167 analog_r2: r2,
168 analog_l2: l2,
169 accel_x: -(value.accel_x as f32 / ACCEL_PER_G),
170 accel_y: -(value.accel_z as f32 / ACCEL_PER_G),
171 accel_z: (value.accel_y as f32 / ACCEL_PER_G),
172 gyro_x: (value.gyro_x as f32 / GYRO_PER_DPS),
173 gyro_y: -(value.gyro_z as f32 / GYRO_PER_DPS),
174 gyro_z: (value.gyro_y as f32 / GYRO_PER_DPS),
175 }
176 }
177}
178
179#[derive(Debug, Clone, Copy, PartialEq)]
181pub enum ConnectionMode {
182 Usb,
183 UsbPuck,
184 Bluetooth,
185}
186
187pub struct Triton {
189 hid: HidDevice,
190}
191
192impl Triton {
193 pub fn find(api: &HidApi, device_path: Option<&str>) -> Result<Self, DeviceError> {
197 let candidates: Vec<_> = api
198 .device_list()
199 .filter(|d| {
200 log::debug!(
201 "Considering VID {:04x}, PID {:04x}, Usage page {:04x}",
202 d.vendor_id(),
203 d.product_id(),
204 d.usage_page()
205 );
206 d.vendor_id() == VID
207 && d.usage_page() >= USAGE_PAGE_VENDOR_MIN
208 && (d.product_id() == PID_PUCK
209 || d.product_id() == PID_WIRED
210 || d.product_id() == PID_BT)
211 })
212 .collect();
213
214 log::debug!("Found {} candidate vendor interfaces", candidates.len());
215
216 if let Some(target) = device_path {
217 let info = candidates
218 .into_iter()
219 .find(|d| d.path().to_str().ok() == Some(target));
220
221 let Some(info) = info else {
222 return Err(DeviceError::NoDeviceFoundAtPath(target.to_string()));
223 };
224
225 let pid = info.product_id();
226 let hid = info.open_device(api)?;
227 let mode = connection_mode_from_pid(pid);
228
229 probe_device(&hid)?;
230
231 log::info!("Opened controller on {} ({:?})", target, mode);
232 return Ok(Triton { hid });
233 }
234
235 for info in candidates {
236 let Ok(path) = info.path().to_str() else {
237 log::debug!("Skipping device, could not get a path: {info:?}");
238 continue;
239 };
240
241 log::debug!("Trying interface at {}", path);
242
243 let pid = info.product_id();
244 let hid = match info.open_device(api) {
245 Ok(hid) => hid,
246 Err(err) => {
247 log::debug!("Failed to obtain handle to device at {path}: {err:?}");
248 continue;
249 }
250 };
251
252 let mode = connection_mode_from_pid(pid);
253
254 if let Err(e) = probe_device(&hid) {
255 log::debug!("Probe failed for device at {path}: {e}");
256 continue;
257 }
258
259 log::info!("Opened controller on {} ({:?})", path, mode);
260 return Ok(Triton { hid });
261 }
262
263 Err(DeviceError::NoDeviceFound)
264 }
265}
266
267impl Device for Triton {
268 fn initialize(&self) -> Result<(), DeviceError> {
269 log::debug!("Sending IMU enable sequence... ");
270 send_setting(&self.hid, SETTING_LIZARD_MODE, LIZARD_MODE_OFF)?;
271
272 std::thread::sleep(SEND_FEATURE_REPORT_SLEEP_DURATION);
273
274 send_setting(&self.hid, SETTING_IMU_MODE, IMU_MODE_GYRO_ACCEL)?;
275
276 log::debug!("IMU enable sequence complete");
277
278 Ok(())
279 }
280
281 fn read_frame(&self) -> Result<DSUFrame, DeviceError> {
282 let mut buf = [0u8; 64];
283 let n = self.hid.read_timeout(&mut buf, READ_TIMEOUT_MILLIS)?;
284
285 if n == 0 {
286 return Err(DeviceError::ShortRead(0, 1));
287 }
288
289 let frame = TritonFrame::parse(&buf[..n]).ok_or(DeviceError::InvalidReport(buf[0]))?;
290
291 Ok(frame.into())
292 }
293}
294
295impl Drop for Triton {
296 fn drop(&mut self) {
297 if send_setting(&self.hid, SETTING_LIZARD_MODE, LIZARD_MODE_OFF).is_ok() {
298 log::debug!("Cleanup complete");
299 }
300 }
301}
302
303fn send_setting(hid: &HidDevice, setting: u8, value: u16) -> Result<(), DeviceError> {
305 let mut buf = [0u8; FEATURE_REPORT_SIZE];
306 buf[0] = FEATURE_REPORT_ID;
307 buf[1] = CMD_SET_SETTINGS_VALUES;
308 buf[2] = 3;
309 buf[3] = setting;
310 buf[4] = (value & 0xFF) as u8;
311 buf[5] = ((value >> 8) & 0xFF) as u8;
312
313 hid.send_feature_report(&buf)?;
314 Ok(())
315}
316
317fn connection_mode_from_pid(pid: u16) -> ConnectionMode {
318 match pid {
319 PID_BT => ConnectionMode::Bluetooth,
320 PID_WIRED => ConnectionMode::Usb,
321 PID_PUCK => ConnectionMode::UsbPuck,
322
323 _ => ConnectionMode::Usb,
325 }
326}
327
328fn probe_device(hid: &HidDevice) -> Result<(), DeviceError> {
330 let mut probe = [0u8; FEATURE_REPORT_SIZE];
331 probe[0] = FEATURE_REPORT_ID;
332 probe[1] = CMD_SET_SETTINGS_VALUES;
333 probe[2] = 3;
334 probe[3] = SETTING_LIZARD_MODE;
335 probe[4] = 0;
336 probe[5] = 0;
337 hid.send_feature_report(&probe)?;
338 Ok(())
339}