mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
synced 2025-01-18 03:06:43 +00:00
Merge branch 'for-6.10/hid-bpf' into for-linus
- updates to HID-BPF infrastructure, with some of the specific fixes (e.g. rdesc fixups) abstracted into separate BPF programs for consumption by libevdev/udev-hid-bpf (Benjamin Tissoires)
This commit is contained in:
commit
e29fd84c5b
@ -179,7 +179,7 @@ Available API that can be used in syscall HID-BPF programs:
|
||||
-----------------------------------------------------------
|
||||
|
||||
.. kernel-doc:: drivers/hid/bpf/hid_bpf_dispatch.c
|
||||
:functions: hid_bpf_attach_prog hid_bpf_hw_request hid_bpf_allocate_context hid_bpf_release_context
|
||||
:functions: hid_bpf_attach_prog hid_bpf_hw_request hid_bpf_hw_output_report hid_bpf_input_report hid_bpf_allocate_context hid_bpf_release_context
|
||||
|
||||
General overview of a HID-BPF program
|
||||
=====================================
|
||||
|
@ -143,48 +143,6 @@ u8 *call_hid_bpf_rdesc_fixup(struct hid_device *hdev, u8 *rdesc, unsigned int *s
|
||||
}
|
||||
EXPORT_SYMBOL_GPL(call_hid_bpf_rdesc_fixup);
|
||||
|
||||
/* Disables missing prototype warnings */
|
||||
__bpf_kfunc_start_defs();
|
||||
|
||||
/**
|
||||
* hid_bpf_get_data - Get the kernel memory pointer associated with the context @ctx
|
||||
*
|
||||
* @ctx: The HID-BPF context
|
||||
* @offset: The offset within the memory
|
||||
* @rdwr_buf_size: the const size of the buffer
|
||||
*
|
||||
* @returns %NULL on error, an %__u8 memory pointer on success
|
||||
*/
|
||||
__bpf_kfunc __u8 *
|
||||
hid_bpf_get_data(struct hid_bpf_ctx *ctx, unsigned int offset, const size_t rdwr_buf_size)
|
||||
{
|
||||
struct hid_bpf_ctx_kern *ctx_kern;
|
||||
|
||||
if (!ctx)
|
||||
return NULL;
|
||||
|
||||
ctx_kern = container_of(ctx, struct hid_bpf_ctx_kern, ctx);
|
||||
|
||||
if (rdwr_buf_size + offset > ctx->allocated_size)
|
||||
return NULL;
|
||||
|
||||
return ctx_kern->data + offset;
|
||||
}
|
||||
__bpf_kfunc_end_defs();
|
||||
|
||||
/*
|
||||
* The following set contains all functions we agree BPF programs
|
||||
* can use.
|
||||
*/
|
||||
BTF_KFUNCS_START(hid_bpf_kfunc_ids)
|
||||
BTF_ID_FLAGS(func, hid_bpf_get_data, KF_RET_NULL)
|
||||
BTF_KFUNCS_END(hid_bpf_kfunc_ids)
|
||||
|
||||
static const struct btf_kfunc_id_set hid_bpf_kfunc_set = {
|
||||
.owner = THIS_MODULE,
|
||||
.set = &hid_bpf_kfunc_ids,
|
||||
};
|
||||
|
||||
static int device_match_id(struct device *dev, const void *id)
|
||||
{
|
||||
struct hid_device *hdev = to_hid_device(dev);
|
||||
@ -281,6 +239,31 @@ static int do_hid_bpf_attach_prog(struct hid_device *hdev, int prog_fd, struct b
|
||||
/* Disables missing prototype warnings */
|
||||
__bpf_kfunc_start_defs();
|
||||
|
||||
/**
|
||||
* hid_bpf_get_data - Get the kernel memory pointer associated with the context @ctx
|
||||
*
|
||||
* @ctx: The HID-BPF context
|
||||
* @offset: The offset within the memory
|
||||
* @rdwr_buf_size: the const size of the buffer
|
||||
*
|
||||
* @returns %NULL on error, an %__u8 memory pointer on success
|
||||
*/
|
||||
__bpf_kfunc __u8 *
|
||||
hid_bpf_get_data(struct hid_bpf_ctx *ctx, unsigned int offset, const size_t rdwr_buf_size)
|
||||
{
|
||||
struct hid_bpf_ctx_kern *ctx_kern;
|
||||
|
||||
if (!ctx)
|
||||
return NULL;
|
||||
|
||||
ctx_kern = container_of(ctx, struct hid_bpf_ctx_kern, ctx);
|
||||
|
||||
if (rdwr_buf_size + offset > ctx->allocated_size)
|
||||
return NULL;
|
||||
|
||||
return ctx_kern->data + offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* hid_bpf_attach_prog - Attach the given @prog_fd to the given HID device
|
||||
*
|
||||
@ -393,6 +376,46 @@ hid_bpf_release_context(struct hid_bpf_ctx *ctx)
|
||||
put_device(&hid->dev);
|
||||
}
|
||||
|
||||
static int
|
||||
__hid_bpf_hw_check_params(struct hid_bpf_ctx *ctx, __u8 *buf, size_t *buf__sz,
|
||||
enum hid_report_type rtype)
|
||||
{
|
||||
struct hid_report_enum *report_enum;
|
||||
struct hid_report *report;
|
||||
struct hid_device *hdev;
|
||||
u32 report_len;
|
||||
|
||||
/* check arguments */
|
||||
if (!ctx || !hid_bpf_ops || !buf)
|
||||
return -EINVAL;
|
||||
|
||||
switch (rtype) {
|
||||
case HID_INPUT_REPORT:
|
||||
case HID_OUTPUT_REPORT:
|
||||
case HID_FEATURE_REPORT:
|
||||
break;
|
||||
default:
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
if (*buf__sz < 1)
|
||||
return -EINVAL;
|
||||
|
||||
hdev = (struct hid_device *)ctx->hid; /* discard const */
|
||||
|
||||
report_enum = hdev->report_enum + rtype;
|
||||
report = hid_bpf_ops->hid_get_report(report_enum, buf);
|
||||
if (!report)
|
||||
return -EINVAL;
|
||||
|
||||
report_len = hid_report_len(report);
|
||||
|
||||
if (*buf__sz > report_len)
|
||||
*buf__sz = report_len;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* hid_bpf_hw_request - Communicate with a HID device
|
||||
*
|
||||
@ -409,24 +432,14 @@ hid_bpf_hw_request(struct hid_bpf_ctx *ctx, __u8 *buf, size_t buf__sz,
|
||||
enum hid_report_type rtype, enum hid_class_request reqtype)
|
||||
{
|
||||
struct hid_device *hdev;
|
||||
struct hid_report *report;
|
||||
struct hid_report_enum *report_enum;
|
||||
size_t size = buf__sz;
|
||||
u8 *dma_data;
|
||||
u32 report_len;
|
||||
int ret;
|
||||
|
||||
/* check arguments */
|
||||
if (!ctx || !hid_bpf_ops || !buf)
|
||||
return -EINVAL;
|
||||
|
||||
switch (rtype) {
|
||||
case HID_INPUT_REPORT:
|
||||
case HID_OUTPUT_REPORT:
|
||||
case HID_FEATURE_REPORT:
|
||||
break;
|
||||
default:
|
||||
return -EINVAL;
|
||||
}
|
||||
ret = __hid_bpf_hw_check_params(ctx, buf, &size, rtype);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
switch (reqtype) {
|
||||
case HID_REQ_GET_REPORT:
|
||||
@ -440,29 +453,16 @@ hid_bpf_hw_request(struct hid_bpf_ctx *ctx, __u8 *buf, size_t buf__sz,
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
if (buf__sz < 1)
|
||||
return -EINVAL;
|
||||
|
||||
hdev = (struct hid_device *)ctx->hid; /* discard const */
|
||||
|
||||
report_enum = hdev->report_enum + rtype;
|
||||
report = hid_bpf_ops->hid_get_report(report_enum, buf);
|
||||
if (!report)
|
||||
return -EINVAL;
|
||||
|
||||
report_len = hid_report_len(report);
|
||||
|
||||
if (buf__sz > report_len)
|
||||
buf__sz = report_len;
|
||||
|
||||
dma_data = kmemdup(buf, buf__sz, GFP_KERNEL);
|
||||
dma_data = kmemdup(buf, size, GFP_KERNEL);
|
||||
if (!dma_data)
|
||||
return -ENOMEM;
|
||||
|
||||
ret = hid_bpf_ops->hid_hw_raw_request(hdev,
|
||||
dma_data[0],
|
||||
dma_data,
|
||||
buf__sz,
|
||||
size,
|
||||
rtype,
|
||||
reqtype);
|
||||
|
||||
@ -472,8 +472,90 @@ hid_bpf_hw_request(struct hid_bpf_ctx *ctx, __u8 *buf, size_t buf__sz,
|
||||
kfree(dma_data);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* hid_bpf_hw_output_report - Send an output report to a HID device
|
||||
*
|
||||
* @ctx: the HID-BPF context previously allocated in hid_bpf_allocate_context()
|
||||
* @buf: a %PTR_TO_MEM buffer
|
||||
* @buf__sz: the size of the data to transfer
|
||||
*
|
||||
* Returns the number of bytes transferred on success, a negative error code otherwise.
|
||||
*/
|
||||
__bpf_kfunc int
|
||||
hid_bpf_hw_output_report(struct hid_bpf_ctx *ctx, __u8 *buf, size_t buf__sz)
|
||||
{
|
||||
struct hid_device *hdev;
|
||||
size_t size = buf__sz;
|
||||
u8 *dma_data;
|
||||
int ret;
|
||||
|
||||
/* check arguments */
|
||||
ret = __hid_bpf_hw_check_params(ctx, buf, &size, HID_OUTPUT_REPORT);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
hdev = (struct hid_device *)ctx->hid; /* discard const */
|
||||
|
||||
dma_data = kmemdup(buf, size, GFP_KERNEL);
|
||||
if (!dma_data)
|
||||
return -ENOMEM;
|
||||
|
||||
ret = hid_bpf_ops->hid_hw_output_report(hdev,
|
||||
dma_data,
|
||||
size);
|
||||
|
||||
kfree(dma_data);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* hid_bpf_input_report - Inject a HID report in the kernel from a HID device
|
||||
*
|
||||
* @ctx: the HID-BPF context previously allocated in hid_bpf_allocate_context()
|
||||
* @type: the type of the report (%HID_INPUT_REPORT, %HID_FEATURE_REPORT, %HID_OUTPUT_REPORT)
|
||||
* @buf: a %PTR_TO_MEM buffer
|
||||
* @buf__sz: the size of the data to transfer
|
||||
*
|
||||
* Returns %0 on success, a negative error code otherwise.
|
||||
*/
|
||||
__bpf_kfunc int
|
||||
hid_bpf_input_report(struct hid_bpf_ctx *ctx, enum hid_report_type type, u8 *buf,
|
||||
const size_t buf__sz)
|
||||
{
|
||||
struct hid_device *hdev;
|
||||
size_t size = buf__sz;
|
||||
int ret;
|
||||
|
||||
/* check arguments */
|
||||
ret = __hid_bpf_hw_check_params(ctx, buf, &size, type);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
hdev = (struct hid_device *)ctx->hid; /* discard const */
|
||||
|
||||
return hid_bpf_ops->hid_input_report(hdev, type, buf, size, 0);
|
||||
}
|
||||
__bpf_kfunc_end_defs();
|
||||
|
||||
/*
|
||||
* The following set contains all functions we agree BPF programs
|
||||
* can use.
|
||||
*/
|
||||
BTF_KFUNCS_START(hid_bpf_kfunc_ids)
|
||||
BTF_ID_FLAGS(func, hid_bpf_get_data, KF_RET_NULL)
|
||||
BTF_ID_FLAGS(func, hid_bpf_allocate_context, KF_ACQUIRE | KF_RET_NULL | KF_SLEEPABLE)
|
||||
BTF_ID_FLAGS(func, hid_bpf_release_context, KF_RELEASE | KF_SLEEPABLE)
|
||||
BTF_ID_FLAGS(func, hid_bpf_hw_request, KF_SLEEPABLE)
|
||||
BTF_ID_FLAGS(func, hid_bpf_hw_output_report, KF_SLEEPABLE)
|
||||
BTF_ID_FLAGS(func, hid_bpf_input_report, KF_SLEEPABLE)
|
||||
BTF_KFUNCS_END(hid_bpf_kfunc_ids)
|
||||
|
||||
static const struct btf_kfunc_id_set hid_bpf_kfunc_set = {
|
||||
.owner = THIS_MODULE,
|
||||
.set = &hid_bpf_kfunc_ids,
|
||||
};
|
||||
|
||||
/* our HID-BPF entrypoints */
|
||||
BTF_SET8_START(hid_bpf_fmodret_ids)
|
||||
BTF_ID_FLAGS(func, hid_bpf_device_event)
|
||||
@ -492,6 +574,8 @@ BTF_ID_FLAGS(func, hid_bpf_attach_prog)
|
||||
BTF_ID_FLAGS(func, hid_bpf_allocate_context, KF_ACQUIRE | KF_RET_NULL)
|
||||
BTF_ID_FLAGS(func, hid_bpf_release_context, KF_RELEASE)
|
||||
BTF_ID_FLAGS(func, hid_bpf_hw_request)
|
||||
BTF_ID_FLAGS(func, hid_bpf_hw_output_report)
|
||||
BTF_ID_FLAGS(func, hid_bpf_input_report)
|
||||
BTF_KFUNCS_END(hid_bpf_syscall_kfunc_ids)
|
||||
|
||||
static const struct btf_kfunc_id_set hid_bpf_syscall_kfunc_set = {
|
||||
|
185
drivers/hid/bpf/progs/FR-TEC__Raptor-Mach-2.bpf.c
Normal file
185
drivers/hid/bpf/progs/FR-TEC__Raptor-Mach-2.bpf.c
Normal file
@ -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";
|
58
drivers/hid/bpf/progs/HP__Elite-Presenter.bpf.c
Normal file
58
drivers/hid/bpf/progs/HP__Elite-Presenter.bpf.c
Normal file
@ -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";
|
290
drivers/hid/bpf/progs/Huion__Kamvas-Pro-19.bpf.c
Normal file
290
drivers/hid/bpf/progs/Huion__Kamvas-Pro-19.bpf.c
Normal file
@ -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";
|
59
drivers/hid/bpf/progs/IOGEAR__Kaliber-MMOmentum.bpf.c
Normal file
59
drivers/hid/bpf/progs/IOGEAR__Kaliber-MMOmentum.bpf.c
Normal file
@ -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";
|
91
drivers/hid/bpf/progs/Makefile
Normal file
91
drivers/hid/bpf/progs/Makefile
Normal file
@ -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
|
133
drivers/hid/bpf/progs/Microsoft__XBox-Elite-2.bpf.c
Normal file
133
drivers/hid/bpf/progs/Microsoft__XBox-Elite-2.bpf.c
Normal file
@ -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";
|
102
drivers/hid/bpf/progs/README
Normal file
102
drivers/hid/bpf/progs/README
Normal file
@ -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
|
||||
```
|
173
drivers/hid/bpf/progs/Wacom__ArtPen.bpf.c
Normal file
173
drivers/hid/bpf/progs/Wacom__ArtPen.bpf.c
Normal file
@ -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";
|
229
drivers/hid/bpf/progs/XPPen__Artist24.bpf.c
Normal file
229
drivers/hid/bpf/progs/XPPen__Artist24.bpf.c
Normal file
@ -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";
|
274
drivers/hid/bpf/progs/XPPen__ArtistPro16Gen2.bpf.c
Normal file
274
drivers/hid/bpf/progs/XPPen__ArtistPro16Gen2.bpf.c
Normal file
@ -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";
|
15
drivers/hid/bpf/progs/hid_bpf.h
Normal file
15
drivers/hid/bpf/progs/hid_bpf.h
Normal file
@ -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 */
|
168
drivers/hid/bpf/progs/hid_bpf_helpers.h
Normal file
168
drivers/hid/bpf/progs/hid_bpf_helpers.h
Normal file
@ -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 */
|
@ -2974,6 +2974,8 @@ EXPORT_SYMBOL_GPL(hid_check_keys_pressed);
|
||||
static struct hid_bpf_ops hid_ops = {
|
||||
.hid_get_report = hid_get_report,
|
||||
.hid_hw_raw_request = hid_hw_raw_request,
|
||||
.hid_hw_output_report = hid_hw_output_report,
|
||||
.hid_input_report = hid_input_report,
|
||||
.owner = THIS_MODULE,
|
||||
.bus_type = &hid_bus_type,
|
||||
};
|
||||
|
@ -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 */
|
||||
};
|
||||
|
||||
|
@ -103,6 +103,9 @@ struct hid_bpf_ops {
|
||||
unsigned char reportnum, __u8 *buf,
|
||||
size_t len, enum hid_report_type rtype,
|
||||
enum hid_class_request reqtype);
|
||||
int (*hid_hw_output_report)(struct hid_device *hdev, __u8 *buf, size_t len);
|
||||
int (*hid_input_report)(struct hid_device *hid, enum hid_report_type type,
|
||||
u8 *data, u32 size, int interrupt);
|
||||
struct module *owner;
|
||||
const struct bus_type *bus_type;
|
||||
};
|
||||
|
@ -238,3 +238,4 @@ CONFIG_VLAN_8021Q=y
|
||||
CONFIG_XFRM_SUB_POLICY=y
|
||||
CONFIG_XFRM_USER=y
|
||||
CONFIG_ZEROPLUS_FF=y
|
||||
CONFIG_KASAN=y
|
||||
|
@ -16,6 +16,11 @@
|
||||
|
||||
#define SHOW_UHID_DEBUG 0
|
||||
|
||||
#define min(a, b) \
|
||||
({ __typeof__(a) _a = (a); \
|
||||
__typeof__(b) _b = (b); \
|
||||
_a < _b ? _a : _b; })
|
||||
|
||||
static unsigned char rdesc[] = {
|
||||
0x06, 0x00, 0xff, /* Usage Page (Vendor Defined Page 1) */
|
||||
0x09, 0x21, /* Usage (Vendor Usage 0x21) */
|
||||
@ -111,6 +116,10 @@ struct hid_hw_request_syscall_args {
|
||||
static pthread_mutex_t uhid_started_mtx = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_cond_t uhid_started = PTHREAD_COND_INITIALIZER;
|
||||
|
||||
static pthread_mutex_t uhid_output_mtx = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_cond_t uhid_output_cond = PTHREAD_COND_INITIALIZER;
|
||||
static unsigned char output_report[10];
|
||||
|
||||
/* no need to protect uhid_stopped, only one thread accesses it */
|
||||
static bool uhid_stopped;
|
||||
|
||||
@ -205,6 +214,13 @@ static int uhid_event(struct __test_metadata *_metadata, int fd)
|
||||
break;
|
||||
case UHID_OUTPUT:
|
||||
UHID_LOG("UHID_OUTPUT from uhid-dev");
|
||||
|
||||
pthread_mutex_lock(&uhid_output_mtx);
|
||||
memcpy(output_report,
|
||||
ev.u.output.data,
|
||||
min(ev.u.output.size, sizeof(output_report)));
|
||||
pthread_cond_signal(&uhid_output_cond);
|
||||
pthread_mutex_unlock(&uhid_output_mtx);
|
||||
break;
|
||||
case UHID_GET_REPORT:
|
||||
UHID_LOG("UHID_GET_REPORT from uhid-dev");
|
||||
@ -734,8 +750,100 @@ TEST_F(hid_bpf, test_hid_change_report)
|
||||
}
|
||||
|
||||
/*
|
||||
* Attach hid_user_raw_request to the given uhid device,
|
||||
* call the bpf program from userspace
|
||||
* Call hid_bpf_input_report against the given uhid device,
|
||||
* check that the program is called and does the expected.
|
||||
*/
|
||||
TEST_F(hid_bpf, test_hid_user_input_report_call)
|
||||
{
|
||||
struct hid_hw_request_syscall_args args = {
|
||||
.retval = -1,
|
||||
.size = 10,
|
||||
};
|
||||
DECLARE_LIBBPF_OPTS(bpf_test_run_opts, tattrs,
|
||||
.ctx_in = &args,
|
||||
.ctx_size_in = sizeof(args),
|
||||
);
|
||||
__u8 buf[10] = {0};
|
||||
int err, prog_fd;
|
||||
|
||||
LOAD_BPF;
|
||||
|
||||
args.hid = self->hid_id;
|
||||
args.data[0] = 1; /* report ID */
|
||||
args.data[1] = 2; /* report ID */
|
||||
args.data[2] = 42; /* report ID */
|
||||
|
||||
prog_fd = bpf_program__fd(self->skel->progs.hid_user_input_report);
|
||||
|
||||
/* check that there is no data to read from hidraw */
|
||||
memset(buf, 0, sizeof(buf));
|
||||
err = read(self->hidraw_fd, buf, sizeof(buf));
|
||||
ASSERT_EQ(err, -1) TH_LOG("read_hidraw");
|
||||
|
||||
err = bpf_prog_test_run_opts(prog_fd, &tattrs);
|
||||
|
||||
ASSERT_OK(err) TH_LOG("error while calling bpf_prog_test_run_opts");
|
||||
|
||||
ASSERT_EQ(args.retval, 0);
|
||||
|
||||
/* read the data from hidraw */
|
||||
memset(buf, 0, sizeof(buf));
|
||||
err = read(self->hidraw_fd, buf, sizeof(buf));
|
||||
ASSERT_EQ(err, 6) TH_LOG("read_hidraw");
|
||||
ASSERT_EQ(buf[0], 1);
|
||||
ASSERT_EQ(buf[1], 2);
|
||||
ASSERT_EQ(buf[2], 42);
|
||||
}
|
||||
|
||||
/*
|
||||
* Call hid_bpf_hw_output_report against the given uhid device,
|
||||
* check that the program is called and does the expected.
|
||||
*/
|
||||
TEST_F(hid_bpf, test_hid_user_output_report_call)
|
||||
{
|
||||
struct hid_hw_request_syscall_args args = {
|
||||
.retval = -1,
|
||||
.size = 10,
|
||||
};
|
||||
DECLARE_LIBBPF_OPTS(bpf_test_run_opts, tattrs,
|
||||
.ctx_in = &args,
|
||||
.ctx_size_in = sizeof(args),
|
||||
);
|
||||
int err, cond_err, prog_fd;
|
||||
struct timespec time_to_wait;
|
||||
|
||||
LOAD_BPF;
|
||||
|
||||
args.hid = self->hid_id;
|
||||
args.data[0] = 1; /* report ID */
|
||||
args.data[1] = 2; /* report ID */
|
||||
args.data[2] = 42; /* report ID */
|
||||
|
||||
prog_fd = bpf_program__fd(self->skel->progs.hid_user_output_report);
|
||||
|
||||
pthread_mutex_lock(&uhid_output_mtx);
|
||||
|
||||
memset(output_report, 0, sizeof(output_report));
|
||||
clock_gettime(CLOCK_REALTIME, &time_to_wait);
|
||||
time_to_wait.tv_sec += 2;
|
||||
|
||||
err = bpf_prog_test_run_opts(prog_fd, &tattrs);
|
||||
cond_err = pthread_cond_timedwait(&uhid_output_cond, &uhid_output_mtx, &time_to_wait);
|
||||
|
||||
ASSERT_OK(err) TH_LOG("error while calling bpf_prog_test_run_opts");
|
||||
ASSERT_OK(cond_err) TH_LOG("error while calling waiting for the condition");
|
||||
|
||||
ASSERT_EQ(args.retval, 3);
|
||||
|
||||
ASSERT_EQ(output_report[0], 1);
|
||||
ASSERT_EQ(output_report[1], 2);
|
||||
ASSERT_EQ(output_report[2], 42);
|
||||
|
||||
pthread_mutex_unlock(&uhid_output_mtx);
|
||||
}
|
||||
|
||||
/*
|
||||
* Call hid_hw_raw_request against the given uhid device,
|
||||
* check that the program is called and does the expected.
|
||||
*/
|
||||
TEST_F(hid_bpf, test_hid_user_raw_request_call)
|
||||
|
@ -101,6 +101,52 @@ int hid_user_raw_request(struct hid_hw_request_syscall_args *args)
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("syscall")
|
||||
int hid_user_output_report(struct hid_hw_request_syscall_args *args)
|
||||
{
|
||||
struct hid_bpf_ctx *ctx;
|
||||
const size_t size = args->size;
|
||||
int i, ret = 0;
|
||||
|
||||
if (size > sizeof(args->data))
|
||||
return -7; /* -E2BIG */
|
||||
|
||||
ctx = hid_bpf_allocate_context(args->hid);
|
||||
if (!ctx)
|
||||
return -1; /* EPERM check */
|
||||
|
||||
ret = hid_bpf_hw_output_report(ctx,
|
||||
args->data,
|
||||
size);
|
||||
args->retval = ret;
|
||||
|
||||
hid_bpf_release_context(ctx);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("syscall")
|
||||
int hid_user_input_report(struct hid_hw_request_syscall_args *args)
|
||||
{
|
||||
struct hid_bpf_ctx *ctx;
|
||||
const size_t size = args->size;
|
||||
int i, ret = 0;
|
||||
|
||||
if (size > sizeof(args->data))
|
||||
return -7; /* -E2BIG */
|
||||
|
||||
ctx = hid_bpf_allocate_context(args->hid);
|
||||
if (!ctx)
|
||||
return -1; /* EPERM check */
|
||||
|
||||
ret = hid_bpf_input_report(ctx, HID_INPUT_REPORT, args->data, size);
|
||||
args->retval = ret;
|
||||
|
||||
hid_bpf_release_context(ctx);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const __u8 rdesc[] = {
|
||||
0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */
|
||||
0x09, 0x32, /* USAGE (Z) */
|
||||
|
@ -94,5 +94,11 @@ extern int hid_bpf_hw_request(struct hid_bpf_ctx *ctx,
|
||||
size_t buf__sz,
|
||||
enum hid_report_type type,
|
||||
enum hid_class_request reqtype) __ksym;
|
||||
extern int hid_bpf_hw_output_report(struct hid_bpf_ctx *ctx,
|
||||
__u8 *buf, size_t buf__sz) __ksym;
|
||||
extern int hid_bpf_input_report(struct hid_bpf_ctx *ctx,
|
||||
enum hid_report_type type,
|
||||
__u8 *data,
|
||||
size_t buf__sz) __ksym;
|
||||
|
||||
#endif /* __HID_BPF_HELPERS_H */
|
||||
|
@ -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
|
||||
|
||||
|
421
tools/testing/selftests/hid/tests/base_device.py
Normal file
421
tools/testing/selftests/hid/tests/base_device.py
Normal file
@ -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
|
238
tools/testing/selftests/hid/tests/base_gamepad.py
Normal file
238
tools/testing/selftests/hid/tests/base_gamepad.py
Normal file
@ -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
|
@ -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),
|
||||
)
|
||||
|
@ -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,
|
||||
)
|
||||
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[BtnPressed]):
|
||||
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,),
|
||||
@ -450,56 +380,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
|
||||
@ -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),
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user