diff options
author | Benjamin Tissoires <bentiss@kernel.org> | 2024-05-07 17:01:50 +0200 |
---|---|---|
committer | Benjamin Tissoires <bentiss@kernel.org> | 2024-05-07 17:01:50 +0200 |
commit | 14ee3d12f37b2594b9aa01fee2f7cff51bc71b26 (patch) | |
tree | 7824756ce551715c8698c1806e51eed8c5c73644 | |
parent | 78515b4e1517e1355907d7744497b0b9af29cfd6 (diff) | |
parent | 89ea968a9d759f71ac7b8d50949a8e5e5bcb1111 (diff) | |
download | hid-for-next.tar.gz |
Merge branch 'for-6.10/hid-bpf' into for-nextfor-next
18 files changed, 3510 insertions, 204 deletions
diff --git a/drivers/hid/bpf/progs/FR-TEC__Raptor-Mach-2.bpf.c b/drivers/hid/bpf/progs/FR-TEC__Raptor-Mach-2.bpf.c new file mode 100644 index 00000000000000..dc26a7677d364a --- /dev/null +++ b/drivers/hid/bpf/progs/FR-TEC__Raptor-Mach-2.bpf.c @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright (c) 2024 Benjamin Tissoires + */ + +#include "vmlinux.h" +#include "hid_bpf.h" +#include "hid_bpf_helpers.h" +#include <bpf/bpf_tracing.h> + +#define VID_BETOP_2185PC 0x11C0 +#define PID_RAPTOR_MACH_2 0x5606 + +HID_BPF_CONFIG( + HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_BETOP_2185PC, PID_RAPTOR_MACH_2), +); + +/* + * For reference, this is the fixed report descriptor + * + * static const __u8 fixed_rdesc[] = { + * 0x05, 0x01, // Usage Page (Generic Desktop) 0 + * 0x09, 0x04, // Usage (Joystick) 2 + * 0xa1, 0x01, // Collection (Application) 4 + * 0x05, 0x01, // Usage Page (Generic Desktop) 6 + * 0x85, 0x01, // Report ID (1) 8 + * 0x05, 0x01, // Usage Page (Generic Desktop) 10 + * 0x09, 0x30, // Usage (X) 12 + * 0x75, 0x10, // Report Size (16) 14 + * 0x95, 0x01, // Report Count (1) 16 + * 0x15, 0x00, // Logical Minimum (0) 18 + * 0x26, 0xff, 0x07, // Logical Maximum (2047) 20 + * 0x46, 0xff, 0x07, // Physical Maximum (2047) 23 + * 0x81, 0x02, // Input (Data,Var,Abs) 26 + * 0x05, 0x01, // Usage Page (Generic Desktop) 28 + * 0x09, 0x31, // Usage (Y) 30 + * 0x75, 0x10, // Report Size (16) 32 + * 0x95, 0x01, // Report Count (1) 34 + * 0x15, 0x00, // Logical Minimum (0) 36 + * 0x26, 0xff, 0x07, // Logical Maximum (2047) 38 + * 0x46, 0xff, 0x07, // Physical Maximum (2047) 41 + * 0x81, 0x02, // Input (Data,Var,Abs) 44 + * 0x05, 0x01, // Usage Page (Generic Desktop) 46 + * 0x09, 0x33, // Usage (Rx) 48 + * 0x75, 0x10, // Report Size (16) 50 + * 0x95, 0x01, // Report Count (1) 52 + * 0x15, 0x00, // Logical Minimum (0) 54 + * 0x26, 0xff, 0x03, // Logical Maximum (1023) 56 + * 0x46, 0xff, 0x03, // Physical Maximum (1023) 59 + * 0x81, 0x02, // Input (Data,Var,Abs) 62 + * 0x05, 0x00, // Usage Page (Undefined) 64 + * 0x09, 0x00, // Usage (Undefined) 66 + * 0x75, 0x10, // Report Size (16) 68 + * 0x95, 0x01, // Report Count (1) 70 + * 0x15, 0x00, // Logical Minimum (0) 72 + * 0x26, 0xff, 0x03, // Logical Maximum (1023) 74 + * 0x46, 0xff, 0x03, // Physical Maximum (1023) 77 + * 0x81, 0x02, // Input (Data,Var,Abs) 80 + * 0x05, 0x01, // Usage Page (Generic Desktop) 82 + * 0x09, 0x32, // Usage (Z) 84 + * 0x75, 0x10, // Report Size (16) 86 + * 0x95, 0x01, // Report Count (1) 88 + * 0x15, 0x00, // Logical Minimum (0) 90 + * 0x26, 0xff, 0x03, // Logical Maximum (1023) 92 + * 0x46, 0xff, 0x03, // Physical Maximum (1023) 95 + * 0x81, 0x02, // Input (Data,Var,Abs) 98 + * 0x05, 0x01, // Usage Page (Generic Desktop) 100 + * 0x09, 0x35, // Usage (Rz) 102 + * 0x75, 0x10, // Report Size (16) 104 + * 0x95, 0x01, // Report Count (1) 106 + * 0x15, 0x00, // Logical Minimum (0) 108 + * 0x26, 0xff, 0x03, // Logical Maximum (1023) 110 + * 0x46, 0xff, 0x03, // Physical Maximum (1023) 113 + * 0x81, 0x02, // Input (Data,Var,Abs) 116 + * 0x05, 0x01, // Usage Page (Generic Desktop) 118 + * 0x09, 0x34, // Usage (Ry) 120 + * 0x75, 0x10, // Report Size (16) 122 + * 0x95, 0x01, // Report Count (1) 124 + * 0x15, 0x00, // Logical Minimum (0) 126 + * 0x26, 0xff, 0x07, // Logical Maximum (2047) 128 + * 0x46, 0xff, 0x07, // Physical Maximum (2047) 131 + * 0x81, 0x02, // Input (Data,Var,Abs) 134 + * 0x05, 0x01, // Usage Page (Generic Desktop) 136 + * 0x09, 0x36, // Usage (Slider) 138 + * 0x75, 0x10, // Report Size (16) 140 + * 0x95, 0x01, // Report Count (1) 142 + * 0x15, 0x00, // Logical Minimum (0) 144 + * 0x26, 0xff, 0x03, // Logical Maximum (1023) 146 + * 0x46, 0xff, 0x03, // Physical Maximum (1023) 149 + * 0x81, 0x02, // Input (Data,Var,Abs) 152 + * 0x05, 0x09, // Usage Page (Button) 154 + * 0x19, 0x01, // Usage Minimum (1) 156 + * 0x2a, 0x1d, 0x00, // Usage Maximum (29) 158 + * 0x15, 0x00, // Logical Minimum (0) 161 + * 0x25, 0x01, // Logical Maximum (1) 163 + * 0x75, 0x01, // Report Size (1) 165 + * 0x96, 0x80, 0x00, // Report Count (128) 167 + * 0x81, 0x02, // Input (Data,Var,Abs) 170 + * 0x05, 0x01, // Usage Page (Generic Desktop) 172 + * 0x09, 0x39, // Usage (Hat switch) 174 + * 0x26, 0x07, 0x00, // Logical Maximum (7) 176 // changed (was 239) + * 0x46, 0x68, 0x01, // Physical Maximum (360) 179 + * 0x65, 0x14, // Unit (EnglishRotation: deg) 182 + * 0x75, 0x10, // Report Size (16) 184 + * 0x95, 0x01, // Report Count (1) 186 + * 0x81, 0x42, // Input (Data,Var,Abs,Null) 188 + * 0x05, 0x01, // Usage Page (Generic Desktop) 190 + * 0x09, 0x00, // Usage (Undefined) 192 + * 0x75, 0x08, // Report Size (8) 194 + * 0x95, 0x1d, // Report Count (29) 196 + * 0x81, 0x01, // Input (Cnst,Arr,Abs) 198 + * 0x15, 0x00, // Logical Minimum (0) 200 + * 0x26, 0xef, 0x00, // Logical Maximum (239) 202 + * 0x85, 0x58, // Report ID (88) 205 + * 0x26, 0xff, 0x00, // Logical Maximum (255) 207 + * 0x46, 0xff, 0x00, // Physical Maximum (255) 210 + * 0x75, 0x08, // Report Size (8) 213 + * 0x95, 0x3f, // Report Count (63) 215 + * 0x09, 0x00, // Usage (Undefined) 217 + * 0x91, 0x02, // Output (Data,Var,Abs) 219 + * 0x85, 0x59, // Report ID (89) 221 + * 0x75, 0x08, // Report Size (8) 223 + * 0x95, 0x80, // Report Count (128) 225 + * 0x09, 0x00, // Usage (Undefined) 227 + * 0xb1, 0x02, // Feature (Data,Var,Abs) 229 + * 0xc0, // End Collection 231 + * }; + */ + +/* + * We need to amend the report descriptor for the following: + * - the joystick sends its hat_switch data between 0 and 239 but + * the kernel expects the logical max to stick into a signed 8 bits + * integer. We thus divide it by 30 to match what other joysticks are + * doing + */ +SEC("fmod_ret/hid_bpf_rdesc_fixup") +int BPF_PROG(hid_fix_rdesc_raptor_mach_2, struct hid_bpf_ctx *hctx) +{ + __u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, HID_MAX_DESCRIPTOR_SIZE /* size */); + + if (!data) + return 0; /* EPERM check */ + + data[177] = 0x07; + + return 0; +} + +/* + * The hat_switch value at offsets 33 and 34 (16 bits) needs + * to be reduced to a single 8 bit signed integer. So we + * divide it by 30. + * Byte 34 is always null, so it is ignored. + */ +SEC("fmod_ret/hid_bpf_device_event") +int BPF_PROG(raptor_mach_2_fix_hat_switch, struct hid_bpf_ctx *hctx) +{ + __u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, 64 /* size */); + + if (!data) + return 0; /* EPERM check */ + + if (data[0] != 0x01) /* not the joystick report ID */ + return 0; + + data[33] /= 30; + + return 0; +} + +SEC("syscall") +int probe(struct hid_bpf_probe_args *ctx) +{ + ctx->retval = ctx->rdesc_size != 232; + if (ctx->retval) + ctx->retval = -EINVAL; + + /* ensure the kernel isn't fixed already */ + if (ctx->rdesc[177] != 0xef) /* Logical Max of 239 */ + ctx->retval = -EINVAL; + + return 0; +} + +char _license[] SEC("license") = "GPL"; diff --git a/drivers/hid/bpf/progs/HP__Elite-Presenter.bpf.c b/drivers/hid/bpf/progs/HP__Elite-Presenter.bpf.c new file mode 100644 index 00000000000000..3d14bbb6f2762d --- /dev/null +++ b/drivers/hid/bpf/progs/HP__Elite-Presenter.bpf.c @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright (c) 2023 Benjamin Tissoires + */ + +#include "vmlinux.h" +#include "hid_bpf.h" +#include "hid_bpf_helpers.h" +#include <bpf/bpf_tracing.h> + +#define VID_HP 0x03F0 +#define PID_ELITE_PRESENTER 0x464A + +HID_BPF_CONFIG( + HID_DEVICE(BUS_BLUETOOTH, HID_GROUP_GENERIC, VID_HP, PID_ELITE_PRESENTER) +); + +/* + * Already fixed as of commit 0db117359e47 ("HID: add quirk for 03f0:464a + * HP Elite Presenter Mouse") in the kernel, but this is a slightly better + * fix. + * + * The HP Elite Presenter Mouse HID Record Descriptor shows + * two mice (Report ID 0x1 and 0x2), one keypad (Report ID 0x5), + * two Consumer Controls (Report IDs 0x6 and 0x3). + * Prior to these fixes it registers one mouse, one keypad + * and one Consumer Control, and it was usable only as a + * digital laser pointer (one of the two mouses). + * We replace the second mouse collection with a pointer collection, + * allowing to use the device both as a mouse and a digital laser + * pointer. + */ + +SEC("fmod_ret/hid_bpf_rdesc_fixup") +int BPF_PROG(hid_fix_rdesc, struct hid_bpf_ctx *hctx) +{ + __u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, 4096 /* size */); + + if (!data) + return 0; /* EPERM check */ + + /* replace application mouse by application pointer on the second collection */ + if (data[79] == 0x02) + data[79] = 0x01; + + return 0; +} + +SEC("syscall") +int probe(struct hid_bpf_probe_args *ctx) +{ + ctx->retval = ctx->rdesc_size != 264; + if (ctx->retval) + ctx->retval = -EINVAL; + + return 0; +} + +char _license[] SEC("license") = "GPL"; diff --git a/drivers/hid/bpf/progs/Huion__Kamvas-Pro-19.bpf.c b/drivers/hid/bpf/progs/Huion__Kamvas-Pro-19.bpf.c new file mode 100644 index 00000000000000..ff759f2276f980 --- /dev/null +++ b/drivers/hid/bpf/progs/Huion__Kamvas-Pro-19.bpf.c @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright (c) 2024 Benjamin Tissoires + */ + +#include "vmlinux.h" +#include "hid_bpf.h" +#include "hid_bpf_helpers.h" +#include <bpf/bpf_tracing.h> + +#define VID_HUION 0x256C +#define PID_KAMVAS_PRO_19 0x006B +#define NAME_KAMVAS_PRO_19 "HUION Huion Tablet_GT1902" + +#define TEST_PREFIX "uhid test " + +HID_BPF_CONFIG( + HID_DEVICE(BUS_USB, HID_GROUP_MULTITOUCH_WIN_8, VID_HUION, PID_KAMVAS_PRO_19), +); + +bool prev_was_out_of_range; +bool in_eraser_mode; + +/* + * We need to amend the report descriptor for the following: + * - the second button is reported through Secondary Tip Switch instead of Secondary Barrel Switch + * - the third button is reported through Invert, and we need some room to report it. + * + */ +static const __u8 fixed_rdesc[] = { + 0x05, 0x0d, // Usage Page (Digitizers) 0 + 0x09, 0x02, // Usage (Pen) 2 + 0xa1, 0x01, // Collection (Application) 4 + 0x85, 0x0a, // Report ID (10) 6 + 0x09, 0x20, // Usage (Stylus) 8 + 0xa1, 0x01, // Collection (Application) 10 + 0x09, 0x42, // Usage (Tip Switch) 12 + 0x09, 0x44, // Usage (Barrel Switch) 14 + 0x09, 0x5a, // Usage (Secondary Barrel Switch) 16 /* changed from Secondary Tip Switch */ + 0x09, 0x3c, // Usage (Invert) 18 + 0x09, 0x45, // Usage (Eraser) 20 + 0x15, 0x00, // Logical Minimum (0) 22 + 0x25, 0x01, // Logical Maximum (1) 24 + 0x75, 0x01, // Report Size (1) 26 + 0x95, 0x05, // Report Count (5) 28 /* changed (was 5) */ + 0x81, 0x02, // Input (Data,Var,Abs) 30 + 0x05, 0x09, // Usage Page (Button) /* inserted */ + 0x09, 0x4a, // Usage (0x4a) /* inserted to be translated as input usage 0x149: BTN_STYLUS3 */ + 0x95, 0x01, // Report Count (1) /* inserted */ + 0x81, 0x02, // Input (Data,Var,Abs) /* inserted */ + 0x05, 0x0d, // Usage Page (Digitizers) /* inserted */ + 0x09, 0x32, // Usage (In Range) 32 + 0x75, 0x01, // Report Size (1) 34 + 0x95, 0x01, // Report Count (1) 36 + 0x81, 0x02, // Input (Data,Var,Abs) 38 + 0x81, 0x03, // Input (Cnst,Var,Abs) 40 + 0x05, 0x01, // Usage Page (Generic Desktop) 42 + 0x09, 0x30, // Usage (X) 44 + 0x09, 0x31, // Usage (Y) 46 + 0x55, 0x0d, // Unit Exponent (-3) 48 + 0x65, 0x33, // Unit (EnglishLinear: in³) 50 + 0x26, 0xff, 0x7f, // Logical Maximum (32767) 52 + 0x35, 0x00, // Physical Minimum (0) 55 + 0x46, 0x00, 0x08, // Physical Maximum (2048) 57 + 0x75, 0x10, // Report Size (16) 60 + 0x95, 0x02, // Report Count (2) 62 + 0x81, 0x02, // Input (Data,Var,Abs) 64 + 0x05, 0x0d, // Usage Page (Digitizers) 66 + 0x09, 0x30, // Usage (Tip Pressure) 68 + 0x26, 0xff, 0x3f, // Logical Maximum (16383) 70 + 0x75, 0x10, // Report Size (16) 73 + 0x95, 0x01, // Report Count (1) 75 + 0x81, 0x02, // Input (Data,Var,Abs) 77 + 0x09, 0x3d, // Usage (X Tilt) 79 + 0x09, 0x3e, // Usage (Y Tilt) 81 + 0x15, 0xa6, // Logical Minimum (-90) 83 + 0x25, 0x5a, // Logical Maximum (90) 85 + 0x75, 0x08, // Report Size (8) 87 + 0x95, 0x02, // Report Count (2) 89 + 0x81, 0x02, // Input (Data,Var,Abs) 91 + 0xc0, // End Collection 93 + 0xc0, // End Collection 94 + 0x05, 0x0d, // Usage Page (Digitizers) 95 + 0x09, 0x04, // Usage (Touch Screen) 97 + 0xa1, 0x01, // Collection (Application) 99 + 0x85, 0x04, // Report ID (4) 101 + 0x09, 0x22, // Usage (Finger) 103 + 0xa1, 0x02, // Collection (Logical) 105 + 0x05, 0x0d, // Usage Page (Digitizers) 107 + 0x95, 0x01, // Report Count (1) 109 + 0x75, 0x06, // Report Size (6) 111 + 0x09, 0x51, // Usage (Contact Id) 113 + 0x15, 0x00, // Logical Minimum (0) 115 + 0x25, 0x3f, // Logical Maximum (63) 117 + 0x81, 0x02, // Input (Data,Var,Abs) 119 + 0x09, 0x42, // Usage (Tip Switch) 121 + 0x25, 0x01, // Logical Maximum (1) 123 + 0x75, 0x01, // Report Size (1) 125 + 0x95, 0x01, // Report Count (1) 127 + 0x81, 0x02, // Input (Data,Var,Abs) 129 + 0x75, 0x01, // Report Size (1) 131 + 0x95, 0x01, // Report Count (1) 133 + 0x81, 0x03, // Input (Cnst,Var,Abs) 135 + 0x05, 0x01, // Usage Page (Generic Desktop) 137 + 0x75, 0x10, // Report Size (16) 139 + 0x55, 0x0e, // Unit Exponent (-2) 141 + 0x65, 0x11, // Unit (SILinear: cm) 143 + 0x09, 0x30, // Usage (X) 145 + 0x26, 0xff, 0x7f, // Logical Maximum (32767) 147 + 0x35, 0x00, // Physical Minimum (0) 150 + 0x46, 0x15, 0x0c, // Physical Maximum (3093) 152 + 0x81, 0x42, // Input (Data,Var,Abs,Null) 155 + 0x09, 0x31, // Usage (Y) 157 + 0x26, 0xff, 0x7f, // Logical Maximum (32767) 159 + 0x46, 0xcb, 0x06, // Physical Maximum (1739) 162 + 0x81, 0x42, // Input (Data,Var,Abs,Null) 165 + 0x05, 0x0d, // Usage Page (Digitizers) 167 + 0x09, 0x30, // Usage (Tip Pressure) 169 + 0x26, 0xff, 0x1f, // Logical Maximum (8191) 171 + 0x75, 0x10, // Report Size (16) 174 + 0x95, 0x01, // Report Count (1) 176 + 0x81, 0x02, // Input (Data,Var,Abs) 178 + 0xc0, // End Collection 180 + 0x05, 0x0d, // Usage Page (Digitizers) 181 + 0x09, 0x22, // Usage (Finger) 183 + 0xa1, 0x02, // Collection (Logical) 185 + 0x05, 0x0d, // Usage Page (Digitizers) 187 + 0x95, 0x01, // Report Count (1) 189 + 0x75, 0x06, // Report Size (6) 191 + 0x09, 0x51, // Usage (Contact Id) 193 + 0x15, 0x00, // Logical Minimum (0) 195 + 0x25, 0x3f, // Logical Maximum (63) 197 + 0x81, 0x02, // Input (Data,Var,Abs) 199 + 0x09, 0x42, // Usage (Tip Switch) 201 + 0x25, 0x01, // Logical Maximum (1) 203 + 0x75, 0x01, // Report Size (1) 205 + 0x95, 0x01, // Report Count (1) 207 + 0x81, 0x02, // Input (Data,Var,Abs) 209 + 0x75, 0x01, // Report Size (1) 211 + 0x95, 0x01, // Report Count (1) 213 + 0x81, 0x03, // Input (Cnst,Var,Abs) 215 + 0x05, 0x01, // Usage Page (Generic Desktop) 217 + 0x75, 0x10, // Report Size (16) 219 + 0x55, 0x0e, // Unit Exponent (-2) 221 + 0x65, 0x11, // Unit (SILinear: cm) 223 + 0x09, 0x30, // Usage (X) 225 + 0x26, 0xff, 0x7f, // Logical Maximum (32767) 227 + 0x35, 0x00, // Physical Minimum (0) 230 + 0x46, 0x15, 0x0c, // Physical Maximum (3093) 232 + 0x81, 0x42, // Input (Data,Var,Abs,Null) 235 + 0x09, 0x31, // Usage (Y) 237 + 0x26, 0xff, 0x7f, // Logical Maximum (32767) 239 + 0x46, 0xcb, 0x06, // Physical Maximum (1739) 242 + 0x81, 0x42, // Input (Data,Var,Abs,Null) 245 + 0x05, 0x0d, // Usage Page (Digitizers) 247 + 0x09, 0x30, // Usage (Tip Pressure) 249 + 0x26, 0xff, 0x1f, // Logical Maximum (8191) 251 + 0x75, 0x10, // Report Size (16) 254 + 0x95, 0x01, // Report Count (1) 256 + 0x81, 0x02, // Input (Data,Var,Abs) 258 + 0xc0, // End Collection 260 + 0x05, 0x0d, // Usage Page (Digitizers) 261 + 0x09, 0x56, // Usage (Scan Time) 263 + 0x55, 0x00, // Unit Exponent (0) 265 + 0x65, 0x00, // Unit (None) 267 + 0x27, 0xff, 0xff, 0xff, 0x7f, // Logical Maximum (2147483647) 269 + 0x95, 0x01, // Report Count (1) 274 + 0x75, 0x20, // Report Size (32) 276 + 0x81, 0x02, // Input (Data,Var,Abs) 278 + 0x09, 0x54, // Usage (Contact Count) 280 + 0x25, 0x7f, // Logical Maximum (127) 282 + 0x95, 0x01, // Report Count (1) 284 + 0x75, 0x08, // Report Size (8) 286 + 0x81, 0x02, // Input (Data,Var,Abs) 288 + 0x75, 0x08, // Report Size (8) 290 + 0x95, 0x08, // Report Count (8) 292 + 0x81, 0x03, // Input (Cnst,Var,Abs) 294 + 0x85, 0x05, // Report ID (5) 296 + 0x09, 0x55, // Usage (Contact Max) 298 + 0x25, 0x0a, // Logical Maximum (10) 300 + 0x75, 0x08, // Report Size (8) 302 + 0x95, 0x01, // Report Count (1) 304 + 0xb1, 0x02, // Feature (Data,Var,Abs) 306 + 0x06, 0x00, 0xff, // Usage Page (Vendor Defined Page 1) 308 + 0x09, 0xc5, // Usage (Vendor Usage 0xc5) 311 + 0x85, 0x06, // Report ID (6) 313 + 0x15, 0x00, // Logical Minimum (0) 315 + 0x26, 0xff, 0x00, // Logical Maximum (255) 317 + 0x75, 0x08, // Report Size (8) 320 + 0x96, 0x00, 0x01, // Report Count (256) 322 + 0xb1, 0x02, // Feature (Data,Var,Abs) 325 + 0xc0, // End Collection 327 +}; + +SEC("fmod_ret/hid_bpf_rdesc_fixup") +int BPF_PROG(hid_fix_rdesc_huion_kamvas_pro_19, struct hid_bpf_ctx *hctx) +{ + __u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, HID_MAX_DESCRIPTOR_SIZE /* size */); + + if (!data) + return 0; /* EPERM check */ + + __builtin_memcpy(data, fixed_rdesc, sizeof(fixed_rdesc)); + + return sizeof(fixed_rdesc); +} + +/* + * This tablet reports the 3rd button through invert, but this conflict + * with the normal eraser mode. + * Fortunately, before entering eraser mode, (so Invert = 1), + * the tablet always sends an out-of-proximity event. + * So we can detect that single event and: + * - if there was none but the invert bit was toggled: this is the + * third button + * - if there was this out-of-proximity event, we are entering + * eraser mode, and we will until the next out-of-proximity. + */ +SEC("fmod_ret/hid_bpf_device_event") +int BPF_PROG(kamvas_pro_19_fix_3rd_button, struct hid_bpf_ctx *hctx) +{ + __u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, 10 /* size */); + + if (!data) + return 0; /* EPERM check */ + + if (data[0] != 0x0a) /* not the pen report ID */ + return 0; + + /* stylus is out of range */ + if (!(data[1] & 0x40)) { + prev_was_out_of_range = true; + in_eraser_mode = false; + return 0; + } + + /* going into eraser mode (Invert = 1) only happens after an + * out of range event + */ + if (prev_was_out_of_range && (data[1] & 0x18)) + in_eraser_mode = true; + + /* eraser mode works fine */ + if (in_eraser_mode) + return 0; + + /* copy the Invert bit reported for the 3rd button in bit 7 */ + if (data[1] & 0x08) + data[1] |= 0x20; + + /* clear Invert bit now that it was copied */ + data[1] &= 0xf7; + + prev_was_out_of_range = false; + + return 0; +} + +SEC("syscall") +int probe(struct hid_bpf_probe_args *ctx) +{ + ctx->retval = ctx->rdesc_size != 328; + if (ctx->retval) + ctx->retval = -EINVAL; + + /* ensure the kernel isn't fixed already */ + if (ctx->rdesc[17] != 0x43) /* Secondary Tip Switch */ + ctx->retval = -EINVAL; + + struct hid_bpf_ctx *hctx = hid_bpf_allocate_context(ctx->hid); + + if (!hctx) { + return ctx->retval = -EINVAL; + return 0; + } + + const char *name = hctx->hid->name; + + /* strip out TEST_PREFIX */ + if (!__builtin_memcmp(name, TEST_PREFIX, sizeof(TEST_PREFIX) - 1)) + name += sizeof(TEST_PREFIX) - 1; + + if (__builtin_memcmp(name, NAME_KAMVAS_PRO_19, sizeof(NAME_KAMVAS_PRO_19))) + ctx->retval = -EINVAL; + + hid_bpf_release_context(hctx); + + return 0; +} + +char _license[] SEC("license") = "GPL"; diff --git a/drivers/hid/bpf/progs/IOGEAR__Kaliber-MMOmentum.bpf.c b/drivers/hid/bpf/progs/IOGEAR__Kaliber-MMOmentum.bpf.c new file mode 100644 index 00000000000000..225cbefdbf0e5a --- /dev/null +++ b/drivers/hid/bpf/progs/IOGEAR__Kaliber-MMOmentum.bpf.c @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright (c) 2023 Benjamin Tissoires + */ + +#include "vmlinux.h" +#include "hid_bpf.h" +#include "hid_bpf_helpers.h" +#include <bpf/bpf_tracing.h> + +#define VID_IOGEAR 0x258A /* VID is shared with SinoWealth and Glorious and prob others */ +#define PID_MOMENTUM 0x0027 + +HID_BPF_CONFIG( + HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_IOGEAR, PID_MOMENTUM) +); + +/* + * The IOGear Kaliber Gaming MMOmentum Pro mouse has multiple buttons (12) + * but only 5 are accessible out of the box because the report descriptor + * marks the other buttons as constants. + * We just fix the report descriptor to enable those missing 7 buttons. + */ + +SEC("fmod_ret/hid_bpf_rdesc_fixup") +int BPF_PROG(hid_fix_rdesc, struct hid_bpf_ctx *hctx) +{ + const u8 offsets[] = {84, 112, 140}; + __u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, 4096 /* size */); + + if (!data) + return 0; /* EPERM check */ + + /* if not Keyboard */ + if (data[3] != 0x06) + return 0; + + for (int idx = 0; idx < ARRAY_SIZE(offsets); idx++) { + u8 offset = offsets[idx]; + + /* if Input (Cnst,Var,Abs) , make it Input (Data,Var,Abs) */ + if (data[offset] == 0x81 && data[offset + 1] == 0x03) + data[offset + 1] = 0x02; + } + + return 0; +} + +SEC("syscall") +int probe(struct hid_bpf_probe_args *ctx) +{ + /* only bind to the keyboard interface */ + ctx->retval = ctx->rdesc_size != 213; + if (ctx->retval) + ctx->retval = -EINVAL; + + return 0; +} + +char _license[] SEC("license") = "GPL"; diff --git a/drivers/hid/bpf/progs/Makefile b/drivers/hid/bpf/progs/Makefile new file mode 100644 index 00000000000000..63ed7e02adf176 --- /dev/null +++ b/drivers/hid/bpf/progs/Makefile @@ -0,0 +1,91 @@ +# SPDX-License-Identifier: GPL-2.0 +OUTPUT := .output +abs_out := $(abspath $(OUTPUT)) + +CLANG ?= clang +LLC ?= llc +LLVM_STRIP ?= llvm-strip + +TOOLS_PATH := $(abspath ../../../../tools) +BPFTOOL_SRC := $(TOOLS_PATH)/bpf/bpftool +BPFTOOL_OUTPUT := $(abs_out)/bpftool +DEFAULT_BPFTOOL := $(BPFTOOL_OUTPUT)/bootstrap/bpftool +BPFTOOL ?= $(DEFAULT_BPFTOOL) + +LIBBPF_SRC := $(TOOLS_PATH)/lib/bpf +LIBBPF_OUTPUT := $(abs_out)/libbpf +LIBBPF_DESTDIR := $(LIBBPF_OUTPUT) +LIBBPF_INCLUDE := $(LIBBPF_DESTDIR)/include +BPFOBJ := $(LIBBPF_OUTPUT)/libbpf.a + +INCLUDES := -I$(OUTPUT) -I$(LIBBPF_INCLUDE) -I$(TOOLS_PATH)/include/uapi +CFLAGS := -g -Wall + +VMLINUX_BTF_PATHS ?= $(if $(O),$(O)/vmlinux) \ + $(if $(KBUILD_OUTPUT),$(KBUILD_OUTPUT)/vmlinux) \ + ../../../../vmlinux \ + /sys/kernel/btf/vmlinux \ + /boot/vmlinux-$(shell uname -r) +VMLINUX_BTF ?= $(abspath $(firstword $(wildcard $(VMLINUX_BTF_PATHS)))) +ifeq ($(VMLINUX_BTF),) +$(error Cannot find a vmlinux for VMLINUX_BTF at any of "$(VMLINUX_BTF_PATHS)") +endif + +ifeq ($(V),1) +Q = +msg = +else +Q = @ +msg = @printf ' %-8s %s%s\n' "$(1)" "$(notdir $(2))" "$(if $(3), $(3))"; +MAKEFLAGS += --no-print-directory +submake_extras := feature_display=0 +endif + +.DELETE_ON_ERROR: + +.PHONY: all clean + +SOURCES = $(wildcard *.bpf.c) +TARGETS = $(SOURCES:.bpf.c=.bpf.o) + +all: $(TARGETS) + +clean: + $(call msg,CLEAN) + $(Q)rm -rf $(OUTPUT) $(TARGETS) + +%.bpf.o: %.bpf.c vmlinux.h $(BPFOBJ) | $(OUTPUT) + $(call msg,BPF,$@) + $(Q)$(CLANG) -g -O2 --target=bpf $(INCLUDES) \ + -c $(filter %.c,$^) -o $@ && \ + $(LLVM_STRIP) -g $@ + +vmlinux.h: $(VMLINUX_BTF) $(BPFTOOL) | $(INCLUDE_DIR) +ifeq ($(VMLINUX_H),) + $(call msg,GEN,,$@) + $(Q)$(BPFTOOL) btf dump file $(VMLINUX_BTF) format c > $@ +else + $(call msg,CP,,$@) + $(Q)cp "$(VMLINUX_H)" $@ +endif + +$(OUTPUT) $(LIBBPF_OUTPUT) $(BPFTOOL_OUTPUT): + $(call msg,MKDIR,$@) + $(Q)mkdir -p $@ + +$(BPFOBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(LIBBPF_OUTPUT) + $(Q)$(MAKE) $(submake_extras) -C $(LIBBPF_SRC) \ + OUTPUT=$(abspath $(dir $@))/ prefix= \ + DESTDIR=$(LIBBPF_DESTDIR) $(abspath $@) install_headers + +ifeq ($(CROSS_COMPILE),) +$(DEFAULT_BPFTOOL): $(BPFOBJ) | $(BPFTOOL_OUTPUT) + $(Q)$(MAKE) $(submake_extras) -C $(BPFTOOL_SRC) \ + OUTPUT=$(BPFTOOL_OUTPUT)/ \ + LIBBPF_BOOTSTRAP_OUTPUT=$(LIBBPF_OUTPUT)/ \ + LIBBPF_BOOTSTRAP_DESTDIR=$(LIBBPF_DESTDIR)/ bootstrap +else +$(DEFAULT_BPFTOOL): | $(BPFTOOL_OUTPUT) + $(Q)$(MAKE) $(submake_extras) -C $(BPFTOOL_SRC) \ + OUTPUT=$(BPFTOOL_OUTPUT)/ bootstrap +endif diff --git a/drivers/hid/bpf/progs/Microsoft__XBox-Elite-2.bpf.c b/drivers/hid/bpf/progs/Microsoft__XBox-Elite-2.bpf.c new file mode 100644 index 00000000000000..c04abecab8ee9b --- /dev/null +++ b/drivers/hid/bpf/progs/Microsoft__XBox-Elite-2.bpf.c @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright (c) 2024 Benjamin Tissoires + */ + +#include "vmlinux.h" +#include "hid_bpf.h" +#include "hid_bpf_helpers.h" +#include <bpf/bpf_tracing.h> + +#define VID_MICROSOFT 0x045e +#define PID_XBOX_ELITE_2 0x0b22 + +HID_BPF_CONFIG( + HID_DEVICE(BUS_BLUETOOTH, HID_GROUP_GENERIC, VID_MICROSOFT, PID_XBOX_ELITE_2) +); + +/* + * When using the XBox Wireless Controller Elite 2 over Bluetooth, + * the device exports the paddle on the back of the device as a single + * bitfield value of usage "Assign Selection". + * + * The kernel doesn't process those usages properly and report KEY_UNKNOWN + * for it. + * + * SDL doesn't know how to interprete that KEY_UNKNOWN and thus ignores the paddles. + * + * Given that over USB the kernel uses BTN_TRIGGER_HAPPY[5-8], we + * can tweak the report descriptor to make the kernel interprete it properly: + * - we need an application collection of gamepad (so we have to close the current + * Consumer Control one) + * - we need to change the usage to be buttons from 0x15 to 0x18 + */ + +#define OFFSET_ASSIGN_SELECTION 211 +#define ORIGINAL_RDESC_SIZE 464 + +const __u8 rdesc_assign_selection[] = { + 0x0a, 0x99, 0x00, // Usage (Media Select Security) 211 + 0x15, 0x00, // Logical Minimum (0) 214 + 0x26, 0xff, 0x00, // Logical Maximum (255) 216 + 0x95, 0x01, // Report Count (1) 219 + 0x75, 0x04, // Report Size (4) 221 + 0x81, 0x02, // Input (Data,Var,Abs) 223 + 0x15, 0x00, // Logical Minimum (0) 225 + 0x25, 0x00, // Logical Maximum (0) 227 + 0x95, 0x01, // Report Count (1) 229 + 0x75, 0x04, // Report Size (4) 231 + 0x81, 0x03, // Input (Cnst,Var,Abs) 233 + 0x0a, 0x81, 0x00, // Usage (Assign Selection) 235 + 0x15, 0x00, // Logical Minimum (0) 238 + 0x26, 0xff, 0x00, // Logical Maximum (255) 240 + 0x95, 0x01, // Report Count (1) 243 + 0x75, 0x04, // Report Size (4) 245 + 0x81, 0x02, // Input (Data,Var,Abs) 247 +}; + +/* + * we replace the above report descriptor extract + * with the one below. + * To make things equal in size, we take out a larger + * portion than just the "Assign Selection" range, because + * we need to insert a new application collection to force + * the kernel to use BTN_TRIGGER_HAPPY[4-7]. + */ +const __u8 fixed_rdesc_assign_selection[] = { + 0x0a, 0x99, 0x00, // Usage (Media Select Security) 211 + 0x15, 0x00, // Logical Minimum (0) 214 + 0x26, 0xff, 0x00, // Logical Maximum (255) 216 + 0x95, 0x01, // Report Count (1) 219 + 0x75, 0x04, // Report Size (4) 221 + 0x81, 0x02, // Input (Data,Var,Abs) 223 + /* 0x15, 0x00, */ // Logical Minimum (0) ignored + 0x25, 0x01, // Logical Maximum (1) 225 + 0x95, 0x04, // Report Count (4) 227 + 0x75, 0x01, // Report Size (1) 229 + 0x81, 0x03, // Input (Cnst,Var,Abs) 231 + 0xc0, // End Collection 233 + 0x05, 0x01, // Usage Page (Generic Desktop) 234 + 0x0a, 0x05, 0x00, // Usage (Game Pad) 236 + 0xa1, 0x01, // Collection (Application) 239 + 0x05, 0x09, // Usage Page (Button) 241 + 0x19, 0x15, // Usage Minimum (21) 243 + 0x29, 0x18, // Usage Maximum (24) 245 + /* 0x15, 0x00, */ // Logical Minimum (0) ignored + /* 0x25, 0x01, */ // Logical Maximum (1) ignored + /* 0x95, 0x01, */ // Report Size (1) ignored + /* 0x75, 0x04, */ // Report Count (4) ignored + 0x81, 0x02, // Input (Data,Var,Abs) 247 +}; + +_Static_assert(sizeof(rdesc_assign_selection) == sizeof(fixed_rdesc_assign_selection), + "Rdesc and fixed rdesc of different size"); +_Static_assert(sizeof(rdesc_assign_selection) + OFFSET_ASSIGN_SELECTION < ORIGINAL_RDESC_SIZE, + "Rdesc at given offset is too big"); + +SEC("fmod_ret/hid_bpf_rdesc_fixup") +int BPF_PROG(hid_fix_rdesc, struct hid_bpf_ctx *hctx) +{ + __u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, 4096 /* size */); + + if (!data) + return 0; /* EPERM check */ + + /* Check that the device is compatible */ + if (__builtin_memcmp(data + OFFSET_ASSIGN_SELECTION, + rdesc_assign_selection, + sizeof(rdesc_assign_selection))) + return 0; + + __builtin_memcpy(data + OFFSET_ASSIGN_SELECTION, + fixed_rdesc_assign_selection, + sizeof(fixed_rdesc_assign_selection)); + + return 0; +} + +SEC("syscall") +int probe(struct hid_bpf_probe_args *ctx) +{ + /* only bind to the keyboard interface */ + ctx->retval = ctx->rdesc_size != ORIGINAL_RDESC_SIZE; + if (ctx->retval) + ctx->retval = -EINVAL; + + if (__builtin_memcmp(ctx->rdesc + OFFSET_ASSIGN_SELECTION, + rdesc_assign_selection, + sizeof(rdesc_assign_selection))) + ctx->retval = -EINVAL; + + return 0; +} + +char _license[] SEC("license") = "GPL"; diff --git a/drivers/hid/bpf/progs/README b/drivers/hid/bpf/progs/README new file mode 100644 index 00000000000000..20b0928f385b1d --- /dev/null +++ b/drivers/hid/bpf/progs/README @@ -0,0 +1,102 @@ +# HID-BPF programs + +This directory contains various fixes for devices. They add new features or +fix some behaviors without being entirely mandatory. It is better to load them +when you have such a device, but they should not be a requirement for a device +to be working during the boot stage. + +The .bpf.c files provided here are not automatically compiled in the kernel. +They should be loaded in the kernel by `udev-hid-bpf`: + +https://gitlab.freedesktop.org/libevdev/udev-hid-bpf + +The main reasons for these fixes to be here is to have a central place to +"upstream" them, but also this way we can test them thanks to the HID +selftests. + +Once a .bpf.c file is accepted here, it is duplicated in `udev-hid-bpf` +in the `src/bpf/stable` directory, and distributions are encouraged to +only ship those bpf objects. So adding a file here should eventually +land in distributions when they update `udev-hid-bpf` + +## Compilation + +Just run `make` + +## Installation + +### Automated way + +Just run `sudo udev-hid-bpf install ./my-awesome-fix.bpf.o` + +### Manual way + +- copy the `.bpf.o` you want in `/etc/udev-hid-bpf/` +- create a new udev rule to automatically load it + +The following should do the trick (assuming udev-hid-bpf is available in +/usr/bin): + +``` +$> cp xppen-ArtistPro16Gen2.bpf.o /etc/udev-hid-bpf/ +$> udev-hid-bpf inspect xppen-ArtistPro16Gen2.bpf.o +[ + { + "name": "xppen-ArtistPro16Gen2.bpf.o", + "devices": [ + { + "bus": "0x0003", + "group": "0x0001", + "vid": "0x28BD", + "pid": "0x095A" + }, + { + "bus": "0x0003", + "group": "0x0001", + "vid": "0x28BD", + "pid": "0x095B" + } + ], +... +$> cat <EOF > /etc/udev/rules.d/99-load-hid-bpf-xppen-ArtistPro16Gen2.rules +ACTION!="add|remove", GOTO="hid_bpf_end" +SUBSYSTEM!="hid", GOTO="hid_bpf_end" + +# xppen-ArtistPro16Gen2.bpf.o +ACTION=="add",ENV{MODALIAS}=="hid:b0003g0001v000028BDp0000095A", RUN{program}+="/usr/local/bin/udev-hid-bpf add $sys$devpath /etc/udev-hid-bpf/xppen-ArtistPro16Gen2.bpf.o" +ACTION=="remove",ENV{MODALIAS}=="hid:b0003g0001v000028BDp0000095A", RUN{program}+="/usr/local/bin/udev-hid-bpf remove $sys$devpath " +# xppen-ArtistPro16Gen2.bpf.o +ACTION=="add",ENV{MODALIAS}=="hid:b0003g0001v000028BDp0000095B", RUN{program}+="/usr/local/bin/udev-hid-bpf add $sys$devpath /etc/udev-hid-bpf/xppen-ArtistPro16Gen2.bpf.o" +ACTION=="remove",ENV{MODALIAS}=="hid:b0003g0001v000028BDp0000095B", RUN{program}+="/usr/local/bin/udev-hid-bpf remove $sys$devpath " + +LABEL="hid_bpf_end" +EOF +$> udevadm control --reload +``` + +Then unplug and replug the device. + +## Checks + +### udev rule + +You can check that the udev rule is correctly working by issuing + +``` +$> udevadm test /sys/bus/hid/devices/0003:28BD:095B* +... +run: '/usr/local/bin/udev-hid-bpf add /sys/devices/virtual/misc/uhid/0003:28BD:095B.0E57 /etc/udev-hid-bpf/xppen-ArtistPro16Gen2.bpf.o' +``` + +### program loaded + +You can check that the program has been properly loaded with `bpftool` + +``` +$> bpftool prog +... +247: tracing name xppen_16_fix_eraser tag 18d389353ed2ef07 gpl + loaded_at 2024-03-28T16:02:28+0100 uid 0 + xlated 120B jited 77B memlock 4096B + btf_id 487 +``` diff --git a/drivers/hid/bpf/progs/Wacom__ArtPen.bpf.c b/drivers/hid/bpf/progs/Wacom__ArtPen.bpf.c new file mode 100644 index 00000000000000..dc05aa48faa750 --- /dev/null +++ b/drivers/hid/bpf/progs/Wacom__ArtPen.bpf.c @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright (c) 2024 Benjamin Tissoires + */ + +#include "vmlinux.h" +#include "hid_bpf.h" +#include "hid_bpf_helpers.h" +#include <bpf/bpf_tracing.h> + +#define VID_WACOM 0x056a +#define ART_PEN_ID 0x0804 +#define PID_INTUOS_PRO_2_M 0x0357 + +HID_BPF_CONFIG( + HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_WACOM, PID_INTUOS_PRO_2_M) +); + +/* + * This filter is here for the Art Pen stylus only: + * - when used on some Wacom devices (see the list of attached PIDs), this pen + * reports pressure every other events. + * - to solve that, given that we know that the next event will be the same as + * the current one, we can emulate a smoother pressure reporting by reporting + * the mean of the previous value and the current one. + * + * We are effectively delaying the pressure by one event every other event, but + * that's less of an annoyance compared to the chunkiness of the reported data. + * + * For example, let's assume the following set of events: + * <Tip switch 0> <X 0> <Y 0> <Pressure 0 > <Tooltype 0x0804> + * <Tip switch 1> <X 1> <Y 1> <Pressure 100 > <Tooltype 0x0804> + * <Tip switch 1> <X 2> <Y 2> <Pressure 100 > <Tooltype 0x0804> + * <Tip switch 1> <X 3> <Y 3> <Pressure 200 > <Tooltype 0x0804> + * <Tip switch 1> <X 4> <Y 4> <Pressure 200 > <Tooltype 0x0804> + * <Tip switch 0> <X 5> <Y 5> <Pressure 0 > <Tooltype 0x0804> + * + * The filter will report: + * <Tip switch 0> <X 0> <Y 0> <Pressure 0 > <Tooltype 0x0804> + * <Tip switch 1> <X 1> <Y 1> <Pressure * 50*> <Tooltype 0x0804> + * <Tip switch 1> <X 2> <Y 2> <Pressure 100 > <Tooltype 0x0804> + * <Tip switch 1> <X 3> <Y 3> <Pressure *150*> <Tooltype 0x0804> + * <Tip switch 1> <X 4> <Y 4> <Pressure 200 > <Tooltype 0x0804> + * <Tip switch 0> <X 5> <Y 5> <Pressure 0 > <Tooltype 0x0804> + * + */ + +struct wacom_params { + __u16 pid; + __u16 rdesc_len; + __u8 report_id; + __u8 report_len; + struct { + __u8 tip_switch; + __u8 pressure; + __u8 tool_type; + } offsets; +}; + +/* + * Multiple device can support the same stylus, so + * we need to know which device has which offsets + */ +static const struct wacom_params devices[] = { + { + .pid = PID_INTUOS_PRO_2_M, + .rdesc_len = 949, + .report_id = 16, + .report_len = 27, + .offsets = { + .tip_switch = 1, + .pressure = 8, + .tool_type = 25, + }, + }, +}; + +static struct wacom_params params = { 0 }; + +/* HID-BPF reports a 64 bytes chunk anyway, so this ensures + * the verifier to know we are addressing the memory correctly + */ +#define PEN_REPORT_LEN 64 + +/* only odd frames are modified */ +static bool odd; + +static __u16 prev_pressure; + +static inline void *get_bits(__u8 *data, unsigned int byte_offset) +{ + return data + byte_offset; +} + +static inline __u16 *get_u16(__u8 *data, unsigned int offset) +{ + return (__u16 *)get_bits(data, offset); +} + +static inline __u8 *get_u8(__u8 *data, unsigned int offset) +{ + return (__u8 *)get_bits(data, offset); +} + +SEC("fmod_ret/hid_bpf_device_event") +int BPF_PROG(artpen_pressure_interpolate, struct hid_bpf_ctx *hctx) +{ + __u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, PEN_REPORT_LEN /* size */); + __u16 *pressure, *tool_type; + __u8 *tip_switch; + + if (!data) + return 0; /* EPERM check */ + + if (data[0] != params.report_id || + params.offsets.tip_switch >= PEN_REPORT_LEN || + params.offsets.pressure >= PEN_REPORT_LEN - 1 || + params.offsets.tool_type >= PEN_REPORT_LEN - 1) + return 0; /* invalid report or parameters */ + + tool_type = get_u16(data, params.offsets.tool_type); + if (*tool_type != ART_PEN_ID) + return 0; + + tip_switch = get_u8(data, params.offsets.tip_switch); + if ((*tip_switch & 0x01) == 0) { + prev_pressure = 0; + odd = true; + return 0; + } + + pressure = get_u16(data, params.offsets.pressure); + + if (odd) + *pressure = (*pressure + prev_pressure) / 2; + + prev_pressure = *pressure; + odd = !odd; + + return 0; +} + +SEC("syscall") +int probe(struct hid_bpf_probe_args *ctx) +{ + struct hid_bpf_ctx *hid_ctx; + __u16 pid; + int i; + + /* get a struct hid_device to access the actual pid of the device */ + hid_ctx = hid_bpf_allocate_context(ctx->hid); + if (!hid_ctx) { + ctx->retval = -ENODEV; + return -1; /* EPERM check */ + } + pid = hid_ctx->hid->product; + + ctx->retval = -EINVAL; + + /* Match the given device with the list of known devices */ + for (i = 0; i < ARRAY_SIZE(devices); i++) { + const struct wacom_params *device = &devices[i]; + + if (device->pid == pid && device->rdesc_len == ctx->rdesc_size) { + params = *device; + ctx->retval = 0; + } + } + + hid_bpf_release_context(hid_ctx); + return 0; +} + +char _license[] SEC("license") = "GPL"; diff --git a/drivers/hid/bpf/progs/XPPen__Artist24.bpf.c b/drivers/hid/bpf/progs/XPPen__Artist24.bpf.c new file mode 100644 index 00000000000000..e1be6a12bb7587 --- /dev/null +++ b/drivers/hid/bpf/progs/XPPen__Artist24.bpf.c @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright (c) 2023 Benjamin Tissoires + */ + +#include "vmlinux.h" +#include "hid_bpf.h" +#include "hid_bpf_helpers.h" +#include <bpf/bpf_tracing.h> + +#define VID_UGEE 0x28BD /* VID is shared with SinoWealth and Glorious and prob others */ +#define PID_ARTIST_24 0x093A +#define PID_ARTIST_24_PRO 0x092D + +HID_BPF_CONFIG( + HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_UGEE, PID_ARTIST_24), + HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_UGEE, PID_ARTIST_24_PRO) +); + +/* + * We need to amend the report descriptor for the following: + * - the device reports Eraser instead of using Secondary Barrel Switch + * - the pen doesn't have a rubber tail, so basically we are removing any + * eraser/invert bits + */ +static const __u8 fixed_rdesc[] = { + 0x05, 0x0d, // Usage Page (Digitizers) 0 + 0x09, 0x02, // Usage (Pen) 2 + 0xa1, 0x01, // Collection (Application) 4 + 0x85, 0x07, // Report ID (7) 6 + 0x09, 0x20, // Usage (Stylus) 8 + 0xa1, 0x00, // Collection (Physical) 10 + 0x09, 0x42, // Usage (Tip Switch) 12 + 0x09, 0x44, // Usage (Barrel Switch) 14 + 0x09, 0x5a, // Usage (Secondary Barrel Switch) 16 /* changed from 0x45 (Eraser) to 0x5a (Secondary Barrel Switch) */ + 0x15, 0x00, // Logical Minimum (0) 18 + 0x25, 0x01, // Logical Maximum (1) 20 + 0x75, 0x01, // Report Size (1) 22 + 0x95, 0x03, // Report Count (3) 24 + 0x81, 0x02, // Input (Data,Var,Abs) 26 + 0x95, 0x02, // Report Count (2) 28 + 0x81, 0x03, // Input (Cnst,Var,Abs) 30 + 0x09, 0x32, // Usage (In Range) 32 + 0x95, 0x01, // Report Count (1) 34 + 0x81, 0x02, // Input (Data,Var,Abs) 36 + 0x95, 0x02, // Report Count (2) 38 + 0x81, 0x03, // Input (Cnst,Var,Abs) 40 + 0x75, 0x10, // Report Size (16) 42 + 0x95, 0x01, // Report Count (1) 44 + 0x35, 0x00, // Physical Minimum (0) 46 + 0xa4, // Push 48 + 0x05, 0x01, // Usage Page (Generic Desktop) 49 + 0x09, 0x30, // Usage (X) 51 + 0x65, 0x13, // Unit (EnglishLinear: in) 53 + 0x55, 0x0d, // Unit Exponent (-3) 55 + 0x46, 0xf0, 0x50, // Physical Maximum (20720) 57 + 0x26, 0xff, 0x7f, // Logical Maximum (32767) 60 + 0x81, 0x02, // Input (Data,Var,Abs) 63 + 0x09, 0x31, // Usage (Y) 65 + 0x46, 0x91, 0x2d, // Physical Maximum (11665) 67 + 0x26, 0xff, 0x7f, // Logical Maximum (32767) 70 + 0x81, 0x02, // Input (Data,Var,Abs) 73 + 0xb4, // Pop 75 + 0x09, 0x30, // Usage (Tip Pressure) 76 + 0x45, 0x00, // Physical Maximum (0) 78 + 0x26, 0xff, 0x1f, // Logical Maximum (8191) 80 + 0x81, 0x42, // Input (Data,Var,Abs,Null) 83 + 0x09, 0x3d, // Usage (X Tilt) 85 + 0x15, 0x81, // Logical Minimum (-127) 87 + 0x25, 0x7f, // Logical Maximum (127) 89 + 0x75, 0x08, // Report Size (8) 91 + 0x95, 0x01, // Report Count (1) 93 + 0x81, 0x02, // Input (Data,Var,Abs) 95 + 0x09, 0x3e, // Usage (Y Tilt) 97 + 0x15, 0x81, // Logical Minimum (-127) 99 + 0x25, 0x7f, // Logical Maximum (127) 101 + 0x81, 0x02, // Input (Data,Var,Abs) 103 + 0xc0, // End Collection 105 + 0xc0, // End Collection 106 +}; + +#define BIT(n) (1UL << n) + +#define TIP_SWITCH BIT(0) +#define BARREL_SWITCH BIT(1) +#define ERASER BIT(2) +/* padding BIT(3) */ +/* padding BIT(4) */ +#define IN_RANGE BIT(5) +/* padding BIT(6) */ +/* padding BIT(7) */ + +#define U16(index) (data[index] | (data[index + 1] << 8)) + +SEC("fmod_ret/hid_bpf_rdesc_fixup") +int BPF_PROG(hid_fix_rdesc_xppen_artist24, struct hid_bpf_ctx *hctx) +{ + __u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, 4096 /* size */); + + if (!data) + return 0; /* EPERM check */ + + __builtin_memcpy(data, fixed_rdesc, sizeof(fixed_rdesc)); + + return sizeof(fixed_rdesc); +} + +static __u8 prev_state = 0; + +/* + * There are a few cases where the device is sending wrong event + * sequences, all related to the second button (the pen doesn't + * have an eraser switch on the tail end): + * + * whenever the second button gets pressed or released, an + * out-of-proximity event is generated and then the firmware + * compensate for the missing state (and the firmware uses + * eraser for that button): + * + * - if the pen is in range, an extra out-of-range is sent + * when the second button is pressed/released: + * // Pen is in range + * E: InRange + * + * // Second button is pressed + * E: + * E: Eraser InRange + * + * // Second button is released + * E: + * E: InRange + * + * This case is ignored by this filter, it's "valid" + * and userspace knows how to deal with it, there are just + * a few out-of-prox events generated, but the user doesn´t + * see them. + * + * - if the pen is in contact, 2 extra events are added when + * the second button is pressed/released: an out of range + * and an in range: + * + * // Pen is in contact + * E: TipSwitch InRange + * + * // Second button is pressed + * E: <- false release, needs to be filtered out + * E: Eraser InRange <- false release, needs to be filtered out + * E: TipSwitch Eraser InRange + * + * // Second button is released + * E: <- false release, needs to be filtered out + * E: InRange <- false release, needs to be filtered out + * E: TipSwitch InRange + * + */ +SEC("fmod_ret/hid_bpf_device_event") +int BPF_PROG(xppen_24_fix_eraser, struct hid_bpf_ctx *hctx) +{ + __u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, 10 /* size */); + __u8 current_state, changed_state; + bool prev_tip; + __u16 tilt; + + if (!data) + return 0; /* EPERM check */ + + current_state = data[1]; + + /* if the state is identical to previously, early return */ + if (current_state == prev_state) + return 0; + + prev_tip = !!(prev_state & TIP_SWITCH); + + /* + * Illegal transition: pen is in range with the tip pressed, and + * it goes into out of proximity. + * + * Ideally we should hold the event, start a timer and deliver it + * only if the timer ends, but we are not capable of that now. + * + * And it doesn't matter because when we are in such cases, this + * means we are detecting a false release. + */ + if ((current_state & IN_RANGE) == 0) { + if (prev_tip) + return HID_IGNORE_EVENT; + return 0; + } + + /* + * XOR to only set the bits that have changed between + * previous and current state + */ + changed_state = prev_state ^ current_state; + + /* Store the new state for future processing */ + prev_state = current_state; + + /* + * We get both a tipswitch and eraser change in the same HID report: + * this is not an authorized transition and is unlikely to happen + * in real life. + * This is likely to be added by the firmware to emulate the + * eraser mode so we can skip the event. + */ + if ((changed_state & (TIP_SWITCH | ERASER)) == (TIP_SWITCH | ERASER)) /* we get both a tipswitch and eraser change at the same time */ + return HID_IGNORE_EVENT; + + return 0; +} + +SEC("syscall") +int probe(struct hid_bpf_probe_args *ctx) +{ + /* + * The device exports 3 interfaces. + */ + ctx->retval = ctx->rdesc_size != 107; + if (ctx->retval) + ctx->retval = -EINVAL; + + /* ensure the kernel isn't fixed already */ + if (ctx->rdesc[17] != 0x45) /* Eraser */ + ctx->retval = -EINVAL; + + return 0; +} + +char _license[] SEC("license") = "GPL"; diff --git a/drivers/hid/bpf/progs/XPPen__ArtistPro16Gen2.bpf.c b/drivers/hid/bpf/progs/XPPen__ArtistPro16Gen2.bpf.c new file mode 100644 index 00000000000000..65ef10036126e7 --- /dev/null +++ b/drivers/hid/bpf/progs/XPPen__ArtistPro16Gen2.bpf.c @@ -0,0 +1,274 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright (c) 2023 Benjamin Tissoires + */ + +#include "vmlinux.h" +#include "hid_bpf.h" +#include "hid_bpf_helpers.h" +#include <bpf/bpf_tracing.h> + +#define VID_UGEE 0x28BD /* VID is shared with SinoWealth and Glorious and prob others */ +#define PID_ARTIST_PRO14_GEN2 0x095A +#define PID_ARTIST_PRO16_GEN2 0x095B + +HID_BPF_CONFIG( + HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_UGEE, PID_ARTIST_PRO14_GEN2), + HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_UGEE, PID_ARTIST_PRO16_GEN2) +); + +/* + * We need to amend the report descriptor for the following: + * - the device reports Eraser instead of using Secondary Barrel Switch + * - when the eraser button is pressed and the stylus is touching the tablet, + * the device sends Tip Switch instead of sending Eraser + * + * This descriptor uses physical dimensions of the 16" device. + */ +static const __u8 fixed_rdesc[] = { + 0x05, 0x0d, // Usage Page (Digitizers) 0 + 0x09, 0x02, // Usage (Pen) 2 + 0xa1, 0x01, // Collection (Application) 4 + 0x85, 0x07, // Report ID (7) 6 + 0x09, 0x20, // Usage (Stylus) 8 + 0xa1, 0x00, // Collection (Physical) 10 + 0x09, 0x42, // Usage (Tip Switch) 12 + 0x09, 0x44, // Usage (Barrel Switch) 14 + 0x09, 0x5a, // Usage (Secondary Barrel Switch) 16 /* changed from 0x45 (Eraser) to 0x5a (Secondary Barrel Switch) */ + 0x09, 0x3c, // Usage (Invert) 18 + 0x09, 0x45, // Usage (Eraser) 16 /* created over a padding bit at offset 29-33 */ + 0x15, 0x00, // Logical Minimum (0) 20 + 0x25, 0x01, // Logical Maximum (1) 22 + 0x75, 0x01, // Report Size (1) 24 + 0x95, 0x05, // Report Count (5) 26 /* changed from 4 to 5 */ + 0x81, 0x02, // Input (Data,Var,Abs) 28 + 0x09, 0x32, // Usage (In Range) 34 + 0x15, 0x00, // Logical Minimum (0) 36 + 0x25, 0x01, // Logical Maximum (1) 38 + 0x95, 0x01, // Report Count (1) 40 + 0x81, 0x02, // Input (Data,Var,Abs) 42 + 0x95, 0x02, // Report Count (2) 44 + 0x81, 0x03, // Input (Cnst,Var,Abs) 46 + 0x75, 0x10, // Report Size (16) 48 + 0x95, 0x01, // Report Count (1) 50 + 0x35, 0x00, // Physical Minimum (0) 52 + 0xa4, // Push 54 + 0x05, 0x01, // Usage Page (Generic Desktop) 55 + 0x09, 0x30, // Usage (X) 57 + 0x65, 0x13, // Unit (EnglishLinear: in) 59 + 0x55, 0x0d, // Unit Exponent (-3) 61 + 0x46, 0xff, 0x34, // Physical Maximum (13567) 63 + 0x26, 0xff, 0x7f, // Logical Maximum (32767) 66 + 0x81, 0x02, // Input (Data,Var,Abs) 69 + 0x09, 0x31, // Usage (Y) 71 + 0x46, 0x20, 0x21, // Physical Maximum (8480) 73 + 0x26, 0xff, 0x7f, // Logical Maximum (32767) 76 + 0x81, 0x02, // Input (Data,Var,Abs) 79 + 0xb4, // Pop 81 + 0x09, 0x30, // Usage (Tip Pressure) 82 + 0x45, 0x00, // Physical Maximum (0) 84 + 0x26, 0xff, 0x3f, // Logical Maximum (16383) 86 + 0x81, 0x42, // Input (Data,Var,Abs,Null) 89 + 0x09, 0x3d, // Usage (X Tilt) 91 + 0x15, 0x81, // Logical Minimum (-127) 93 + 0x25, 0x7f, // Logical Maximum (127) 95 + 0x75, 0x08, // Report Size (8) 97 + 0x95, 0x01, // Report Count (1) 99 + 0x81, 0x02, // Input (Data,Var,Abs) 101 + 0x09, 0x3e, // Usage (Y Tilt) 103 + 0x15, 0x81, // Logical Minimum (-127) 105 + 0x25, 0x7f, // Logical Maximum (127) 107 + 0x81, 0x02, // Input (Data,Var,Abs) 109 + 0xc0, // End Collection 111 + 0xc0, // End Collection 112 +}; + +SEC("fmod_ret/hid_bpf_rdesc_fixup") +int BPF_PROG(hid_fix_rdesc_xppen_artistpro16gen2, struct hid_bpf_ctx *hctx) +{ + __u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, 4096 /* size */); + + if (!data) + return 0; /* EPERM check */ + + __builtin_memcpy(data, fixed_rdesc, sizeof(fixed_rdesc)); + + /* Fix the Physical maximum values for different sizes of the device + * The 14" screen device descriptor size is 11.874" x 7.421" + */ + if (hctx->hid->product == PID_ARTIST_PRO14_GEN2) { + data[63] = 0x2e; + data[62] = 0x62; + data[73] = 0x1c; + data[72] = 0xfd; + } + + return sizeof(fixed_rdesc); +} + +SEC("fmod_ret/hid_bpf_device_event") +int BPF_PROG(xppen_16_fix_eraser, struct hid_bpf_ctx *hctx) +{ + __u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, 10 /* size */); + + if (!data) + return 0; /* EPERM check */ + + if ((data[1] & 0x29) != 0x29) /* tip switch=1 invert=1 inrange=1 */ + return 0; + + /* xor bits 0,3 and 4: convert Tip Switch + Invert into Eraser only */ + data[1] ^= 0x19; + + return 0; +} + +/* + * Static coordinate offset table based on positive only angles + * Two tables are needed, because the logical coordinates are scaled + * + * The table can be generated by Python like this: + * >>> full_scale = 11.874 # the display width/height in inches + * >>> tip_height = 0.055677699 # the center of the pen coil distance from screen in inch (empirical) + * >>> h = tip_height * (32767 / full_scale) # height of the coil in logical coordinates + * >>> [round(h*math.sin(math.radians(d))) for d in range(0, 128)] + * [0, 13, 26, ....] + */ + +/* 14" inch screen 11.874" x 7.421" */ +static const __u16 angle_offsets_horizontal_14[128] = { + 0, 3, 5, 8, 11, 13, 16, 19, 21, 24, 27, 29, 32, 35, 37, 40, 42, 45, 47, 50, 53, + 55, 58, 60, 62, 65, 67, 70, 72, 74, 77, 79, 81, 84, 86, 88, 90, 92, 95, 97, 99, + 101, 103, 105, 107, 109, 111, 112, 114, 116, 118, 119, 121, 123, 124, 126, 127, + 129, 130, 132, 133, 134, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, + 147, 148, 148, 149, 150, 150, 151, 151, 152, 152, 153, 153, 153, 153, 153, 154, + 154, 154, 154, 154, 153, 153, 153, 153, 153, 152, 152, 151, 151, 150, 150, 149, + 148, 148, 147, 146, 145, 144, 143, 142, 141, 140, 139, 138, 137, 136, 134, 133, + 132, 130, 129, 127, 126, 124, 123 +}; +static const __u16 angle_offsets_vertical_14[128] = { + 0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 59, 64, 68, 72, 76, 80, 84, + 88, 92, 96, 100, 104, 108, 112, 115, 119, 123, 127, 130, 134, 137, 141, 145, 148, + 151, 155, 158, 161, 165, 168, 171, 174, 177, 180, 183, 186, 188, 191, 194, 196, + 199, 201, 204, 206, 208, 211, 213, 215, 217, 219, 221, 223, 225, 226, 228, 230, + 231, 232, 234, 235, 236, 237, 239, 240, 240, 241, 242, 243, 243, 244, 244, 245, + 245, 246, 246, 246, 246, 246, 246, 246, 245, 245, 244, 244, 243, 243, 242, 241, + 240, 240, 239, 237, 236, 235, 234, 232, 231, 230, 228, 226, 225, 223, 221, 219, + 217, 215, 213, 211, 208, 206, 204, 201, 199, 196 +}; + +/* 16" inch screen 13.567" x 8.480" */ +static const __u16 angle_offsets_horizontal_16[128] = { + 0, 2, 5, 7, 9, 12, 14, 16, 19, 21, 23, 26, 28, 30, 33, 35, 37, 39, 42, 44, 46, 48, + 50, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 86, 88, 90, + 92, 93, 95, 97, 98, 100, 101, 103, 105, 106, 107, 109, 110, 111, 113, 114, 115, + 116, 118, 119, 120, 121, 122, 123, 124, 125, 126, 126, 127, 128, 129, 129, 130, + 130, 131, 132, 132, 132, 133, 133, 133, 134, 134, 134, 134, 134, 134, 134, 134, + 134, 134, 134, 134, 134, 133, 133, 133, 132, 132, 132, 131, 130, 130, 129, 129, + 128, 127, 126, 126, 125, 124, 123, 122, 121, 120, 119, 118, 116, 115, 114, 113, + 111, 110, 109, 107 +}; +static const __u16 angle_offsets_vertical_16[128] = { + 0, 4, 8, 11, 15, 19, 22, 26, 30, 34, 37, 41, 45, 48, 52, 56, 59, 63, 66, 70, 74, + 77, 81, 84, 88, 91, 94, 98, 101, 104, 108, 111, 114, 117, 120, 123, 126, 129, 132, + 135, 138, 141, 144, 147, 149, 152, 155, 157, 160, 162, 165, 167, 170, 172, 174, + 176, 178, 180, 182, 184, 186, 188, 190, 192, 193, 195, 197, 198, 199, 201, 202, + 203, 205, 206, 207, 208, 209, 210, 210, 211, 212, 212, 213, 214, 214, 214, 215, + 215, 215, 215, 215, 215, 215, 215, 215, 214, 214, 214, 213, 212, 212, 211, 210, + 210, 209, 208, 207, 206, 205, 203, 202, 201, 199, 198, 197, 195, 193, 192, 190, + 188, 186, 184, 182, 180, 178, 176, 174, 172 +}; + +static void compensate_coordinates_by_tilt(__u8 *data, const __u8 idx, + const __s8 tilt, const __u16 (*compensation_table)[128]) +{ + __u16 coords = data[idx+1]; + + coords <<= 8; + coords += data[idx]; + + __u8 direction = tilt > 0 ? 0 : 1; /* Positive tilt means we need to subtract the compensation (vs. negative angle where we need to add) */ + __u8 angle = tilt > 0 ? tilt : -tilt; + + if (angle > 127) + return; + + __u16 compensation = (*compensation_table)[angle]; + + if (direction == 0) { + coords = (coords > compensation) ? coords - compensation : 0; + } else { + const __u16 logical_maximum = 32767; + __u16 max = logical_maximum - compensation; + + coords = (coords < max) ? coords + compensation : logical_maximum; + } + + data[idx] = coords & 0xff; + data[idx+1] = coords >> 8; +} + +SEC("fmod_ret/hid_bpf_device_event") +int BPF_PROG(xppen_16_fix_angle_offset, struct hid_bpf_ctx *hctx) +{ + __u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, 10 /* size */); + + if (!data) + return 0; /* EPERM check */ + + /* + * Compensate X and Y offset caused by tilt. + * + * The magnetic center moves when the pen is tilted, because the coil + * is not touching the screen. + * + * a (tilt angle) + * | /... h (coil distance from tip) + * | / + * |/______ + * |x (position offset) + * + * x = sin a * h + * + * Subtract the offset from the coordinates. Use the precomputed table! + * + * bytes 0 - report id + * 1 - buttons + * 2-3 - X coords (logical) + * 4-5 - Y coords + * 6-7 - pressure (ignore) + * 8 - tilt X + * 9 - tilt Y + */ + + __s8 tilt_x = (__s8) data[8]; + __s8 tilt_y = (__s8) data[9]; + + if (hctx->hid->product == PID_ARTIST_PRO14_GEN2) { + compensate_coordinates_by_tilt(data, 2, tilt_x, &angle_offsets_horizontal_14); + compensate_coordinates_by_tilt(data, 4, tilt_y, &angle_offsets_vertical_14); + } else if (hctx->hid->product == PID_ARTIST_PRO16_GEN2) { + compensate_coordinates_by_tilt(data, 2, tilt_x, &angle_offsets_horizontal_16); + compensate_coordinates_by_tilt(data, 4, tilt_y, &angle_offsets_vertical_16); + } + + return 0; +} + +SEC("syscall") +int probe(struct hid_bpf_probe_args *ctx) +{ + /* + * The device exports 3 interfaces. + */ + ctx->retval = ctx->rdesc_size != 113; + if (ctx->retval) + ctx->retval = -EINVAL; + + /* ensure the kernel isn't fixed already */ + if (ctx->rdesc[17] != 0x45) /* Eraser */ + ctx->retval = -EINVAL; + + return 0; +} + +char _license[] SEC("license") = "GPL"; diff --git a/drivers/hid/bpf/progs/hid_bpf.h b/drivers/hid/bpf/progs/hid_bpf.h new file mode 100644 index 00000000000000..7ee371cac2e149 --- /dev/null +++ b/drivers/hid/bpf/progs/hid_bpf.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* Copyright (c) 2022 Benjamin Tissoires + */ + +#ifndef ____HID_BPF__H +#define ____HID_BPF__H + +struct hid_bpf_probe_args { + unsigned int hid; + unsigned int rdesc_size; + unsigned char rdesc[4096]; + int retval; +}; + +#endif /* ____HID_BPF__H */ diff --git a/drivers/hid/bpf/progs/hid_bpf_helpers.h b/drivers/hid/bpf/progs/hid_bpf_helpers.h new file mode 100644 index 00000000000000..8f226f6e886b56 --- /dev/null +++ b/drivers/hid/bpf/progs/hid_bpf_helpers.h @@ -0,0 +1,168 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* Copyright (c) 2022 Benjamin Tissoires + */ + +#ifndef __HID_BPF_HELPERS_H +#define __HID_BPF_HELPERS_H + +#include "vmlinux.h" +#include <bpf/bpf_helpers.h> +#include <linux/errno.h> + +extern __u8 *hid_bpf_get_data(struct hid_bpf_ctx *ctx, + unsigned int offset, + const size_t __sz) __ksym; +extern struct hid_bpf_ctx *hid_bpf_allocate_context(unsigned int hid_id) __ksym; +extern void hid_bpf_release_context(struct hid_bpf_ctx *ctx) __ksym; +extern int hid_bpf_hw_request(struct hid_bpf_ctx *ctx, + __u8 *data, + size_t buf__sz, + enum hid_report_type type, + enum hid_class_request reqtype) __ksym; + +#define HID_MAX_DESCRIPTOR_SIZE 4096 +#define HID_IGNORE_EVENT -1 + +/* extracted from <linux/input.h> */ +#define BUS_ANY 0x00 +#define BUS_PCI 0x01 +#define BUS_ISAPNP 0x02 +#define BUS_USB 0x03 +#define BUS_HIL 0x04 +#define BUS_BLUETOOTH 0x05 +#define BUS_VIRTUAL 0x06 +#define BUS_ISA 0x10 +#define BUS_I8042 0x11 +#define BUS_XTKBD 0x12 +#define BUS_RS232 0x13 +#define BUS_GAMEPORT 0x14 +#define BUS_PARPORT 0x15 +#define BUS_AMIGA 0x16 +#define BUS_ADB 0x17 +#define BUS_I2C 0x18 +#define BUS_HOST 0x19 +#define BUS_GSC 0x1A +#define BUS_ATARI 0x1B +#define BUS_SPI 0x1C +#define BUS_RMI 0x1D +#define BUS_CEC 0x1E +#define BUS_INTEL_ISHTP 0x1F +#define BUS_AMD_SFH 0x20 + +/* extracted from <linux/hid.h> */ +#define HID_GROUP_ANY 0x0000 +#define HID_GROUP_GENERIC 0x0001 +#define HID_GROUP_MULTITOUCH 0x0002 +#define HID_GROUP_SENSOR_HUB 0x0003 +#define HID_GROUP_MULTITOUCH_WIN_8 0x0004 +#define HID_GROUP_RMI 0x0100 +#define HID_GROUP_WACOM 0x0101 +#define HID_GROUP_LOGITECH_DJ_DEVICE 0x0102 +#define HID_GROUP_STEAM 0x0103 +#define HID_GROUP_LOGITECH_27MHZ_DEVICE 0x0104 +#define HID_GROUP_VIVALDI 0x0105 + +/* include/linux/mod_devicetable.h defines as (~0), but that gives us negative size arrays */ +#define HID_VID_ANY 0x0000 +#define HID_PID_ANY 0x0000 + +#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) + +/* Helper macro to convert (foo, __LINE__) into foo134 so we can use __LINE__ for + * field/variable names + */ +#define COMBINE1(X, Y) X ## Y +#define COMBINE(X, Y) COMBINE1(X, Y) + +/* Macro magic: + * __uint(foo, 123) creates a int (*foo)[1234] + * + * We use that macro to declare an anonymous struct with several + * fields, each is the declaration of an pointer to an array of size + * bus/group/vid/pid. (Because it's a pointer to such an array, actual storage + * would be sizeof(pointer) rather than sizeof(array). Not that we ever + * instantiate it anyway). + * + * This is only used for BTF introspection, we can later check "what size + * is the bus array" in the introspection data and thus extract the bus ID + * again. + * + * And we use the __LINE__ to give each of our structs a unique name so the + * BPF program writer doesn't have to. + * + * $ bpftool btf dump file target/bpf/HP_Elite_Presenter.bpf.o + * shows the inspection data, start by searching for .hid_bpf_config + * and working backwards from that (each entry references the type_id of the + * content). + */ + +#define HID_DEVICE(b, g, ven, prod) \ + struct { \ + __uint(name, 0); \ + __uint(bus, (b)); \ + __uint(group, (g)); \ + __uint(vid, (ven)); \ + __uint(pid, (prod)); \ + } COMBINE(_entry, __LINE__) + +/* Macro magic below is to make HID_BPF_CONFIG() look like a function call that + * we can pass multiple HID_DEVICE() invocations in. + * + * For up to 16 arguments, HID_BPF_CONFIG(one, two) resolves to + * + * union { + * HID_DEVICE(...); + * HID_DEVICE(...); + * } _device_ids SEC(".hid_bpf_config") + * + */ + +/* Returns the number of macro arguments, this expands + * NARGS(a, b, c) to NTH_ARG(a, b, c, 15, 14, 13, .... 4, 3, 2, 1). + * NTH_ARG always returns the 16th argument which in our case is 3. + * + * If we want more than 16 values _COUNTDOWN and _NTH_ARG both need to be + * updated. + */ +#define _NARGS(...) _NARGS1(__VA_ARGS__, _COUNTDOWN) +#define _NARGS1(...) _NTH_ARG(__VA_ARGS__) + +/* Add to this if we need more than 16 args */ +#define _COUNTDOWN \ + 15, 14, 13, 12, 11, 10, 9, 8, \ + 7, 6, 5, 4, 3, 2, 1, 0 + +/* Return the 16 argument passed in. See _NARGS above for usage. Note this is + * 1-indexed. + */ +#define _NTH_ARG( \ + _1, _2, _3, _4, _5, _6, _7, _8, \ + _9, _10, _11, _12, _13, _14, _15,\ + N, ...) N + +/* Turns EXPAND(_ARG, a, b, c) into _ARG3(a, b, c) */ +#define _EXPAND(func, ...) COMBINE(func, _NARGS(__VA_ARGS__)) (__VA_ARGS__) + +/* And now define all the ARG macros for each number of args we want to accept */ +#define _ARG1(_1) _1; +#define _ARG2(_1, _2) _1; _2; +#define _ARG3(_1, _2, _3) _1; _2; _3; +#define _ARG4(_1, _2, _3, _4) _1; _2; _3; _4; +#define _ARG5(_1, _2, _3, _4, _5) _1; _2; _3; _4; _5; +#define _ARG6(_1, _2, _3, _4, _5, _6) _1; _2; _3; _4; _5; _6; +#define _ARG7(_1, _2, _3, _4, _5, _6, _7) _1; _2; _3; _4; _5; _6; _7; +#define _ARG8(_1, _2, _3, _4, _5, _6, _7, _8) _1; _2; _3; _4; _5; _6; _7; _8; +#define _ARG9(_1, _2, _3, _4, _5, _6, _7, _8, _9) _1; _2; _3; _4; _5; _6; _7; _8; _9; +#define _ARG10(_1, _2, _3, _4, _5, _6, _7, _8, _9, _a) _1; _2; _3; _4; _5; _6; _7; _8; _9; _a; +#define _ARG11(_1, _2, _3, _4, _5, _6, _7, _8, _9, _a, _b) _1; _2; _3; _4; _5; _6; _7; _8; _9; _a; _b; +#define _ARG12(_1, _2, _3, _4, _5, _6, _7, _8, _9, _a, _b, _c) _1; _2; _3; _4; _5; _6; _7; _8; _9; _a; _b; _c; +#define _ARG13(_1, _2, _3, _4, _5, _6, _7, _8, _9, _a, _b, _c, _d) _1; _2; _3; _4; _5; _6; _7; _8; _9; _a; _b; _c; _d; +#define _ARG14(_1, _2, _3, _4, _5, _6, _7, _8, _9, _a, _b, _c, _d, _e) _1; _2; _3; _4; _5; _6; _7; _8; _9; _a; _b; _c; _d; _e; +#define _ARG15(_1, _2, _3, _4, _5, _6, _7, _8, _9, _a, _b, _c, _d, _e, _f) _1; _2; _3; _4; _5; _6; _7; _8; _9; _a; _b; _c; _d; _e; _f; + + +#define HID_BPF_CONFIG(...) union { \ + _EXPAND(_ARG, __VA_ARGS__) \ +} _device_ids SEC(".hid_bpf_config") + +#endif /* __HID_BPF_HELPERS_H */ diff --git a/include/linux/hid.h b/include/linux/hid.h index b12cb1c8e68214..8e06d89698e671 100644 --- a/include/linux/hid.h +++ b/include/linux/hid.h @@ -474,9 +474,9 @@ struct hid_usage { __s8 wheel_factor; /* 120/resolution_multiplier */ __u16 code; /* input driver code */ __u8 type; /* input driver type */ - __s8 hat_min; /* hat switch fun */ - __s8 hat_max; /* ditto */ - __s8 hat_dir; /* ditto */ + __s16 hat_min; /* hat switch fun */ + __s16 hat_max; /* ditto */ + __s16 hat_dir; /* ditto */ __s16 wheel_accumulated; /* hi-res wheel */ }; diff --git a/tools/testing/selftests/hid/tests/base.py b/tools/testing/selftests/hid/tests/base.py index 51433063b22707..3a465768e507dd 100644 --- a/tools/testing/selftests/hid/tests/base.py +++ b/tools/testing/selftests/hid/tests/base.py @@ -8,11 +8,13 @@ import libevdev import os import pytest +import shutil +import subprocess import time import logging -from hidtools.device.base_device import BaseDevice, EvdevMatch, SysfsFile +from .base_device import BaseDevice, EvdevMatch, SysfsFile from pathlib import Path from typing import Final, List, Tuple @@ -157,6 +159,17 @@ class BaseTestCase: # for example ("playstation", "hid-playstation") kernel_modules: List[Tuple[str, str]] = [] + # List of in kernel HID-BPF object files to load + # before starting the test + # Any existing pre-loaded HID-BPF module will be removed + # before the ones in this list will be manually loaded. + # Each Element is a tuple '(hid_bpf_object, rdesc_fixup_present)', + # for example '("xppen-ArtistPro16Gen2.bpf.o", True)' + # If 'rdesc_fixup_present' is True, the test needs to wait + # for one unbind and rebind before it can be sure the kernel is + # ready + hid_bpfs: List[Tuple[str, bool]] = [] + def assertInputEventsIn(self, expected_events, effective_events): effective_events = effective_events.copy() for ev in expected_events: @@ -211,8 +224,6 @@ class BaseTestCase: # we don't know beforehand the name of the module from modinfo sysfs_path = Path("/sys/module") / kernel_module.replace("-", "_") if not sysfs_path.exists(): - import subprocess - ret = subprocess.run(["/usr/sbin/modprobe", kernel_module]) if ret.returncode != 0: pytest.skip( @@ -225,6 +236,64 @@ class BaseTestCase: self._load_kernel_module(kernel_driver, kernel_module) yield + def load_hid_bpfs(self): + script_dir = Path(os.path.dirname(os.path.realpath(__file__))) + root_dir = (script_dir / "../../../../..").resolve() + bpf_dir = root_dir / "drivers/hid/bpf/progs" + + udev_hid_bpf = shutil.which("udev-hid-bpf") + if not udev_hid_bpf: + pytest.skip("udev-hid-bpf not found in $PATH, skipping") + + wait = False + for _, rdesc_fixup in self.hid_bpfs: + if rdesc_fixup: + wait = True + + for hid_bpf, _ in self.hid_bpfs: + # We need to start `udev-hid-bpf` in the background + # and dispatch uhid events in case the kernel needs + # to fetch features on the device + process = subprocess.Popen( + [ + "udev-hid-bpf", + "--verbose", + "add", + str(self.uhdev.sys_path), + str(bpf_dir / hid_bpf), + ], + ) + while process.poll() is None: + self.uhdev.dispatch(1) + + if process.poll() != 0: + pytest.fail( + f"Couldn't insert hid-bpf program '{hid_bpf}', marking the test as failed" + ) + + if wait: + # the HID-BPF program exports a rdesc fixup, so it needs to be + # unbound by the kernel and then rebound. + # Ensure we get the bound event exactly 2 times (one for the normal + # uhid loading, and then the reload from HID-BPF) + now = time.time() + while self.uhdev.kernel_ready_count < 2 and time.time() - now < 2: + self.uhdev.dispatch(1) + + if self.uhdev.kernel_ready_count < 2: + pytest.fail( + f"Couldn't insert hid-bpf programs, marking the test as failed" + ) + + def unload_hid_bpfs(self): + ret = subprocess.run( + ["udev-hid-bpf", "--verbose", "remove", str(self.uhdev.sys_path)], + ) + if ret.returncode != 0: + pytest.fail( + f"Couldn't unload hid-bpf programs, marking the test as failed" + ) + @pytest.fixture() def new_uhdev(self, load_kernel_module): return self.create_device() @@ -248,12 +317,18 @@ class BaseTestCase: now = time.time() while not self.uhdev.is_ready() and time.time() - now < 5: self.uhdev.dispatch(1) + + if self.hid_bpfs: + self.load_hid_bpfs() + if self.uhdev.get_evdev() is None: logger.warning( f"available list of input nodes: (default application is '{self.uhdev.application}')" ) logger.warning(self.uhdev.input_nodes) yield + if self.hid_bpfs: + self.unload_hid_bpfs() self.uhdev = None except PermissionError: pytest.skip("Insufficient permissions, run me as root") @@ -313,8 +388,6 @@ class HIDTestUdevRule(object): self.reload_udev_rules() def reload_udev_rules(self): - import subprocess - subprocess.run("udevadm control --reload-rules".split()) subprocess.run("systemd-hwdb update".split()) @@ -330,10 +403,11 @@ class HIDTestUdevRule(object): delete=False, ) as f: f.write( - 'KERNELS=="*input*", ATTRS{name}=="*uhid test *", ENV{LIBINPUT_IGNORE_DEVICE}="1"\n' - ) - f.write( - 'KERNELS=="*input*", ATTRS{name}=="*uhid test * System Multi Axis", ENV{ID_INPUT_TOUCHSCREEN}="", ENV{ID_INPUT_SYSTEM_MULTIAXIS}="1"\n' + """ +KERNELS=="*input*", ATTRS{name}=="*uhid test *", ENV{LIBINPUT_IGNORE_DEVICE}="1" +KERNELS=="*hid*", ENV{HID_NAME}=="*uhid test *", ENV{HID_BPF_IGNORE_DEVICE}="1" +KERNELS=="*input*", ATTRS{name}=="*uhid test * System Multi Axis", ENV{ID_INPUT_TOUCHSCREEN}="", ENV{ID_INPUT_SYSTEM_MULTIAXIS}="1" +""" ) self.rulesfile = f diff --git a/tools/testing/selftests/hid/tests/base_device.py b/tools/testing/selftests/hid/tests/base_device.py new file mode 100644 index 00000000000000..e0515be97f83a4 --- /dev/null +++ b/tools/testing/selftests/hid/tests/base_device.py @@ -0,0 +1,421 @@ +#!/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Benjamin Tissoires <benjamin.tissoires@gmail.com> +# Copyright (c) 2017 Red Hat, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import fcntl +import functools +import libevdev +import os + +try: + import pyudev +except ImportError: + raise ImportError("UHID is not supported due to missing pyudev dependency") + +import logging + +import hidtools.hid as hid +from hidtools.uhid import UHIDDevice +from hidtools.util import BusType + +from pathlib import Path +from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type, Union + +logger = logging.getLogger("hidtools.device.base_device") + + +class SysfsFile(object): + def __init__(self, path): + self.path = path + + def __set_value(self, value): + with open(self.path, "w") as f: + return f.write(f"{value}\n") + + def __get_value(self): + with open(self.path) as f: + return f.read().strip() + + @property + def int_value(self) -> int: + return int(self.__get_value()) + + @int_value.setter + def int_value(self, v: int) -> None: + self.__set_value(v) + + @property + def str_value(self) -> str: + return self.__get_value() + + @str_value.setter + def str_value(self, v: str) -> None: + self.__set_value(v) + + +class LED(object): + def __init__(self, sys_path): + self.max_brightness = SysfsFile(sys_path / "max_brightness").int_value + self.__brightness = SysfsFile(sys_path / "brightness") + + @property + def brightness(self) -> int: + return self.__brightness.int_value + + @brightness.setter + def brightness(self, value: int) -> None: + self.__brightness.int_value = value + + +class PowerSupply(object): + """Represents Linux power_supply_class sysfs nodes.""" + + def __init__(self, sys_path): + self._capacity = SysfsFile(sys_path / "capacity") + self._status = SysfsFile(sys_path / "status") + self._type = SysfsFile(sys_path / "type") + + @property + def capacity(self) -> int: + return self._capacity.int_value + + @property + def status(self) -> str: + return self._status.str_value + + @property + def type(self) -> str: + return self._type.str_value + + +class HIDIsReady(object): + """ + Companion class that binds to a kernel mechanism + and that allows to know when a uhid device is ready or not. + + See :meth:`is_ready` for details. + """ + + def __init__(self: "HIDIsReady", uhid: UHIDDevice) -> None: + self.uhid = uhid + + def is_ready(self: "HIDIsReady") -> bool: + """ + Overwrite in subclasses: should return True or False whether + the attached uhid device is ready or not. + """ + return False + + +class UdevHIDIsReady(HIDIsReady): + _pyudev_context: ClassVar[Optional[pyudev.Context]] = None + _pyudev_monitor: ClassVar[Optional[pyudev.Monitor]] = None + _uhid_devices: ClassVar[Dict[int, Tuple[bool, int]]] = {} + + def __init__(self: "UdevHIDIsReady", uhid: UHIDDevice) -> None: + super().__init__(uhid) + self._init_pyudev() + + @classmethod + def _init_pyudev(cls: Type["UdevHIDIsReady"]) -> None: + if cls._pyudev_context is None: + cls._pyudev_context = pyudev.Context() + cls._pyudev_monitor = pyudev.Monitor.from_netlink(cls._pyudev_context) + cls._pyudev_monitor.filter_by("hid") + cls._pyudev_monitor.start() + + UHIDDevice._append_fd_to_poll( + cls._pyudev_monitor.fileno(), cls._cls_udev_event_callback + ) + + @classmethod + def _cls_udev_event_callback(cls: Type["UdevHIDIsReady"]) -> None: + if cls._pyudev_monitor is None: + return + event: pyudev.Device + for event in iter(functools.partial(cls._pyudev_monitor.poll, 0.02), None): + if event.action not in ["bind", "remove", "unbind"]: + return + + logger.debug(f"udev event: {event.action} -> {event}") + + id = int(event.sys_path.strip().split(".")[-1], 16) + + device_ready, count = cls._uhid_devices.get(id, (False, 0)) + + ready = event.action == "bind" + if not device_ready and ready: + count += 1 + cls._uhid_devices[id] = (ready, count) + + def is_ready(self: "UdevHIDIsReady") -> Tuple[bool, int]: + try: + return self._uhid_devices[self.uhid.hid_id] + except KeyError: + return (False, 0) + + +class EvdevMatch(object): + def __init__( + self: "EvdevMatch", + *, + requires: List[Any] = [], + excludes: List[Any] = [], + req_properties: List[Any] = [], + excl_properties: List[Any] = [], + ) -> None: + self.requires = requires + self.excludes = excludes + self.req_properties = req_properties + self.excl_properties = excl_properties + + def is_a_match(self: "EvdevMatch", evdev: libevdev.Device) -> bool: + for m in self.requires: + if not evdev.has(m): + return False + for m in self.excludes: + if evdev.has(m): + return False + for p in self.req_properties: + if not evdev.has_property(p): + return False + for p in self.excl_properties: + if evdev.has_property(p): + return False + return True + + +class EvdevDevice(object): + """ + Represents an Evdev node and its properties. + This is a stub for the libevdev devices, as they are relying on + uevent to get the data, saving us some ioctls to fetch the names + and properties. + """ + + def __init__(self: "EvdevDevice", sysfs: Path) -> None: + self.sysfs = sysfs + self.event_node: Any = None + self.libevdev: Optional[libevdev.Device] = None + + self.uevents = {} + # all of the interesting properties are stored in the input uevent, so in the parent + # so convert the uevent file of the parent input node into a dict + with open(sysfs.parent / "uevent") as f: + for line in f.readlines(): + key, value = line.strip().split("=") + self.uevents[key] = value.strip('"') + + # we open all evdev nodes in order to not miss any event + self.open() + + @property + def name(self: "EvdevDevice") -> str: + assert "NAME" in self.uevents + + return self.uevents["NAME"] + + @property + def evdev(self: "EvdevDevice") -> Path: + return Path("/dev/input") / self.sysfs.name + + def matches_application( + self: "EvdevDevice", application: str, matches: Dict[str, EvdevMatch] + ) -> bool: + if self.libevdev is None: + return False + + if application in matches: + return matches[application].is_a_match(self.libevdev) + + logger.error( + f"application '{application}' is unknown, please update/fix hid-tools" + ) + assert False # hid-tools likely needs an update + + def open(self: "EvdevDevice") -> libevdev.Device: + self.event_node = open(self.evdev, "rb") + self.libevdev = libevdev.Device(self.event_node) + + assert self.libevdev.fd is not None + + fd = self.libevdev.fd.fileno() + flag = fcntl.fcntl(fd, fcntl.F_GETFD) + fcntl.fcntl(fd, fcntl.F_SETFL, flag | os.O_NONBLOCK) + + return self.libevdev + + def close(self: "EvdevDevice") -> None: + if self.libevdev is not None and self.libevdev.fd is not None: + self.libevdev.fd.close() + self.libevdev = None + if self.event_node is not None: + self.event_node.close() + self.event_node = None + + +class BaseDevice(UHIDDevice): + # default _application_matches that matches nothing. This needs + # to be set in the subclasses to have get_evdev() working + _application_matches: Dict[str, EvdevMatch] = {} + + def __init__( + self, + name, + application, + rdesc_str: Optional[str] = None, + rdesc: Optional[Union[hid.ReportDescriptor, str, bytes]] = None, + input_info=None, + ) -> None: + self._kernel_is_ready: HIDIsReady = UdevHIDIsReady(self) + if rdesc_str is None and rdesc is None: + raise Exception("Please provide at least a rdesc or rdesc_str") + super().__init__() + if name is None: + name = f"uhid gamepad test {self.__class__.__name__}" + if input_info is None: + input_info = (BusType.USB, 1, 2) + self.name = name + self.info = input_info + self.default_reportID = None + self.opened = False + self.started = False + self.application = application + self._input_nodes: Optional[list[EvdevDevice]] = None + if rdesc is None: + assert rdesc_str is not None + self.rdesc = hid.ReportDescriptor.from_human_descr(rdesc_str) # type: ignore + else: + self.rdesc = rdesc # type: ignore + + @property + def power_supply_class(self: "BaseDevice") -> Optional[PowerSupply]: + ps = self.walk_sysfs("power_supply", "power_supply/*") + if ps is None or len(ps) < 1: + return None + + return PowerSupply(ps[0]) + + @property + def led_classes(self: "BaseDevice") -> List[LED]: + leds = self.walk_sysfs("led", "**/max_brightness") + if leds is None: + return [] + + return [LED(led.parent) for led in leds] + + @property + def kernel_is_ready(self: "BaseDevice") -> bool: + return self._kernel_is_ready.is_ready()[0] and self.started + + @property + def kernel_ready_count(self: "BaseDevice") -> int: + return self._kernel_is_ready.is_ready()[1] + + @property + def input_nodes(self: "BaseDevice") -> List[EvdevDevice]: + if self._input_nodes is not None: + return self._input_nodes + + if not self.kernel_is_ready or not self.started: + return [] + + self._input_nodes = [ + EvdevDevice(path) + for path in self.walk_sysfs("input", "input/input*/event*") + ] + return self._input_nodes + + def match_evdev_rule(self, application, evdev): + """Replace this in subclasses if the device has multiple reports + of the same type and we need to filter based on the actual evdev + node. + + returning True will append the corresponding report to + `self.input_nodes[type]` + returning False will ignore this report / type combination + for the device. + """ + return True + + def open(self): + self.opened = True + + def _close_all_opened_evdev(self): + if self._input_nodes is not None: + for e in self._input_nodes: + e.close() + + def __del__(self): + self._close_all_opened_evdev() + + def close(self): + self.opened = False + + def start(self, flags): + self.started = True + + def stop(self): + self.started = False + self._close_all_opened_evdev() + + def next_sync_events(self, application=None): + evdev = self.get_evdev(application) + if evdev is not None: + return list(evdev.events()) + return [] + + @property + def application_matches(self: "BaseDevice") -> Dict[str, EvdevMatch]: + return self._application_matches + + @application_matches.setter + def application_matches(self: "BaseDevice", data: Dict[str, EvdevMatch]) -> None: + self._application_matches = data + + def get_evdev(self, application=None): + if application is None: + application = self.application + + if len(self.input_nodes) == 0: + return None + + assert self._input_nodes is not None + + if len(self._input_nodes) == 1: + evdev = self._input_nodes[0] + if self.match_evdev_rule(application, evdev.libevdev): + return evdev.libevdev + else: + for _evdev in self._input_nodes: + if _evdev.matches_application(application, self.application_matches): + if self.match_evdev_rule(application, _evdev.libevdev): + return _evdev.libevdev + + def is_ready(self): + """Returns whether a UHID device is ready. Can be overwritten in + subclasses to add extra conditions on when to consider a UHID + device ready. This can be: + + - we need to wait on different types of input devices to be ready + (Touch Screen and Pen for example) + - we need to have at least 4 LEDs present + (len(self.uhdev.leds_classes) == 4) + - or any other combinations""" + return self.kernel_is_ready diff --git a/tools/testing/selftests/hid/tests/base_gamepad.py b/tools/testing/selftests/hid/tests/base_gamepad.py new file mode 100644 index 00000000000000..ec74d75767a22b --- /dev/null +++ b/tools/testing/selftests/hid/tests/base_gamepad.py @@ -0,0 +1,238 @@ +# SPDX-License-Identifier: GPL-2.0 +import libevdev + +from .base_device import BaseDevice +from hidtools.util import BusType + + +class InvalidHIDCommunication(Exception): + pass + + +class GamepadData(object): + pass + + +class AxisMapping(object): + """Represents a mapping between a HID type + and an evdev event""" + + def __init__(self, hid, evdev=None): + self.hid = hid.lower() + + if evdev is None: + evdev = f"ABS_{hid.upper()}" + + self.evdev = libevdev.evbit("EV_ABS", evdev) + + +class BaseGamepad(BaseDevice): + buttons_map = { + 1: "BTN_SOUTH", + 2: "BTN_EAST", + 3: "BTN_C", + 4: "BTN_NORTH", + 5: "BTN_WEST", + 6: "BTN_Z", + 7: "BTN_TL", + 8: "BTN_TR", + 9: "BTN_TL2", + 10: "BTN_TR2", + 11: "BTN_SELECT", + 12: "BTN_START", + 13: "BTN_MODE", + 14: "BTN_THUMBL", + 15: "BTN_THUMBR", + } + + axes_map = { + "left_stick": { + "x": AxisMapping("x"), + "y": AxisMapping("y"), + }, + "right_stick": { + "x": AxisMapping("z"), + "y": AxisMapping("Rz"), + }, + } + + def __init__(self, rdesc, application="Game Pad", name=None, input_info=None): + assert rdesc is not None + super().__init__(name, application, input_info=input_info, rdesc=rdesc) + self.buttons = (1, 2, 3) + self._buttons = {} + self.left = (127, 127) + self.right = (127, 127) + self.hat_switch = 15 + assert self.parsed_rdesc is not None + + self.fields = [] + for r in self.parsed_rdesc.input_reports.values(): + if r.application_name == self.application: + self.fields.extend([f.usage_name for f in r]) + + def store_axes(self, which, gamepad, data): + amap = self.axes_map[which] + x, y = data + setattr(gamepad, amap["x"].hid, x) + setattr(gamepad, amap["y"].hid, y) + + def create_report( + self, + *, + left=(None, None), + right=(None, None), + hat_switch=None, + buttons=None, + reportID=None, + application="Game Pad", + ): + """ + Return an input report for this device. + + :param left: a tuple of absolute (x, y) value of the left joypad + where ``None`` is "leave unchanged" + :param right: a tuple of absolute (x, y) value of the right joypad + where ``None`` is "leave unchanged" + :param hat_switch: an absolute angular value of the hat switch + (expressed in 1/8 of circle, 0 being North, 2 East) + where ``None`` is "leave unchanged" + :param buttons: a dict of index/bool for the button states, + where ``None`` is "leave unchanged" + :param reportID: the numeric report ID for this report, if needed + :param application: the application used to report the values + """ + if buttons is not None: + for i, b in buttons.items(): + if i not in self.buttons: + raise InvalidHIDCommunication( + f"button {i} is not part of this {self.application}" + ) + if b is not None: + self._buttons[i] = b + + def replace_none_in_tuple(item, default): + if item is None: + item = (None, None) + + if None in item: + if item[0] is None: + item = (default[0], item[1]) + if item[1] is None: + item = (item[0], default[1]) + + return item + + right = replace_none_in_tuple(right, self.right) + self.right = right + left = replace_none_in_tuple(left, self.left) + self.left = left + + if hat_switch is None: + hat_switch = self.hat_switch + else: + self.hat_switch = hat_switch + + reportID = reportID or self.default_reportID + + gamepad = GamepadData() + for i, b in self._buttons.items(): + gamepad.__setattr__(f"b{i}", int(b) if b is not None else 0) + + self.store_axes("left_stick", gamepad, left) + self.store_axes("right_stick", gamepad, right) + gamepad.hatswitch = hat_switch # type: ignore ### gamepad is by default empty + return super().create_report( + gamepad, reportID=reportID, application=application + ) + + def event( + self, *, left=(None, None), right=(None, None), hat_switch=None, buttons=None + ): + """ + Send an input event on the default report ID. + + :param left: a tuple of absolute (x, y) value of the left joypad + where ``None`` is "leave unchanged" + :param right: a tuple of absolute (x, y) value of the right joypad + where ``None`` is "leave unchanged" + :param hat_switch: an absolute angular value of the hat switch + where ``None`` is "leave unchanged" + :param buttons: a dict of index/bool for the button states, + where ``None`` is "leave unchanged" + """ + r = self.create_report( + left=left, right=right, hat_switch=hat_switch, buttons=buttons + ) + self.call_input_event(r) + return [r] + + +class JoystickGamepad(BaseGamepad): + buttons_map = { + 1: "BTN_TRIGGER", + 2: "BTN_THUMB", + 3: "BTN_THUMB2", + 4: "BTN_TOP", + 5: "BTN_TOP2", + 6: "BTN_PINKIE", + 7: "BTN_BASE", + 8: "BTN_BASE2", + 9: "BTN_BASE3", + 10: "BTN_BASE4", + 11: "BTN_BASE5", + 12: "BTN_BASE6", + 13: "BTN_DEAD", + } + + axes_map = { + "left_stick": { + "x": AxisMapping("x"), + "y": AxisMapping("y"), + }, + "right_stick": { + "x": AxisMapping("rudder"), + "y": AxisMapping("throttle"), + }, + } + + def __init__(self, rdesc, application="Joystick", name=None, input_info=None): + super().__init__(rdesc, application, name, input_info) + + def create_report( + self, + *, + left=(None, None), + right=(None, None), + hat_switch=None, + buttons=None, + reportID=None, + application=None, + ): + """ + Return an input report for this device. + + :param left: a tuple of absolute (x, y) value of the left joypad + where ``None`` is "leave unchanged" + :param right: a tuple of absolute (x, y) value of the right joypad + where ``None`` is "leave unchanged" + :param hat_switch: an absolute angular value of the hat switch + where ``None`` is "leave unchanged" + :param buttons: a dict of index/bool for the button states, + where ``None`` is "leave unchanged" + :param reportID: the numeric report ID for this report, if needed + :param application: the application for this report, if needed + """ + if application is None: + application = "Joystick" + return super().create_report( + left=left, + right=right, + hat_switch=hat_switch, + buttons=buttons, + reportID=reportID, + application=application, + ) + + def store_right_joystick(self, gamepad, data): + gamepad.rudder, gamepad.throttle = data diff --git a/tools/testing/selftests/hid/tests/test_gamepad.py b/tools/testing/selftests/hid/tests/test_gamepad.py index 26c74040b796ae..8d5b5ffdae4950 100644 --- a/tools/testing/selftests/hid/tests/test_gamepad.py +++ b/tools/testing/selftests/hid/tests/test_gamepad.py @@ -10,7 +10,8 @@ from . import base import libevdev import pytest -from hidtools.device.base_gamepad import AsusGamepad, SaitekGamepad +from .base_gamepad import BaseGamepad, JoystickGamepad, AxisMapping +from hidtools.util import BusType import logging @@ -199,6 +200,449 @@ class BaseTest: ) +class SaitekGamepad(JoystickGamepad): + # fmt: off + report_descriptor = [ + 0x05, 0x01, # Usage Page (Generic Desktop) 0 + 0x09, 0x04, # Usage (Joystick) 2 + 0xa1, 0x01, # Collection (Application) 4 + 0x09, 0x01, # .Usage (Pointer) 6 + 0xa1, 0x00, # .Collection (Physical) 8 + 0x85, 0x01, # ..Report ID (1) 10 + 0x09, 0x30, # ..Usage (X) 12 + 0x15, 0x00, # ..Logical Minimum (0) 14 + 0x26, 0xff, 0x00, # ..Logical Maximum (255) 16 + 0x35, 0x00, # ..Physical Minimum (0) 19 + 0x46, 0xff, 0x00, # ..Physical Maximum (255) 21 + 0x75, 0x08, # ..Report Size (8) 24 + 0x95, 0x01, # ..Report Count (1) 26 + 0x81, 0x02, # ..Input (Data,Var,Abs) 28 + 0x09, 0x31, # ..Usage (Y) 30 + 0x81, 0x02, # ..Input (Data,Var,Abs) 32 + 0x05, 0x02, # ..Usage Page (Simulation Controls) 34 + 0x09, 0xba, # ..Usage (Rudder) 36 + 0x81, 0x02, # ..Input (Data,Var,Abs) 38 + 0x09, 0xbb, # ..Usage (Throttle) 40 + 0x81, 0x02, # ..Input (Data,Var,Abs) 42 + 0x05, 0x09, # ..Usage Page (Button) 44 + 0x19, 0x01, # ..Usage Minimum (1) 46 + 0x29, 0x0c, # ..Usage Maximum (12) 48 + 0x25, 0x01, # ..Logical Maximum (1) 50 + 0x45, 0x01, # ..Physical Maximum (1) 52 + 0x75, 0x01, # ..Report Size (1) 54 + 0x95, 0x0c, # ..Report Count (12) 56 + 0x81, 0x02, # ..Input (Data,Var,Abs) 58 + 0x95, 0x01, # ..Report Count (1) 60 + 0x75, 0x00, # ..Report Size (0) 62 + 0x81, 0x03, # ..Input (Cnst,Var,Abs) 64 + 0x05, 0x01, # ..Usage Page (Generic Desktop) 66 + 0x09, 0x39, # ..Usage (Hat switch) 68 + 0x25, 0x07, # ..Logical Maximum (7) 70 + 0x46, 0x3b, 0x01, # ..Physical Maximum (315) 72 + 0x55, 0x00, # ..Unit Exponent (0) 75 + 0x65, 0x44, # ..Unit (Degrees^4,EngRotation) 77 + 0x75, 0x04, # ..Report Size (4) 79 + 0x81, 0x42, # ..Input (Data,Var,Abs,Null) 81 + 0x65, 0x00, # ..Unit (None) 83 + 0xc0, # .End Collection 85 + 0x05, 0x0f, # .Usage Page (Vendor Usage Page 0x0f) 86 + 0x09, 0x92, # .Usage (Vendor Usage 0x92) 88 + 0xa1, 0x02, # .Collection (Logical) 90 + 0x85, 0x02, # ..Report ID (2) 92 + 0x09, 0xa0, # ..Usage (Vendor Usage 0xa0) 94 + 0x09, 0x9f, # ..Usage (Vendor Usage 0x9f) 96 + 0x25, 0x01, # ..Logical Maximum (1) 98 + 0x45, 0x00, # ..Physical Maximum (0) 100 + 0x75, 0x01, # ..Report Size (1) 102 + 0x95, 0x02, # ..Report Count (2) 104 + 0x81, 0x02, # ..Input (Data,Var,Abs) 106 + 0x75, 0x06, # ..Report Size (6) 108 + 0x95, 0x01, # ..Report Count (1) 110 + 0x81, 0x03, # ..Input (Cnst,Var,Abs) 112 + 0x09, 0x22, # ..Usage (Vendor Usage 0x22) 114 + 0x75, 0x07, # ..Report Size (7) 116 + 0x25, 0x7f, # ..Logical Maximum (127) 118 + 0x81, 0x02, # ..Input (Data,Var,Abs) 120 + 0x09, 0x94, # ..Usage (Vendor Usage 0x94) 122 + 0x75, 0x01, # ..Report Size (1) 124 + 0x25, 0x01, # ..Logical Maximum (1) 126 + 0x81, 0x02, # ..Input (Data,Var,Abs) 128 + 0xc0, # .End Collection 130 + 0x09, 0x21, # .Usage (Vendor Usage 0x21) 131 + 0xa1, 0x02, # .Collection (Logical) 133 + 0x85, 0x0b, # ..Report ID (11) 135 + 0x09, 0x22, # ..Usage (Vendor Usage 0x22) 137 + 0x26, 0xff, 0x00, # ..Logical Maximum (255) 139 + 0x75, 0x08, # ..Report Size (8) 142 + 0x91, 0x02, # ..Output (Data,Var,Abs) 144 + 0x09, 0x53, # ..Usage (Vendor Usage 0x53) 146 + 0x25, 0x0a, # ..Logical Maximum (10) 148 + 0x91, 0x02, # ..Output (Data,Var,Abs) 150 + 0x09, 0x50, # ..Usage (Vendor Usage 0x50) 152 + 0x27, 0xfe, 0xff, 0x00, 0x00, # ..Logical Maximum (65534) 154 + 0x47, 0xfe, 0xff, 0x00, 0x00, # ..Physical Maximum (65534) 159 + 0x75, 0x10, # ..Report Size (16) 164 + 0x55, 0xfd, # ..Unit Exponent (237) 166 + 0x66, 0x01, 0x10, # ..Unit (Seconds,SILinear) 168 + 0x91, 0x02, # ..Output (Data,Var,Abs) 171 + 0x55, 0x00, # ..Unit Exponent (0) 173 + 0x65, 0x00, # ..Unit (None) 175 + 0x09, 0x54, # ..Usage (Vendor Usage 0x54) 177 + 0x55, 0xfd, # ..Unit Exponent (237) 179 + 0x66, 0x01, 0x10, # ..Unit (Seconds,SILinear) 181 + 0x91, 0x02, # ..Output (Data,Var,Abs) 184 + 0x55, 0x00, # ..Unit Exponent (0) 186 + 0x65, 0x00, # ..Unit (None) 188 + 0x09, 0xa7, # ..Usage (Vendor Usage 0xa7) 190 + 0x55, 0xfd, # ..Unit Exponent (237) 192 + 0x66, 0x01, 0x10, # ..Unit (Seconds,SILinear) 194 + 0x91, 0x02, # ..Output (Data,Var,Abs) 197 + 0x55, 0x00, # ..Unit Exponent (0) 199 + 0x65, 0x00, # ..Unit (None) 201 + 0xc0, # .End Collection 203 + 0x09, 0x5a, # .Usage (Vendor Usage 0x5a) 204 + 0xa1, 0x02, # .Collection (Logical) 206 + 0x85, 0x0c, # ..Report ID (12) 208 + 0x09, 0x22, # ..Usage (Vendor Usage 0x22) 210 + 0x26, 0xff, 0x00, # ..Logical Maximum (255) 212 + 0x45, 0x00, # ..Physical Maximum (0) 215 + 0x75, 0x08, # ..Report Size (8) 217 + 0x91, 0x02, # ..Output (Data,Var,Abs) 219 + 0x09, 0x5c, # ..Usage (Vendor Usage 0x5c) 221 + 0x26, 0x10, 0x27, # ..Logical Maximum (10000) 223 + 0x46, 0x10, 0x27, # ..Physical Maximum (10000) 226 + 0x75, 0x10, # ..Report Size (16) 229 + 0x55, 0xfd, # ..Unit Exponent (237) 231 + 0x66, 0x01, 0x10, # ..Unit (Seconds,SILinear) 233 + 0x91, 0x02, # ..Output (Data,Var,Abs) 236 + 0x55, 0x00, # ..Unit Exponent (0) 238 + 0x65, 0x00, # ..Unit (None) 240 + 0x09, 0x5b, # ..Usage (Vendor Usage 0x5b) 242 + 0x25, 0x7f, # ..Logical Maximum (127) 244 + 0x75, 0x08, # ..Report Size (8) 246 + 0x91, 0x02, # ..Output (Data,Var,Abs) 248 + 0x09, 0x5e, # ..Usage (Vendor Usage 0x5e) 250 + 0x26, 0x10, 0x27, # ..Logical Maximum (10000) 252 + 0x75, 0x10, # ..Report Size (16) 255 + 0x55, 0xfd, # ..Unit Exponent (237) 257 + 0x66, 0x01, 0x10, # ..Unit (Seconds,SILinear) 259 + 0x91, 0x02, # ..Output (Data,Var,Abs) 262 + 0x55, 0x00, # ..Unit Exponent (0) 264 + 0x65, 0x00, # ..Unit (None) 266 + 0x09, 0x5d, # ..Usage (Vendor Usage 0x5d) 268 + 0x25, 0x7f, # ..Logical Maximum (127) 270 + 0x75, 0x08, # ..Report Size (8) 272 + 0x91, 0x02, # ..Output (Data,Var,Abs) 274 + 0xc0, # .End Collection 276 + 0x09, 0x73, # .Usage (Vendor Usage 0x73) 277 + 0xa1, 0x02, # .Collection (Logical) 279 + 0x85, 0x0d, # ..Report ID (13) 281 + 0x09, 0x22, # ..Usage (Vendor Usage 0x22) 283 + 0x26, 0xff, 0x00, # ..Logical Maximum (255) 285 + 0x45, 0x00, # ..Physical Maximum (0) 288 + 0x91, 0x02, # ..Output (Data,Var,Abs) 290 + 0x09, 0x70, # ..Usage (Vendor Usage 0x70) 292 + 0x15, 0x81, # ..Logical Minimum (-127) 294 + 0x25, 0x7f, # ..Logical Maximum (127) 296 + 0x36, 0xf0, 0xd8, # ..Physical Minimum (-10000) 298 + 0x46, 0x10, 0x27, # ..Physical Maximum (10000) 301 + 0x91, 0x02, # ..Output (Data,Var,Abs) 304 + 0xc0, # .End Collection 306 + 0x09, 0x6e, # .Usage (Vendor Usage 0x6e) 307 + 0xa1, 0x02, # .Collection (Logical) 309 + 0x85, 0x0e, # ..Report ID (14) 311 + 0x09, 0x22, # ..Usage (Vendor Usage 0x22) 313 + 0x15, 0x00, # ..Logical Minimum (0) 315 + 0x26, 0xff, 0x00, # ..Logical Maximum (255) 317 + 0x35, 0x00, # ..Physical Minimum (0) 320 + 0x45, 0x00, # ..Physical Maximum (0) 322 + 0x91, 0x02, # ..Output (Data,Var,Abs) 324 + 0x09, 0x70, # ..Usage (Vendor Usage 0x70) 326 + 0x25, 0x7f, # ..Logical Maximum (127) 328 + 0x46, 0x10, 0x27, # ..Physical Maximum (10000) 330 + 0x91, 0x02, # ..Output (Data,Var,Abs) 333 + 0x09, 0x6f, # ..Usage (Vendor Usage 0x6f) 335 + 0x15, 0x81, # ..Logical Minimum (-127) 337 + 0x36, 0xf0, 0xd8, # ..Physical Minimum (-10000) 339 + 0x91, 0x02, # ..Output (Data,Var,Abs) 342 + 0x09, 0x71, # ..Usage (Vendor Usage 0x71) 344 + 0x15, 0x00, # ..Logical Minimum (0) 346 + 0x26, 0xff, 0x00, # ..Logical Maximum (255) 348 + 0x35, 0x00, # ..Physical Minimum (0) 351 + 0x46, 0x68, 0x01, # ..Physical Maximum (360) 353 + 0x91, 0x02, # ..Output (Data,Var,Abs) 356 + 0x09, 0x72, # ..Usage (Vendor Usage 0x72) 358 + 0x75, 0x10, # ..Report Size (16) 360 + 0x26, 0x10, 0x27, # ..Logical Maximum (10000) 362 + 0x46, 0x10, 0x27, # ..Physical Maximum (10000) 365 + 0x55, 0xfd, # ..Unit Exponent (237) 368 + 0x66, 0x01, 0x10, # ..Unit (Seconds,SILinear) 370 + 0x91, 0x02, # ..Output (Data,Var,Abs) 373 + 0x55, 0x00, # ..Unit Exponent (0) 375 + 0x65, 0x00, # ..Unit (None) 377 + 0xc0, # .End Collection 379 + 0x09, 0x77, # .Usage (Vendor Usage 0x77) 380 + 0xa1, 0x02, # .Collection (Logical) 382 + 0x85, 0x51, # ..Report ID (81) 384 + 0x09, 0x22, # ..Usage (Vendor Usage 0x22) 386 + 0x25, 0x7f, # ..Logical Maximum (127) 388 + 0x45, 0x00, # ..Physical Maximum (0) 390 + 0x75, 0x08, # ..Report Size (8) 392 + 0x91, 0x02, # ..Output (Data,Var,Abs) 394 + 0x09, 0x78, # ..Usage (Vendor Usage 0x78) 396 + 0xa1, 0x02, # ..Collection (Logical) 398 + 0x09, 0x7b, # ...Usage (Vendor Usage 0x7b) 400 + 0x09, 0x79, # ...Usage (Vendor Usage 0x79) 402 + 0x09, 0x7a, # ...Usage (Vendor Usage 0x7a) 404 + 0x15, 0x01, # ...Logical Minimum (1) 406 + 0x25, 0x03, # ...Logical Maximum (3) 408 + 0x91, 0x00, # ...Output (Data,Arr,Abs) 410 + 0xc0, # ..End Collection 412 + 0x09, 0x7c, # ..Usage (Vendor Usage 0x7c) 413 + 0x15, 0x00, # ..Logical Minimum (0) 415 + 0x26, 0xfe, 0x00, # ..Logical Maximum (254) 417 + 0x91, 0x02, # ..Output (Data,Var,Abs) 420 + 0xc0, # .End Collection 422 + 0x09, 0x92, # .Usage (Vendor Usage 0x92) 423 + 0xa1, 0x02, # .Collection (Logical) 425 + 0x85, 0x52, # ..Report ID (82) 427 + 0x09, 0x96, # ..Usage (Vendor Usage 0x96) 429 + 0xa1, 0x02, # ..Collection (Logical) 431 + 0x09, 0x9a, # ...Usage (Vendor Usage 0x9a) 433 + 0x09, 0x99, # ...Usage (Vendor Usage 0x99) 435 + 0x09, 0x97, # ...Usage (Vendor Usage 0x97) 437 + 0x09, 0x98, # ...Usage (Vendor Usage 0x98) 439 + 0x09, 0x9b, # ...Usage (Vendor Usage 0x9b) 441 + 0x09, 0x9c, # ...Usage (Vendor Usage 0x9c) 443 + 0x15, 0x01, # ...Logical Minimum (1) 445 + 0x25, 0x06, # ...Logical Maximum (6) 447 + 0x91, 0x00, # ...Output (Data,Arr,Abs) 449 + 0xc0, # ..End Collection 451 + 0xc0, # .End Collection 452 + 0x05, 0xff, # .Usage Page (Vendor Usage Page 0xff) 453 + 0x0a, 0x01, 0x03, # .Usage (Vendor Usage 0x301) 455 + 0xa1, 0x02, # .Collection (Logical) 458 + 0x85, 0x40, # ..Report ID (64) 460 + 0x0a, 0x02, 0x03, # ..Usage (Vendor Usage 0x302) 462 + 0xa1, 0x02, # ..Collection (Logical) 465 + 0x1a, 0x11, 0x03, # ...Usage Minimum (785) 467 + 0x2a, 0x20, 0x03, # ...Usage Maximum (800) 470 + 0x25, 0x10, # ...Logical Maximum (16) 473 + 0x91, 0x00, # ...Output (Data,Arr,Abs) 475 + 0xc0, # ..End Collection 477 + 0x0a, 0x03, 0x03, # ..Usage (Vendor Usage 0x303) 478 + 0x15, 0x00, # ..Logical Minimum (0) 481 + 0x27, 0xff, 0xff, 0x00, 0x00, # ..Logical Maximum (65535) 483 + 0x75, 0x10, # ..Report Size (16) 488 + 0x91, 0x02, # ..Output (Data,Var,Abs) 490 + 0xc0, # .End Collection 492 + 0x05, 0x0f, # .Usage Page (Vendor Usage Page 0x0f) 493 + 0x09, 0x7d, # .Usage (Vendor Usage 0x7d) 495 + 0xa1, 0x02, # .Collection (Logical) 497 + 0x85, 0x43, # ..Report ID (67) 499 + 0x09, 0x7e, # ..Usage (Vendor Usage 0x7e) 501 + 0x26, 0x80, 0x00, # ..Logical Maximum (128) 503 + 0x46, 0x10, 0x27, # ..Physical Maximum (10000) 506 + 0x75, 0x08, # ..Report Size (8) 509 + 0x91, 0x02, # ..Output (Data,Var,Abs) 511 + 0xc0, # .End Collection 513 + 0x09, 0x7f, # .Usage (Vendor Usage 0x7f) 514 + 0xa1, 0x02, # .Collection (Logical) 516 + 0x85, 0x0b, # ..Report ID (11) 518 + 0x09, 0x80, # ..Usage (Vendor Usage 0x80) 520 + 0x26, 0xff, 0x7f, # ..Logical Maximum (32767) 522 + 0x45, 0x00, # ..Physical Maximum (0) 525 + 0x75, 0x0f, # ..Report Size (15) 527 + 0xb1, 0x03, # ..Feature (Cnst,Var,Abs) 529 + 0x09, 0xa9, # ..Usage (Vendor Usage 0xa9) 531 + 0x25, 0x01, # ..Logical Maximum (1) 533 + 0x75, 0x01, # ..Report Size (1) 535 + 0xb1, 0x03, # ..Feature (Cnst,Var,Abs) 537 + 0x09, 0x83, # ..Usage (Vendor Usage 0x83) 539 + 0x26, 0xff, 0x00, # ..Logical Maximum (255) 541 + 0x75, 0x08, # ..Report Size (8) 544 + 0xb1, 0x03, # ..Feature (Cnst,Var,Abs) 546 + 0xc0, # .End Collection 548 + 0x09, 0xab, # .Usage (Vendor Usage 0xab) 549 + 0xa1, 0x03, # .Collection (Report) 551 + 0x85, 0x15, # ..Report ID (21) 553 + 0x09, 0x25, # ..Usage (Vendor Usage 0x25) 555 + 0xa1, 0x02, # ..Collection (Logical) 557 + 0x09, 0x26, # ...Usage (Vendor Usage 0x26) 559 + 0x09, 0x30, # ...Usage (Vendor Usage 0x30) 561 + 0x09, 0x32, # ...Usage (Vendor Usage 0x32) 563 + 0x09, 0x31, # ...Usage (Vendor Usage 0x31) 565 + 0x09, 0x33, # ...Usage (Vendor Usage 0x33) 567 + 0x09, 0x34, # ...Usage (Vendor Usage 0x34) 569 + 0x15, 0x01, # ...Logical Minimum (1) 571 + 0x25, 0x06, # ...Logical Maximum (6) 573 + 0xb1, 0x00, # ...Feature (Data,Arr,Abs) 575 + 0xc0, # ..End Collection 577 + 0xc0, # .End Collection 578 + 0x09, 0x89, # .Usage (Vendor Usage 0x89) 579 + 0xa1, 0x03, # .Collection (Report) 581 + 0x85, 0x16, # ..Report ID (22) 583 + 0x09, 0x8b, # ..Usage (Vendor Usage 0x8b) 585 + 0xa1, 0x02, # ..Collection (Logical) 587 + 0x09, 0x8c, # ...Usage (Vendor Usage 0x8c) 589 + 0x09, 0x8d, # ...Usage (Vendor Usage 0x8d) 591 + 0x09, 0x8e, # ...Usage (Vendor Usage 0x8e) 593 + 0x25, 0x03, # ...Logical Maximum (3) 595 + 0xb1, 0x00, # ...Feature (Data,Arr,Abs) 597 + 0xc0, # ..End Collection 599 + 0x09, 0x22, # ..Usage (Vendor Usage 0x22) 600 + 0x15, 0x00, # ..Logical Minimum (0) 602 + 0x26, 0xfe, 0x00, # ..Logical Maximum (254) 604 + 0xb1, 0x02, # ..Feature (Data,Var,Abs) 607 + 0xc0, # .End Collection 609 + 0x09, 0x90, # .Usage (Vendor Usage 0x90) 610 + 0xa1, 0x03, # .Collection (Report) 612 + 0x85, 0x50, # ..Report ID (80) 614 + 0x09, 0x22, # ..Usage (Vendor Usage 0x22) 616 + 0x26, 0xff, 0x00, # ..Logical Maximum (255) 618 + 0x91, 0x02, # ..Output (Data,Var,Abs) 621 + 0xc0, # .End Collection 623 + 0xc0, # End Collection 624 + ] + # fmt: on + + def __init__(self, rdesc=report_descriptor, name=None): + super().__init__(rdesc, name=name, input_info=(BusType.USB, 0x06A3, 0xFF0D)) + self.buttons = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12) + + +class AsusGamepad(BaseGamepad): + # fmt: off + report_descriptor = [ + 0x05, 0x01, # Usage Page (Generic Desktop) 0 + 0x09, 0x05, # Usage (Game Pad) 2 + 0xa1, 0x01, # Collection (Application) 4 + 0x85, 0x01, # .Report ID (1) 6 + 0x05, 0x09, # .Usage Page (Button) 8 + 0x0a, 0x01, 0x00, # .Usage (Vendor Usage 0x01) 10 + 0x0a, 0x02, 0x00, # .Usage (Vendor Usage 0x02) 13 + 0x0a, 0x04, 0x00, # .Usage (Vendor Usage 0x04) 16 + 0x0a, 0x05, 0x00, # .Usage (Vendor Usage 0x05) 19 + 0x0a, 0x07, 0x00, # .Usage (Vendor Usage 0x07) 22 + 0x0a, 0x08, 0x00, # .Usage (Vendor Usage 0x08) 25 + 0x0a, 0x0e, 0x00, # .Usage (Vendor Usage 0x0e) 28 + 0x0a, 0x0f, 0x00, # .Usage (Vendor Usage 0x0f) 31 + 0x0a, 0x0d, 0x00, # .Usage (Vendor Usage 0x0d) 34 + 0x05, 0x0c, # .Usage Page (Consumer Devices) 37 + 0x0a, 0x24, 0x02, # .Usage (AC Back) 39 + 0x0a, 0x23, 0x02, # .Usage (AC Home) 42 + 0x15, 0x00, # .Logical Minimum (0) 45 + 0x25, 0x01, # .Logical Maximum (1) 47 + 0x75, 0x01, # .Report Size (1) 49 + 0x95, 0x0b, # .Report Count (11) 51 + 0x81, 0x02, # .Input (Data,Var,Abs) 53 + 0x75, 0x01, # .Report Size (1) 55 + 0x95, 0x01, # .Report Count (1) 57 + 0x81, 0x03, # .Input (Cnst,Var,Abs) 59 + 0x05, 0x01, # .Usage Page (Generic Desktop) 61 + 0x75, 0x04, # .Report Size (4) 63 + 0x95, 0x01, # .Report Count (1) 65 + 0x25, 0x07, # .Logical Maximum (7) 67 + 0x46, 0x3b, 0x01, # .Physical Maximum (315) 69 + 0x66, 0x14, 0x00, # .Unit (Degrees,EngRotation) 72 + 0x09, 0x39, # .Usage (Hat switch) 75 + 0x81, 0x42, # .Input (Data,Var,Abs,Null) 77 + 0x66, 0x00, 0x00, # .Unit (None) 79 + 0x09, 0x01, # .Usage (Pointer) 82 + 0xa1, 0x00, # .Collection (Physical) 84 + 0x09, 0x30, # ..Usage (X) 86 + 0x09, 0x31, # ..Usage (Y) 88 + 0x09, 0x32, # ..Usage (Z) 90 + 0x09, 0x35, # ..Usage (Rz) 92 + 0x05, 0x02, # ..Usage Page (Simulation Controls) 94 + 0x09, 0xc5, # ..Usage (Brake) 96 + 0x09, 0xc4, # ..Usage (Accelerator) 98 + 0x15, 0x00, # ..Logical Minimum (0) 100 + 0x26, 0xff, 0x00, # ..Logical Maximum (255) 102 + 0x35, 0x00, # ..Physical Minimum (0) 105 + 0x46, 0xff, 0x00, # ..Physical Maximum (255) 107 + 0x75, 0x08, # ..Report Size (8) 110 + 0x95, 0x06, # ..Report Count (6) 112 + 0x81, 0x02, # ..Input (Data,Var,Abs) 114 + 0xc0, # .End Collection 116 + 0x85, 0x02, # .Report ID (2) 117 + 0x05, 0x08, # .Usage Page (LEDs) 119 + 0x0a, 0x01, 0x00, # .Usage (Num Lock) 121 + 0x0a, 0x02, 0x00, # .Usage (Caps Lock) 124 + 0x0a, 0x03, 0x00, # .Usage (Scroll Lock) 127 + 0x0a, 0x04, 0x00, # .Usage (Compose) 130 + 0x15, 0x00, # .Logical Minimum (0) 133 + 0x25, 0x01, # .Logical Maximum (1) 135 + 0x75, 0x01, # .Report Size (1) 137 + 0x95, 0x04, # .Report Count (4) 139 + 0x91, 0x02, # .Output (Data,Var,Abs) 141 + 0x75, 0x04, # .Report Size (4) 143 + 0x95, 0x01, # .Report Count (1) 145 + 0x91, 0x03, # .Output (Cnst,Var,Abs) 147 + 0xc0, # End Collection 149 + 0x05, 0x0c, # Usage Page (Consumer Devices) 150 + 0x09, 0x01, # Usage (Consumer Control) 152 + 0xa1, 0x01, # Collection (Application) 154 + 0x85, 0x03, # .Report ID (3) 156 + 0x05, 0x01, # .Usage Page (Generic Desktop) 158 + 0x09, 0x06, # .Usage (Keyboard) 160 + 0xa1, 0x02, # .Collection (Logical) 162 + 0x05, 0x06, # ..Usage Page (Generic Device Controls) 164 + 0x09, 0x20, # ..Usage (Battery Strength) 166 + 0x15, 0x00, # ..Logical Minimum (0) 168 + 0x26, 0xff, 0x00, # ..Logical Maximum (255) 170 + 0x75, 0x08, # ..Report Size (8) 173 + 0x95, 0x01, # ..Report Count (1) 175 + 0x81, 0x02, # ..Input (Data,Var,Abs) 177 + 0x06, 0xbc, 0xff, # ..Usage Page (Vendor Usage Page 0xffbc) 179 + 0x0a, 0xad, 0xbd, # ..Usage (Vendor Usage 0xbdad) 182 + 0x75, 0x08, # ..Report Size (8) 185 + 0x95, 0x06, # ..Report Count (6) 187 + 0x81, 0x02, # ..Input (Data,Var,Abs) 189 + 0xc0, # .End Collection 191 + 0xc0, # End Collection 192 + ] + # fmt: on + + def __init__(self, rdesc=report_descriptor, name=None): + super().__init__(rdesc, name=name, input_info=(BusType.USB, 0x18D1, 0x2C40)) + self.buttons = (1, 2, 4, 5, 7, 8, 14, 15, 13) + + +class RaptorMach2Joystick(JoystickGamepad): + axes_map = { + "left_stick": { + "x": AxisMapping("x"), + "y": AxisMapping("y"), + }, + "right_stick": { + "x": AxisMapping("z"), + "y": AxisMapping("Rz"), + }, + } + + def __init__( + self, + name, + rdesc=None, + application="Joystick", + input_info=(BusType.USB, 0x11C0, 0x5606), + ): + super().__init__(rdesc, application, name, input_info) + self.buttons = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12) + self.hat_switch = 240 # null value is 240 as max is 239 + + def event( + self, *, left=(None, None), right=(None, None), hat_switch=None, buttons=None + ): + if hat_switch is not None: + hat_switch *= 30 + + return super().event( + left=left, right=right, hat_switch=hat_switch, buttons=buttons + ) + + class TestSaitekGamepad(BaseTest.TestGamepad): def create_device(self): return SaitekGamepad() @@ -207,3 +651,14 @@ class TestSaitekGamepad(BaseTest.TestGamepad): class TestAsusGamepad(BaseTest.TestGamepad): def create_device(self): return AsusGamepad() + + +class TestRaptorMach2Joystick(BaseTest.TestGamepad): + hid_bpfs = [("FR-TEC__Raptor-Mach-2.bpf.o", True)] + + def create_device(self): + return RaptorMach2Joystick( + "uhid test Sanmos Group FR-TEC Raptor MACH 2", + rdesc="05 01 09 04 a1 01 05 01 85 01 05 01 09 30 75 10 95 01 15 00 26 ff 07 46 ff 07 81 02 05 01 09 31 75 10 95 01 15 00 26 ff 07 46 ff 07 81 02 05 01 09 33 75 10 95 01 15 00 26 ff 03 46 ff 03 81 02 05 00 09 00 75 10 95 01 15 00 26 ff 03 46 ff 03 81 02 05 01 09 32 75 10 95 01 15 00 26 ff 03 46 ff 03 81 02 05 01 09 35 75 10 95 01 15 00 26 ff 03 46 ff 03 81 02 05 01 09 34 75 10 95 01 15 00 26 ff 07 46 ff 07 81 02 05 01 09 36 75 10 95 01 15 00 26 ff 03 46 ff 03 81 02 05 09 19 01 2a 1d 00 15 00 25 01 75 01 96 80 00 81 02 05 01 09 39 26 ef 00 46 68 01 65 14 75 10 95 01 81 42 05 01 09 00 75 08 95 1d 81 01 15 00 26 ef 00 85 58 26 ff 00 46 ff 00 75 08 95 3f 09 00 91 02 85 59 75 08 95 80 09 00 b1 02 c0", + input_info=(BusType.USB, 0x11C0, 0x5606), + ) diff --git a/tools/testing/selftests/hid/tests/test_tablet.py b/tools/testing/selftests/hid/tests/test_tablet.py index 903f19f7cbe9f4..a9e2de1e8861b2 100644 --- a/tools/testing/selftests/hid/tests/test_tablet.py +++ b/tools/testing/selftests/hid/tests/test_tablet.py @@ -35,6 +35,7 @@ class BtnPressed(Enum): PRIMARY_PRESSED = libevdev.EV_KEY.BTN_STYLUS SECONDARY_PRESSED = libevdev.EV_KEY.BTN_STYLUS2 + THIRD_PRESSED = libevdev.EV_KEY.BTN_STYLUS3 class PenState(Enum): @@ -44,58 +45,28 @@ class PenState(Enum): We extend it with the various buttons when we need to check them. """ - PEN_IS_OUT_OF_RANGE = BtnTouch.UP, None, None - PEN_IS_IN_RANGE = BtnTouch.UP, ToolType.PEN, None - PEN_IS_IN_RANGE_WITH_BUTTON = BtnTouch.UP, ToolType.PEN, BtnPressed.PRIMARY_PRESSED - PEN_IS_IN_RANGE_WITH_SECOND_BUTTON = ( - BtnTouch.UP, - ToolType.PEN, - BtnPressed.SECONDARY_PRESSED, - ) - PEN_IS_IN_CONTACT = BtnTouch.DOWN, ToolType.PEN, None - PEN_IS_IN_CONTACT_WITH_BUTTON = ( - BtnTouch.DOWN, - ToolType.PEN, - BtnPressed.PRIMARY_PRESSED, - ) - PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON = ( - BtnTouch.DOWN, - ToolType.PEN, - BtnPressed.SECONDARY_PRESSED, - ) - PEN_IS_IN_RANGE_WITH_ERASING_INTENT = BtnTouch.UP, ToolType.RUBBER, None - PEN_IS_IN_RANGE_WITH_ERASING_INTENT_WITH_BUTTON = ( - BtnTouch.UP, - ToolType.RUBBER, - BtnPressed.PRIMARY_PRESSED, - ) - PEN_IS_IN_RANGE_WITH_ERASING_INTENT_WITH_SECOND_BUTTON = ( - BtnTouch.UP, - ToolType.RUBBER, - BtnPressed.SECONDARY_PRESSED, - ) - PEN_IS_ERASING = BtnTouch.DOWN, ToolType.RUBBER, None - PEN_IS_ERASING_WITH_BUTTON = ( - BtnTouch.DOWN, - ToolType.RUBBER, - BtnPressed.PRIMARY_PRESSED, - ) - PEN_IS_ERASING_WITH_SECOND_BUTTON = ( - BtnTouch.DOWN, - ToolType.RUBBER, - BtnPressed.SECONDARY_PRESSED, - ) - - def __init__(self, touch: BtnTouch, tool: Optional[ToolType], button: Optional[BtnPressed]): + PEN_IS_OUT_OF_RANGE = BtnTouch.UP, None, False + PEN_IS_IN_RANGE = BtnTouch.UP, ToolType.PEN, False + PEN_IS_IN_RANGE_WITH_BUTTON = BtnTouch.UP, ToolType.PEN, True + PEN_IS_IN_CONTACT = BtnTouch.DOWN, ToolType.PEN, False + PEN_IS_IN_CONTACT_WITH_BUTTON = BtnTouch.DOWN, ToolType.PEN, True + PEN_IS_IN_RANGE_WITH_ERASING_INTENT = BtnTouch.UP, ToolType.RUBBER, False + PEN_IS_IN_RANGE_WITH_ERASING_INTENT_WITH_BUTTON = BtnTouch.UP, ToolType.RUBBER, True + PEN_IS_ERASING = BtnTouch.DOWN, ToolType.RUBBER, False + PEN_IS_ERASING_WITH_BUTTON = BtnTouch.DOWN, ToolType.RUBBER, True + + def __init__( + self, touch: BtnTouch, tool: Optional[ToolType], button: Optional[bool] + ): self.touch = touch # type: ignore self.tool = tool # type: ignore self.button = button # type: ignore @classmethod - def from_evdev(cls, evdev) -> "PenState": + def from_evdev(cls, evdev, test_button) -> "PenState": touch = BtnTouch(evdev.value[libevdev.EV_KEY.BTN_TOUCH]) tool = None - button = None + button = False if ( evdev.value[libevdev.EV_KEY.BTN_TOOL_RUBBER] and not evdev.value[libevdev.EV_KEY.BTN_TOOL_PEN] @@ -112,19 +83,20 @@ class PenState(Enum): ): raise ValueError("2 tools are not allowed") - # we take only the highest button in account - for b in [libevdev.EV_KEY.BTN_STYLUS, libevdev.EV_KEY.BTN_STYLUS2]: - if bool(evdev.value[b]): - button = BtnPressed(b) + # we take only the provided button into account + if test_button is not None: + button = bool(evdev.value[test_button.value]) # the kernel tends to insert an EV_SYN once removing the tool, so # the button will be released after if tool is None: - button = None + button = False return cls((touch, tool, button)) # type: ignore - def apply(self, events: List[libevdev.InputEvent], strict: bool) -> "PenState": + def apply( + self, events: List[libevdev.InputEvent], strict: bool, test_button: BtnPressed + ) -> "PenState": if libevdev.EV_SYN.SYN_REPORT in events: raise ValueError("EV_SYN is in the event sequence") touch = self.touch @@ -148,19 +120,16 @@ class PenState(Enum): raise ValueError(f"duplicated BTN_TOOL_* in {events}") tool_found = True tool = ToolType(ev.code) if ev.value else None - elif ev in ( - libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS), - libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS2), - ): + elif test_button is not None and ev in (test_button.value,): if button_found: raise ValueError(f"duplicated BTN_STYLUS* in {events}") button_found = True - button = BtnPressed(ev.code) if ev.value else None + button = bool(ev.value) # the kernel tends to insert an EV_SYN once removing the tool, so # the button will be released after if tool is None: - button = None + button = False new_state = PenState((touch, tool, button)) # type: ignore if strict: @@ -183,11 +152,9 @@ class PenState(Enum): PenState.PEN_IS_OUT_OF_RANGE, PenState.PEN_IS_IN_RANGE, PenState.PEN_IS_IN_RANGE_WITH_BUTTON, - PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON, PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT, PenState.PEN_IS_IN_CONTACT, PenState.PEN_IS_IN_CONTACT_WITH_BUTTON, - PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON, PenState.PEN_IS_ERASING, ) @@ -195,7 +162,6 @@ class PenState(Enum): return ( PenState.PEN_IS_IN_RANGE, PenState.PEN_IS_IN_RANGE_WITH_BUTTON, - PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON, PenState.PEN_IS_OUT_OF_RANGE, PenState.PEN_IS_IN_CONTACT, ) @@ -204,7 +170,6 @@ class PenState(Enum): return ( PenState.PEN_IS_IN_CONTACT, PenState.PEN_IS_IN_CONTACT_WITH_BUTTON, - PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON, PenState.PEN_IS_IN_RANGE, ) @@ -236,21 +201,6 @@ class PenState(Enum): PenState.PEN_IS_IN_RANGE_WITH_BUTTON, ) - if self == PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON: - return ( - PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON, - PenState.PEN_IS_IN_RANGE, - PenState.PEN_IS_OUT_OF_RANGE, - PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON, - ) - - if self == PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON: - return ( - PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON, - PenState.PEN_IS_IN_CONTACT, - PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON, - ) - return tuple() def historically_tolerated_transitions(self) -> Tuple["PenState", ...]: @@ -263,11 +213,9 @@ class PenState(Enum): PenState.PEN_IS_OUT_OF_RANGE, PenState.PEN_IS_IN_RANGE, PenState.PEN_IS_IN_RANGE_WITH_BUTTON, - PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON, PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT, PenState.PEN_IS_IN_CONTACT, PenState.PEN_IS_IN_CONTACT_WITH_BUTTON, - PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON, PenState.PEN_IS_ERASING, ) @@ -275,7 +223,6 @@ class PenState(Enum): return ( PenState.PEN_IS_IN_RANGE, PenState.PEN_IS_IN_RANGE_WITH_BUTTON, - PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON, PenState.PEN_IS_OUT_OF_RANGE, PenState.PEN_IS_IN_CONTACT, ) @@ -284,7 +231,6 @@ class PenState(Enum): return ( PenState.PEN_IS_IN_CONTACT, PenState.PEN_IS_IN_CONTACT_WITH_BUTTON, - PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON, PenState.PEN_IS_IN_RANGE, PenState.PEN_IS_OUT_OF_RANGE, ) @@ -319,22 +265,6 @@ class PenState(Enum): PenState.PEN_IS_OUT_OF_RANGE, ) - if self == PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON: - return ( - PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON, - PenState.PEN_IS_IN_RANGE, - PenState.PEN_IS_OUT_OF_RANGE, - PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON, - ) - - if self == PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON: - return ( - PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON, - PenState.PEN_IS_IN_CONTACT, - PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON, - PenState.PEN_IS_OUT_OF_RANGE, - ) - return tuple() @staticmethod @@ -402,9 +332,9 @@ class PenState(Enum): } @staticmethod - def legal_transitions_with_primary_button() -> Dict[str, Tuple["PenState", ...]]: + def legal_transitions_with_button() -> Dict[str, Tuple["PenState", ...]]: """We revisit the Windows Pen Implementation state machine: - we now have a primary button. + we now have a button. """ return { "hover-button": (PenState.PEN_IS_IN_RANGE_WITH_BUTTON,), @@ -451,56 +381,6 @@ class PenState(Enum): } @staticmethod - def legal_transitions_with_secondary_button() -> Dict[str, Tuple["PenState", ...]]: - """We revisit the Windows Pen Implementation state machine: - we now have a secondary button. - Note: we don't looks for 2 buttons interactions. - """ - return { - "hover-button": (PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON,), - "hover-button -> out-of-range": ( - PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON, - PenState.PEN_IS_OUT_OF_RANGE, - ), - "in-range -> button-press": ( - PenState.PEN_IS_IN_RANGE, - PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON, - ), - "in-range -> button-press -> button-release": ( - PenState.PEN_IS_IN_RANGE, - PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON, - PenState.PEN_IS_IN_RANGE, - ), - "in-range -> touch -> button-press -> button-release": ( - PenState.PEN_IS_IN_RANGE, - PenState.PEN_IS_IN_CONTACT, - PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON, - PenState.PEN_IS_IN_CONTACT, - ), - "in-range -> touch -> button-press -> release -> button-release": ( - PenState.PEN_IS_IN_RANGE, - PenState.PEN_IS_IN_CONTACT, - PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON, - PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON, - PenState.PEN_IS_IN_RANGE, - ), - "in-range -> button-press -> touch -> release -> button-release": ( - PenState.PEN_IS_IN_RANGE, - PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON, - PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON, - PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON, - PenState.PEN_IS_IN_RANGE, - ), - "in-range -> button-press -> touch -> button-release -> release": ( - PenState.PEN_IS_IN_RANGE, - PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON, - PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON, - PenState.PEN_IS_IN_CONTACT, - PenState.PEN_IS_IN_RANGE, - ), - } - - @staticmethod def tolerated_transitions() -> Dict[str, Tuple["PenState", ...]]: """This is not adhering to the Windows Pen Implementation state machine but we should expect the kernel to behave properly, mostly for historical @@ -616,10 +496,22 @@ class Pen(object): evdev.value[axis] == value ), f"assert evdev.value[{axis}] ({evdev.value[axis]}) != {value}" - def assert_expected_input_events(self, evdev): + def assert_expected_input_events(self, evdev, button): assert evdev.value[libevdev.EV_ABS.ABS_X] == self.x assert evdev.value[libevdev.EV_ABS.ABS_Y] == self.y - assert self.current_state == PenState.from_evdev(evdev) + + # assert no other buttons than the tested ones are set + buttons = [ + BtnPressed.PRIMARY_PRESSED, + BtnPressed.SECONDARY_PRESSED, + BtnPressed.THIRD_PRESSED, + ] + if button is not None: + buttons.remove(button) + for b in buttons: + assert evdev.value[b.value] is None or evdev.value[b.value] == False + + assert self.current_state == PenState.from_evdev(evdev, button) class PenDigitizer(base.UHIDTestDevice): @@ -647,7 +539,7 @@ class PenDigitizer(base.UHIDTestDevice): continue self.fields = [f.usage_name for f in r] - def move_to(self, pen, state): + def move_to(self, pen, state, button): # fill in the previous values if pen.current_state == PenState.PEN_IS_OUT_OF_RANGE: pen.restore() @@ -690,29 +582,17 @@ class PenDigitizer(base.UHIDTestDevice): pen.inrange = True pen.invert = False pen.eraser = False - pen.barrelswitch = True - pen.secondarybarrelswitch = False + assert button is not None + pen.barrelswitch = button == BtnPressed.PRIMARY_PRESSED + pen.secondarybarrelswitch = button == BtnPressed.SECONDARY_PRESSED elif state == PenState.PEN_IS_IN_CONTACT_WITH_BUTTON: pen.tipswitch = True pen.inrange = True pen.invert = False pen.eraser = False - pen.barrelswitch = True - pen.secondarybarrelswitch = False - elif state == PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON: - pen.tipswitch = False - pen.inrange = True - pen.invert = False - pen.eraser = False - pen.barrelswitch = False - pen.secondarybarrelswitch = True - elif state == PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON: - pen.tipswitch = True - pen.inrange = True - pen.invert = False - pen.eraser = False - pen.barrelswitch = False - pen.secondarybarrelswitch = True + assert button is not None + pen.barrelswitch = button == BtnPressed.PRIMARY_PRESSED + pen.secondarybarrelswitch = button == BtnPressed.SECONDARY_PRESSED elif state == PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT: pen.tipswitch = False pen.inrange = True @@ -730,7 +610,7 @@ class PenDigitizer(base.UHIDTestDevice): pen.current_state = state - def event(self, pen): + def event(self, pen, button): rs = [] r = self.create_report(application=self.cur_application, data=pen) self.call_input_event(r) @@ -771,17 +651,17 @@ class BaseTest: def create_device(self): raise Exception("please reimplement me in subclasses") - def post(self, uhdev, pen): - r = uhdev.event(pen) + def post(self, uhdev, pen, test_button): + r = uhdev.event(pen, test_button) events = uhdev.next_sync_events() self.debug_reports(r, uhdev, events) return events def validate_transitions( - self, from_state, pen, evdev, events, allow_intermediate_states + self, from_state, pen, evdev, events, allow_intermediate_states, button ): # check that the final state is correct - pen.assert_expected_input_events(evdev) + pen.assert_expected_input_events(evdev, button) state = from_state @@ -794,12 +674,14 @@ class BaseTest: events = events[idx + 1 :] # now check for a valid transition - state = state.apply(sync_events, not allow_intermediate_states) + state = state.apply(sync_events, not allow_intermediate_states, button) if events: - state = state.apply(sync_events, not allow_intermediate_states) + state = state.apply(sync_events, not allow_intermediate_states, button) - def _test_states(self, state_list, scribble, allow_intermediate_states): + def _test_states( + self, state_list, scribble, allow_intermediate_states, button=None + ): """Internal method to test against a list of transition between states. state_list is a list of PenState objects @@ -812,10 +694,10 @@ class BaseTest: cur_state = PenState.PEN_IS_OUT_OF_RANGE p = Pen(50, 60) - uhdev.move_to(p, PenState.PEN_IS_OUT_OF_RANGE) - events = self.post(uhdev, p) + uhdev.move_to(p, PenState.PEN_IS_OUT_OF_RANGE, button) + events = self.post(uhdev, p, button) self.validate_transitions( - cur_state, p, evdev, events, allow_intermediate_states + cur_state, p, evdev, events, allow_intermediate_states, button ) cur_state = p.current_state @@ -824,18 +706,18 @@ class BaseTest: if scribble and cur_state != PenState.PEN_IS_OUT_OF_RANGE: p.x += 1 p.y -= 1 - events = self.post(uhdev, p) + events = self.post(uhdev, p, button) self.validate_transitions( - cur_state, p, evdev, events, allow_intermediate_states + cur_state, p, evdev, events, allow_intermediate_states, button ) assert len(events) >= 3 # X, Y, SYN - uhdev.move_to(p, state) + uhdev.move_to(p, state, button) if scribble and state != PenState.PEN_IS_OUT_OF_RANGE: p.x += 1 p.y -= 1 - events = self.post(uhdev, p) + events = self.post(uhdev, p, button) self.validate_transitions( - cur_state, p, evdev, events, allow_intermediate_states + cur_state, p, evdev, events, allow_intermediate_states, button ) cur_state = p.current_state @@ -874,12 +756,17 @@ class BaseTest: "state_list", [ pytest.param(v, id=k) - for k, v in PenState.legal_transitions_with_primary_button().items() + for k, v in PenState.legal_transitions_with_button().items() ], ) def test_valid_primary_button_pen_states(self, state_list, scribble): """Rework the transition state machine by adding the primary button.""" - self._test_states(state_list, scribble, allow_intermediate_states=False) + self._test_states( + state_list, + scribble, + allow_intermediate_states=False, + button=BtnPressed.PRIMARY_PRESSED, + ) @pytest.mark.skip_if_uhdev( lambda uhdev: "Secondary Barrel Switch" not in uhdev.fields, @@ -890,12 +777,38 @@ class BaseTest: "state_list", [ pytest.param(v, id=k) - for k, v in PenState.legal_transitions_with_secondary_button().items() + for k, v in PenState.legal_transitions_with_button().items() ], ) def test_valid_secondary_button_pen_states(self, state_list, scribble): """Rework the transition state machine by adding the secondary button.""" - self._test_states(state_list, scribble, allow_intermediate_states=False) + self._test_states( + state_list, + scribble, + allow_intermediate_states=False, + button=BtnPressed.SECONDARY_PRESSED, + ) + + @pytest.mark.skip_if_uhdev( + lambda uhdev: "Third Barrel Switch" not in uhdev.fields, + "Device not compatible, missing Third Barrel Switch usage", + ) + @pytest.mark.parametrize("scribble", [True, False], ids=["scribble", "static"]) + @pytest.mark.parametrize( + "state_list", + [ + pytest.param(v, id=k) + for k, v in PenState.legal_transitions_with_button().items() + ], + ) + def test_valid_third_button_pen_states(self, state_list, scribble): + """Rework the transition state machine by adding the secondary button.""" + self._test_states( + state_list, + scribble, + allow_intermediate_states=False, + button=BtnPressed.THIRD_PRESSED, + ) @pytest.mark.skip_if_uhdev( lambda uhdev: "Invert" not in uhdev.fields, @@ -956,7 +869,7 @@ class BaseTest: class GXTP_pen(PenDigitizer): - def event(self, pen): + def event(self, pen, test_button): if not hasattr(self, "prev_tip_state"): self.prev_tip_state = False @@ -977,13 +890,407 @@ class GXTP_pen(PenDigitizer): if pen.eraser: internal_pen.invert = False - return super().event(internal_pen) + return super().event(internal_pen, test_button) class USIPen(PenDigitizer): pass +class XPPen_ArtistPro16Gen2_28bd_095b(PenDigitizer): + """ + Pen with two buttons and a rubber end, but which reports + the second button as an eraser + """ + + def __init__( + self, + name, + rdesc_str=None, + rdesc=None, + application="Pen", + physical="Stylus", + input_info=(BusType.USB, 0x28BD, 0x095B), + evdev_name_suffix=None, + ): + super().__init__( + name, rdesc_str, rdesc, application, physical, input_info, evdev_name_suffix + ) + self.fields.append("Secondary Barrel Switch") + + def move_to(self, pen, state, button): + # fill in the previous values + if pen.current_state == PenState.PEN_IS_OUT_OF_RANGE: + pen.restore() + + print(f"\n *** pen is moving to {state} ***") + + if state == PenState.PEN_IS_OUT_OF_RANGE: + pen.backup() + pen.x = 0 + pen.y = 0 + pen.tipswitch = False + pen.tippressure = 0 + pen.azimuth = 0 + pen.inrange = False + pen.width = 0 + pen.height = 0 + pen.invert = False + pen.eraser = False + pen.xtilt = 0 + pen.ytilt = 0 + pen.twist = 0 + pen.barrelswitch = False + elif state == PenState.PEN_IS_IN_RANGE: + pen.tipswitch = False + pen.inrange = True + pen.invert = False + pen.eraser = False + pen.barrelswitch = False + elif state == PenState.PEN_IS_IN_CONTACT: + pen.tipswitch = True + pen.inrange = True + pen.invert = False + pen.eraser = False + pen.barrelswitch = False + elif state == PenState.PEN_IS_IN_RANGE_WITH_BUTTON: + pen.tipswitch = False + pen.inrange = True + pen.invert = False + assert button is not None + pen.barrelswitch = button == BtnPressed.PRIMARY_PRESSED + pen.eraser = button == BtnPressed.SECONDARY_PRESSED + elif state == PenState.PEN_IS_IN_CONTACT_WITH_BUTTON: + pen.tipswitch = True + pen.inrange = True + pen.invert = False + assert button is not None + pen.barrelswitch = button == BtnPressed.PRIMARY_PRESSED + pen.eraser = button == BtnPressed.SECONDARY_PRESSED + elif state == PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT: + pen.tipswitch = False + pen.inrange = True + pen.invert = True + pen.eraser = False + pen.barrelswitch = False + elif state == PenState.PEN_IS_ERASING: + pen.tipswitch = True + pen.inrange = True + pen.invert = True + pen.eraser = False + pen.barrelswitch = False + + pen.current_state = state + + def event(self, pen, test_button): + import math + + pen_copy = copy.copy(pen) + width = 13.567 + height = 8.480 + tip_height = 0.055677699 + hx = tip_height * (32767 / width) + hy = tip_height * (32767 / height) + if pen_copy.xtilt != 0: + pen_copy.x += round(hx * math.sin(math.radians(pen_copy.xtilt))) + if pen_copy.ytilt != 0: + pen_copy.y += round(hy * math.sin(math.radians(pen_copy.ytilt))) + + return super().event(pen_copy, test_button) + + +class XPPen_Artist24_28bd_093a(PenDigitizer): + """ + Pen that reports secondary barrel switch through eraser + """ + + def __init__( + self, + name, + rdesc_str=None, + rdesc=None, + application="Pen", + physical="Stylus", + input_info=(BusType.USB, 0x28BD, 0x093A), + evdev_name_suffix=None, + ): + super().__init__( + name, rdesc_str, rdesc, application, physical, input_info, evdev_name_suffix + ) + self.fields.append("Secondary Barrel Switch") + self.previous_state = PenState.PEN_IS_OUT_OF_RANGE + + def move_to(self, pen, state, button, debug=True): + # fill in the previous values + if pen.current_state == PenState.PEN_IS_OUT_OF_RANGE: + pen.restore() + + if debug: + print(f"\n *** pen is moving to {state} ***") + + if state == PenState.PEN_IS_OUT_OF_RANGE: + pen.backup() + pen.tipswitch = False + pen.tippressure = 0 + pen.azimuth = 0 + pen.inrange = False + pen.width = 0 + pen.height = 0 + pen.invert = False + pen.eraser = False + pen.xtilt = 0 + pen.ytilt = 0 + pen.twist = 0 + pen.barrelswitch = False + elif state == PenState.PEN_IS_IN_RANGE: + pen.tipswitch = False + pen.inrange = True + pen.invert = False + pen.eraser = False + pen.barrelswitch = False + elif state == PenState.PEN_IS_IN_CONTACT: + pen.tipswitch = True + pen.inrange = True + pen.invert = False + pen.eraser = False + pen.barrelswitch = False + elif state == PenState.PEN_IS_IN_RANGE_WITH_BUTTON: + pen.tipswitch = False + pen.inrange = True + pen.invert = False + assert button is not None + pen.barrelswitch = button == BtnPressed.PRIMARY_PRESSED + pen.eraser = button == BtnPressed.SECONDARY_PRESSED + elif state == PenState.PEN_IS_IN_CONTACT_WITH_BUTTON: + pen.tipswitch = True + pen.inrange = True + pen.invert = False + assert button is not None + pen.barrelswitch = button == BtnPressed.PRIMARY_PRESSED + pen.eraser = button == BtnPressed.SECONDARY_PRESSED + + pen.current_state = state + + def send_intermediate_state(self, pen, state, button): + intermediate_pen = copy.copy(pen) + self.move_to(intermediate_pen, state, button, debug=False) + return super().event(intermediate_pen, button) + + def event(self, pen, button): + rs = [] + + # the pen reliably sends in-range events in a normal case (non emulation of eraser mode) + if self.previous_state == PenState.PEN_IS_IN_CONTACT: + if pen.current_state == PenState.PEN_IS_OUT_OF_RANGE: + rs.extend( + self.send_intermediate_state(pen, PenState.PEN_IS_IN_RANGE, button) + ) + + if button == BtnPressed.SECONDARY_PRESSED: + if self.previous_state == PenState.PEN_IS_IN_RANGE: + if pen.current_state == PenState.PEN_IS_IN_RANGE_WITH_BUTTON: + rs.extend( + self.send_intermediate_state( + pen, PenState.PEN_IS_OUT_OF_RANGE, button + ) + ) + + if self.previous_state == PenState.PEN_IS_IN_RANGE_WITH_BUTTON: + if pen.current_state == PenState.PEN_IS_IN_RANGE: + rs.extend( + self.send_intermediate_state( + pen, PenState.PEN_IS_OUT_OF_RANGE, button + ) + ) + + if self.previous_state == PenState.PEN_IS_IN_CONTACT: + if pen.current_state == PenState.PEN_IS_IN_CONTACT_WITH_BUTTON: + rs.extend( + self.send_intermediate_state( + pen, PenState.PEN_IS_OUT_OF_RANGE, button + ) + ) + rs.extend( + self.send_intermediate_state( + pen, PenState.PEN_IS_IN_RANGE_WITH_BUTTON, button + ) + ) + + if self.previous_state == PenState.PEN_IS_IN_CONTACT_WITH_BUTTON: + if pen.current_state == PenState.PEN_IS_IN_CONTACT: + rs.extend( + self.send_intermediate_state( + pen, PenState.PEN_IS_OUT_OF_RANGE, button + ) + ) + rs.extend( + self.send_intermediate_state( + pen, PenState.PEN_IS_IN_RANGE, button + ) + ) + + rs.extend(super().event(pen, button)) + self.previous_state = pen.current_state + return rs + + +class Huion_Kamvas_Pro_19_256c_006b(PenDigitizer): + """ + Pen that reports secondary barrel switch through secondary TipSwtich + and 3rd button through Invert + """ + + def __init__( + self, + name, + rdesc_str=None, + rdesc=None, + application="Stylus", + physical=None, + input_info=(BusType.USB, 0x256C, 0x006B), + evdev_name_suffix=None, + ): + super().__init__( + name, rdesc_str, rdesc, application, physical, input_info, evdev_name_suffix + ) + self.fields.append("Secondary Barrel Switch") + self.fields.append("Third Barrel Switch") + self.previous_state = PenState.PEN_IS_OUT_OF_RANGE + + def move_to(self, pen, state, button, debug=True): + # fill in the previous values + if pen.current_state == PenState.PEN_IS_OUT_OF_RANGE: + pen.restore() + + if debug: + print(f"\n *** pen is moving to {state} ***") + + if state == PenState.PEN_IS_OUT_OF_RANGE: + pen.backup() + pen.tipswitch = False + pen.tippressure = 0 + pen.azimuth = 0 + pen.inrange = False + pen.width = 0 + pen.height = 0 + pen.invert = False + pen.eraser = False + pen.xtilt = 0 + pen.ytilt = 0 + pen.twist = 0 + pen.barrelswitch = False + pen.secondarytipswitch = False + elif state == PenState.PEN_IS_IN_RANGE: + pen.tipswitch = False + pen.inrange = True + pen.invert = False + pen.eraser = False + pen.barrelswitch = False + pen.secondarytipswitch = False + elif state == PenState.PEN_IS_IN_CONTACT: + pen.tipswitch = True + pen.inrange = True + pen.invert = False + pen.eraser = False + pen.barrelswitch = False + pen.secondarytipswitch = False + elif state == PenState.PEN_IS_IN_RANGE_WITH_BUTTON: + pen.tipswitch = False + pen.inrange = True + pen.eraser = False + assert button is not None + pen.barrelswitch = button == BtnPressed.PRIMARY_PRESSED + pen.secondarytipswitch = button == BtnPressed.SECONDARY_PRESSED + pen.invert = button == BtnPressed.THIRD_PRESSED + elif state == PenState.PEN_IS_IN_CONTACT_WITH_BUTTON: + pen.tipswitch = True + pen.inrange = True + pen.eraser = False + assert button is not None + pen.barrelswitch = button == BtnPressed.PRIMARY_PRESSED + pen.secondarytipswitch = button == BtnPressed.SECONDARY_PRESSED + pen.invert = button == BtnPressed.THIRD_PRESSED + elif state == PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT: + pen.tipswitch = False + pen.inrange = True + pen.invert = True + pen.eraser = False + pen.barrelswitch = False + pen.secondarytipswitch = False + elif state == PenState.PEN_IS_ERASING: + pen.tipswitch = False + pen.inrange = True + pen.invert = False + pen.eraser = True + pen.barrelswitch = False + pen.secondarytipswitch = False + + pen.current_state = state + + def call_input_event(self, report): + if report[0] == 0x0a: + # ensures the original second Eraser usage is null + report[1] &= 0xdf + + # ensures the original last bit is equal to bit 6 (In Range) + if report[1] & 0x40: + report[1] |= 0x80 + + super().call_input_event(report) + + def send_intermediate_state(self, pen, state, test_button): + intermediate_pen = copy.copy(pen) + self.move_to(intermediate_pen, state, test_button, debug=False) + return super().event(intermediate_pen, test_button) + + def event(self, pen, button): + rs = [] + + # it's not possible to go between eraser mode or not without + # going out-of-prox: the eraser mode is activated by presenting + # the tail of the pen + if self.previous_state in ( + PenState.PEN_IS_IN_RANGE, + PenState.PEN_IS_IN_RANGE_WITH_BUTTON, + PenState.PEN_IS_IN_CONTACT, + PenState.PEN_IS_IN_CONTACT_WITH_BUTTON, + ) and pen.current_state in ( + PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT, + PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT_WITH_BUTTON, + PenState.PEN_IS_ERASING, + PenState.PEN_IS_ERASING_WITH_BUTTON, + ): + rs.extend( + self.send_intermediate_state(pen, PenState.PEN_IS_OUT_OF_RANGE, button) + ) + + # same than above except from eraser to normal + if self.previous_state in ( + PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT, + PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT_WITH_BUTTON, + PenState.PEN_IS_ERASING, + PenState.PEN_IS_ERASING_WITH_BUTTON, + ) and pen.current_state in ( + PenState.PEN_IS_IN_RANGE, + PenState.PEN_IS_IN_RANGE_WITH_BUTTON, + PenState.PEN_IS_IN_CONTACT, + PenState.PEN_IS_IN_CONTACT_WITH_BUTTON, + ): + rs.extend( + self.send_intermediate_state(pen, PenState.PEN_IS_OUT_OF_RANGE, button) + ) + + if self.previous_state == PenState.PEN_IS_OUT_OF_RANGE: + if pen.current_state == PenState.PEN_IS_IN_RANGE_WITH_BUTTON: + rs.extend( + self.send_intermediate_state(pen, PenState.PEN_IS_IN_RANGE, button) + ) + + rs.extend(super().event(pen, button)) + self.previous_state = pen.current_state + return rs + + ################################################################################ # # Windows 7 compatible devices @@ -1162,3 +1469,37 @@ class TestGoodix_27c6_0e00(BaseTest.TestTablet): rdesc="05 0d 09 04 a1 01 85 01 09 22 a1 02 55 0e 65 11 35 00 15 00 09 42 25 01 75 01 95 01 81 02 25 7f 09 30 75 07 81 42 95 01 75 08 09 51 81 02 75 10 05 01 26 04 20 46 e6 09 09 30 81 02 26 60 15 46 9a 06 09 31 81 02 05 0d 55 0f 75 08 25 ff 45 ff 09 48 81 42 09 49 81 42 55 0e c0 09 22 a1 02 09 42 25 01 75 01 95 01 81 02 25 7f 09 30 75 07 81 42 95 01 75 08 09 51 81 02 75 10 05 01 26 04 20 46 e6 09 09 30 81 02 26 60 15 46 9a 06 09 31 81 02 05 0d 55 0f 75 08 25 ff 45 ff 09 48 81 42 09 49 81 42 55 0e c0 09 22 a1 02 09 42 25 01 75 01 95 01 81 02 25 7f 09 30 75 07 81 42 95 01 75 08 09 51 81 02 75 10 05 01 26 04 20 46 e6 09 09 30 81 02 26 60 15 46 9a 06 09 31 81 02 05 0d 55 0f 75 08 25 ff 45 ff 09 48 81 42 09 49 81 42 55 0e c0 09 22 a1 02 09 42 15 00 25 01 75 01 95 01 81 02 25 7f 09 30 75 07 81 42 75 08 09 51 95 01 81 02 05 01 26 04 20 75 10 55 0e 65 11 09 30 35 00 46 e6 09 81 02 26 60 15 46 9a 06 09 31 81 02 05 0d 55 0f 75 08 25 ff 45 ff 09 48 81 42 09 49 81 42 55 0e c0 09 22 a1 02 09 42 15 00 25 01 75 01 95 01 81 02 25 7f 09 30 75 07 81 42 75 08 09 51 95 01 81 02 05 01 26 04 20 75 10 55 0e 65 11 09 30 35 00 46 e6 09 81 02 26 60 15 46 9a 06 09 31 81 02 05 0d 55 0f 75 08 25 ff 45 ff 09 48 81 42 09 49 81 42 55 0e c0 09 54 15 00 25 7f 75 08 95 01 81 02 85 02 09 55 95 01 25 0a b1 02 85 03 06 00 ff 09 c5 15 00 26 ff 00 75 08 96 00 01 b1 02 c0 05 0d 09 02 a1 01 09 20 a1 00 85 08 05 01 a4 09 30 35 00 46 e6 09 15 00 26 04 20 55 0d 65 13 75 10 95 01 81 02 09 31 46 9a 06 26 60 15 81 02 b4 05 0d 09 38 95 01 75 08 15 00 25 01 81 02 09 30 75 10 26 ff 0f 81 02 09 31 81 02 09 42 09 44 09 5a 09 3c 09 45 09 32 75 01 95 06 25 01 81 02 95 02 81 03 09 3d 55 0e 65 14 36 d8 dc 46 28 23 16 d8 dc 26 28 23 95 01 75 10 81 02 09 3e 81 02 09 41 15 00 27 a0 8c 00 00 35 00 47 a0 8c 00 00 81 02 05 20 0a 53 04 65 00 16 01 f8 26 ff 07 75 10 95 01 81 02 0a 54 04 81 02 0a 55 04 81 02 0a 57 04 81 02 0a 58 04 81 02 0a 59 04 81 02 0a 72 04 81 02 0a 73 04 81 02 0a 74 04 81 02 05 0d 09 3b 15 00 25 64 75 08 81 02 09 5b 25 ff 75 40 81 02 06 00 ff 09 5b 75 20 81 02 05 0d 09 5c 26 ff 00 75 08 81 02 09 5e 81 02 09 70 a1 02 15 01 25 06 09 72 09 73 09 74 09 75 09 76 09 77 81 20 c0 06 00 ff 09 01 15 00 27 ff ff 00 00 75 10 95 01 81 02 85 09 09 81 a1 02 09 81 15 01 25 04 09 82 09 83 09 84 09 85 81 20 c0 85 10 09 5c a1 02 15 00 25 01 75 08 95 01 09 38 b1 02 09 5c 26 ff 00 b1 02 09 5d 75 01 95 01 25 01 b1 02 95 07 b1 03 c0 85 11 09 5e a1 02 09 38 15 00 25 01 75 08 95 01 b1 02 09 5e 26 ff 00 b1 02 09 5f 75 01 25 01 b1 02 75 07 b1 03 c0 85 12 09 70 a1 02 75 08 95 01 15 00 25 01 09 38 b1 02 09 70 a1 02 25 06 09 72 09 73 09 74 09 75 09 76 09 77 b1 20 c0 09 71 75 01 25 01 b1 02 75 07 b1 03 c0 85 13 09 80 15 00 25 ff 75 40 95 01 b1 02 85 14 09 44 a1 02 09 38 75 08 95 01 25 01 b1 02 15 01 25 03 09 44 a1 02 09 a4 09 44 09 5a 09 45 09 a3 b1 20 c0 09 5a a1 02 09 a4 09 44 09 5a 09 45 09 a3 b1 20 c0 09 45 a1 02 09 a4 09 44 09 5a 09 45 09 a3 b1 20 c0 c0 85 15 75 08 95 01 05 0d 09 90 a1 02 09 38 25 01 b1 02 09 91 75 10 26 ff 0f b1 02 09 92 75 40 25 ff b1 02 05 06 09 2a 75 08 26 ff 00 a1 02 09 2d b1 02 09 2e b1 02 c0 c0 85 16 05 06 09 2b a1 02 05 0d 25 01 09 38 b1 02 05 06 09 2b a1 02 09 2d 26 ff 00 b1 02 09 2e b1 02 c0 c0 85 17 06 00 ff 09 01 a1 02 05 0d 09 38 75 08 95 01 25 01 b1 02 06 00 ff 09 01 75 10 27 ff ff 00 00 b1 02 c0 85 18 05 0d 09 38 75 08 95 01 15 00 25 01 b1 02 c0 c0 06 f0 ff 09 01 a1 01 85 0e 09 01 15 00 25 ff 75 08 95 40 91 02 09 01 15 00 25 ff 75 08 95 40 81 02 c0", input_info=(BusType.I2C, 0x27C6, 0x0E00), ) + + +class TestXPPen_ArtistPro16Gen2_28bd_095b(BaseTest.TestTablet): + hid_bpfs = [("XPPen__ArtistPro16Gen2.bpf.o", True)] + + def create_device(self): + dev = XPPen_ArtistPro16Gen2_28bd_095b( + "uhid test XPPen Artist Pro 16 Gen2 28bd 095b", + rdesc="05 0d 09 02 a1 01 85 07 09 20 a1 00 09 42 09 44 09 45 09 3c 15 00 25 01 75 01 95 04 81 02 95 01 81 03 09 32 15 00 25 01 95 01 81 02 95 02 81 03 75 10 95 01 35 00 a4 05 01 09 30 65 13 55 0d 46 ff 34 26 ff 7f 81 02 09 31 46 20 21 26 ff 7f 81 02 b4 09 30 45 00 26 ff 3f 81 42 09 3d 15 81 25 7f 75 08 95 01 81 02 09 3e 15 81 25 7f 81 02 c0 c0", + input_info=(BusType.USB, 0x28BD, 0x095B), + ) + return dev + + +class TestXPPen_Artist24_28bd_093a(BaseTest.TestTablet): + hid_bpfs = [("XPPen__Artist24.bpf.o", True)] + + def create_device(self): + return XPPen_Artist24_28bd_093a( + "uhid test XPPen Artist 24 28bd 093a", + rdesc="05 0d 09 02 a1 01 85 07 09 20 a1 00 09 42 09 44 09 45 15 00 25 01 75 01 95 03 81 02 95 02 81 03 09 32 95 01 81 02 95 02 81 03 75 10 95 01 35 00 a4 05 01 09 30 65 13 55 0d 46 f0 50 26 ff 7f 81 02 09 31 46 91 2d 26 ff 7f 81 02 b4 09 30 45 00 26 ff 1f 81 42 09 3d 15 81 25 7f 75 08 95 01 81 02 09 3e 15 81 25 7f 81 02 c0 c0", + input_info=(BusType.USB, 0x28BD, 0x093A), + ) + + +class TestHuion_Kamvas_Pro_19_256c_006b(BaseTest.TestTablet): + hid_bpfs = [("Huion__Kamvas-Pro-19.bpf.o", True)] + + def create_device(self): + return Huion_Kamvas_Pro_19_256c_006b( + "uhid test HUION Huion Tablet_GT1902", + rdesc="05 0d 09 02 a1 01 85 0a 09 20 a1 01 09 42 09 44 09 43 09 3c 09 45 15 00 25 01 75 01 95 06 81 02 09 32 75 01 95 01 81 02 81 03 05 01 09 30 09 31 55 0d 65 33 26 ff 7f 35 00 46 00 08 75 10 95 02 81 02 05 0d 09 30 26 ff 3f 75 10 95 01 81 02 09 3d 09 3e 15 a6 25 5a 75 08 95 02 81 02 c0 c0 05 0d 09 04 a1 01 85 04 09 22 a1 02 05 0d 95 01 75 06 09 51 15 00 25 3f 81 02 09 42 25 01 75 01 95 01 81 02 75 01 95 01 81 03 05 01 75 10 55 0e 65 11 09 30 26 ff 7f 35 00 46 15 0c 81 42 09 31 26 ff 7f 46 cb 06 81 42 05 0d 09 30 26 ff 1f 75 10 95 01 81 02 c0 05 0d 09 22 a1 02 05 0d 95 01 75 06 09 51 15 00 25 3f 81 02 09 42 25 01 75 01 95 01 81 02 75 01 95 01 81 03 05 01 75 10 55 0e 65 11 09 30 26 ff 7f 35 00 46 15 0c 81 42 09 31 26 ff 7f 46 cb 06 81 42 05 0d 09 30 26 ff 1f 75 10 95 01 81 02 c0 05 0d 09 56 55 00 65 00 27 ff ff ff 7f 95 01 75 20 81 02 09 54 25 7f 95 01 75 08 81 02 75 08 95 08 81 03 85 05 09 55 25 0a 75 08 95 01 b1 02 06 00 ff 09 c5 85 06 15 00 26 ff 00 75 08 96 00 01 b1 02 c0", + input_info=(BusType.USB, 0x256C, 0x006B), + ) |