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 {
243 let raw = read_unsigned(report, ax.report_offset, ax.size_bits).unwrap_or(0);
244 let val = scale_i32(raw as i32, ax.logical_min, ax.logical_max, -32768, 32767) as i16;
245 if ax.inverted {
246 -val
247 } else {
248 val
249 }
250}
251
252fn read_trigger_to_u8(report: &[u8], t: TriggerDef) -> u8 {
254 let raw = read_unsigned(report, t.report_offset, t.size_bits).unwrap_or(0);
255 scale_i32(raw as i32, t.logical_min, t.logical_max, 0, 255) as u8
256}
257
258fn scale_i32(v: i32, src_min: i32, src_max: i32, dst_min: i32, dst_max: i32) -> i32 {
260 if src_max <= src_min {
261 return if dst_min <= dst_max { dst_min } else { dst_max };
262 }
263 let v_clamped = v.clamp(src_min, src_max);
264 let num = ((v_clamped - src_min) as i64) * ((dst_max - dst_min) as i64);
265 let den = (src_max - src_min) as i64;
266 ((dst_min as i64) + num / den) as i32
267}
268
269fn read_unsigned(report: &[u8], offset: usize, size_bits: u8) -> Option<u32> {
271 match size_bits {
272 8 =>
273 report
274 .get(offset)
275 .copied()
276 .map(|b| b as u32),
277 16 => {
278 let lo = *report.get(offset)? as u32;
279 let hi = *report.get(offset + 1)? as u32;
280 Some(lo | (hi << 8))
281 }
282 _ => None,
283 }
284}
285
286fn is_bit_set_flat(report: &[u8], flat_idx: usize) -> bool {
288 let byte = flat_idx / 8;
289 let bit = (flat_idx % 8) as u8;
290 if byte >= report.len() {
291 return false;
292 }
293 (report[byte] & (1 << bit)) != 0
294}
295
296fn decode_hat_8way(v: u8, neutral: u8) -> (bool, bool, bool, bool) {
298 if v == neutral {
299 return (false, false, false, false);
300 }
301 let up = v == 0 || v == 1 || v == 7;
302 let right = v == 1 || v == 2 || v == 3;
303 let down = v == 3 || v == 4 || v == 5;
304 let left = v == 5 || v == 6 || v == 7;
305 (up, right, down, left)
306}
307
308#[cfg(test)]
311mod tests {
312 use super::*;
313 use pretty_assertions::assert_eq;
314
315 fn sample_yaml() -> &'static str {
316 r#"device:
317 name: "Sample"
318 vid: "0x1234"
319 pid: "0xabcd"
320axes:
321 left_x: { report_offset: 0, size_bits: 8, logical_min: 0, logical_max: 255, inverted: false }
322 left_y: { report_offset: 1, size_bits: 8, logical_min: 0, logical_max: 255, inverted: true }
323 right_x: { report_offset: 2, size_bits: 8, logical_min: 0, logical_max: 255, inverted: false }
324 right_y: { report_offset: 3, size_bits: 8, logical_min: 0, logical_max: 255, inverted: true }
325triggers:
326 left_trigger: { report_offset: 4, size_bits: 8, logical_min: 0, logical_max: 255 }
327 right_trigger: { report_offset: 5, size_bits: 8, logical_min: 0, logical_max: 255 }
328dpad:
329 type: "hat"
330 report_offset: 6
331 logical_min: 0
332 logical_max: 7
333 neutral: 8
334buttons:
335 a: 56
336 b: 57
337 x: 58
338 y: 59
339 lb: 60
340 rb: 61
341 back: 62
342 start: 63
343 lt_click: 64
344 rt_click: 65
345"#
346 }
347
348 #[test]
349 fn parse_yaml_ok() {
350 let m = parse_mapping_yaml_str(sample_yaml()).unwrap();
351 assert_eq!(m.device.name, "Sample");
352 assert!(m.axes.left_x.is_some());
353 assert!(m.triggers.is_some());
354 assert!(m.dpad.is_some());
355 assert!(m.buttons.is_some());
356 }
357
358 #[test]
359 fn map_report_basic() {
360 let m = parse_mapping_yaml_str(sample_yaml()).unwrap();
361
362 let mut report = vec![0u8; 9];
363 report[0] = 128;
364 report[1] = 128;
365 report[2] = 255;
366 report[3] = 0;
367 report[4] = 200;
368 report[5] = 10;
369 report[6] = 2;
370 report[7] = 0b1000_0011;
371 report[8] = 0b0000_0001;
372
373 let xs = map_report_to_xinput(&m, &report);
374
375 use XButtons::*;
376 let expected_mask = A | B | START | LEFT_THUMB | DPAD_RIGHT;
377 assert_eq!(xs.buttons & expected_mask, expected_mask);
378 assert_eq!(xs.left_trigger, 200);
379 assert_eq!(xs.right_trigger, 10);
380 assert!(xs.thumb_lx >= -512 && xs.thumb_lx <= 512);
381 assert!(xs.thumb_ly >= -512 && xs.thumb_ly <= 512);
382 assert!(xs.thumb_rx > 32000);
383 assert!(xs.thumb_ry > 30000);
384 }
385
386 #[test]
387 fn hat_diagonals() {
388 assert_eq!(super::decode_hat_8way(0, 8), (true, false, false, false));
389 assert_eq!(super::decode_hat_8way(1, 8), (true, true, false, false));
390 assert_eq!(super::decode_hat_8way(3, 8), (false, true, true, false));
391 assert_eq!(super::decode_hat_8way(7, 8), (true, false, false, true));
392 assert_eq!(super::decode_hat_8way(8, 8), (false, false, false, false));
393 }
394}