1mod utils;
7pub use utils::*;
8
9use serde::Deserialize;
10use thiserror::Error;
11
12#[derive(Debug, Deserialize)]
15pub struct MappingYaml {
16 pub device: Device,
17 pub axes: Axes,
18 #[serde(default)]
19 pub triggers: Option<Triggers>,
20 #[serde(default)]
21 pub dpad: Option<Dpad>,
22 #[serde(default)]
23 pub buttons: Option<Buttons>,
24}
25
26#[derive(Debug, Deserialize)]
27pub struct Device {
28 pub name: String,
29 pub vid: String,
30 pub pid: String,
31}
32
33#[derive(Debug, Deserialize, Clone, Copy)]
34pub struct Axis {
35 pub report_offset: usize,
36 pub size_bits: u8,
37 pub logical_min: i32,
38 pub logical_max: i32,
39 pub inverted: bool,
40}
41
42#[derive(Debug, Deserialize)]
43pub struct Axes {
44 #[serde(default)]
45 pub left_x: Option<Axis>,
46 #[serde(default)]
47 pub left_y: Option<Axis>,
48 #[serde(default)]
49 pub right_x: Option<Axis>,
50 #[serde(default)]
51 pub right_y: Option<Axis>,
52}
53
54#[derive(Debug, Deserialize, Clone, Copy)]
55pub struct TriggerDef {
56 pub report_offset: usize,
57 pub size_bits: u8,
58 pub logical_min: i32,
59 pub logical_max: i32,
60}
61
62#[derive(Debug, Deserialize)]
63pub struct Triggers {
64 #[serde(default)]
65 pub left_trigger: Option<TriggerDef>,
66 #[serde(default)]
67 pub right_trigger: Option<TriggerDef>,
68 #[serde(default)]
70 pub combined_axis: Option<Axis>,
71}
72
73#[derive(Debug, Deserialize)]
74pub struct Dpad {
75 #[serde(rename = "type")]
76 pub dtype: String, pub report_offset: usize,
78 pub logical_min: i32, pub logical_max: i32, pub neutral: u8, }
82
83#[derive(Debug, Deserialize, Default)]
84pub struct Buttons(pub std::collections::BTreeMap<String, usize>);
85
86#[allow(non_snake_case)]
90pub mod XButtons {
91 pub const DPAD_UP: u16 = 0x0001;
92 pub const DPAD_DOWN: u16 = 0x0002;
93 pub const DPAD_LEFT: u16 = 0x0004;
94 pub const DPAD_RIGHT: u16 = 0x0008;
95 pub const START: u16 = 0x0010;
96 pub const BACK: u16 = 0x0020;
97 pub const LEFT_THUMB: u16 = 0x0040;
98 pub const RIGHT_THUMB: u16 = 0x0080;
99 pub const LEFT_SHOULDER: u16 = 0x0100;
100 pub const RIGHT_SHOULDER: u16 = 0x0200;
101 pub const A: u16 = 0x1000;
103 pub const B: u16 = 0x2000;
104 pub const X: u16 = 0x4000;
105 pub const Y: u16 = 0x8000;
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct XInputState {
114 pub buttons: u16,
115 pub left_trigger: u8,
116 pub right_trigger: u8,
117 pub thumb_lx: i16,
118 pub thumb_ly: i16,
119 pub thumb_rx: i16,
120 pub thumb_ry: i16,
121}
122
123#[derive(Debug, Error)]
127pub enum LoadError {
128 #[error("io error: {0}")] Io(#[from] std::io::Error),
129 #[error("yaml error: {0}")] Yaml(#[from] serde_yaml::Error),
130}
131
132pub fn parse_mapping_yaml_str(s: &str) -> Result<MappingYaml, serde_yaml::Error> {
136 serde_yaml::from_str::<MappingYaml>(s)
137}
138
139pub fn parse_mapping_yaml_file(path: &std::path::Path) -> Result<MappingYaml, LoadError> {
142 let txt = std::fs::read_to_string(path)?; let mapping = parse_mapping_yaml_str(&txt)?; Ok(mapping)
145}
146
147pub fn map_report_to_xinput(m: &MappingYaml, report: &[u8]) -> XInputState {
150 let lx = m.axes.left_x.map(|a| read_axis_to_i16(report, a)).unwrap_or(0);
152 let ly = m.axes.left_y.map(|a| read_axis_to_i16(report, a)).unwrap_or(0);
153 let rx = m.axes.right_x.map(|a| read_axis_to_i16(report, a)).unwrap_or(0);
154 let ry = m.axes.right_y.map(|a| read_axis_to_i16(report, a)).unwrap_or(0);
155
156 let (lt, rt) = if let Some(tr) = &m.triggers {
158 (
159 tr.left_trigger.map(|t| read_trigger_to_u8(report, t)).unwrap_or(0),
160 tr.right_trigger.map(|t| read_trigger_to_u8(report, t)).unwrap_or(0),
161 )
162 } else {
163 (0, 0)
164 };
165
166 let mut mask: u16 = 0;
168 if let Some(btns) = &m.buttons {
169 for (name, bit_index) in btns.0.iter() {
170 if is_bit_set_flat(report, *bit_index) {
171 match name.as_str() {
172 "a" => {
173 mask |= XButtons::A;
174 }
175 "b" => {
176 mask |= XButtons::B;
177 }
178 "x" => {
179 mask |= XButtons::X;
180 }
181 "y" => {
182 mask |= XButtons::Y;
183 }
184 "lb" => {
185 mask |= XButtons::LEFT_SHOULDER;
186 }
187 "rb" => {
188 mask |= XButtons::RIGHT_SHOULDER;
189 }
190 "back" => {
191 mask |= XButtons::BACK;
192 }
193 "start" => {
194 mask |= XButtons::START;
195 }
196 "lt_click" | "ls" | "left_thumb" => {
197 mask |= XButtons::LEFT_THUMB;
198 }
199 "rt_click" | "rs" | "right_thumb" => {
200 mask |= XButtons::RIGHT_THUMB;
201 }
202 _ => {}
203 }
204 }
205 }
206 }
207
208 if let Some(h) = &m.dpad {
210 if h.report_offset < report.len() && h.dtype == "hat" {
211 let v = report[h.report_offset];
212 let (up, right, down, left) = decode_hat_8way(v, h.neutral);
213 if up {
214 mask |= XButtons::DPAD_UP;
215 }
216 if right {
217 mask |= XButtons::DPAD_RIGHT;
218 }
219 if down {
220 mask |= XButtons::DPAD_DOWN;
221 }
222 if left {
223 mask |= XButtons::DPAD_LEFT;
224 }
225 }
226 }
227
228 XInputState {
229 buttons: mask,
230 left_trigger: lt,
231 right_trigger: rt,
232 thumb_lx: lx,
233 thumb_ly: ly,
234 thumb_rx: rx,
235 thumb_ry: ry,
236 }
237}
238
239fn read_axis_to_i16(report: &[u8], ax: Axis) -> i16 {
244 let raw = read_unsigned(report, ax.report_offset, ax.size_bits).unwrap_or(0);
245
246 let mut v = scale_i32(raw as i32, ax.logical_min, ax.logical_max, -32768, 32767);
248
249 if ax.inverted {
251 v = -v;
252 }
253
254 v = v.clamp(i16::MIN as i32, i16::MAX as i32);
256 v as i16
257}
258
259fn read_trigger_to_u8(report: &[u8], t: TriggerDef) -> u8 {
261 let raw = read_unsigned(report, t.report_offset, t.size_bits).unwrap_or(0);
262 scale_i32(raw as i32, t.logical_min, t.logical_max, 0, 255) as u8
263}
264
265fn scale_i32(v: i32, src_min: i32, src_max: i32, dst_min: i32, dst_max: i32) -> i32 {
267 if src_max <= src_min {
268 return if dst_min <= dst_max { dst_min } else { dst_max };
269 }
270 let v_clamped = v.clamp(src_min, src_max);
271 let num = ((v_clamped - src_min) as i64) * ((dst_max - dst_min) as i64);
272 let den = (src_max - src_min) as i64;
273 ((dst_min as i64) + num / den) as i32
274}
275
276fn read_unsigned(report: &[u8], offset: usize, size_bits: u8) -> Option<u32> {
278 match size_bits {
279 8 =>
280 report
281 .get(offset)
282 .copied()
283 .map(|b| b as u32),
284 16 => {
285 let lo = *report.get(offset)? as u32;
286 let hi = *report.get(offset + 1)? as u32;
287 Some(lo | (hi << 8))
288 }
289 _ => None,
290 }
291}
292
293fn is_bit_set_flat(report: &[u8], flat_idx: usize) -> bool {
295 let byte = flat_idx / 8;
296 let bit = (flat_idx % 8) as u8;
297 if byte >= report.len() {
298 return false;
299 }
300 (report[byte] & (1 << bit)) != 0
301}
302
303fn decode_hat_8way(v: u8, neutral: u8) -> (bool, bool, bool, bool) {
305 if v == neutral {
306 return (false, false, false, false);
307 }
308 let up = v == 0 || v == 1 || v == 7;
309 let right = v == 1 || v == 2 || v == 3;
310 let down = v == 3 || v == 4 || v == 5;
311 let left = v == 5 || v == 6 || v == 7;
312 (up, right, down, left)
313}
314
315#[cfg(test)]
318mod tests {
319 use super::*;
320 use pretty_assertions::assert_eq;
321
322 fn sample_yaml() -> &'static str {
323 r#"device:
324 name: "Sample"
325 vid: "0x1234"
326 pid: "0xabcd"
327axes:
328 left_x: { report_offset: 0, size_bits: 8, logical_min: 0, logical_max: 255, inverted: false }
329 left_y: { report_offset: 1, size_bits: 8, logical_min: 0, logical_max: 255, inverted: true }
330 right_x: { report_offset: 2, size_bits: 8, logical_min: 0, logical_max: 255, inverted: false }
331 right_y: { report_offset: 3, size_bits: 8, logical_min: 0, logical_max: 255, inverted: true }
332triggers:
333 left_trigger: { report_offset: 4, size_bits: 8, logical_min: 0, logical_max: 255 }
334 right_trigger: { report_offset: 5, size_bits: 8, logical_min: 0, logical_max: 255 }
335dpad:
336 type: "hat"
337 report_offset: 6
338 logical_min: 0
339 logical_max: 7
340 neutral: 8
341buttons:
342 a: 56
343 b: 57
344 x: 58
345 y: 59
346 lb: 60
347 rb: 61
348 back: 62
349 start: 63
350 lt_click: 64
351 rt_click: 65
352"#
353 }
354
355 #[test]
356 fn parse_yaml_ok() {
357 let m = parse_mapping_yaml_str(sample_yaml()).unwrap();
358 assert_eq!(m.device.name, "Sample");
359 assert!(m.axes.left_x.is_some());
360 assert!(m.triggers.is_some());
361 assert!(m.dpad.is_some());
362 assert!(m.buttons.is_some());
363 }
364
365 #[test]
366 fn map_report_basic() {
367 let m = parse_mapping_yaml_str(sample_yaml()).unwrap();
368
369 let mut report = vec![0u8; 9];
370 report[0] = 128;
371 report[1] = 128;
372 report[2] = 255;
373 report[3] = 0;
374 report[4] = 200;
375 report[5] = 10;
376 report[6] = 2;
377 report[7] = 0b1000_0011;
378 report[8] = 0b0000_0001;
379
380 let xs = map_report_to_xinput(&m, &report);
381
382 use XButtons::*;
383 let expected_mask = A | B | START | LEFT_THUMB | DPAD_RIGHT;
384 assert_eq!(xs.buttons & expected_mask, expected_mask);
385 assert_eq!(xs.left_trigger, 200);
386 assert_eq!(xs.right_trigger, 10);
387 assert!(xs.thumb_lx >= -512 && xs.thumb_lx <= 512);
388 assert!(xs.thumb_ly >= -512 && xs.thumb_ly <= 512);
389 assert!(xs.thumb_rx > 32000);
390 assert!(xs.thumb_ry > 30000);
391 }
392
393 #[test]
394 fn hat_diagonals() {
395 assert_eq!(super::decode_hat_8way(0, 8), (true, false, false, false));
396 assert_eq!(super::decode_hat_8way(1, 8), (true, true, false, false));
397 assert_eq!(super::decode_hat_8way(3, 8), (false, true, true, false));
398 assert_eq!(super::decode_hat_8way(7, 8), (true, false, false, true));
399 assert_eq!(super::decode_hat_8way(8, 8), (false, false, false, false));
400 }
401}