1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4#[repr(i32)]
5pub enum CapabilityKind {
6 Unknown = 0,
7 Keyboard = 1,
8 Key = 2,
9 Mouse = 3,
10 Touchpad = 4,
11 Touchscreen = 5,
12 Tablet = 6,
13 Joystick = 7,
14 Switch = 8,
15}
16
17impl CapabilityKind {
18 fn from_code(code: i32) -> Self {
19 match code {
20 1 => Self::Keyboard,
21 2 => Self::Key,
22 3 => Self::Mouse,
23 4 => Self::Touchpad,
24 5 => Self::Touchscreen,
25 6 => Self::Tablet,
26 7 => Self::Joystick,
27 8 => Self::Switch,
28 _ => Self::Unknown,
29 }
30 }
31}
32
33#[derive(Clone, Debug, Default, PartialEq, Eq)]
34pub struct CapabilityBits {
35 ev: [i64; 1],
36 key: [i64; 12],
37 rel: [i64; 1],
38 absolute: [i64; 2],
39 properties: [i64; 1],
40}
41
42impl CapabilityBits {
43 pub fn from_sysfs_event_node(devnode: &std::path::Path) -> Self {
44 let Some(name) = devnode.file_name() else {
45 return Self::default();
46 };
47 let device = std::path::Path::new("/sys/class/input")
48 .join(name)
49 .join("device");
50 let capabilities = device.join("capabilities");
51 let read =
52 |name: &str| std::fs::read_to_string(capabilities.join(name)).unwrap_or_default();
53 Self {
54 ev: parse_sysfs_hex::<1>(&read("ev")),
55 key: parse_sysfs_hex::<12>(&read("key")),
56 rel: parse_sysfs_hex::<1>(&read("rel")),
57 absolute: parse_sysfs_hex::<2>(&read("abs")),
58 properties: parse_sysfs_hex::<1>(
59 &std::fs::read_to_string(device.join("properties")).unwrap_or_default(),
60 ),
61 }
62 }
63
64 pub fn set_event(&mut self, code: u16) {
65 set_bit(&mut self.ev, code);
66 }
67
68 pub fn set_key(&mut self, code: u16) {
69 set_bit(&mut self.key, code);
70 }
71
72 pub fn set_relative(&mut self, code: u16) {
73 set_bit(&mut self.rel, code);
74 }
75
76 pub fn set_absolute(&mut self, code: u16) {
77 set_bit(&mut self.absolute, code);
78 }
79
80 pub fn set_property(&mut self, code: u16) {
81 set_bit(&mut self.properties, code);
82 }
83
84 pub fn classify(&self) -> CapabilityKind {
85 native_classify(self)
86 .map(CapabilityKind::from_code)
87 .unwrap_or_else(|| rust_classify(self))
88 }
89
90 pub fn has_event(&self, code: u16) -> bool {
91 bit(&self.ev, code)
92 }
93
94 pub fn has_key(&self, code: u16) -> bool {
95 bit(&self.key, code)
96 }
97
98 pub fn has_relative(&self, code: u16) -> bool {
99 bit(&self.rel, code)
100 }
101
102 pub fn has_absolute(&self, code: u16) -> bool {
103 bit(&self.absolute, code)
104 }
105
106 pub fn key_count(&self) -> usize {
107 (0..=KEY_MAX).filter(|code| bit(&self.key, *code)).count()
108 }
109}
110
111const KEY_MAX: u16 = 0x2ff;
112
113fn set_bit(words: &mut [i64], code: u16) {
114 let index = usize::from(code / 64);
115 let offset = u32::from(code % 64);
116 if let Some(word) = words.get_mut(index) {
117 *word |= 1_i64.wrapping_shl(offset);
118 }
119}
120
121fn bit(words: &[i64], code: u16) -> bool {
122 let index = usize::from(code / 64);
123 let offset = u32::from(code % 64);
124 words
125 .get(index)
126 .is_some_and(|word| (word.wrapping_shr(offset) & 1) == 1)
127}
128
129fn rust_classify(bits: &CapabilityBits) -> CapabilityKind {
130 let has_key = bit(&bits.ev, 1);
131 let has_rel = bit(&bits.ev, 2);
132 let has_abs = bit(&bits.ev, 3);
133 let xy = has_abs && bit(&bits.absolute, 0) && bit(&bits.absolute, 1);
134 let multitouch = bit(&bits.absolute, 0x2f) || bit(&bits.absolute, 0x35);
135 let finger = bit(&bits.key, 0x145);
136 let touch = bit(&bits.key, 0x14a);
137 let pen = bit(&bits.key, 0x140);
138 let left = bit(&bits.key, 0x110);
139 let joystick = bit(&bits.key, 0x120);
140 let relative_xy = has_rel && (bit(&bits.rel, 0) || bit(&bits.rel, 1));
141 let direct = bit(&bits.properties, 1);
142 let pointer = bit(&bits.properties, 0);
143
144 if pen && xy {
145 CapabilityKind::Tablet
146 } else if (finger || (touch && pointer && !direct)) && xy {
147 CapabilityKind::Touchpad
148 } else if (direct || (touch && multitouch)) && xy {
149 CapabilityKind::Touchscreen
150 } else if relative_xy && left {
151 CapabilityKind::Mouse
152 } else if joystick && has_abs {
153 CapabilityKind::Joystick
154 } else if has_key {
155 let count = (1..255).filter(|code| bit(&bits.key, *code)).count();
156 if count > 20 {
157 CapabilityKind::Keyboard
158 } else if count > 0 {
159 CapabilityKind::Key
160 } else {
161 CapabilityKind::Unknown
162 }
163 } else if bit(&bits.ev, 5) {
164 CapabilityKind::Switch
165 } else {
166 CapabilityKind::Unknown
167 }
168}
169
170pub fn parse_sysfs_hex<const WORDS: usize>(input: &str) -> [i64; WORDS] {
171 let mut words = [0_i64; WORDS];
172 if native_parse(input, &mut words) {
173 return words;
174 }
175 for (index, token) in input.split_whitespace().rev().take(WORDS).enumerate() {
176 words[index] = u64::from_str_radix(token, 16).unwrap_or(0) as i64;
177 }
178 words
179}
180
181pub fn knn_scores(features: &[f64], centroids: &[f64], profiles: usize) -> Vec<f64> {
182 if features.is_empty() || profiles == 0 || centroids.len() != features.len() * profiles {
183 return Vec::new();
184 }
185 let mut scores = vec![0.0; profiles];
186 if native_knn_scores(features, centroids, &mut scores) {
187 return scores;
188 }
189 for (score, centroid) in scores
190 .iter_mut()
191 .zip(centroids.chunks_exact(features.len()))
192 {
193 *score = -features
194 .iter()
195 .zip(centroid)
196 .map(|(feature, center)| (feature - center).powi(2))
197 .sum::<f64>();
198 }
199 scores
200}
201
202pub fn tiny_mlp_scores(
203 features: &[f64],
204 input_weights: &[f64],
205 hidden_bias: &[f64],
206 output_weights: &[f64],
207 output_bias: &[f64],
208) -> Vec<f64> {
209 if features.is_empty()
210 || hidden_bias.is_empty()
211 || output_bias.is_empty()
212 || input_weights.len() != features.len() * hidden_bias.len()
213 || output_weights.len() != hidden_bias.len() * output_bias.len()
214 {
215 return Vec::new();
216 }
217 let mut scores = vec![0.0; output_bias.len()];
218 if native_mlp_scores(
219 features,
220 input_weights,
221 hidden_bias,
222 output_weights,
223 output_bias,
224 &mut scores,
225 ) {
226 return scores;
227 }
228 let hidden = input_weights
229 .chunks_exact(features.len())
230 .zip(hidden_bias)
231 .map(|(weights, bias)| {
232 (bias
233 + weights
234 .iter()
235 .zip(features)
236 .map(|(weight, feature)| weight * feature)
237 .sum::<f64>())
238 .tanh()
239 })
240 .collect::<Vec<_>>();
241 for ((score, weights), bias) in scores
242 .iter_mut()
243 .zip(output_weights.chunks_exact(hidden.len()))
244 .zip(output_bias)
245 {
246 *score = bias
247 + weights
248 .iter()
249 .zip(&hidden)
250 .map(|(weight, value)| weight * value)
251 .sum::<f64>();
252 }
253 scores
254}
255
256include!(concat!(env!("OUT_DIR"), "/capforge_bindings.rs"));
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 fn assert_native_matches(bits: &CapabilityBits, expected: CapabilityKind) {
263 assert_eq!(rust_classify(bits), expected);
264 if NATIVE_CAPFORGE {
265 assert_eq!(bits.classify(), expected);
266 }
267 }
268
269 #[test]
270 fn sysfs_words_are_reversed_into_ioctl_order() {
271 let words = parse_sysfs_hex::<3>("8000000000000000 20 1\n");
272 assert_eq!(words[0] as u64, 1);
273 assert_eq!(words[1] as u64, 0x20);
274 assert_eq!(words[2] as u64, 0x8000_0000_0000_0000);
275 }
276
277 #[test]
278 fn classifiers_agree_on_touchpad_and_mixed_keyboard_evidence() {
279 let mut touchpad = CapabilityBits::default();
280 touchpad.set_event(1);
281 touchpad.set_event(3);
282 touchpad.set_absolute(0);
283 touchpad.set_absolute(1);
284 touchpad.set_key(0x145);
285 touchpad.set_key(0x14a);
286 touchpad.set_property(0);
287 assert_native_matches(&touchpad, CapabilityKind::Touchpad);
288
289 for key in 1..=30 {
290 touchpad.set_key(key);
291 }
292 assert_native_matches(&touchpad, CapabilityKind::Touchpad);
293 }
294
295 #[test]
296 fn classifiers_agree_on_relative_pointer_and_switch() {
297 let mut mouse = CapabilityBits::default();
298 mouse.set_event(1);
299 mouse.set_event(2);
300 mouse.set_relative(0);
301 mouse.set_relative(1);
302 mouse.set_key(0x110);
303 assert_native_matches(&mouse, CapabilityKind::Mouse);
304
305 let mut switch = CapabilityBits::default();
306 switch.set_event(5);
307 assert_native_matches(&switch, CapabilityKind::Switch);
308 }
309
310 #[test]
311 fn native_and_rust_profile_scorers_agree() {
312 let features = [0.25, 0.5, 0.75];
313 let centroids = [0.0, 0.5, 1.0, 0.5, 0.5, 0.5];
314 let expected_knn = [-0.125, -0.125];
315 let actual_knn = knn_scores(&features, ¢roids, 2);
316 for (actual, expected) in actual_knn.iter().zip(expected_knn) {
317 assert!((actual - expected).abs() < 1e-12);
318 }
319
320 let actual_mlp = tiny_mlp_scores(
321 &features,
322 &[1.0, 0.0, -1.0, -0.5, 1.0, 0.5],
323 &[0.1, -0.1],
324 &[0.5, -0.25, -0.75, 0.25],
325 &[0.0, 0.2],
326 );
327 assert_eq!(actual_mlp.len(), 2);
328 assert!(actual_mlp.iter().all(|score| score.is_finite()));
329 }
330}