1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
//! Generic HID — the escape hatch for devices azul does not model.
//!
//! `GamepadState` assumes an Xbox-shaped controller: two sticks, two triggers,
//! a d-pad, a fixed button set. Plenty of real input hardware is not that
//! shape — flight sticks with dozens of axes, racing wheels with pedal
//! clusters and force feedback, 6-DOF SpaceMice, foot pedals, Stream Decks,
//! MIDI controllers, barcode wedges. SDL keeps *joystick* events (arbitrary
//! axes, hats, balls) separate from *gamepad* events for exactly this reason.
//!
//! Rather than grow a taxonomy that can never be complete, this exposes the
//! raw report and lets the app decode it. That is the same trade the web made
//! with WebHID, and it is the right one here: azul cannot know what a
//! particular device's bytes mean, but the app that chose to support that
//! device does.
//!
//! This is deliberately NOT a filter family of its own — a HID report is
//! window-scoped like raw pointer motion, because it has no position and no
//! node it belongs to.
use alloc::vec::Vec;
use azul_css::{
impl_option, impl_option_inner, impl_vec, impl_vec_clone, impl_vec_debug, impl_vec_partialeq,
AzString,
};
/// Identity of one HID device.
///
/// The four numbers are the HID descriptor's own vocabulary, not azul's:
/// `usage_page` + `usage` say what KIND of device it claims to be (page 0x01
/// usage 0x04 is a joystick, 0x05 a gamepad, 0x08 a multi-axis controller),
/// while vendor and product identify the model. An app matches on whichever
/// pair it needs — usage for "any joystick", vid/pid for "this exact wheel".
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct HidDevice {
/// USB vendor id.
pub vendor_id: u16,
/// USB product id.
pub product_id: u16,
/// HID usage page of the device's top-level collection.
pub usage_page: u16,
/// HID usage within that page.
pub usage: u16,
/// Human-readable product string, empty when the device reports none.
pub name: AzString,
/// The device's SERIAL NUMBER as it reports it (USB `iSerial`; a
/// DualSense reports its Bluetooth address), empty when it reports none
/// or the platform does not expose it (Windows, see the backend).
pub serial: AzString,
/// PER-INSTANCE identity (8f-i-a-i, user ruling: two identical pads must
/// stay distinct for multiplayer). Stable for the device's connected
/// lifetime and never `0` for a real device. Derived from vendor,
/// product and serial when a serial is reported - so it survives a
/// reconnect - and from the platform's own handle for the device
/// otherwise, which is unique among the devices present but may change
/// when the device is re-plugged. Reports carry it, so an app keyed on
/// this field reads each pad's stream apart from its twin's.
pub instance: u64,
}
impl HidDevice {
/// The reconnect-stable instance id of a device with a serial number:
/// an FNV-1a hash over vendor, product and serial. `None` when the
/// device reports no serial - the caller falls back to its platform
/// handle through [`Self::handle_instance`].
#[must_use]
pub fn serial_instance(vendor_id: u16, product_id: u16, serial: &str) -> Option<u64> {
if serial.is_empty() {
return None;
}
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in vendor_id
.to_le_bytes()
.iter()
.chain(product_id.to_le_bytes().iter())
.chain(serial.as_bytes())
{
h ^= u64::from(*b);
h = h.wrapping_mul(0x0100_0000_01b3);
}
Some(h | 1)
}
/// An instance id from a platform handle (a device path, a kernel
/// object address, a hidraw number) for a device with no serial. Never
/// `0`, so "no identity" stays distinguishable from a real one.
#[must_use]
pub fn handle_instance(handle: &[u8]) -> u64 {
let mut h: u64 = 0x84222325_cbf29ce4;
for b in handle {
h ^= u64::from(*b);
h = h.wrapping_mul(0x0100_0000_01b3);
}
h | 1
}
/// The instance id for this device's identity: the serial-derived one
/// when a serial is present, else the handle-derived one.
#[must_use]
pub fn instance_for(vendor_id: u16, product_id: u16, serial: &str, handle: &[u8]) -> u64 {
Self::serial_instance(vendor_id, product_id, serial)
.unwrap_or_else(|| Self::handle_instance(handle))
}
}
/// One input report from a HID device.
///
/// Bytes exactly as the device sent them. Decoding is the app's job: the
/// report descriptor that explains the layout is device-specific, and a
/// framework guessing at it would be wrong more often than useful.
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C)]
pub struct HidReport {
/// Which device sent it.
pub device: HidDevice,
/// Report id, or `0` for a device whose descriptor uses no ids.
pub report_id: u8,
/// The report payload.
pub bytes: azul_css::U8Vec,
}
// FFI collection types. `CallbackInfo::get_hid_devices`/`get_hid_reports` hand
// these to bindings, and a borrowed `&[T]` is not C-compatible - a slice is a
// fat pointer whose layout C has no name for. Same treatment `TouchPointVec`
// and `MonitorVec` already get.
impl_option!(
HidDevice,
OptionHidDevice,
copy = false,
[Debug, Clone, PartialEq, Eq]
);
impl_vec!(
HidDevice,
HidDeviceVec,
HidDeviceVecDestructor,
HidDeviceVecDestructorType,
HidDeviceVecSlice,
OptionHidDevice
);
impl_vec_debug!(HidDevice, HidDeviceVec);
impl_vec_clone!(HidDevice, HidDeviceVec, HidDeviceVecDestructor);
impl_vec_partialeq!(HidDevice, HidDeviceVec);
impl_option!(
HidReport,
OptionHidReport,
copy = false,
[Debug, Clone, PartialEq, Eq]
);
impl_vec!(
HidReport,
HidReportVec,
HidReportVecDestructor,
HidReportVecDestructorType,
HidReportVecSlice,
OptionHidReport
);
impl_vec_debug!(HidReport, HidReportVec);
impl_vec_clone!(HidReport, HidReportVec, HidReportVecDestructor);
impl_vec_partialeq!(HidReport, HidReportVec);
/// Collects HID reports from the platform backends.
///
/// Poll-and-drain like the sensor and gamepad managers, for the same reason:
/// a device can report faster than the frame rate, and a callback per report
/// would swamp an app that only wants this frame's state.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct HidManager {
devices: Vec<HidDevice>,
pending: Vec<HidReport>,
}
impl HidManager {
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Replace the enumerated device list. Called by the backend at startup
/// and on hotplug.
pub fn set_devices(&mut self, devices: Vec<HidDevice>) {
self.devices = devices;
}
/// The devices the platform found.
#[must_use]
pub fn devices(&self) -> &[HidDevice] {
&self.devices
}
/// Queue an input report.
pub fn push_report(&mut self, report: HidReport) {
self.pending.push(report);
}
/// Read the queued reports without consuming them — what a callback does
/// while the event is being dispatched.
#[must_use]
pub fn reports(&self) -> &[HidReport] {
&self.pending
}
/// Drain the queue.
pub fn take_reports(&mut self) -> Vec<HidReport> {
core::mem::take(&mut self.pending)
}
}
#[cfg(test)]
mod instance_tests {
use super::*;
/// Two identical pads (same vendor and product) with different serials
/// get different, reconnect-stable ids; the same pad gets the same id
/// every time.
#[test]
fn identical_pads_with_different_serials_stay_distinct() {
let a = HidDevice::serial_instance(0x054c, 0x0ce6, "AA:BB:CC:DD:EE:01").unwrap();
let b = HidDevice::serial_instance(0x054c, 0x0ce6, "AA:BB:CC:DD:EE:02").unwrap();
assert_ne!(a, b);
assert_eq!(
a,
HidDevice::serial_instance(0x054c, 0x0ce6, "AA:BB:CC:DD:EE:01").unwrap(),
"stable across reconnects"
);
assert_ne!(a, 0);
}
/// No serial: the platform handle decides, and it is never zero.
#[test]
fn a_device_without_a_serial_falls_back_to_its_handle() {
assert_eq!(HidDevice::serial_instance(1, 2, ""), None);
let x = HidDevice::instance_for(1, 2, "", b"/dev/hidraw3");
let y = HidDevice::instance_for(1, 2, "", b"/dev/hidraw4");
assert_ne!(x, y);
assert_ne!(x, 0);
assert_eq!(x, HidDevice::handle_instance(b"/dev/hidraw3"));
// A serial outranks the handle.
assert_eq!(
HidDevice::instance_for(1, 2, "S1", b"/dev/hidraw3"),
HidDevice::serial_instance(1, 2, "S1").unwrap()
);
}
}