Resguardo inicial

This commit is contained in:
lastdragon
2026-09-06 12:53:21 -06:00
parent 662efe3c2a
commit ad2088d178
94853 changed files with 43200404 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
# SPDX-License-Identifier: GPL-2.0-only
menu "HID-BPF support"
config HID_BPF
bool "HID-BPF support"
depends on BPF_JIT
depends on BPF_SYSCALL
depends on DYNAMIC_FTRACE_WITH_DIRECT_CALLS
help
This option allows to support eBPF programs on the HID subsystem.
eBPF programs can fix HID devices in a lighter way than a full
kernel patch and allow a lot more flexibility.
For documentation, see Documentation/hid/hid-bpf.rst
endmenu
+11
View File
@@ -0,0 +1,11 @@
# SPDX-License-Identifier: GPL-2.0
#
# Makefile for HID-BPF
#
LIBBPF_INCLUDE = $(srctree)/tools/lib
obj-$(CONFIG_HID_BPF) += hid_bpf.o
CFLAGS_hid_bpf_dispatch.o += -I$(LIBBPF_INCLUDE)
CFLAGS_hid_bpf_jmp_table.o += -I$(LIBBPF_INCLUDE)
hid_bpf-objs += hid_bpf_dispatch.o hid_bpf_struct_ops.o
@@ -0,0 +1,685 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* HID-BPF support for Linux
*
* Copyright (c) 2022-2024 Benjamin Tissoires
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/bitops.h>
#include <linux/btf.h>
#include <linux/btf_ids.h>
#include <linux/filter.h>
#include <linux/hid.h>
#include <linux/hid_bpf.h>
#include <linux/init.h>
#include <linux/kfifo.h>
#include <linux/minmax.h>
#include <linux/module.h>
#include <linux/overflow.h>
#include "hid_bpf_dispatch.h"
const struct hid_ops *hid_ops;
EXPORT_SYMBOL(hid_ops);
u8 *
dispatch_hid_bpf_device_event(struct hid_device *hdev, enum hid_report_type type, u8 *data,
size_t *buf_size, u32 *size, int interrupt, u64 source,
bool from_bpf)
{
struct hid_bpf_ctx_kern ctx_kern = {
.ctx = {
.hid = hdev,
.allocated_size = hdev->bpf.allocated_data,
.size = *size,
},
.data = hdev->bpf.device_data,
.from_bpf = from_bpf,
};
struct hid_bpf_ops *e;
int ret;
if (unlikely(hdev->bpf.destroyed))
return ERR_PTR(-ENODEV);
if (type >= HID_REPORT_TYPES)
return ERR_PTR(-EINVAL);
/* no program has been attached yet */
if (!hdev->bpf.device_data)
return data;
memset(ctx_kern.data, 0, hdev->bpf.allocated_data);
memcpy(ctx_kern.data, data, *size);
rcu_read_lock();
list_for_each_entry_rcu(e, &hdev->bpf.prog_list, list) {
if (e->hid_device_event) {
ret = e->hid_device_event(&ctx_kern.ctx, type, source);
if (ret < 0) {
rcu_read_unlock();
return ERR_PTR(ret);
}
if (ret)
ctx_kern.ctx.size = ret;
}
}
rcu_read_unlock();
ret = ctx_kern.ctx.size;
if (ret) {
if (ret > ctx_kern.ctx.allocated_size)
return ERR_PTR(-EINVAL);
*size = ret;
}
*buf_size = ctx_kern.ctx.allocated_size;
return ctx_kern.data;
}
EXPORT_SYMBOL_GPL(dispatch_hid_bpf_device_event);
int dispatch_hid_bpf_raw_requests(struct hid_device *hdev,
unsigned char reportnum, u8 *buf,
u32 size, enum hid_report_type rtype,
enum hid_class_request reqtype,
u64 source, bool from_bpf)
{
struct hid_bpf_ctx_kern ctx_kern = {
.ctx = {
.hid = hdev,
.allocated_size = size,
.size = size,
},
.data = buf,
.from_bpf = from_bpf,
};
struct hid_bpf_ops *e;
int ret, idx;
if (unlikely(hdev->bpf.destroyed))
return -ENODEV;
if (rtype >= HID_REPORT_TYPES)
return -EINVAL;
idx = srcu_read_lock(&hdev->bpf.srcu);
list_for_each_entry_srcu(e, &hdev->bpf.prog_list, list,
srcu_read_lock_held(&hdev->bpf.srcu)) {
if (!e->hid_hw_request)
continue;
ret = e->hid_hw_request(&ctx_kern.ctx, reportnum, rtype, reqtype, source);
if (ret)
goto out;
}
ret = 0;
out:
srcu_read_unlock(&hdev->bpf.srcu, idx);
return ret;
}
EXPORT_SYMBOL_GPL(dispatch_hid_bpf_raw_requests);
int dispatch_hid_bpf_output_report(struct hid_device *hdev,
__u8 *buf, u32 size, u64 source,
bool from_bpf)
{
struct hid_bpf_ctx_kern ctx_kern = {
.ctx = {
.hid = hdev,
.allocated_size = size,
.size = size,
},
.data = buf,
.from_bpf = from_bpf,
};
struct hid_bpf_ops *e;
int ret, idx;
if (unlikely(hdev->bpf.destroyed))
return -ENODEV;
idx = srcu_read_lock(&hdev->bpf.srcu);
list_for_each_entry_srcu(e, &hdev->bpf.prog_list, list,
srcu_read_lock_held(&hdev->bpf.srcu)) {
if (!e->hid_hw_output_report)
continue;
ret = e->hid_hw_output_report(&ctx_kern.ctx, source);
if (ret)
goto out;
}
ret = 0;
out:
srcu_read_unlock(&hdev->bpf.srcu, idx);
return ret;
}
EXPORT_SYMBOL_GPL(dispatch_hid_bpf_output_report);
const u8 *call_hid_bpf_rdesc_fixup(struct hid_device *hdev, const u8 *rdesc, unsigned int *size)
{
int ret;
struct hid_bpf_ctx_kern ctx_kern = {
.ctx = {
.hid = hdev,
.size = *size,
.allocated_size = HID_MAX_DESCRIPTOR_SIZE,
},
};
if (!hdev->bpf.rdesc_ops)
goto ignore_bpf;
ctx_kern.data = kzalloc(ctx_kern.ctx.allocated_size, GFP_KERNEL);
if (!ctx_kern.data)
goto ignore_bpf;
memcpy(ctx_kern.data, rdesc, min_t(unsigned int, *size, HID_MAX_DESCRIPTOR_SIZE));
ret = hdev->bpf.rdesc_ops->hid_rdesc_fixup(&ctx_kern.ctx);
if (ret < 0)
goto ignore_bpf;
if (ret) {
if (ret > ctx_kern.ctx.allocated_size)
goto ignore_bpf;
*size = ret;
}
return krealloc(ctx_kern.data, *size, GFP_KERNEL);
ignore_bpf:
kfree(ctx_kern.data);
return rdesc;
}
EXPORT_SYMBOL_GPL(call_hid_bpf_rdesc_fixup);
static int device_match_id(struct device *dev, const void *id)
{
struct hid_device *hdev = to_hid_device(dev);
return hdev->id == *(int *)id;
}
struct hid_device *hid_get_device(unsigned int hid_id)
{
struct device *dev;
if (!hid_ops)
return ERR_PTR(-EINVAL);
dev = bus_find_device(hid_ops->bus_type, NULL, &hid_id, device_match_id);
if (!dev)
return ERR_PTR(-EINVAL);
return to_hid_device(dev);
}
void hid_put_device(struct hid_device *hid)
{
put_device(&hid->dev);
}
static int __hid_bpf_allocate_data(struct hid_device *hdev, u8 **data, u32 *size)
{
u8 *alloc_data;
unsigned int i, j, max_report_len = 0;
size_t alloc_size = 0;
/* compute the maximum report length for this device */
for (i = 0; i < HID_REPORT_TYPES; i++) {
struct hid_report_enum *report_enum = hdev->report_enum + i;
for (j = 0; j < HID_MAX_IDS; j++) {
struct hid_report *report = report_enum->report_id_hash[j];
if (report)
max_report_len = max(max_report_len, hid_report_len(report));
}
}
/*
* Give us a little bit of extra space and some predictability in the
* buffer length we create. This way, we can tell users that they can
* work on chunks of 64 bytes of memory without having the bpf verifier
* scream at them.
*/
alloc_size = DIV_ROUND_UP(max_report_len, 64) * 64;
alloc_data = kzalloc(alloc_size, GFP_KERNEL);
if (!alloc_data)
return -ENOMEM;
*data = alloc_data;
*size = alloc_size;
return 0;
}
int hid_bpf_allocate_event_data(struct hid_device *hdev)
{
/* hdev->bpf.device_data is already allocated, abort */
if (hdev->bpf.device_data)
return 0;
return __hid_bpf_allocate_data(hdev, &hdev->bpf.device_data, &hdev->bpf.allocated_data);
}
int hid_bpf_reconnect(struct hid_device *hdev)
{
if (!test_and_set_bit(ffs(HID_STAT_REPROBED), &hdev->status)) {
/* trigger call to call_hid_bpf_rdesc_fixup() during the next probe */
hdev->bpf_rsize = 0;
return device_reprobe(&hdev->dev);
}
return 0;
}
/* 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;
size_t end;
ctx_kern = container_of(ctx, struct hid_bpf_ctx_kern, ctx);
if (check_add_overflow(rdwr_buf_size, offset, &end) ||
end > ctx->allocated_size)
return NULL;
return ctx_kern->data + offset;
}
/**
* hid_bpf_allocate_context - Allocate a context to the given HID device
*
* @hid_id: the system unique identifier of the HID device
*
* @returns A pointer to &struct hid_bpf_ctx on success, %NULL on error.
*/
__bpf_kfunc struct hid_bpf_ctx *
hid_bpf_allocate_context(unsigned int hid_id)
{
struct hid_device *hdev;
struct hid_bpf_ctx_kern *ctx_kern = NULL;
hdev = hid_get_device(hid_id);
if (IS_ERR(hdev))
return NULL;
ctx_kern = kzalloc_obj(*ctx_kern);
if (!ctx_kern) {
hid_put_device(hdev);
return NULL;
}
ctx_kern->ctx.hid = hdev;
return &ctx_kern->ctx;
}
/**
* hid_bpf_release_context - Release the previously allocated context @ctx
*
* @ctx: the HID-BPF context to release
*
*/
__bpf_kfunc void
hid_bpf_release_context(struct hid_bpf_ctx *ctx)
{
struct hid_bpf_ctx_kern *ctx_kern;
struct hid_device *hid;
ctx_kern = container_of(ctx, struct hid_bpf_ctx_kern, ctx);
hid = (struct hid_device *)ctx_kern->ctx.hid; /* ignore const */
kfree(ctx_kern);
/* get_device() is called by bus_find_device() */
hid_put_device(hid);
}
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;
u32 report_len;
/* check arguments */
if (!hid_ops)
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;
report_enum = ctx->hid->report_enum + rtype;
report = hid_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
*
* @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
* @rtype: the type of the report (%HID_INPUT_REPORT, %HID_FEATURE_REPORT, %HID_OUTPUT_REPORT)
* @reqtype: the type of the request (%HID_REQ_GET_REPORT, %HID_REQ_SET_REPORT, ...)
*
* @returns %0 on success, a negative error code otherwise.
*/
__bpf_kfunc int
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_bpf_ctx_kern *ctx_kern;
size_t size = buf__sz;
u8 *dma_data;
int ret;
ctx_kern = container_of(ctx, struct hid_bpf_ctx_kern, ctx);
if (ctx_kern->from_bpf)
return -EDEADLOCK;
/* check arguments */
ret = __hid_bpf_hw_check_params(ctx, buf, &size, rtype);
if (ret)
return ret;
switch (reqtype) {
case HID_REQ_GET_REPORT:
case HID_REQ_GET_IDLE:
case HID_REQ_GET_PROTOCOL:
case HID_REQ_SET_REPORT:
case HID_REQ_SET_IDLE:
case HID_REQ_SET_PROTOCOL:
break;
default:
return -EINVAL;
}
dma_data = kmemdup(buf, size, GFP_KERNEL);
if (!dma_data)
return -ENOMEM;
ret = hid_ops->hid_hw_raw_request(ctx->hid,
dma_data[0],
dma_data,
size,
rtype,
reqtype,
(u64)(long)ctx,
true); /* prevent infinite recursions */
if (ret > size)
ret = size;
if (ret > 0)
memcpy(buf, dma_data, ret);
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_bpf_ctx_kern *ctx_kern;
size_t size = buf__sz;
u8 *dma_data;
int ret;
ctx_kern = container_of(ctx, struct hid_bpf_ctx_kern, ctx);
if (ctx_kern->from_bpf)
return -EDEADLOCK;
/* check arguments */
ret = __hid_bpf_hw_check_params(ctx, buf, &size, HID_OUTPUT_REPORT);
if (ret)
return ret;
dma_data = kmemdup(buf, size, GFP_KERNEL);
if (!dma_data)
return -ENOMEM;
ret = hid_ops->hid_hw_output_report(ctx->hid, dma_data, size, (u64)(long)ctx, true);
kfree(dma_data);
return ret;
}
static int
__hid_bpf_input_report(struct hid_bpf_ctx *ctx, enum hid_report_type type, u8 *buf,
size_t size, bool lock_already_taken)
{
struct hid_bpf_ctx_kern *ctx_kern;
int ret;
ctx_kern = container_of(ctx, struct hid_bpf_ctx_kern, ctx);
if (ctx_kern->from_bpf)
return -EDEADLOCK;
/* check arguments */
ret = __hid_bpf_hw_check_params(ctx, buf, &size, type);
if (ret)
return ret;
return hid_ops->hid_input_report(ctx->hid, type, buf, size, size, 0, (u64)(long)ctx, true,
lock_already_taken);
}
/**
* hid_bpf_try_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. This function will immediately
* fail if the device is not available, thus can be safely used in IRQ context.
*/
__bpf_kfunc int
hid_bpf_try_input_report(struct hid_bpf_ctx *ctx, enum hid_report_type type, u8 *buf,
const size_t buf__sz)
{
struct hid_bpf_ctx_kern *ctx_kern;
bool from_hid_event_hook;
ctx_kern = container_of(ctx, struct hid_bpf_ctx_kern, ctx);
from_hid_event_hook = ctx_kern->data && ctx_kern->data == ctx->hid->bpf.device_data;
return __hid_bpf_input_report(ctx, type, buf, buf__sz, from_hid_event_hook);
}
/**
* 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. This function will wait for the
* device to be available before injecting the event, thus needs to be called in sleepable
* context.
*/
__bpf_kfunc int
hid_bpf_input_report(struct hid_bpf_ctx *ctx, enum hid_report_type type, u8 *buf,
const size_t buf__sz)
{
int ret;
ret = down_interruptible(&ctx->hid->driver_input_lock);
if (ret)
return ret;
/* check arguments */
ret = __hid_bpf_input_report(ctx, type, buf, buf__sz, true /* lock_already_taken */);
up(&ctx->hid->driver_input_lock);
return ret;
}
__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_ID_FLAGS(func, hid_bpf_try_input_report)
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,
};
/* for syscall HID-BPF */
BTF_KFUNCS_START(hid_bpf_syscall_kfunc_ids)
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 = {
.owner = THIS_MODULE,
.set = &hid_bpf_syscall_kfunc_ids,
};
int hid_bpf_connect_device(struct hid_device *hdev)
{
bool need_to_allocate = false;
struct hid_bpf_ops *e;
rcu_read_lock();
list_for_each_entry_rcu(e, &hdev->bpf.prog_list, list) {
if (e->hid_device_event) {
need_to_allocate = true;
break;
}
}
rcu_read_unlock();
/* only allocate BPF data if there are programs attached */
if (!need_to_allocate)
return 0;
return hid_bpf_allocate_event_data(hdev);
}
EXPORT_SYMBOL_GPL(hid_bpf_connect_device);
void hid_bpf_disconnect_device(struct hid_device *hdev)
{
kfree(hdev->bpf.device_data);
hdev->bpf.device_data = NULL;
hdev->bpf.allocated_data = 0;
}
EXPORT_SYMBOL_GPL(hid_bpf_disconnect_device);
void hid_bpf_destroy_device(struct hid_device *hdev)
{
if (!hdev)
return;
/* mark the device as destroyed in bpf so we don't reattach it */
hdev->bpf.destroyed = true;
__hid_bpf_ops_destroy_device(hdev);
synchronize_srcu(&hdev->bpf.srcu);
cleanup_srcu_struct(&hdev->bpf.srcu);
}
EXPORT_SYMBOL_GPL(hid_bpf_destroy_device);
int hid_bpf_device_init(struct hid_device *hdev)
{
INIT_LIST_HEAD(&hdev->bpf.prog_list);
mutex_init(&hdev->bpf.prog_list_lock);
return init_srcu_struct(&hdev->bpf.srcu);
}
EXPORT_SYMBOL_GPL(hid_bpf_device_init);
static int __init hid_bpf_init(void)
{
int err;
/* Note: if we exit with an error any time here, we would entirely break HID, which
* is probably not something we want. So we log an error and return success.
*
* This is not a big deal: nobody will be able to use the functionality.
*/
err = register_btf_kfunc_id_set(BPF_PROG_TYPE_STRUCT_OPS, &hid_bpf_kfunc_set);
if (err) {
pr_warn("error while setting HID BPF tracing kfuncs: %d", err);
return 0;
}
err = register_btf_kfunc_id_set(BPF_PROG_TYPE_SYSCALL, &hid_bpf_syscall_kfunc_set);
if (err) {
pr_warn("error while setting HID BPF syscall kfuncs: %d", err);
return 0;
}
return 0;
}
late_initcall(hid_bpf_init);
MODULE_AUTHOR("Benjamin Tissoires");
MODULE_LICENSE("GPL");
@@ -0,0 +1,22 @@
/* SPDX-License-Identifier: GPL-2.0-only */
#ifndef _BPF_HID_BPF_DISPATCH_H
#define _BPF_HID_BPF_DISPATCH_H
#include <linux/hid.h>
struct hid_bpf_ctx_kern {
struct hid_bpf_ctx ctx;
u8 *data;
bool from_bpf;
};
struct hid_device *hid_get_device(unsigned int hid_id);
void hid_put_device(struct hid_device *hid);
int hid_bpf_allocate_event_data(struct hid_device *hdev);
void __hid_bpf_ops_destroy_device(struct hid_device *hdev);
int hid_bpf_reconnect(struct hid_device *hdev);
struct bpf_prog;
#endif
@@ -0,0 +1,326 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* HID-BPF support for Linux
*
* Copyright (c) 2024 Benjamin Tissoires
*/
#include <linux/bitops.h>
#include <linux/bpf_verifier.h>
#include <linux/bpf.h>
#include <linux/btf.h>
#include <linux/btf_ids.h>
#include <linux/filter.h>
#include <linux/hid.h>
#include <linux/hid_bpf.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/stddef.h>
#include <linux/workqueue.h>
#include "hid_bpf_dispatch.h"
static struct btf *hid_bpf_ops_btf;
static int hid_bpf_ops_init(struct btf *btf)
{
hid_bpf_ops_btf = btf;
return 0;
}
static bool hid_bpf_ops_is_valid_access(int off, int size,
enum bpf_access_type type,
const struct bpf_prog *prog,
struct bpf_insn_access_aux *info)
{
return bpf_tracing_btf_ctx_access(off, size, type, prog, info);
}
static int hid_bpf_ops_check_member(const struct btf_type *t,
const struct btf_member *member,
const struct bpf_prog *prog)
{
u32 moff = __btf_member_bit_offset(t, member) / 8;
switch (moff) {
case offsetof(struct hid_bpf_ops, hid_rdesc_fixup):
case offsetof(struct hid_bpf_ops, hid_hw_request):
case offsetof(struct hid_bpf_ops, hid_hw_output_report):
break;
default:
if (prog->sleepable)
return -EINVAL;
}
return 0;
}
struct hid_bpf_offset_write_range {
const char *struct_name;
u32 struct_length;
u32 start;
u32 end;
};
static int hid_bpf_ops_btf_struct_access(struct bpf_verifier_log *log,
const struct bpf_reg_state *reg,
int off, int size)
{
#define WRITE_RANGE(_name, _field, _is_string) \
{ \
.struct_name = #_name, \
.struct_length = sizeof(struct _name), \
.start = offsetof(struct _name, _field), \
.end = offsetofend(struct _name, _field) - !!(_is_string), \
}
const struct hid_bpf_offset_write_range write_ranges[] = {
WRITE_RANGE(hid_bpf_ctx, retval, false),
WRITE_RANGE(hid_device, name, true),
WRITE_RANGE(hid_device, uniq, true),
WRITE_RANGE(hid_device, phys, true),
};
#undef WRITE_RANGE
const struct btf_type *state = NULL;
const struct btf_type *t;
const char *cur = NULL;
int i;
t = btf_type_by_id(reg->btf, reg->btf_id);
for (i = 0; i < ARRAY_SIZE(write_ranges); i++) {
const struct hid_bpf_offset_write_range *write_range = &write_ranges[i];
s32 type_id;
/* we already found a writeable struct, but there is a
* new one, let's break the loop.
*/
if (t == state && write_range->struct_name != cur)
break;
/* new struct to look for */
if (write_range->struct_name != cur) {
type_id = btf_find_by_name_kind(reg->btf, write_range->struct_name,
BTF_KIND_STRUCT);
if (type_id < 0)
return -EINVAL;
state = btf_type_by_id(reg->btf, type_id);
}
/* this is not the struct we are looking for */
if (t != state) {
cur = write_range->struct_name;
continue;
}
/* first time we see this struct, check for out of bounds */
if (cur != write_range->struct_name &&
off + size > write_range->struct_length) {
bpf_log(log, "write access for struct %s at off %d with size %d\n",
write_range->struct_name, off, size);
return -EACCES;
}
/* now check if we are in our boundaries */
if (off >= write_range->start && off + size <= write_range->end)
return NOT_INIT;
cur = write_range->struct_name;
}
if (t != state)
bpf_log(log, "write access to this struct is not supported\n");
else
bpf_log(log,
"write access at off %d with size %d on read-only part of %s\n",
off, size, cur);
return -EACCES;
}
static const struct bpf_verifier_ops hid_bpf_verifier_ops = {
.get_func_proto = bpf_base_func_proto,
.is_valid_access = hid_bpf_ops_is_valid_access,
.btf_struct_access = hid_bpf_ops_btf_struct_access,
};
static int hid_bpf_ops_init_member(const struct btf_type *t,
const struct btf_member *member,
void *kdata, const void *udata)
{
const struct hid_bpf_ops *uhid_bpf_ops;
struct hid_bpf_ops *khid_bpf_ops;
u32 moff;
uhid_bpf_ops = (const struct hid_bpf_ops *)udata;
khid_bpf_ops = (struct hid_bpf_ops *)kdata;
moff = __btf_member_bit_offset(t, member) / 8;
switch (moff) {
case offsetof(struct hid_bpf_ops, hid_id):
/* For hid_id and flags fields, this function has to copy it
* and return 1 to indicate that the data has been handled by
* the struct_ops type, or the verifier will reject the map if
* the value of those fields is not zero.
*/
khid_bpf_ops->hid_id = uhid_bpf_ops->hid_id;
return 1;
case offsetof(struct hid_bpf_ops, flags):
if (uhid_bpf_ops->flags & ~BPF_F_BEFORE)
return -EINVAL;
khid_bpf_ops->flags = uhid_bpf_ops->flags;
return 1;
}
return 0;
}
static int hid_bpf_reg(void *kdata, struct bpf_link *link)
{
struct hid_bpf_ops *ops = kdata;
struct hid_device *hdev;
int count, err = 0;
/* prevent multiple attach of the same struct_ops */
if (ops->hdev)
return -EINVAL;
hdev = hid_get_device(ops->hid_id);
if (IS_ERR(hdev))
return PTR_ERR(hdev);
ops->hdev = hdev;
mutex_lock(&hdev->bpf.prog_list_lock);
count = list_count_nodes(&hdev->bpf.prog_list);
if (count >= HID_BPF_MAX_PROGS_PER_DEV) {
err = -E2BIG;
goto out_unlock;
}
if (ops->hid_rdesc_fixup) {
if (hdev->bpf.rdesc_ops) {
err = -EINVAL;
goto out_unlock;
}
hdev->bpf.rdesc_ops = ops;
}
if (ops->hid_device_event) {
err = hid_bpf_allocate_event_data(hdev);
if (err)
goto out_unlock;
}
if (ops->flags & BPF_F_BEFORE)
list_add_rcu(&ops->list, &hdev->bpf.prog_list);
else
list_add_tail_rcu(&ops->list, &hdev->bpf.prog_list);
synchronize_srcu(&hdev->bpf.srcu);
out_unlock:
mutex_unlock(&hdev->bpf.prog_list_lock);
if (err) {
if (hdev->bpf.rdesc_ops == ops)
hdev->bpf.rdesc_ops = NULL;
hid_put_device(hdev);
} else if (ops->hid_rdesc_fixup) {
hid_bpf_reconnect(hdev);
}
return err;
}
static void hid_bpf_unreg(void *kdata, struct bpf_link *link)
{
struct hid_bpf_ops *ops = kdata;
struct hid_device *hdev;
bool reconnect = false;
hdev = ops->hdev;
/* check if __hid_bpf_ops_destroy_device() has been called */
if (!hdev)
return;
mutex_lock(&hdev->bpf.prog_list_lock);
list_del_rcu(&ops->list);
synchronize_srcu(&hdev->bpf.srcu);
ops->hdev = NULL;
reconnect = hdev->bpf.rdesc_ops == ops;
if (reconnect)
hdev->bpf.rdesc_ops = NULL;
mutex_unlock(&hdev->bpf.prog_list_lock);
if (reconnect)
hid_bpf_reconnect(hdev);
hid_put_device(hdev);
}
static int __hid_bpf_device_event(struct hid_bpf_ctx *ctx, enum hid_report_type type, u64 source)
{
return 0;
}
static int __hid_bpf_rdesc_fixup(struct hid_bpf_ctx *ctx)
{
return 0;
}
static int __hid_bpf_hw_request(struct hid_bpf_ctx *ctx, unsigned char reportnum,
enum hid_report_type rtype, enum hid_class_request reqtype,
u64 source)
{
return 0;
}
static int __hid_bpf_hw_output_report(struct hid_bpf_ctx *ctx, u64 source)
{
return 0;
}
static struct hid_bpf_ops __bpf_hid_bpf_ops = {
.hid_device_event = __hid_bpf_device_event,
.hid_rdesc_fixup = __hid_bpf_rdesc_fixup,
.hid_hw_request = __hid_bpf_hw_request,
.hid_hw_output_report = __hid_bpf_hw_output_report,
};
static struct bpf_struct_ops bpf_hid_bpf_ops = {
.verifier_ops = &hid_bpf_verifier_ops,
.init = hid_bpf_ops_init,
.check_member = hid_bpf_ops_check_member,
.init_member = hid_bpf_ops_init_member,
.reg = hid_bpf_reg,
.unreg = hid_bpf_unreg,
.name = "hid_bpf_ops",
.cfi_stubs = &__bpf_hid_bpf_ops,
.owner = THIS_MODULE,
};
void __hid_bpf_ops_destroy_device(struct hid_device *hdev)
{
struct hid_bpf_ops *e;
rcu_read_lock();
list_for_each_entry_rcu(e, &hdev->bpf.prog_list, list) {
hid_put_device(hdev);
e->hdev = NULL;
}
rcu_read_unlock();
}
static int __init hid_bpf_struct_ops_init(void)
{
return register_bpf_struct_ops(&bpf_hid_bpf_ops, hid_bpf_ops);
}
late_initcall(hid_bpf_struct_ops_init);
@@ -0,0 +1,190 @@
// 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(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(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;
}
HID_BPF_OPS(raptor_mach_2) = {
.hid_rdesc_fixup = (void *)hid_fix_rdesc_raptor_mach_2,
.hid_device_event = (void *)raptor_mach_2_fix_hat_switch,
};
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";
@@ -0,0 +1,90 @@
// SPDX-License-Identifier: GPL-2.0-only
/* Copyright (c) 2025 Benjamin Tissoires
*/
#include "vmlinux.h"
#include "hid_bpf.h"
#include "hid_bpf_helpers.h"
#include "hid_report_helpers.h"
#include "hid_usages.h"
#include <bpf/bpf_tracing.h>
HID_BPF_CONFIG(
HID_DEVICE(BUS_ANY, HID_GROUP_MULTITOUCH_WIN_8, HID_VID_ANY, HID_PID_ANY),
);
EXPORT_UDEV_PROP(HID_DIGITIZER_PAD_TYPE, 32);
__u8 hw_req_buf[1024];
/* to be filled by udev-hid-bpf */
struct hid_rdesc_descriptor HID_REPORT_DESCRIPTOR;
SEC("syscall")
int probe(struct hid_bpf_probe_args *ctx)
{
struct hid_rdesc_report *pad_type_feature = NULL;
struct hid_rdesc_field *pad_type = NULL;
struct hid_rdesc_report *feature;
struct hid_bpf_ctx *hid_ctx;
char *pad_type_str = "";
int ret;
hid_bpf_for_each_feature_report(&HID_REPORT_DESCRIPTOR, feature) {
struct hid_rdesc_field *field;
hid_bpf_for_each_field(feature, field) {
if (field->usage_page == HidUsagePage_Digitizers &&
field->usage_id == HidUsage_Dig_PadType) {
pad_type = field;
pad_type_feature = feature;
break;
}
}
if (pad_type)
break;
}
if (!pad_type || !pad_type_feature) {
ctx->retval = -EINVAL;
return 0;
}
hid_ctx = hid_bpf_allocate_context(ctx->hid);
if (!hid_ctx)
return -1; /* EPERM check */
hw_req_buf[0] = pad_type_feature->report_id;
ret = hid_bpf_hw_request(hid_ctx, hw_req_buf, sizeof(hw_req_buf),
HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
hid_bpf_release_context(hid_ctx);
if (ret < 0) {
ctx->retval = ret;
return 0;
}
ctx->retval = 0;
switch (EXTRACT_BITS(hw_req_buf, pad_type)) {
case 0:
pad_type_str = "Clickpad";
break;
case 1:
pad_type_str = "Pressurepad";
break;
case 2:
pad_type_str = "Discrete";
break;
default:
pad_type_str = "Unknown";
}
UDEV_PROP_SPRINTF(HID_DIGITIZER_PAD_TYPE, "%s", pad_type_str);
return 0;
}
char _license[] SEC("license") = "GPL";
@@ -0,0 +1,62 @@
// 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(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;
}
HID_BPF_OPS(hp_elite_presenter) = {
.hid_rdesc_fixup = (void *)hid_fix_rdesc,
};
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";
@@ -0,0 +1,636 @@
// SPDX-License-Identifier: GPL-2.0-only
/* Copyright (c) 2024 Red Hat, Inc
*/
#include "vmlinux.h"
#include "hid_bpf.h"
#include "hid_bpf_helpers.h"
#include "hid_report_helpers.h"
#include <bpf/bpf_tracing.h>
#define VID_HUION 0x256C
#define PID_DIAL_2 0x0060
HID_BPF_CONFIG(
HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_HUION, PID_DIAL_2),
);
/* Filled in by udev-hid-bpf */
char UDEV_PROP_HUION_FIRMWARE_ID[64];
/* The prefix of the firmware ID we expect for this device. The full firmware
* string has a date suffix, e.g. HUION_T21j_221221
*/
char EXPECTED_FIRMWARE_ID[] = "HUION_T216_";
/* How this BPF program works: the tablet has two modes, firmware mode and
* tablet mode. In firmware mode (out of the box) the tablet sends button events
* and the dial as keyboard combinations. In tablet mode it uses a vendor specific
* hid report to report everything instead.
* Depending on the mode some hid reports are never sent and the corresponding
* devices are mute.
*
* To switch the tablet use e.g. https://github.com/whot/huion-switcher
* or one of the tools from the digimend project
*
* This BPF works for both modes. The huion-switcher tool sets the
* HUION_FIRMWARE_ID udev property - if that is set then we disable the firmware
* pad and pen reports (by making them vendor collections that are ignored).
* If that property is not set we fix all hidraw nodes so the tablet works in
* either mode though the drawback is that the device will show up twice if
* you bind it to all event nodes
*
* Default report descriptor for the first exposed hidraw node:
*
* # HUION Huion Tablet_Q630M
* # 0x06, 0x00, 0xff, // Usage Page (Vendor Defined Page 1) 0
* # 0x09, 0x01, // Usage (Vendor Usage 1) 3
* # 0xa1, 0x01, // Collection (Application) 5
* # 0x85, 0x08, // Report ID (8) 7
* # 0x75, 0x58, // Report Size (88) 9
* # 0x95, 0x01, // Report Count (1) 11
* # 0x09, 0x01, // Usage (Vendor Usage 1) 13
* # 0x81, 0x02, // Input (Data,Var,Abs) 15
* # 0xc0, // End Collection 17
* R: 18 06 00 ff 09 01 a1 01 85 08 75 58 95 01 09 01 81 02 c0
*
* This rdesc does nothing until the tablet is switched to raw mode, see
* https://github.com/whot/huion-switcher
*
*
* Second hidraw node is the Pen. This one sends events until the tablet is
* switched to raw mode, then it's mute.
*
* # Report descriptor length: 93 bytes
* # HUION Huion Tablet_Q630M
* # 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, 0x45, // Usage (Eraser) 16
* # 0x09, 0x3c, // Usage (Invert) 18
* # 0x15, 0x00, // Logical Minimum (0) 20
* # 0x25, 0x01, // Logical Maximum (1) 22
* # 0x75, 0x01, // Report Size (1) 24
* # 0x95, 0x06, // Report Count (6) 26
* # 0x81, 0x02, // Input (Data,Var,Abs) 28
* # 0x09, 0x32, // Usage (In Range) 30
* # 0x75, 0x01, // Report Size (1) 32
* # 0x95, 0x01, // Report Count (1) 34
* # 0x81, 0x02, // Input (Data,Var,Abs) 36
* # 0x81, 0x03, // Input (Cnst,Var,Abs) 38
* # 0x05, 0x01, // Usage Page (Generic Desktop) 40
* # 0x09, 0x30, // Usage (X) 42
* # 0x09, 0x31, // Usage (Y) 44
* # 0x55, 0x0d, // Unit Exponent (-3) 46
* # 0x65, 0x33, // Unit (EnglishLinear: in³) 48
* # 0x26, 0xff, 0x7f, // Logical Maximum (32767) 50
* # 0x35, 0x00, // Physical Minimum (0) 53
* # 0x46, 0x00, 0x08, // Physical Maximum (2048) 55
* # 0x75, 0x10, // Report Size (16) 58
* # 0x95, 0x02, // Report Count (2) 60
* # 0x81, 0x02, // Input (Data,Var,Abs) 62
* # 0x05, 0x0d, // Usage Page (Digitizers) 64
* # 0x09, 0x30, // Usage (Tip Pressure) 66
* # 0x26, 0xff, 0x1f, // Logical Maximum (8191) 68
* # 0x75, 0x10, // Report Size (16) 71
* # 0x95, 0x01, // Report Count (1) 73
* # 0x81, 0x02, // Input (Data,Var,Abs) 75
* # 0x09, 0x3d, // Usage (X Tilt) 77
* # 0x09, 0x3e, // Usage (Y Tilt) 79
* # 0x15, 0x81, // Logical Minimum (-127) 81
* # 0x25, 0x7f, // Logical Maximum (127) 83
* # 0x75, 0x08, // Report Size (8) 85
* # 0x95, 0x02, // Report Count (2) 87
* # 0x81, 0x02, // Input (Data,Var,Abs) 89
* # 0xc0, // End Collection 91
* # 0xc0, // End Collection 92
* R: 93 05 0d 09 02 a1 01 85 0a 09 20 a1 01 09 42 09 44 09 45 09 3c 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 1f 75 10 95 01 81 02 09 3d 09 3e 15 81 25 7f 75 08 95 02 81 02 c0 c0
*
* Third hidraw node is the pad which sends a combination of keyboard shortcuts until
* the tablet is switched to raw mode, then it's mute:
*
* # Report descriptor length: 148 bytes
* # HUION Huion Tablet_Q630M
* # 0x05, 0x01, // Usage Page (Generic Desktop) 0
* # 0x09, 0x0e, // Usage (System Multi-Axis Controller) 2
* # 0xa1, 0x01, // Collection (Application) 4
* # 0x85, 0x11, // Report ID (17) 6
* # 0x05, 0x0d, // Usage Page (Digitizers) 8
* # 0x09, 0x21, // Usage (Puck) 10
* # 0xa1, 0x02, // Collection (Logical) 12
* # 0x15, 0x00, // Logical Minimum (0) 14
* # 0x25, 0x01, // Logical Maximum (1) 16
* # 0x75, 0x01, // Report Size (1) 18
* # 0x95, 0x01, // Report Count (1) 20
* # 0xa1, 0x00, // Collection (Physical) 22
* # 0x05, 0x09, // Usage Page (Button) 24
* # 0x09, 0x01, // Usage (Vendor Usage 0x01) 26
* # 0x81, 0x02, // Input (Data,Var,Abs) 28
* # 0x05, 0x0d, // Usage Page (Digitizers) 30
* # 0x09, 0x33, // Usage (Touch) 32
* # 0x81, 0x02, // Input (Data,Var,Abs) 34
* # 0x95, 0x06, // Report Count (6) 36
* # 0x81, 0x03, // Input (Cnst,Var,Abs) 38
* # 0xa1, 0x02, // Collection (Logical) 40
* # 0x05, 0x01, // Usage Page (Generic Desktop) 42
* # 0x09, 0x37, // Usage (Dial) 44
* # 0x16, 0x00, 0x80, // Logical Minimum (-32768) 46
* # 0x26, 0xff, 0x7f, // Logical Maximum (32767) 49
* # 0x75, 0x10, // Report Size (16) 52
* # 0x95, 0x01, // Report Count (1) 54
* # 0x81, 0x06, // Input (Data,Var,Rel) 56
* # 0x35, 0x00, // Physical Minimum (0) 58
* # 0x46, 0x10, 0x0e, // Physical Maximum (3600) 60
* # 0x15, 0x00, // Logical Minimum (0) 63
* # 0x26, 0x10, 0x0e, // Logical Maximum (3600) 65
* # 0x09, 0x48, // Usage (Resolution Multiplier) 68
* # 0xb1, 0x02, // Feature (Data,Var,Abs) 70
* # 0x45, 0x00, // Physical Maximum (0) 72
* # 0xc0, // End Collection 74
* # 0x75, 0x08, // Report Size (8) 75
* # 0x95, 0x01, // Report Count (1) 77
* # 0x81, 0x01, // Input (Cnst,Arr,Abs) 79
* # 0x75, 0x08, // Report Size (8) 81
* # 0x95, 0x01, // Report Count (1) 83
* # 0x81, 0x01, // Input (Cnst,Arr,Abs) 85
* # 0x75, 0x08, // Report Size (8) 87
* # 0x95, 0x01, // Report Count (1) 89
* # 0x81, 0x01, // Input (Cnst,Arr,Abs) 91
* # 0x75, 0x08, // Report Size (8) 93
* # 0x95, 0x01, // Report Count (1) 95
* # 0x81, 0x01, // Input (Cnst,Arr,Abs) 97
* # 0x75, 0x08, // Report Size (8) 99
* # 0x95, 0x01, // Report Count (1) 101
* # 0x81, 0x01, // Input (Cnst,Arr,Abs) 103
* # 0xc0, // End Collection 105
* # 0xc0, // End Collection 106
* # 0xc0, // End Collection 107
* # 0x05, 0x01, // Usage Page (Generic Desktop) 108
* # 0x09, 0x06, // Usage (Keyboard) 110
* # 0xa1, 0x01, // Collection (Application) 112
* # 0x85, 0x03, // Report ID (3) 114
* # 0x05, 0x07, // Usage Page (Keyboard) 116
* # 0x19, 0xe0, // Usage Minimum (224) 118
* # 0x29, 0xe7, // Usage Maximum (231) 120
* # 0x15, 0x00, // Logical Minimum (0) 122
* # 0x25, 0x01, // Logical Maximum (1) 124
* # 0x75, 0x01, // Report Size (1) 126
* # 0x95, 0x08, // Report Count (8) 128
* # 0x81, 0x02, // Input (Data,Var,Abs) 130
* # 0x05, 0x07, // Usage Page (Keyboard) 132
* # 0x19, 0x00, // Usage Minimum (0) 134
* # 0x29, 0xff, // Usage Maximum (255) 136
* # 0x26, 0xff, 0x00, // Logical Maximum (255) 138
* # 0x75, 0x08, // Report Size (8) 141
* # 0x95, 0x06, // Report Count (6) 143
* # 0x81, 0x00, // Input (Data,Arr,Abs) 145
* # 0xc0, // End Collection 147
* R: 148 05 01 09 0e a1 01 85 11 05 0d 09 21 a1 02 15 00 25 01 75 01 95 01 a1 00 05 09 09 01 81 02 05 0d 09 33 81 02 95 06 81 03 a1 02 05 01 09 37 16 00 80 26 ff 7f 75 10 95 01 81 06 35 00 46 10 0e 15 00 26 10 0e 09 48 b1 02 45 00 c0 75 08 95 01 81 01 75 08 95 01 81 01 75 08 95 01 81 01 75 08 95 01 81 01 75 08 95 01 81 01 c0 c0 c0 05 01 09 06 a1 01 85 03 05 07 19 e0 29 e7 15 00 25 01 75 01 95 08 81 02 05 07 19 00 29 ff 26 ff 00 75 08 95 06 81 00 c0
*/
#define PAD_REPORT_DESCRIPTOR_LENGTH 148
#define PEN_REPORT_DESCRIPTOR_LENGTH 93
#define VENDOR_REPORT_DESCRIPTOR_LENGTH 18
#define PAD_REPORT_ID 3
#define DIAL_REPORT_ID 17
#define PEN_REPORT_ID 10
#define VENDOR_REPORT_ID 8
#define PAD_REPORT_LENGTH 9
#define PEN_REPORT_LENGTH 10
#define VENDOR_REPORT_LENGTH 12
__u8 last_button_state;
static const __u8 fixed_rdesc_pad[] = {
UsagePage_GenericDesktop
Usage_GD_Keypad
CollectionApplication(
// -- Byte 0 in report
ReportId(PAD_REPORT_ID)
LogicalMaximum_i8(0)
LogicalMaximum_i8(1)
UsagePage_Digitizers
Usage_Dig_TabletFunctionKeys
CollectionPhysical(
// Byte 1 in report - just exists so we get to be a tablet pad
Usage_Dig_BarrelSwitch // BTN_STYLUS
ReportCount(1)
ReportSize(1)
Input(Var|Abs)
ReportCount(7) // padding
Input(Const)
// Bytes 2/3 in report - just exists so we get to be a tablet pad
UsagePage_GenericDesktop
Usage_GD_X
Usage_GD_Y
ReportCount(2)
ReportSize(8)
Input(Var|Abs)
// Byte 4 in report is the dial
Usage_GD_Wheel
LogicalMinimum_i8(-1)
LogicalMaximum_i8(1)
ReportCount(1)
ReportSize(8)
Input(Var|Rel)
// Byte 5 is the button state
UsagePage_Button
UsageMinimum_i8(0x01)
UsageMaximum_i8(0x08)
LogicalMinimum_i8(0x0)
LogicalMaximum_i8(0x1)
ReportCount(7)
ReportSize(1)
Input(Var|Abs)
ReportCount(1) // padding
Input(Const)
)
// Make sure we match our original report length
FixedSizeVendorReport(PAD_REPORT_LENGTH)
)
};
static const __u8 fixed_rdesc_pen[] = {
UsagePage_Digitizers
Usage_Dig_Pen
CollectionApplication(
// -- Byte 0 in report
ReportId(PEN_REPORT_ID)
Usage_Dig_Pen
CollectionPhysical(
// -- Byte 1 in report
Usage_Dig_TipSwitch
Usage_Dig_BarrelSwitch
Usage_Dig_SecondaryBarrelSwitch // maps eraser to BTN_STYLUS2
LogicalMinimum_i8(0)
LogicalMaximum_i8(1)
ReportSize(1)
ReportCount(3)
Input(Var|Abs)
ReportCount(4) // Padding
Input(Const)
Usage_Dig_InRange
ReportCount(1)
Input(Var|Abs)
ReportSize(16)
ReportCount(1)
PushPop(
UsagePage_GenericDesktop
Unit(cm)
UnitExponent(-1)
PhysicalMinimum_i16(0)
PhysicalMaximum_i16(266)
LogicalMinimum_i16(0)
LogicalMaximum_i16(32767)
Usage_GD_X
Input(Var|Abs) // Bytes 2+3
PhysicalMinimum_i16(0)
PhysicalMaximum_i16(166)
LogicalMinimum_i16(0)
LogicalMaximum_i16(32767)
Usage_GD_Y
Input(Var|Abs) // Bytes 4+5
)
UsagePage_Digitizers
Usage_Dig_TipPressure
LogicalMinimum_i16(0)
LogicalMaximum_i16(8191)
Input(Var|Abs) // Byte 6+7
ReportSize(8)
ReportCount(2)
LogicalMinimum_i8(-60)
LogicalMaximum_i8(60)
Usage_Dig_XTilt
Usage_Dig_YTilt
Input(Var|Abs) // Byte 8+9
)
)
};
static const __u8 fixed_rdesc_vendor[] = {
UsagePage_Digitizers
Usage_Dig_Pen
CollectionApplication(
// Byte 0
// We leave the pen on the vendor report ID
ReportId(VENDOR_REPORT_ID)
Usage_Dig_Pen
CollectionPhysical(
// Byte 1 are the buttons
LogicalMinimum_i8(0)
LogicalMaximum_i8(1)
ReportSize(1)
Usage_Dig_TipSwitch
Usage_Dig_BarrelSwitch
Usage_Dig_SecondaryBarrelSwitch
ReportCount(3)
Input(Var|Abs)
ReportCount(4) // Padding
Input(Const)
Usage_Dig_InRange
ReportCount(1)
Input(Var|Abs)
ReportSize(16)
ReportCount(1)
PushPop(
UsagePage_GenericDesktop
Unit(cm)
UnitExponent(-1)
// Note: reported logical range differs
// from the pen report ID for x and y
LogicalMinimum_i16(0)
LogicalMaximum_i16(53340)
PhysicalMinimum_i16(0)
PhysicalMaximum_i16(266)
// Bytes 2/3 in report
Usage_GD_X
Input(Var|Abs)
LogicalMinimum_i16(0)
LogicalMaximum_i16(33340)
PhysicalMinimum_i16(0)
PhysicalMaximum_i16(166)
// Bytes 4/5 in report
Usage_GD_Y
Input(Var|Abs)
)
// Bytes 6/7 in report
LogicalMinimum_i16(0)
LogicalMaximum_i16(8191)
Usage_Dig_TipPressure
Input(Var|Abs)
// Bytes 8/9 in report
ReportCount(1) // Padding
Input(Const)
LogicalMinimum_i8(-60)
LogicalMaximum_i8(60)
// Byte 10 in report
Usage_Dig_XTilt
// Byte 11 in report
Usage_Dig_YTilt
ReportSize(8)
ReportCount(2)
Input(Var|Abs)
)
)
UsagePage_GenericDesktop
Usage_GD_Keypad
CollectionApplication(
// Byte 0
ReportId(PAD_REPORT_ID)
LogicalMinimum_i8(0)
LogicalMaximum_i8(1)
UsagePage_Digitizers
Usage_Dig_TabletFunctionKeys
CollectionPhysical(
// Byte 1 are the buttons
Usage_Dig_BarrelSwitch // BTN_STYLUS, needed so we get to be a tablet pad
ReportCount(1)
ReportSize(1)
Input(Var|Abs)
ReportCount(7) // Padding
Input(Const)
// Bytes 2/3 - x/y just exist so we get to be a tablet pad
UsagePage_GenericDesktop
Usage_GD_X
Usage_GD_Y
ReportCount(2)
ReportSize(8)
Input(Var|Abs)
// Byte 4 is the button state
UsagePage_Button
UsageMinimum_i8(0x1)
UsageMaximum_i8(0x8)
LogicalMinimum_i8(0x0)
LogicalMaximum_i8(0x1)
ReportCount(8)
ReportSize(1)
Input(Var|Abs)
// Byte 5 is the top dial
UsagePage_GenericDesktop
Usage_GD_Wheel
LogicalMinimum_i8(-1)
LogicalMaximum_i8(1)
ReportCount(1)
ReportSize(8)
Input(Var|Rel)
// Byte 6 is the bottom dial
UsagePage_Consumer
Usage_Con_ACPan
Input(Var|Rel)
)
// Make sure we match our original report length
FixedSizeVendorReport(VENDOR_REPORT_LENGTH)
)
};
static const __u8 disabled_rdesc_pen[] = {
FixedSizeVendorReport(PEN_REPORT_LENGTH)
};
static const __u8 disabled_rdesc_pad[] = {
FixedSizeVendorReport(PAD_REPORT_LENGTH)
};
SEC(HID_BPF_RDESC_FIXUP)
int BPF_PROG(dial_2_fix_rdesc, struct hid_bpf_ctx *hctx)
{
__u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, HID_MAX_DESCRIPTOR_SIZE /* size */);
__s32 rdesc_size = hctx->size;
__u8 have_fw_id;
if (!data)
return 0; /* EPERM check */
/* If we have a firmware ID and it matches our expected prefix, we
* disable the default pad/pen nodes. They won't send events
* but cause duplicate devices.
*/
have_fw_id = __builtin_memcmp(UDEV_PROP_HUION_FIRMWARE_ID,
EXPECTED_FIRMWARE_ID,
sizeof(EXPECTED_FIRMWARE_ID) - 1) == 0;
if (rdesc_size == PAD_REPORT_DESCRIPTOR_LENGTH) {
if (have_fw_id) {
__builtin_memcpy(data, disabled_rdesc_pad, sizeof(disabled_rdesc_pad));
return sizeof(disabled_rdesc_pad);
}
__builtin_memcpy(data, fixed_rdesc_pad, sizeof(fixed_rdesc_pad));
return sizeof(fixed_rdesc_pad);
}
if (rdesc_size == PEN_REPORT_DESCRIPTOR_LENGTH) {
if (have_fw_id) {
__builtin_memcpy(data, disabled_rdesc_pen, sizeof(disabled_rdesc_pen));
return sizeof(disabled_rdesc_pen);
}
__builtin_memcpy(data, fixed_rdesc_pen, sizeof(fixed_rdesc_pen));
return sizeof(fixed_rdesc_pen);
}
/* Always fix the vendor mode so the tablet will work even if nothing sets
* the udev property (e.g. huion-switcher run manually)
*/
if (rdesc_size == VENDOR_REPORT_DESCRIPTOR_LENGTH) {
__builtin_memcpy(data, fixed_rdesc_vendor, sizeof(fixed_rdesc_vendor));
return sizeof(fixed_rdesc_vendor);
}
return 0;
}
SEC(HID_BPF_DEVICE_EVENT)
int BPF_PROG(dial_2_fix_events, struct hid_bpf_ctx *hctx)
{
__u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, 16 /* size */);
static __u8 button;
if (!data)
return 0; /* EPERM check */
/* Only sent if tablet is in default mode */
if (data[0] == PAD_REPORT_ID) {
/* Nicely enough, this device only supports one button down at a time so
* the reports are easy to match. Buttons numbered from the top
* Button released: 03 00 00 00 00 00 00 00
* Button 1: 03 00 05 00 00 00 00 00 -> b
* Button 2: 03 00 08 00 00 00 00 00 -> e
* Button 3: 03 00 0c 00 00 00 00 00 -> i
* Button 4: 03 00 e0 16 00 00 00 00 -> Ctrl S
* Button 5: 03 00 2c 00 00 00 00 00 -> space
* Button 6: 03 00 e0 e2 1d 00 00 00 -> Ctrl Alt Z
*/
button &= 0xc0;
switch ((data[2] << 16) | (data[3] << 8) | data[4]) {
case 0x000000:
break;
case 0x050000:
button |= BIT(0);
break;
case 0x080000:
button |= BIT(1);
break;
case 0x0c0000:
button |= BIT(2);
break;
case 0xe01600:
button |= BIT(3);
break;
case 0x2c0000:
button |= BIT(4);
break;
case 0xe0e21d:
button |= BIT(5);
break;
}
__u8 report[8] = {PAD_REPORT_ID, 0x0, 0x0, 0x0, 0x00, button};
__builtin_memcpy(data, report, sizeof(report));
return sizeof(report);
}
/* Only sent if tablet is in default mode */
if (data[0] == DIAL_REPORT_ID) {
/*
* In default mode, both dials are merged together:
*
* Dial down: 11 00 ff ff 00 00 00 00 00 -> Dial -1
* Dial up: 11 00 01 00 00 00 00 00 00 -> Dial 1
*/
__u16 dial = data[3] << 8 | data[2];
button &= 0x3f;
button |= !!data[1] << 6;
__u8 report[] = {PAD_REPORT_ID, 0x0, 0x0, 0x0, dial, button};
__builtin_memcpy(data, report, sizeof(report));
return sizeof(report);
}
/* Nothing to do for the PEN_REPORT_ID, it's already mapped */
/* Only sent if tablet is in raw mode */
if (data[0] == VENDOR_REPORT_ID) {
/* Pad reports */
if (data[1] & 0x20) {
/* See fixed_rdesc_pad */
struct pad_report {
__u8 report_id;
__u8 btn_stylus;
__u8 x;
__u8 y;
__u8 buttons;
__u8 dial_1;
__u8 dial_2;
} __attribute__((packed)) *pad_report;
__u8 dial_1 = 0, dial_2 = 0;
/* Dial report */
if (data[1] == 0xf1) {
__u8 d = 0;
if (data[5] == 2)
d = 0xff;
else
d = data[5];
if (data[3] == 1)
dial_1 = d;
else
dial_2 = d;
} else {
/* data[4] are the buttons, mapped correctly */
last_button_state = data[4];
dial_1 = 0; // dial
dial_2 = 0;
}
pad_report = (struct pad_report *)data;
pad_report->report_id = PAD_REPORT_ID;
pad_report->btn_stylus = 0;
pad_report->x = 0;
pad_report->y = 0;
pad_report->buttons = last_button_state;
pad_report->dial_1 = dial_1;
pad_report->dial_2 = dial_2;
return sizeof(struct pad_report);
}
/* Pen reports need nothing done */
}
return 0;
}
HID_BPF_OPS(inspiroy_dial2) = {
.hid_device_event = (void *)dial_2_fix_events,
.hid_rdesc_fixup = (void *)dial_2_fix_rdesc,
};
SEC("syscall")
int probe(struct hid_bpf_probe_args *ctx)
{
switch (ctx->rdesc_size) {
case PAD_REPORT_DESCRIPTOR_LENGTH:
case PEN_REPORT_DESCRIPTOR_LENGTH:
case VENDOR_REPORT_DESCRIPTOR_LENGTH:
ctx->retval = 0;
break;
default:
ctx->retval = -EINVAL;
}
return 0;
}
char _license[] SEC("license") = "GPL";
@@ -0,0 +1,563 @@
// SPDX-License-Identifier: GPL-2.0-only
/* Copyright (c) 2024 Red Hat, Inc
*/
#include "vmlinux.h"
#include "hid_bpf.h"
#include "hid_bpf_helpers.h"
#include "hid_report_helpers.h"
#include <bpf/bpf_tracing.h>
#define VID_HUION 0x256C
#define PID_INSPIROY_2_M 0x0067
HID_BPF_CONFIG(
HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_HUION, PID_INSPIROY_2_M),
);
/* Filled in by udev-hid-bpf */
char UDEV_PROP_HUION_FIRMWARE_ID[64];
/* The prefix of the firmware ID we expect for this device. The full firmware
* string has a date suffix, e.g. HUION_T21j_221221
*/
char EXPECTED_FIRMWARE_ID[] = "HUION_T21k_";
/* How this BPF program works: the tablet has two modes, firmware mode and
* tablet mode. In firmware mode (out of the box) the tablet sends button events
* and the dial as keyboard combinations. In tablet mode it uses a vendor specific
* hid report to report everything instead.
* Depending on the mode some hid reports are never sent and the corresponding
* devices are mute.
*
* To switch the tablet use e.g. https://github.com/whot/huion-switcher
* or one of the tools from the digimend project
*
* This BPF works for both modes. The huion-switcher tool sets the
* HUION_FIRMWARE_ID udev property - if that is set then we disable the firmware
* pad and pen reports (by making them vendor collections that are ignored).
* If that property is not set we fix all hidraw nodes so the tablet works in
* either mode though the drawback is that the device will show up twice if
* you bind it to all event nodes
*
* Default report descriptor for the first exposed hidraw node:
*
* # HUION Huion Tablet_H641P
* # Report descriptor length: 18 bytes
* # 0x06, 0x00, 0xff, // Usage Page (Vendor Defined Page 0xFF00) 0
* # 0x09, 0x01, // Usage (Vendor Usage 0x01) 3
* # 0xa1, 0x01, // Collection (Application) 5
* # 0x85, 0x08, // Report ID (8) 7
* # 0x75, 0x58, // Report Size (88) 9
* # 0x95, 0x01, // Report Count (1) 11
* # 0x09, 0x01, // Usage (Vendor Usage 0x01) 13
* # 0x81, 0x02, // Input (Data,Var,Abs) 15
* # 0xc0, // End Collection 17
* R: 18 06 00 ff 09 01 a1 01 85 08 75 58 95 01 09 01 81 02 c0
*
* This rdesc does nothing until the tablet is switched to raw mode, see
* https://github.com/whot/huion-switcher
*
*
* Second hidraw node is the Pen. This one sends events until the tablet is
* switched to raw mode, then it's mute.
*
* # Report descriptor length: 93 bytes
* # 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, 0x45, // Usage (Eraser) 16
* # 0x09, 0x3c, // Usage (Invert) 18 <-- has no Invert eraser
* # 0x15, 0x00, // Logical Minimum (0) 20
* # 0x25, 0x01, // Logical Maximum (1) 22
* # 0x75, 0x01, // Report Size (1) 24
* # 0x95, 0x06, // Report Count (6) 26
* # 0x81, 0x02, // Input (Data,Var,Abs) 28
* # 0x09, 0x32, // Usage (In Range) 30
* # 0x75, 0x01, // Report Size (1) 32
* # 0x95, 0x01, // Report Count (1) 34
* # 0x81, 0x02, // Input (Data,Var,Abs) 36
* # 0x81, 0x03, // Input (Cnst,Var,Abs) 38
* # 0x05, 0x01, // Usage Page (Generic Desktop) 40
* # 0x09, 0x30, // Usage (X) 42
* # 0x09, 0x31, // Usage (Y) 44
* # 0x55, 0x0d, // Unit Exponent (-3) 46 <-- change to -2
* # 0x65, 0x33, // Unit (EnglishLinear: in³) 48 <-- change in³ to in
* # 0x26, 0xff, 0x7f, // Logical Maximum (32767) 50
* # 0x35, 0x00, // Physical Minimum (0) 53
* # 0x46, 0x00, 0x08, // Physical Maximum (2048) 55 <-- invalid size
* # 0x75, 0x10, // Report Size (16) 58
* # 0x95, 0x02, // Report Count (2) 60
* # 0x81, 0x02, // Input (Data,Var,Abs) 62
* # 0x05, 0x0d, // Usage Page (Digitizers) 64
* # 0x09, 0x30, // Usage (Tip Pressure) 66
* # 0x26, 0xff, 0x1f, // Logical Maximum (8191) 68
* # 0x75, 0x10, // Report Size (16) 71
* # 0x95, 0x01, // Report Count (1) 73
* # 0x81, 0x02, // Input (Data,Var,Abs) 75
* # 0x09, 0x3d, // Usage (X Tilt) 77 <-- No tilt reported
* # 0x09, 0x3e, // Usage (Y Tilt) 79
* # 0x15, 0x81, // Logical Minimum (-127) 81
* # 0x25, 0x7f, // Logical Maximum (127) 83
* # 0x75, 0x08, // Report Size (8) 85
* # 0x95, 0x02, // Report Count (2) 87
* # 0x81, 0x02, // Input (Data,Var,Abs) 89
* # 0xc0, // End Collection 91
* # 0xc0, // End Collection 92
* R: 93 05 0d 09 02 a1 01 85 0a 09 20 a1 01 09 42 09 44 09 45 09 3c 15 00 25 01 7501 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 ff7f 35 00 46 00 08 75 10 95 02 81 02 05 0d 09 30 26 ff 1f 75 10 95 01 81 02 09 3d09 3e 15 81 25 7f 75 08 95 02 81 02 c0 c0
*
* Third hidraw node is the pad which sends a combination of keyboard shortcuts until
* the tablet is switched to raw mode, then it's mute:
*
* # Report descriptor length: 65 bytes
* # 0x05, 0x01, // Usage Page (Generic Desktop) 0
* # 0x09, 0x06, // Usage (Keyboard) 2
* # 0xa1, 0x01, // Collection (Application) 4
* # 0x85, 0x03, // Report ID (3) 6
* # 0x05, 0x07, // Usage Page (Keyboard/Keypad) 8
* # 0x19, 0xe0, // UsageMinimum (224) 10
* # 0x29, 0xe7, // UsageMaximum (231) 12
* # 0x15, 0x00, // Logical Minimum (0) 14
* # 0x25, 0x01, // Logical Maximum (1) 16
* # 0x75, 0x01, // Report Size (1) 18
* # 0x95, 0x08, // Report Count (8) 20
* # 0x81, 0x02, // Input (Data,Var,Abs) 22
* # 0x05, 0x07, // Usage Page (Keyboard/Keypad) 24
* # 0x19, 0x00, // UsageMinimum (0) 26
* # 0x29, 0xff, // UsageMaximum (255) 28
* # 0x26, 0xff, 0x00, // Logical Maximum (255) 30
* # 0x75, 0x08, // Report Size (8) 33
* # 0x95, 0x06, // Report Count (6) 35
* # 0x81, 0x00, // Input (Data,Arr,Abs) 37
* # 0xc0, // End Collection 39
* # 0x05, 0x0c, // Usage Page (Consumer) 40
* # 0x09, 0x01, // Usage (Consumer Control) 42
* # 0xa1, 0x01, // Collection (Application) 44
* # 0x85, 0x04, // Report ID (4) 46
* # 0x19, 0x00, // UsageMinimum (0) 48
* # 0x2a, 0x3c, 0x02, // UsageMaximum (572) 50
* # 0x15, 0x00, // Logical Minimum (0) 53
* # 0x26, 0x3c, 0x02, // Logical Maximum (572) 55
* # 0x95, 0x01, // Report Count (1) 58
* # 0x75, 0x10, // Report Size (16) 60
* # 0x81, 0x00, // Input (Data,Arr,Abs) 62
* # 0xc0, // End Collection 64
* R: 65 05 01 09 06 a1 01 85 03 05 07 19 e0 29 e7 15 00 25 01 75 01 95 08 81 02 0507 19 00 29 ff 26 ff 00 75 08 95 06 81 00 c0 05 0c 09 01 a1 01 85 04 19 00 2a 3c02 15 00 26 3c 02 95 01 75 10 81 00 c0
* N: HUION Huion Tablet_H641P
*/
#define PAD_REPORT_DESCRIPTOR_LENGTH 133
#define PEN_REPORT_DESCRIPTOR_LENGTH 93
#define VENDOR_REPORT_DESCRIPTOR_LENGTH 36
#define PAD_REPORT_ID 3
#define PEN_REPORT_ID 10
#define VENDOR_REPORT_ID 8
#define PAD_REPORT_LENGTH 8
#define PEN_REPORT_LENGTH 10
#define VENDOR_REPORT_LENGTH 12
__u16 last_button_state;
static const __u8 fixed_rdesc_pad[] = {
UsagePage_GenericDesktop
Usage_GD_Keypad
CollectionApplication(
// -- Byte 0 in report
ReportId(PAD_REPORT_ID)
LogicalMinimum_i8(0)
LogicalMaximum_i8(1)
UsagePage_Digitizers
Usage_Dig_TabletFunctionKeys
CollectionPhysical(
// Byte 1 in report - just exists so we get to be a tablet pad
Usage_Dig_BarrelSwitch // BTN_STYLUS
ReportCount(1)
ReportSize(1)
Input(Var|Abs)
ReportCount(7) // padding
Input(Const)
// Bytes 2/3 in report - just exists so we get to be a tablet pad
UsagePage_GenericDesktop
Usage_GD_X
Usage_GD_Y
ReportCount(2)
ReportSize(8)
Input(Var|Abs)
// Byte 4 in report is the wheel
Usage_GD_Wheel
LogicalMinimum_i8(-1)
LogicalMaximum_i8(1)
ReportCount(1)
ReportSize(8)
Input(Var|Rel)
// Byte 5 is the button state
UsagePage_Button
UsageMinimum_i8(0x1)
UsageMaximum_i8(0x8)
LogicalMinimum_i8(0x1)
LogicalMaximum_i8(0x8)
ReportCount(1)
ReportSize(8)
Input(Arr|Abs)
)
// Make sure we match our original report length
FixedSizeVendorReport(PAD_REPORT_LENGTH)
)
};
static const __u8 fixed_rdesc_pen[] = {
UsagePage_Digitizers
Usage_Dig_Pen
CollectionApplication(
// -- Byte 0 in report
ReportId(PEN_REPORT_ID)
Usage_Dig_Pen
CollectionPhysical(
// -- Byte 1 in report
Usage_Dig_TipSwitch
Usage_Dig_BarrelSwitch
Usage_Dig_SecondaryBarrelSwitch // maps eraser to BTN_STYLUS2
LogicalMinimum_i8(0)
LogicalMaximum_i8(1)
ReportSize(1)
ReportCount(3)
Input(Var|Abs)
ReportCount(4) // Padding
Input(Const)
Usage_Dig_InRange
ReportCount(1)
Input(Var|Abs)
ReportSize(16)
ReportCount(1)
PushPop(
UsagePage_GenericDesktop
Unit(cm)
UnitExponent(-1)
PhysicalMinimum_i16(0)
PhysicalMaximum_i16(160)
LogicalMinimum_i16(0)
LogicalMaximum_i16(32767)
Usage_GD_X
Input(Var|Abs) // Bytes 2+3
PhysicalMinimum_i16(0)
PhysicalMaximum_i16(100)
LogicalMinimum_i16(0)
LogicalMaximum_i16(32767)
Usage_GD_Y
Input(Var|Abs) // Bytes 4+5
)
UsagePage_Digitizers
Usage_Dig_TipPressure
LogicalMinimum_i16(0)
LogicalMaximum_i16(8191)
Input(Var|Abs) // Byte 6+7
// Two bytes padding so we don't need to change the report at all
ReportSize(8)
ReportCount(2)
Input(Const) // Byte 6+7
)
)
};
static const __u8 fixed_rdesc_vendor[] = {
UsagePage_Digitizers
Usage_Dig_Pen
CollectionApplication(
// Byte 0
// We leave the pen on the vendor report ID
ReportId(VENDOR_REPORT_ID)
Usage_Dig_Pen
CollectionPhysical(
// Byte 1 are the buttons
LogicalMinimum_i8(0)
LogicalMaximum_i8(1)
ReportSize(1)
Usage_Dig_TipSwitch
Usage_Dig_BarrelSwitch
Usage_Dig_SecondaryBarrelSwitch
ReportCount(3)
Input(Var|Abs)
ReportCount(4) // Padding
Input(Const)
Usage_Dig_InRange
ReportCount(1)
Input(Var|Abs)
ReportSize(16)
ReportCount(1)
PushPop(
UsagePage_GenericDesktop
Unit(cm)
UnitExponent(-1)
// Note: reported logical range differs
// from the pen report ID for x and y
LogicalMinimum_i16(0)
LogicalMaximum_i16(32000)
PhysicalMinimum_i16(0)
PhysicalMaximum_i16(160)
// Bytes 2/3 in report
Usage_GD_X
Input(Var|Abs)
LogicalMinimum_i16(0)
LogicalMaximum_i16(20000)
PhysicalMinimum_i16(0)
PhysicalMaximum_i16(100)
// Bytes 4/5 in report
Usage_GD_Y
Input(Var|Abs)
)
// Bytes 6/7 in report
LogicalMinimum_i16(0)
LogicalMaximum_i16(8192)
Usage_Dig_TipPressure
Input(Var|Abs)
)
)
UsagePage_GenericDesktop
Usage_GD_Keypad
CollectionApplication(
// Byte 0
ReportId(PAD_REPORT_ID)
LogicalMinimum_i8(0)
LogicalMaximum_i8(1)
UsagePage_Digitizers
Usage_Dig_TabletFunctionKeys
CollectionPhysical(
// Byte 1 are the buttons
Usage_Dig_BarrelSwitch // BTN_STYLUS, needed so we get to be a tablet pad
ReportCount(1)
ReportSize(1)
Input(Var|Abs)
ReportCount(7) // Padding
Input(Const)
// Bytes 2/3 - x/y just exist so we get to be a tablet pad
UsagePage_GenericDesktop
Usage_GD_X
Usage_GD_Y
ReportCount(2)
ReportSize(8)
Input(Var|Abs)
// Bytes 4 and 5 are the button state
UsagePage_Button
UsageMinimum_i8(0x1)
UsageMaximum_i8(0xa)
LogicalMinimum_i8(0x0)
LogicalMaximum_i8(0x1)
ReportCount(10)
ReportSize(1)
Input(Var|Abs)
Usage_i8(0x31) // maps to BTN_SOUTH
ReportCount(1)
Input(Var|Abs)
ReportCount(5)
Input(Const)
// Byte 6 is the wheel
UsagePage_GenericDesktop
Usage_GD_Wheel
LogicalMinimum_i8(-1)
LogicalMaximum_i8(1)
ReportCount(1)
ReportSize(8)
Input(Var|Rel)
)
// Make sure we match our original report length
FixedSizeVendorReport(VENDOR_REPORT_LENGTH)
)
};
static const __u8 disabled_rdesc_pen[] = {
FixedSizeVendorReport(PEN_REPORT_LENGTH)
};
static const __u8 disabled_rdesc_pad[] = {
FixedSizeVendorReport(PAD_REPORT_LENGTH)
};
SEC(HID_BPF_RDESC_FIXUP)
int BPF_PROG(hid_fix_rdesc, struct hid_bpf_ctx *hctx)
{
__u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, HID_MAX_DESCRIPTOR_SIZE /* size */);
__s32 rdesc_size = hctx->size;
__u8 have_fw_id;
if (!data)
return 0; /* EPERM check */
/* If we have a firmware ID and it matches our expected prefix, we
* disable the default pad/pen nodes. They won't send events
* but cause duplicate devices.
*/
have_fw_id = __builtin_memcmp(UDEV_PROP_HUION_FIRMWARE_ID,
EXPECTED_FIRMWARE_ID,
sizeof(EXPECTED_FIRMWARE_ID) - 1) == 0;
if (rdesc_size == PAD_REPORT_DESCRIPTOR_LENGTH) {
if (have_fw_id) {
__builtin_memcpy(data, disabled_rdesc_pad, sizeof(disabled_rdesc_pad));
return sizeof(disabled_rdesc_pad);
}
__builtin_memcpy(data, fixed_rdesc_pad, sizeof(fixed_rdesc_pad));
return sizeof(fixed_rdesc_pad);
}
if (rdesc_size == PEN_REPORT_DESCRIPTOR_LENGTH) {
if (have_fw_id) {
__builtin_memcpy(data, disabled_rdesc_pen, sizeof(disabled_rdesc_pen));
return sizeof(disabled_rdesc_pen);
}
__builtin_memcpy(data, fixed_rdesc_pen, sizeof(fixed_rdesc_pen));
return sizeof(fixed_rdesc_pen);
}
/* Always fix the vendor mode so the tablet will work even if nothing sets
* the udev property (e.g. huion-switcher run manually)
*/
if (rdesc_size == VENDOR_REPORT_DESCRIPTOR_LENGTH) {
__builtin_memcpy(data, fixed_rdesc_vendor, sizeof(fixed_rdesc_vendor));
return sizeof(fixed_rdesc_vendor);
}
return 0;
}
SEC(HID_BPF_DEVICE_EVENT)
int BPF_PROG(inspiroy_2_fix_events, struct hid_bpf_ctx *hctx)
{
__u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, 10 /* size */);
if (!data)
return 0; /* EPERM check */
/* Only sent if tablet is in default mode */
if (data[0] == PAD_REPORT_ID) {
/* Nicely enough, this device only supports one button down at a time so
* the reports are easy to match. Buttons numbered from the top
* Button released: 03 00 00 00 00 00 00 00
* Button 1: 03 00 05 00 00 00 00 00 -> b
* Button 2: 03 07 11 00 00 00 00 00 -> Ctrl Shift N
* Button 3: 03 00 08 00 00 00 00 00 -> e
* Button 4: 03 00 0c 00 00 00 00 00 -> i
* Button 5: 03 00 2c 00 00 00 00 00 -> space
* Button 6: 03 01 08 00 00 00 00 00 -> Ctrl E
* Button 7: 03 01 16 00 00 00 00 00 -> Ctrl S
* Button 8: 03 05 1d 00 00 00 00 00 -> Ctrl Alt Z
*
* Wheel down: 03 01 2d 00 00 00 00 00 -> Ctrl -
* Wheel up: 03 01 2e 00 00 00 00 00 -> Ctrl =
*/
__u8 button = 0;
__u8 wheel = 0;
switch (data[1] << 8 | data[2]) {
case 0x0000:
break;
case 0x0005:
button = 1;
break;
case 0x0711:
button = 2;
break;
case 0x0008:
button = 3;
break;
case 0x000c:
button = 4;
break;
case 0x002c:
button = 5;
break;
case 0x0108:
button = 6;
break;
case 0x0116:
button = 7;
break;
case 0x051d:
button = 8;
break;
case 0x012d:
wheel = -1;
break;
case 0x012e:
wheel = 1;
break;
}
__u8 report[6] = {PAD_REPORT_ID, 0x0, 0x0, 0x0, wheel, button};
__builtin_memcpy(data, report, sizeof(report));
return sizeof(report);
}
/* Nothing to do for the PEN_REPORT_ID, it's already mapped */
/* Only sent if tablet is in raw mode */
if (data[0] == VENDOR_REPORT_ID) {
/* Pad reports */
if (data[1] & 0x20) {
/* See fixed_rdesc_pad */
struct pad_report {
__u8 report_id;
__u8 btn_stylus;
__u8 x;
__u8 y;
__u16 buttons;
__u8 wheel;
} __attribute__((packed)) *pad_report;
__u8 wheel = 0;
/* Wheel report */
if (data[1] == 0xf1) {
if (data[5] == 2)
wheel = 0xff;
else
wheel = data[5];
} else {
/* data[4] and data[5] are the buttons, mapped correctly */
last_button_state = data[4] | (data[5] << 8);
wheel = 0; // wheel
}
pad_report = (struct pad_report *)data;
pad_report->report_id = PAD_REPORT_ID;
pad_report->btn_stylus = 0;
pad_report->x = 0;
pad_report->y = 0;
pad_report->buttons = last_button_state;
pad_report->wheel = wheel;
return sizeof(struct pad_report);
}
/* Pen reports need nothing done */
}
return 0;
}
HID_BPF_OPS(inspiroy_2) = {
.hid_device_event = (void *)inspiroy_2_fix_events,
.hid_rdesc_fixup = (void *)hid_fix_rdesc,
};
SEC("syscall")
int probe(struct hid_bpf_probe_args *ctx)
{
switch (ctx->rdesc_size) {
case PAD_REPORT_DESCRIPTOR_LENGTH:
case PEN_REPORT_DESCRIPTOR_LENGTH:
case VENDOR_REPORT_DESCRIPTOR_LENGTH:
ctx->retval = 0;
break;
default:
ctx->retval = -EINVAL;
}
return 0;
}
char _license[] SEC("license") = "GPL";
@@ -0,0 +1,579 @@
// SPDX-License-Identifier: GPL-2.0-only
/* Copyright (c) 2024 Red Hat, Inc
*/
#include "vmlinux.h"
#include "hid_bpf.h"
#include "hid_bpf_helpers.h"
#include "hid_report_helpers.h"
#include <bpf/bpf_tracing.h>
#define VID_HUION 0x256C
#define PID_INSPIROY_2_S 0x0066
HID_BPF_CONFIG(
HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_HUION, PID_INSPIROY_2_S),
);
/* Filled in by udev-hid-bpf */
char UDEV_PROP_HUION_FIRMWARE_ID[64];
/* The prefix of the firmware ID we expect for this device. The full firmware
* string has a date suffix, e.g. HUION_T21j_221221
*/
char EXPECTED_FIRMWARE_ID[] = "HUION_T21j_";
/* How this BPF program works: the tablet has two modes, firmware mode and
* tablet mode. In firmware mode (out of the box) the tablet sends button events
* and the dial as keyboard combinations. In tablet mode it uses a vendor specific
* hid report to report everything instead.
* Depending on the mode some hid reports are never sent and the corresponding
* devices are mute.
*
* To switch the tablet use e.g. https://github.com/whot/huion-switcher
* or one of the tools from the digimend project
*
* This BPF works for both modes. The huion-switcher tool sets the
* HUION_FIRMWARE_ID udev property - if that is set then we disable the firmware
* pad and pen reports (by making them vendor collections that are ignored).
* If that property is not set we fix all hidraw nodes so the tablet works in
* either mode though the drawback is that the device will show up twice if
* you bind it to all event nodes
*
* Default report descriptor for the first exposed hidraw node:
*
* # HUION Huion Tablet_H641P
* # Report descriptor length: 18 bytes
* # 0x06, 0x00, 0xff, // Usage Page (Vendor Defined Page 0xFF00) 0
* # 0x09, 0x01, // Usage (Vendor Usage 0x01) 3
* # 0xa1, 0x01, // Collection (Application) 5
* # 0x85, 0x08, // Report ID (8) 7
* # 0x75, 0x58, // Report Size (88) 9
* # 0x95, 0x01, // Report Count (1) 11
* # 0x09, 0x01, // Usage (Vendor Usage 0x01) 13
* # 0x81, 0x02, // Input (Data,Var,Abs) 15
* # 0xc0, // End Collection 17
* R: 18 06 00 ff 09 01 a1 01 85 08 75 58 95 01 09 01 81 02 c0
*
* This rdesc does nothing until the tablet is switched to raw mode, see
* https://github.com/whot/huion-switcher
*
*
* Second hidraw node is the Pen. This one sends events until the tablet is
* switched to raw mode, then it's mute.
*
* # Report descriptor length: 93 bytes
* # 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, 0x45, // Usage (Eraser) 16
* # 0x09, 0x3c, // Usage (Invert) 18 <-- has no Invert eraser
* # 0x15, 0x00, // Logical Minimum (0) 20
* # 0x25, 0x01, // Logical Maximum (1) 22
* # 0x75, 0x01, // Report Size (1) 24
* # 0x95, 0x06, // Report Count (6) 26
* # 0x81, 0x02, // Input (Data,Var,Abs) 28
* # 0x09, 0x32, // Usage (In Range) 30
* # 0x75, 0x01, // Report Size (1) 32
* # 0x95, 0x01, // Report Count (1) 34
* # 0x81, 0x02, // Input (Data,Var,Abs) 36
* # 0x81, 0x03, // Input (Cnst,Var,Abs) 38
* # 0x05, 0x01, // Usage Page (Generic Desktop) 40
* # 0x09, 0x30, // Usage (X) 42
* # 0x09, 0x31, // Usage (Y) 44
* # 0x55, 0x0d, // Unit Exponent (-3) 46 <-- change to -2
* # 0x65, 0x33, // Unit (EnglishLinear: in³) 48 <-- change in³ to in
* # 0x26, 0xff, 0x7f, // Logical Maximum (32767) 50
* # 0x35, 0x00, // Physical Minimum (0) 53
* # 0x46, 0x00, 0x08, // Physical Maximum (2048) 55 <-- invalid size
* # 0x75, 0x10, // Report Size (16) 58
* # 0x95, 0x02, // Report Count (2) 60
* # 0x81, 0x02, // Input (Data,Var,Abs) 62
* # 0x05, 0x0d, // Usage Page (Digitizers) 64
* # 0x09, 0x30, // Usage (Tip Pressure) 66
* # 0x26, 0xff, 0x1f, // Logical Maximum (8191) 68
* # 0x75, 0x10, // Report Size (16) 71
* # 0x95, 0x01, // Report Count (1) 73
* # 0x81, 0x02, // Input (Data,Var,Abs) 75
* # 0x09, 0x3d, // Usage (X Tilt) 77 <-- No tilt reported
* # 0x09, 0x3e, // Usage (Y Tilt) 79
* # 0x15, 0x81, // Logical Minimum (-127) 81
* # 0x25, 0x7f, // Logical Maximum (127) 83
* # 0x75, 0x08, // Report Size (8) 85
* # 0x95, 0x02, // Report Count (2) 87
* # 0x81, 0x02, // Input (Data,Var,Abs) 89
* # 0xc0, // End Collection 91
* # 0xc0, // End Collection 92
* R: 93 05 0d 09 02 a1 01 85 0a 09 20 a1 01 09 42 09 44 09 45 09 3c 15 00 25 01 7501 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 ff7f 35 00 46 00 08 75 10 95 02 81 02 05 0d 09 30 26 ff 1f 75 10 95 01 81 02 09 3d09 3e 15 81 25 7f 75 08 95 02 81 02 c0 c0
*
* Third hidraw node is the pad which sends a combination of keyboard shortcuts until
* the tablet is switched to raw mode, then it's mute:
*
* # Report descriptor length: 65 bytes
* # 0x05, 0x01, // Usage Page (Generic Desktop) 0
* # 0x09, 0x06, // Usage (Keyboard) 2
* # 0xa1, 0x01, // Collection (Application) 4
* # 0x85, 0x03, // Report ID (3) 6
* # 0x05, 0x07, // Usage Page (Keyboard/Keypad) 8
* # 0x19, 0xe0, // UsageMinimum (224) 10
* # 0x29, 0xe7, // UsageMaximum (231) 12
* # 0x15, 0x00, // Logical Minimum (0) 14
* # 0x25, 0x01, // Logical Maximum (1) 16
* # 0x75, 0x01, // Report Size (1) 18
* # 0x95, 0x08, // Report Count (8) 20
* # 0x81, 0x02, // Input (Data,Var,Abs) 22
* # 0x05, 0x07, // Usage Page (Keyboard/Keypad) 24
* # 0x19, 0x00, // UsageMinimum (0) 26
* # 0x29, 0xff, // UsageMaximum (255) 28
* # 0x26, 0xff, 0x00, // Logical Maximum (255) 30
* # 0x75, 0x08, // Report Size (8) 33
* # 0x95, 0x06, // Report Count (6) 35
* # 0x81, 0x00, // Input (Data,Arr,Abs) 37
* # 0xc0, // End Collection 39
* # 0x05, 0x0c, // Usage Page (Consumer) 40
* # 0x09, 0x01, // Usage (Consumer Control) 42
* # 0xa1, 0x01, // Collection (Application) 44
* # 0x85, 0x04, // Report ID (4) 46
* # 0x19, 0x00, // UsageMinimum (0) 48
* # 0x2a, 0x3c, 0x02, // UsageMaximum (572) 50
* # 0x15, 0x00, // Logical Minimum (0) 53
* # 0x26, 0x3c, 0x02, // Logical Maximum (572) 55
* # 0x95, 0x01, // Report Count (1) 58
* # 0x75, 0x10, // Report Size (16) 60
* # 0x81, 0x00, // Input (Data,Arr,Abs) 62
* # 0xc0, // End Collection 64
* R: 65 05 01 09 06 a1 01 85 03 05 07 19 e0 29 e7 15 00 25 01 75 01 95 08 81 02 0507 19 00 29 ff 26 ff 00 75 08 95 06 81 00 c0 05 0c 09 01 a1 01 85 04 19 00 2a 3c02 15 00 26 3c 02 95 01 75 10 81 00 c0
* N: HUION Huion Tablet_H641P
*/
#define PAD_REPORT_DESCRIPTOR_LENGTH 65
#define PEN_REPORT_DESCRIPTOR_LENGTH 93
#define VENDOR_REPORT_DESCRIPTOR_LENGTH 18
#define PAD_REPORT_ID 3
#define PEN_REPORT_ID 10
#define VENDOR_REPORT_ID 8
#define PAD_REPORT_LENGTH 8
#define PEN_REPORT_LENGTH 10
#define VENDOR_REPORT_LENGTH 12
__u8 last_button_state;
__u8 last_tip_state;
__u8 last_sec_barrel_state;
__u8 force_tip_down_count;
static const __u8 fixed_rdesc_pad[] = {
UsagePage_GenericDesktop
Usage_GD_Keypad
CollectionApplication(
// -- Byte 0 in report
ReportId(PAD_REPORT_ID)
LogicalMinimum_i8(0)
LogicalMaximum_i8(1)
UsagePage_Digitizers
Usage_Dig_TabletFunctionKeys
CollectionPhysical(
// Byte 1 in report - just exists so we get to be a tablet pad
Usage_Dig_BarrelSwitch // BTN_STYLUS
ReportCount(1)
ReportSize(1)
Input(Var|Abs)
ReportCount(7) // padding
Input(Const)
// Bytes 2/3 in report - just exists so we get to be a tablet pad
UsagePage_GenericDesktop
Usage_GD_X
Usage_GD_Y
ReportCount(2)
ReportSize(8)
Input(Var|Abs)
// Byte 4 in report is the wheel
Usage_GD_Wheel
LogicalMinimum_i8(-1)
LogicalMaximum_i8(1)
ReportCount(1)
ReportSize(8)
Input(Var|Rel)
// Byte 5 is the button state
UsagePage_Button
UsageMinimum_i8(0x1)
UsageMaximum_i8(0x6)
LogicalMinimum_i8(0x1)
LogicalMaximum_i8(0x6)
ReportCount(1)
ReportSize(8)
Input(Arr|Abs)
)
// Make sure we match our original report length
FixedSizeVendorReport(PAD_REPORT_LENGTH)
)
};
static const __u8 fixed_rdesc_pen[] = {
UsagePage_Digitizers
Usage_Dig_Pen
CollectionApplication(
// -- Byte 0 in report
ReportId(PEN_REPORT_ID)
Usage_Dig_Pen
CollectionPhysical(
// -- Byte 1 in report
Usage_Dig_TipSwitch
Usage_Dig_BarrelSwitch
Usage_Dig_SecondaryBarrelSwitch // maps eraser to BTN_STYLUS2
LogicalMinimum_i8(0)
LogicalMaximum_i8(1)
ReportSize(1)
ReportCount(3)
Input(Var|Abs)
ReportCount(4) // Padding
Input(Const)
Usage_Dig_InRange
ReportCount(1)
Input(Var|Abs)
ReportSize(16)
ReportCount(1)
PushPop(
UsagePage_GenericDesktop
Unit(cm)
UnitExponent(-1)
PhysicalMinimum_i16(0)
PhysicalMaximum_i16(160)
LogicalMinimum_i16(0)
LogicalMaximum_i16(32767)
Usage_GD_X
Input(Var|Abs) // Bytes 2+3
PhysicalMinimum_i16(0)
PhysicalMaximum_i16(100)
LogicalMinimum_i16(0)
LogicalMaximum_i16(32767)
Usage_GD_Y
Input(Var|Abs) // Bytes 4+5
)
UsagePage_Digitizers
Usage_Dig_TipPressure
LogicalMinimum_i16(0)
LogicalMaximum_i16(8191)
Input(Var|Abs) // Byte 6+7
// Two bytes padding so we don't need to change the report at all
ReportSize(8)
ReportCount(2)
Input(Const) // Byte 6+7
)
)
};
static const __u8 fixed_rdesc_vendor[] = {
UsagePage_Digitizers
Usage_Dig_Pen
CollectionApplication(
// Byte 0
// We leave the pen on the vendor report ID
ReportId(VENDOR_REPORT_ID)
Usage_Dig_Pen
CollectionPhysical(
// Byte 1 are the buttons
LogicalMinimum_i8(0)
LogicalMaximum_i8(1)
ReportSize(1)
Usage_Dig_TipSwitch
Usage_Dig_BarrelSwitch
Usage_Dig_SecondaryBarrelSwitch
ReportCount(3)
Input(Var|Abs)
ReportCount(4) // Padding
Input(Const)
Usage_Dig_InRange
ReportCount(1)
Input(Var|Abs)
ReportSize(16)
ReportCount(1)
PushPop(
UsagePage_GenericDesktop
Unit(cm)
UnitExponent(-1)
// Note: reported logical range differs
// from the pen report ID for x and y
LogicalMinimum_i16(0)
LogicalMaximum_i16(32000)
PhysicalMinimum_i16(0)
PhysicalMaximum_i16(160)
// Bytes 2/3 in report
Usage_GD_X
Input(Var|Abs)
LogicalMinimum_i16(0)
LogicalMaximum_i16(20000)
PhysicalMinimum_i16(0)
PhysicalMaximum_i16(100)
// Bytes 4/5 in report
Usage_GD_Y
Input(Var|Abs)
)
// Bytes 6/7 in report
LogicalMinimum_i16(0)
LogicalMaximum_i16(8192)
Usage_Dig_TipPressure
Input(Var|Abs)
)
)
UsagePage_GenericDesktop
Usage_GD_Keypad
CollectionApplication(
// Byte 0
ReportId(PAD_REPORT_ID)
LogicalMinimum_i8(0)
LogicalMaximum_i8(1)
UsagePage_Digitizers
Usage_Dig_TabletFunctionKeys
CollectionPhysical(
// Byte 1 are the buttons
Usage_Dig_BarrelSwitch // BTN_STYLUS, needed so we get to be a tablet pad
ReportCount(1)
ReportSize(1)
Input(Var|Abs)
ReportCount(7) // Padding
Input(Const)
// Bytes 2/3 - x/y just exist so we get to be a tablet pad
UsagePage_GenericDesktop
Usage_GD_X
Usage_GD_Y
ReportCount(2)
ReportSize(8)
Input(Var|Abs)
// Byte 4 is the button state
UsagePage_Button
UsageMinimum_i8(0x1)
UsageMaximum_i8(0x6)
LogicalMinimum_i8(0x0)
LogicalMaximum_i8(0x1)
ReportCount(6)
ReportSize(1)
Input(Var|Abs)
ReportCount(2)
Input(Const)
// Byte 5 is the wheel
UsagePage_GenericDesktop
Usage_GD_Wheel
LogicalMinimum_i8(-1)
LogicalMaximum_i8(1)
ReportCount(1)
ReportSize(8)
Input(Var|Rel)
)
// Make sure we match our original report length
FixedSizeVendorReport(VENDOR_REPORT_LENGTH)
)
};
static const __u8 disabled_rdesc_pen[] = {
FixedSizeVendorReport(PEN_REPORT_LENGTH)
};
static const __u8 disabled_rdesc_pad[] = {
FixedSizeVendorReport(PAD_REPORT_LENGTH)
};
SEC(HID_BPF_RDESC_FIXUP)
int BPF_PROG(hid_fix_rdesc, struct hid_bpf_ctx *hctx)
{
__u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, HID_MAX_DESCRIPTOR_SIZE /* size */);
__s32 rdesc_size = hctx->size;
__u8 have_fw_id;
if (!data)
return 0; /* EPERM check */
/* If we have a firmware ID and it matches our expected prefix, we
* disable the default pad/pen nodes. They won't send events
* but cause duplicate devices.
*/
have_fw_id = __builtin_memcmp(UDEV_PROP_HUION_FIRMWARE_ID,
EXPECTED_FIRMWARE_ID,
sizeof(EXPECTED_FIRMWARE_ID) - 1) == 0;
if (rdesc_size == PAD_REPORT_DESCRIPTOR_LENGTH) {
if (have_fw_id) {
__builtin_memcpy(data, disabled_rdesc_pad, sizeof(disabled_rdesc_pad));
return sizeof(disabled_rdesc_pad);
}
__builtin_memcpy(data, fixed_rdesc_pad, sizeof(fixed_rdesc_pad));
return sizeof(fixed_rdesc_pad);
}
if (rdesc_size == PEN_REPORT_DESCRIPTOR_LENGTH) {
if (have_fw_id) {
__builtin_memcpy(data, disabled_rdesc_pen, sizeof(disabled_rdesc_pen));
return sizeof(disabled_rdesc_pen);
}
__builtin_memcpy(data, fixed_rdesc_pen, sizeof(fixed_rdesc_pen));
return sizeof(fixed_rdesc_pen);
}
/* Always fix the vendor mode so the tablet will work even if nothing sets
* the udev property (e.g. huion-switcher run manually)
*/
if (rdesc_size == VENDOR_REPORT_DESCRIPTOR_LENGTH) {
__builtin_memcpy(data, fixed_rdesc_vendor, sizeof(fixed_rdesc_vendor));
return sizeof(fixed_rdesc_vendor);
}
return 0;
}
SEC(HID_BPF_DEVICE_EVENT)
int BPF_PROG(inspiroy_2_fix_events, struct hid_bpf_ctx *hctx)
{
__u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, 10 /* size */);
if (!data)
return 0; /* EPERM check */
/* Only sent if tablet is in default mode */
if (data[0] == PAD_REPORT_ID) {
/* Nicely enough, this device only supports one button down at a time so
* the reports are easy to match. Buttons numbered from the top
* Button released: 03 00 00 00 00 00 00 00
* Button 1: 03 00 05 00 00 00 00 00 -> b
* Button 2: 03 00 0c 00 00 00 00 00 -> i
* Button 3: 03 00 08 00 00 00 00 00 -> e
* Button 4: 03 01 16 00 00 00 00 00 -> Ctrl S
* Button 5: 03 00 2c 00 00 00 00 00 -> space
* Button 6: 03 05 1d 00 00 00 00 00 -> Ctrl Alt Z
*
* Wheel down: 03 01 2d 00 00 00 00 00 -> Ctrl -
* Wheel up: 03 01 2e 00 00 00 00 00 -> Ctrl =
*/
__u8 button = 0;
__u8 wheel = 0;
switch (data[1] << 8 | data[2]) {
case 0x0000:
break;
case 0x0005:
button = 1;
break;
case 0x000c:
button = 2;
break;
case 0x0008:
button = 3;
break;
case 0x0116:
button = 4;
break;
case 0x002c:
button = 5;
break;
case 0x051d:
button = 6;
break;
case 0x012d:
wheel = -1;
break;
case 0x012e:
wheel = 1;
break;
}
__u8 report[6] = {PAD_REPORT_ID, 0x0, 0x0, 0x0, wheel, button};
__builtin_memcpy(data, report, sizeof(report));
return sizeof(report);
}
/* Nothing to do for the PEN_REPORT_ID, it's already mapped */
/* Only sent if tablet is in raw mode */
if (data[0] == VENDOR_REPORT_ID) {
/* Pad reports */
if (data[1] & 0x20) {
/* See fixed_rdesc_pad */
struct pad_report {
__u8 report_id;
__u8 btn_stylus;
__u8 x;
__u8 y;
__u8 buttons;
__u8 wheel;
} __attribute__((packed)) *pad_report;
__u8 wheel = 0;
/* Wheel report */
if (data[1] == 0xf1) {
if (data[5] == 2)
wheel = 0xff;
else
wheel = data[5];
} else {
/* data[4] are the buttons, mapped correctly */
last_button_state = data[4];
wheel = 0; // wheel
}
pad_report = (struct pad_report *)data;
pad_report->report_id = PAD_REPORT_ID;
pad_report->btn_stylus = 0;
pad_report->x = 0;
pad_report->y = 0;
pad_report->buttons = last_button_state;
pad_report->wheel = wheel;
return sizeof(struct pad_report);
} else if (data[1] & 0x80) { /* Pen reports with InRange 1 */
__u8 tip_state = data[1] & 0x1;
__u8 sec_barrel_state = data[1] & 0x4;
if (force_tip_down_count > 0) {
data[1] |= 0x1;
--force_tip_down_count;
if (tip_state)
force_tip_down_count = 0;
}
/* Tip was down and we just pressed or released the
* secondary barrel switch (the physical eraser
* button). The device will send up to 4
* reports with Tip Switch 0 and sometimes
* this report has Tip Switch 0.
*/
if (last_tip_state &&
last_sec_barrel_state != sec_barrel_state) {
force_tip_down_count = 4;
data[1] |= 0x1;
}
last_tip_state = tip_state;
last_sec_barrel_state = sec_barrel_state;
}
}
return 0;
}
HID_BPF_OPS(inspiroy_2) = {
.hid_device_event = (void *)inspiroy_2_fix_events,
.hid_rdesc_fixup = (void *)hid_fix_rdesc,
};
SEC("syscall")
int probe(struct hid_bpf_probe_args *ctx)
{
switch (ctx->rdesc_size) {
case PAD_REPORT_DESCRIPTOR_LENGTH:
case PEN_REPORT_DESCRIPTOR_LENGTH:
case VENDOR_REPORT_DESCRIPTOR_LENGTH:
ctx->retval = 0;
break;
default:
ctx->retval = -EINVAL;
}
return 0;
}
char _license[] SEC("license") = "GPL";
@@ -0,0 +1,87 @@
// SPDX-License-Identifier: GPL-2.0-only
#include "vmlinux.h"
#include "hid_bpf.h"
#include "hid_bpf_helpers.h"
#include <bpf/bpf_tracing.h>
/*
* Huion Inspiroy Frego M Pen Tablet
* Model L610
* 256c:8251 (Bluetooth)
* 256c:2012 (USB)
*/
#define VID_HUION 0x256C
#define PID_INSPIROY_FREGO_M 0x8251
#define PID_L610 0x2012
#define PEN_RDESC_SIZE 125
#define SECONDARY_SWITCH_OFFSET 17
HID_BPF_CONFIG(
HID_DEVICE(BUS_BLUETOOTH, HID_GROUP_GENERIC, VID_HUION, PID_INSPIROY_FREGO_M),
HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_HUION, PID_L610)
);
/*
* The pen descriptor reports the second side button as Secondary Tip Switch
* instead of Secondary Barrel Switch.
*
* Relevant part of the original pen report descriptor:
*
* 0x09, 0x42, // Usage (Tip Switch) 12
* 0x09, 0x44, // Usage (Barrel Switch) 14
* 0x09, 0x43, // Usage (Secondary Tip Switch) 16 <- change to 0x5a
* 0x09, 0x3c, // Usage (Invert) 18
* 0x09, 0x45, // Usage (Eraser) 20
* 0x15, 0x00, // Logical Minimum (0) 22
* 0x25, 0x01, // Logical Maximum (1) 24
*/
SEC(HID_BPF_RDESC_FIXUP)
int BPF_PROG(fix_secondary_barrel_rdesc, 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 */
if (hctx->size != PEN_RDESC_SIZE)
return 0;
if (data[0] != 0x05 || data[1] != 0x0d || /* Usage Page (Digitizers) */
data[2] != 0x09 || data[3] != 0x02 || /* Usage (Pen) */
data[16] != 0x09 ||
data[SECONDARY_SWITCH_OFFSET] != 0x43) /* Secondary Tip Switch */
return 0;
data[SECONDARY_SWITCH_OFFSET] = 0x5a;
return 0;
}
HID_BPF_OPS(fix_secondary_barrel) = {
.hid_rdesc_fixup = (void *)fix_secondary_barrel_rdesc,
};
SEC("syscall")
int probe(struct hid_bpf_probe_args *ctx)
{
ctx->retval = ctx->rdesc_size != PEN_RDESC_SIZE;
if (ctx->retval) {
ctx->retval = -EINVAL;
return 0;
}
if (ctx->rdesc[0] != 0x05 || ctx->rdesc[1] != 0x0d || /* Usage Page (Digitizers) */
ctx->rdesc[2] != 0x09 || ctx->rdesc[3] != 0x02 || /* Usage (Pen) */
ctx->rdesc[16] != 0x09 ||
ctx->rdesc[SECONDARY_SWITCH_OFFSET] != 0x43) { /* Secondary Tip Switch */
ctx->retval = -EINVAL;
return 0;
}
ctx->retval = 0;
return 0;
}
char _license[] SEC("license") = "GPL";
@@ -0,0 +1,366 @@
// 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 PID_KAMVAS_PRO_27 0x006c
#define NAME_KAMVAS_PRO_19 "HUION Huion Tablet_GT1902"
#define NAME_KAMVAS_PRO_27 "HUION Huion Tablet_GT2701"
#define TEST_PREFIX "uhid test "
HID_BPF_CONFIG(
HID_DEVICE(BUS_USB, HID_GROUP_MULTITOUCH_WIN_8, VID_HUION, PID_KAMVAS_PRO_19),
HID_DEVICE(BUS_USB, HID_GROUP_MULTITOUCH_WIN_8, VID_HUION, PID_KAMVAS_PRO_27),
);
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 6) */
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
/* New in Firmware Version: HUION_M220_240524 */
0x05, 0x01, // Usage Page (Generic Desktop) 328
0x09, 0x01, // Usage (Pointer) 330
0xa1, 0x01, // Collection (Application) 332
0x09, 0x01, // Usage (Pointer) 334
0xa1, 0x00, // Collection (Physical) 336
0x05, 0x09, // Usage Page (Button) 338
0x19, 0x01, // UsageMinimum (1) 340
0x29, 0x03, // UsageMaximum (3) 342
0x15, 0x00, // Logical Minimum (0) 344
0x25, 0x01, // Logical Maximum (1) 346
0x85, 0x02, // Report ID (2) 348
0x95, 0x03, // Report Count (3) 350
0x75, 0x01, // Report Size (1) 352
0x81, 0x02, // Input (Data,Var,Abs) 354
0x95, 0x01, // Report Count (1) 356
0x75, 0x05, // Report Size (5) 358
0x81, 0x01, // Input (Cnst,Arr,Abs) 360
0x05, 0x01, // Usage Page (Generic Desktop) 362
0x09, 0x30, // Usage (X) 364
0x09, 0x31, // Usage (Y) 366
0x15, 0x81, // Logical Minimum (-127) 368
0x25, 0x7f, // Logical Maximum (127) 370
0x75, 0x08, // Report Size (8) 372
0x95, 0x02, // Report Count (2) 374
0x81, 0x06, // Input (Data,Var,Rel) 376
0x95, 0x04, // Report Count (4) 378
0x75, 0x08, // Report Size (8) 380
0x81, 0x01, // Input (Cnst,Arr,Abs) 382
0xc0, // End Collection 384
0xc0, // End Collection 385
0x05, 0x0d, // Usage Page (Digitizers) 386
0x09, 0x05, // Usage (Touch Pad) 388
0xa1, 0x01, // Collection (Application) 390
0x06, 0x00, 0xff, // Usage Page (Vendor Defined Page FF00) 392
0x09, 0x0c, // Usage (Vendor Usage 0x0c) 395
0x15, 0x00, // Logical Minimum (0) 397
0x26, 0xff, 0x00, // Logical Maximum (255) 399
0x75, 0x08, // Report Size (8) 402
0x95, 0x10, // Report Count (16) 404
0x85, 0x3f, // Report ID (63) 406
0x81, 0x22, // Input (Data,Var,Abs,NoPref) 408
0xc0, // End Collection 410
0x06, 0x00, 0xff, // Usage Page (Vendor Defined Page FF00) 411
0x09, 0x0c, // Usage (Vendor Usage 0x0c) 414
0xa1, 0x01, // Collection (Application) 416
0x06, 0x00, 0xff, // Usage Page (Vendor Defined Page FF00) 418
0x09, 0x0c, // Usage (Vendor Usage 0x0c) 421
0x15, 0x00, // Logical Minimum (0) 423
0x26, 0xff, 0x00, // Logical Maximum (255) 425
0x85, 0x44, // Report ID (68) 428
0x75, 0x08, // Report Size (8) 430
0x96, 0x6b, 0x05, // Report Count (1387) 432
0x81, 0x00, // Input (Data,Arr,Abs) 435
0xc0, // End Collection 437
};
#define PRE_240524_RDESC_SIZE 328
#define PRE_240524_RDESC_FIXED_SIZE 338 /* The original bits of the descriptor */
#define FW_240524_RDESC_SIZE 438
#define FW_240524_RDESC_FIXED_SIZE sizeof(fixed_rdesc)
SEC(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 */
if (hctx->size == FW_240524_RDESC_SIZE) {
__builtin_memcpy(data, fixed_rdesc, FW_240524_RDESC_FIXED_SIZE);
return sizeof(fixed_rdesc);
}
__builtin_memcpy(data, fixed_rdesc, PRE_240524_RDESC_FIXED_SIZE);
return PRE_240524_RDESC_FIXED_SIZE;
}
/*
* 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(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;
}
HID_BPF_OPS(huion_Kamvas_pro_19) = {
.hid_rdesc_fixup = (void *)hid_fix_rdesc_huion_kamvas_pro_19,
.hid_device_event = (void *)kamvas_pro_19_fix_3rd_button,
};
SEC("syscall")
int probe(struct hid_bpf_probe_args *ctx)
{
ctx->retval = !((ctx->rdesc_size == PRE_240524_RDESC_SIZE) ||
(ctx->rdesc_size == FW_240524_RDESC_SIZE));
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)) &&
__builtin_memcmp(name, NAME_KAMVAS_PRO_27, sizeof(NAME_KAMVAS_PRO_27)))
ctx->retval = -EINVAL;
hid_bpf_release_context(hctx);
return 0;
}
char _license[] SEC("license") = "GPL";
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,724 @@
// SPDX-License-Identifier: GPL-2.0-only
/* Copyright (c) 2025 Nicholas LaPointe
* Copyright (c) 2025 Higgins Dragon
*/
#include "vmlinux.h"
#include "hid_bpf.h"
#include "hid_bpf_helpers.h"
#include "hid_report_helpers.h"
#include <bpf/bpf_tracing.h>
#define VID_HUION 0x256c
#define PID_KAMVAS16_GEN3 0x2009
#define VENDOR_DESCRIPTOR_LENGTH 36
#define TABLET_DESCRIPTOR_LENGTH 328
#define WHEEL_DESCRIPTOR_LENGTH 200
#define VENDOR_REPORT_ID 8
#define VENDOR_REPORT_LENGTH 14
#define VENDOR_REPORT_SUBTYPE_PEN 0x08
#define VENDOR_REPORT_SUBTYPE_PEN_OUT 0x00
#define VENDOR_REPORT_SUBTYPE_BUTTONS 0x0e
#define VENDOR_REPORT_SUBTYPE_WHEELS 0x0f
/* For the reports that we create ourselves */
#define CUSTOM_PAD_REPORT_ID 9
HID_BPF_CONFIG(
HID_DEVICE(BUS_USB, HID_GROUP_ANY, VID_HUION, PID_KAMVAS16_GEN3),
);
/*
* This tablet can send reports using one of two different data formats,
* depending on what "mode" the tablet is in.
*
* By default, the tablet will send reports that can be decoded using its
* included HID descriptors (descriptors 1 and 2, shown below).
* This mode will be called "firmware mode" throughout this file.
*
* The HID descriptor that describes pen events in firmware mode (descriptor 1)
* has multiple bugs:
* * "Secondary Tip Switch" instead of "Secondary Barrel Switch"
* * "Invert" instead of (or potentially shared with) third barrel button
* * Specified tablet area of 2048 in³ instead of 293.8 x 165.2mm
* * Specified tilt range of -90 to +90 instead of -60 to +60
*
* While these can be easily patched up by editing the descriptor, a larger
* problem with the firmware mode exists: it is impossible to tell which of the
* two wheels are being rotated (or having their central button pressed).
*
*
* By using a tool such as huion-switcher (https://github.com/whot/huion-switcher),
* the tablet can be made to send reports using a proprietary format that is not
* adequately described by its relevant descriptor (descriptor 0, shown below).
* This mode will be called "vendor mode" throughout this file.
*
* The reports sent while in vendor mode allow for proper decoding of the wheels.
*
* For simplicity and maximum functionality, this BPF focuses strictly on
* enabling one to make use of the vendor mode.
*/
/*
* DESCRIPTORS
* DESCRIPTOR 0
* # 0x06, 0x00, 0xff, // Usage Page (Vendor Defined Page 1) 0
* # 0x09, 0x01, // Usage (Vendor Usage 1) 3
* # 0xa1, 0x01, // Collection (Application) 5
* # 0x85, 0x08, // Report ID (8) 7
* # 0x75, 0x68, // Report Size (104) 9
* # 0x95, 0x01, // Report Count (1) 11
* # 0x09, 0x01, // Usage (Vendor Usage 1) 13
* # 0x81, 0x02, // Input (Data,Var,Abs) 15
* # 0xc0, // End Collection 17
* # 0x06, 0x00, 0xff, // Usage Page (Vendor Defined Page 1) 18
* # 0x09, 0x01, // Usage (Vendor Usage 1) 21
* # 0xa1, 0x01, // Collection (Application) 23
* # 0x85, 0x16, // Report ID (22) 25
* # 0x75, 0x08, // Report Size (8) 27
* # 0x95, 0x07, // Report Count (7) 29
* # 0x09, 0x01, // Usage (Vendor Usage 1) 31
* # 0xb1, 0x02, // Feature (Data,Var,Abs) 33
* # 0xc0, // End Collection 35
* #
* R: 36 06 00 ff 09 01 a1 01 85 08 75 68 95 01 09 01 81 02 c0 06 00 ff 09 01 a1 01 85 16 75 08 95 07 09 01 b1 02 c0
* N: HUION Huion Tablet_GS1563
* I: 3 256c 2009
*
*
* DESCRIPTOR 1
* # 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, 0x43, // Usage (Secondary Tip Switch) 16
* # 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, 0x06, // Report Count (6) 28
* # 0x81, 0x02, // Input (Data,Var,Abs) 30
* # 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
* #
* R: 328 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
* N: HUION Huion Tablet_GS1563
* I: 3 256c 2009
*
* DESCRIPTOR 2
* # 0x05, 0x01, // Usage Page (Generic Desktop) 0
* # 0x09, 0x0e, // Usage (System Multi-Axis Controller) 2
* # 0xa1, 0x01, // Collection (Application) 4
* # 0x85, 0x11, // Report ID (17) 6
* # 0x05, 0x0d, // Usage Page (Digitizers) 8
* # 0x09, 0x21, // Usage (Puck) 10
* # 0xa1, 0x02, // Collection (Logical) 12
* # 0x15, 0x00, // Logical Minimum (0) 14
* # 0x25, 0x01, // Logical Maximum (1) 16
* # 0x75, 0x01, // Report Size (1) 18
* # 0x95, 0x01, // Report Count (1) 20
* # 0xa1, 0x00, // Collection (Physical) 22
* # 0x05, 0x09, // Usage Page (Button) 24
* # 0x09, 0x01, // Usage (Vendor Usage 0x01) 26
* # 0x81, 0x02, // Input (Data,Var,Abs) 28
* # 0x05, 0x0d, // Usage Page (Digitizers) 30
* # 0x09, 0x33, // Usage (Touch) 32
* # 0x81, 0x02, // Input (Data,Var,Abs) 34
* # 0x95, 0x06, // Report Count (6) 36
* # 0x81, 0x03, // Input (Cnst,Var,Abs) 38
* # 0xa1, 0x02, // Collection (Logical) 40
* # 0x05, 0x01, // Usage Page (Generic Desktop) 42
* # 0x09, 0x37, // Usage (Dial) 44
* # 0x16, 0x00, 0x80, // Logical Minimum (-32768) 46
* # 0x26, 0xff, 0x7f, // Logical Maximum (32767) 49
* # 0x75, 0x10, // Report Size (16) 52
* # 0x95, 0x01, // Report Count (1) 54
* # 0x81, 0x06, // Input (Data,Var,Rel) 56
* # 0x35, 0x00, // Physical Minimum (0) 58
* # 0x46, 0x10, 0x0e, // Physical Maximum (3600) 60
* # 0x15, 0x00, // Logical Minimum (0) 63
* # 0x26, 0x10, 0x0e, // Logical Maximum (3600) 65
* # 0x09, 0x48, // Usage (Resolution Multiplier) 68
* # 0xb1, 0x02, // Feature (Data,Var,Abs) 70
* # 0x45, 0x00, // Physical Maximum (0) 72
* # 0xc0, // End Collection 74
* # 0x75, 0x08, // Report Size (8) 75
* # 0x95, 0x01, // Report Count (1) 77
* # 0x81, 0x01, // Input (Cnst,Arr,Abs) 79
* # 0x75, 0x08, // Report Size (8) 81
* # 0x95, 0x01, // Report Count (1) 83
* # 0x81, 0x01, // Input (Cnst,Arr,Abs) 85
* # 0x75, 0x08, // Report Size (8) 87
* # 0x95, 0x01, // Report Count (1) 89
* # 0x81, 0x01, // Input (Cnst,Arr,Abs) 91
* # 0x75, 0x08, // Report Size (8) 93
* # 0x95, 0x01, // Report Count (1) 95
* # 0x81, 0x01, // Input (Cnst,Arr,Abs) 97
* # 0x75, 0x08, // Report Size (8) 99
* # 0x95, 0x01, // Report Count (1) 101
* # 0x81, 0x01, // Input (Cnst,Arr,Abs) 103
* # 0xc0, // End Collection 105
* # 0xc0, // End Collection 106
* # 0xc0, // End Collection 107
* # 0x05, 0x01, // Usage Page (Generic Desktop) 108
* # 0x09, 0x06, // Usage (Keyboard) 110
* # 0xa1, 0x01, // Collection (Application) 112
* # 0x85, 0x03, // Report ID (3) 114
* # 0x05, 0x07, // Usage Page (Keyboard) 116
* # 0x19, 0xe0, // Usage Minimum (224) 118
* # 0x29, 0xe7, // Usage Maximum (231) 120
* # 0x15, 0x00, // Logical Minimum (0) 122
* # 0x25, 0x01, // Logical Maximum (1) 124
* # 0x75, 0x01, // Report Size (1) 126
* # 0x95, 0x08, // Report Count (8) 128
* # 0x81, 0x02, // Input (Data,Var,Abs) 130
* # 0x05, 0x07, // Usage Page (Keyboard) 132
* # 0x19, 0x00, // Usage Minimum (0) 134
* # 0x29, 0xff, // Usage Maximum (255) 136
* # 0x26, 0xff, 0x00, // Logical Maximum (255) 138
* # 0x75, 0x08, // Report Size (8) 141
* # 0x95, 0x06, // Report Count (6) 143
* # 0x81, 0x00, // Input (Data,Arr,Abs) 145
* # 0xc0, // End Collection 147
* # 0x05, 0x0c, // Usage Page (Consumer Devices) 148
* # 0x09, 0x01, // Usage (Consumer Control) 150
* # 0xa1, 0x01, // Collection (Application) 152
* # 0x85, 0x04, // Report ID (4) 154
* # 0x19, 0x01, // Usage Minimum (1) 156
* # 0x2a, 0x9c, 0x02, // Usage Maximum (668) 158
* # 0x15, 0x01, // Logical Minimum (1) 161
* # 0x26, 0x9c, 0x02, // Logical Maximum (668) 163
* # 0x95, 0x01, // Report Count (1) 166
* # 0x75, 0x10, // Report Size (16) 168
* # 0x81, 0x00, // Input (Data,Arr,Abs) 170
* # 0xc0, // End Collection 172
* # 0x05, 0x01, // Usage Page (Generic Desktop) 173
* # 0x09, 0x80, // Usage (System Control) 175
* # 0xa1, 0x01, // Collection (Application) 177
* # 0x85, 0x05, // Report ID (5) 179
* # 0x19, 0x81, // Usage Minimum (129) 181
* # 0x29, 0x83, // Usage Maximum (131) 183
* # 0x15, 0x00, // Logical Minimum (0) 185
* # 0x25, 0x01, // Logical Maximum (1) 187
* # 0x75, 0x01, // Report Size (1) 189
* # 0x95, 0x03, // Report Count (3) 191
* # 0x81, 0x02, // Input (Data,Var,Abs) 193
* # 0x95, 0x05, // Report Count (5) 195
* # 0x81, 0x01, // Input (Cnst,Arr,Abs) 197
* # 0xc0, // End Collection 199
* #
* R: 200 05 01 09 0e a1 01 85 11 05 0d 09 21 a1 02 15 00 25 01 75 01 95 01 a1 00 05 09 09 01 81 02 05 0d 09 33 81 02 95 06 81 03 a1 02 05 01 09 37 16 00 80 26 ff 7f 75 10 95 01 81 06 35 00 46 10 0e 15 00 26 10 0e 09 48 b1 02 45 00 c0 75 08 95 01 81 01 75 08 95 01 81 01 75 08 95 01 81 01 75 08 95 01 81 01 75 08 95 01 81 01 c0 c0 c0 05 01 09 06 a1 01 85 03 05 07 19 e0 29 e7 15 00 25 01 75 01 95 08 81 02 05 07 19 00 29 ff 26 ff 00 75 08 95 06 81 00 c0 05 0c 09 01 a1 01 85 04 19 01 2a 9c 02 15 01 26 9c 02 95 01 75 10 81 00 c0 05 01 09 80 a1 01 85 05 19 81 29 83 15 00 25 01 75 01 95 03 81 02 95 05 81 01 c0
* N: HUION Huion Tablet_GS1563
* I: 3 256c 2009
*
*
*
* VENDOR MODE
* HUION_FIRMWARE_ID="HUION_M22d_241101"
* HUION_MAGIC_BYTES="1403201101ac9900ff3fd81305080080083c4010"
*
* MAGIC BYTES
* [LogicalMaximum, X ] [LogicalMaximum, Y ] [LogicalMaximum, Pressure] [ LPI]
* 14 03 [ 20 11 01] [ ac 99 00] [ ff 3f] [d8 13] 05 08 00 80 08 3c 40 10
*
* See Huion__Kamvas13Gen3.bpf.c for more details on detailed button/dial reports and caveats. It's very
* similar to the Kamvas 16 Gen 3.
*/
/* Filled in by udev-hid-bpf */
char UDEV_PROP_HUION_FIRMWARE_ID[64];
char EXPECTED_FIRMWARE_ID[] = "HUION_M22d_";
__u8 last_button_state;
static const __u8 disabled_rdesc_tablet[] = {
FixedSizeVendorReport(28) /* Input report 4 */
};
static const __u8 disabled_rdesc_wheel[] = {
FixedSizeVendorReport(9) /* Input report 17 */
};
static const __u8 fixed_rdesc_vendor[] = {
UsagePage_Digitizers
Usage_Dig_Pen
CollectionApplication(
ReportId(VENDOR_REPORT_ID)
UsagePage_Digitizers
Usage_Dig_Pen
CollectionPhysical(
/*
* I have only examined the tablet's behavior while using
* the PW600L pen, which does not have an eraser.
* Because of this, I don't know where the Eraser and Invert
* bits will go, or if they work as one would expect.
*
* For the time being, there is no expectation that a pen
* with an eraser will work without modifications here.
*/
ReportSize(1)
LogicalMinimum_i8(0)
LogicalMaximum_i8(1)
ReportCount(3)
Usage_Dig_TipSwitch
Usage_Dig_BarrelSwitch
Usage_Dig_SecondaryBarrelSwitch
Input(Var|Abs)
PushPop(
ReportCount(1)
UsagePage_Button
Usage_i8(0x4a) /* (BTN_STYLUS3 + 1) & 0xff */
Input(Var|Abs)
)
ReportCount(3)
Input(Const)
ReportCount(1)
Usage_Dig_InRange
Input(Var|Abs)
ReportSize(16)
ReportCount(1)
PushPop(
UsagePage_GenericDesktop
Unit(cm)
UnitExponent(-2)
LogicalMinimum_i16(0)
PhysicalMinimum_i16(0)
/*
* The tablet has a logical maximum of 69920 x 39340
* and a claimed resolution of 5080 LPI (200 L/mm)
* This works out to a physical maximum of
* 349.6 x 196.7mm, which matches Huion's advertised
* (rounded) active area dimensions from
* https://www.huion.com/products/pen_display/Kamvas/kamvas-16-gen-3.html
*
* The Kamvas uses data[8] for the 3rd byte of the X-axis, and adding
* that after data[2] and data[3] makes a contiguous little-endian
* 24-bit value. (See BPF_PROG below)
*/
ReportSize(24)
LogicalMaximum_i32(69920)
PhysicalMaximum_i16(3496)
Usage_GD_X
Input(Var|Abs)
ReportSize(16)
LogicalMaximum_i16(39340)
PhysicalMaximum_i16(1967)
Usage_GD_Y
Input(Var|Abs)
)
ReportSize(16)
LogicalMinimum_i16(0)
LogicalMaximum_i16(16383)
Usage_Dig_TipPressure
Input(Var|Abs)
ReportSize(8)
ReportCount(1)
Input(Const)
ReportCount(2)
PushPop(
Unit(deg)
UnitExponent(0)
LogicalMinimum_i8(-60)
PhysicalMinimum_i8(-60)
LogicalMaximum_i8(60)
PhysicalMaximum_i8(60)
Usage_Dig_XTilt
Usage_Dig_YTilt
Input(Var|Abs)
)
)
)
UsagePage_GenericDesktop
Usage_GD_Keypad
CollectionApplication(
ReportId(CUSTOM_PAD_REPORT_ID)
LogicalMinimum_i8(0)
LogicalMaximum_i8(1)
UsagePage_Digitizers
Usage_Dig_TabletFunctionKeys
CollectionPhysical(
/*
* The first 3 bytes are somewhat vestigial and will
* always be set to zero. Their presence here is needed
* to ensure that this device will be detected as a
* tablet pad by software that otherwise wouldn't know
* any better.
*/
/* (data[1] & 0x01) barrel switch */
ReportSize(1)
ReportCount(1)
Usage_Dig_BarrelSwitch
Input(Var|Abs)
ReportCount(7)
Input(Const)
/* data[2] X */
/* data[3] Y */
ReportSize(8)
ReportCount(2)
UsagePage_GenericDesktop
Usage_GD_X
Usage_GD_Y
Input(Var|Abs)
/*
* (data[4] & 0x01) button 1
* (data[4] & 0x02) button 2
* (data[4] & 0x04) button 3
* (data[4] & 0x08) button 4
* (data[4] & 0x10) button 5
* (data[4] & 0x20) button 6
* (data[4] & 0x40) button 7 (top wheel button)
* (data[4] & 0x80) button 8 (bottom wheel button)
*/
ReportSize(1)
ReportCount(8)
UsagePage_Button
UsageMinimum_i8(1)
UsageMaximum_i8(8)
Input(Var|Abs)
/* data[5] top wheel (signed, positive clockwise) */
ReportSize(8)
ReportCount(1)
UsagePage_GenericDesktop
Usage_GD_Wheel
LogicalMinimum_i8(-1)
LogicalMaximum_i8(1)
Input(Var|Rel)
/* data[6] bottom wheel (signed, positive clockwise) */
UsagePage_Consumer
Usage_Con_ACPan
Input(Var|Rel)
)
/*
* The kernel will drop reports that are bigger than the
* largest report specified in the HID descriptor.
* Therefore, our modified descriptor needs to have at least one
* HID report that is as long as, or longer than, the largest
* report in the original descriptor.
*
* This macro expands to a no-op report that is padded to the
* provided length.
*/
FixedSizeVendorReport(VENDOR_REPORT_LENGTH)
)
};
SEC(HID_BPF_RDESC_FIXUP)
int BPF_PROG(hid_fix_rdesc_huion_kamvas16_gen3, struct hid_bpf_ctx *hid_ctx)
{
__u8 *data = hid_bpf_get_data(hid_ctx, 0 /* offset */, HID_MAX_DESCRIPTOR_SIZE /* size */);
__s32 rdesc_size = hid_ctx->size;
__u8 have_fw_id;
if (!data)
return 0; /* EPERM check */
have_fw_id = __builtin_memcmp(UDEV_PROP_HUION_FIRMWARE_ID,
EXPECTED_FIRMWARE_ID,
sizeof(EXPECTED_FIRMWARE_ID) - 1) == 0;
if (have_fw_id) {
/*
* Tablet should be in vendor mode.
* Disable the unused devices
*/
if (rdesc_size == TABLET_DESCRIPTOR_LENGTH) {
__builtin_memcpy(data, disabled_rdesc_tablet,
sizeof(disabled_rdesc_tablet));
return sizeof(disabled_rdesc_tablet);
}
if (rdesc_size == WHEEL_DESCRIPTOR_LENGTH) {
__builtin_memcpy(data, disabled_rdesc_wheel,
sizeof(disabled_rdesc_wheel));
return sizeof(disabled_rdesc_wheel);
}
}
/*
* Regardless of which mode the tablet is in, always fix the vendor
* descriptor in case the udev property just happened to not be set
*/
if (rdesc_size == VENDOR_DESCRIPTOR_LENGTH) {
__builtin_memcpy(data, fixed_rdesc_vendor, sizeof(fixed_rdesc_vendor));
return sizeof(fixed_rdesc_vendor);
}
return 0;
}
SEC(HID_BPF_DEVICE_EVENT)
int BPF_PROG(hid_fix_event_huion_kamvas16_gen3, struct hid_bpf_ctx *hid_ctx)
{
__u8 *data = hid_bpf_get_data(hid_ctx, 0 /* offset */, VENDOR_REPORT_LENGTH /* size */);
if (!data)
return 0; /* EPERM check */
/* Handle vendor reports only */
if (hid_ctx->size != VENDOR_REPORT_LENGTH)
return 0;
if (data[0] != VENDOR_REPORT_ID)
return 0;
__u8 report_subtype = (data[1] >> 4) & 0x0f;
if (report_subtype == VENDOR_REPORT_SUBTYPE_PEN ||
report_subtype == VENDOR_REPORT_SUBTYPE_PEN_OUT) {
/* Invert Y tilt */
data[11] = -data[11];
/*
* Rearrange the bytes of the report so that
* [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
* will be arranged as
* [0, 1, 2, 3, 8, 4, 5, 6, 7, 9, 10, 11, 12, 13]
*/
__u8 x_24 = data[8];
data[8] = data[7];
data[7] = data[6];
data[6] = data[5];
data[5] = data[4];
data[4] = x_24;
} else if (report_subtype == VENDOR_REPORT_SUBTYPE_BUTTONS ||
report_subtype == VENDOR_REPORT_SUBTYPE_WHEELS) {
struct pad_report {
__u8 report_id;
__u8 btn_stylus:1;
__u8 padding:7;
__u8 x;
__u8 y;
__u8 buttons;
__s8 top_wheel;
__s8 bottom_wheel;
} __attribute__((packed)) *pad_report;
__s8 top_wheel = 0;
__s8 bottom_wheel = 0;
switch (report_subtype) {
case VENDOR_REPORT_SUBTYPE_WHEELS:
/*
* The wheel direction byte is 1 for clockwise rotation
* and 2 for counter-clockwise.
* Change it to 1 and -1, respectively.
*/
switch (data[3]) {
case 1:
top_wheel = (data[5] == 1) ? 1 : -1;
break;
case 2:
bottom_wheel = (data[5] == 1) ? 1 : -1;
break;
}
break;
case VENDOR_REPORT_SUBTYPE_BUTTONS:
/*
* If a button is already being held, ignore any new
* button event unless it's a release.
*
* The tablet only cleanly handles one button being held
* at a time, and trying to hold multiple buttons
* (particularly wheel+pad buttons) can result in sequences
* of reports that look like imaginary presses and releases.
*
* This is an imperfect way to filter out some of these
* reports.
*/
if (last_button_state != 0x00 && data[4] != 0x00)
break;
last_button_state = data[4];
break;
}
pad_report = (struct pad_report *)data;
pad_report->report_id = CUSTOM_PAD_REPORT_ID;
pad_report->btn_stylus = 0;
pad_report->x = 0;
pad_report->y = 0;
pad_report->buttons = last_button_state;
pad_report->top_wheel = top_wheel;
pad_report->bottom_wheel = bottom_wheel;
return sizeof(struct pad_report);
}
return 0;
}
HID_BPF_OPS(huion_kamvas16_gen3) = {
.hid_device_event = (void *)hid_fix_event_huion_kamvas16_gen3,
.hid_rdesc_fixup = (void *)hid_fix_rdesc_huion_kamvas16_gen3,
};
SEC("syscall")
int probe(struct hid_bpf_probe_args *ctx)
{
switch (ctx->rdesc_size) {
case VENDOR_DESCRIPTOR_LENGTH:
case TABLET_DESCRIPTOR_LENGTH:
case WHEEL_DESCRIPTOR_LENGTH:
ctx->retval = 0;
break;
default:
ctx->retval = -EINVAL;
}
return 0;
}
char _license[] SEC("license") = "GPL";
@@ -0,0 +1,492 @@
// SPDX-License-Identifier: GPL-2.0-only
/* Copyright (c) 2024 Red Hat, Inc
*/
#include "vmlinux.h"
#include "hid_bpf.h"
#include "hid_bpf_helpers.h"
#include "hid_report_helpers.h"
#include <bpf/bpf_tracing.h>
#define VID_HUION 0x256C
#define PID_KEYDIAL_K20_BLUETOOTH 0x8251
HID_BPF_CONFIG(
HID_DEVICE(BUS_BLUETOOTH, HID_GROUP_GENERIC, VID_HUION, PID_KEYDIAL_K20_BLUETOOTH),
);
/* This is the same device as in 0010-Huion__KeydialK20 but connected via Bluetooth.
* It does not need (to support?) switching to a vendor mode so we just modify the
* existing mode.
*
* By default it exports two hidraw nodes, only the second one sends events.
*
* This is the first hidraw node which we disable:
*
* # Keydial mini-050
* # Report descriptor length: 114 bytes
* # Bytes // Field Name Offset
* # ----------------------------------------------------------------------------------
* # 🮥 0x05, 0x01, // Usage Page (Generic Desktop) 0
* # 🭬 0x09, 0x0e, // Usage (System Multi-Axis Controller) 2
* # 0xa1, 0x01, // Collection (Application) 4
* # ┅ 0x85, 0x03, // Report ID (3) 6
* # 🮥 0x05, 0x0d, // Usage Page (Digitizers) 8
* # 0x75, 0x08, // Report Size (8) 10
* # 0x95, 0x01, // Report Count (1) 12
* # ┇ 0x81, 0x01, // Input (Cnst,Arr,Abs) 14
* # 🭬 0x09, 0x21, // Usage (Puck) 16
* # 0xa1, 0x02, // Collection (Logical) 18
* # 0x15, 0x00, // Logical Minimum (0) 20
* # 0x25, 0x01, // Logical Maximum (1) 22
* # 0x75, 0x01, // Report Size (1) 24
* # 0x95, 0x01, // Report Count (1) 26
* # 0xa1, 0x00, // Collection (Physical) 28
* # 🮥 0x05, 0x09, // Usage Page (Button) 30
* # 🭬 0x09, 0x01, // Usage (Button 1) 32
* # ┇ 0x81, 0x02, // Input (Data,Var,Abs) 34
* # 🮥 0x05, 0x0d, // Usage Page (Digitizers) 36
* # 🭬 0x09, 0x33, // Usage (Touch) 38
* # ┇ 0x81, 0x02, // Input (Data,Var,Abs) 40
* # 0x95, 0x06, // Report Count (6) 42
* # ┇ 0x81, 0x03, // Input (Cnst,Var,Abs) 44
* # 0xa1, 0x02, // Collection (Logical) 46
* # 🮥 0x05, 0x01, // Usage Page (Generic Desktop) 48
* # 🭬 0x09, 0x37, // Usage (Dial) 50
* # 0x16, 0x00, 0x80, // Logical Minimum (32768) 52
* # 0x26, 0xff, 0x7f, // Logical Maximum (32767) 55
* # 0x75, 0x10, // Report Size (16) 58
* # 0x95, 0x01, // Report Count (1) 60
* # ┇ 0x81, 0x06, // Input (Data,Var,Rel) 62
* # 0x35, 0x00, // Physical Minimum (0) 64
* # 0x46, 0x10, 0x0e, // Physical Maximum (3600) 66
* # 0x15, 0x00, // Logical Minimum (0) 69
* # 0x26, 0x10, 0x0e, // Logical Maximum (3600) 71
* # 🭬 0x09, 0x48, // Usage (Resolution Multiplier) 74
* # ║ 0xb1, 0x02, // Feature (Data,Var,Abs) 76
* # 0x45, 0x00, // Physical Maximum (0) 78
* # 0xc0, // End Collection 80
* # 0x75, 0x08, // Report Size (8) 81
* # 0x95, 0x01, // Report Count (1) 83
* # ┇ 0x81, 0x01, // Input (Cnst,Arr,Abs) 85
* # 0x75, 0x08, // Report Size (8) 87
* # 0x95, 0x01, // Report Count (1) 89
* # ┇ 0x81, 0x01, // Input (Cnst,Arr,Abs) 91
* # 0x75, 0x08, // Report Size (8) 93
* # 0x95, 0x01, // Report Count (1) 95
* # ┇ 0x81, 0x01, // Input (Cnst,Arr,Abs) 97
* # 0x75, 0x08, // Report Size (8) 99
* # 0x95, 0x01, // Report Count (1) 101
* # ┇ 0x81, 0x01, // Input (Cnst,Arr,Abs) 103
* # 0x75, 0x08, // Report Size (8) 105
* # 0x95, 0x01, // Report Count (1) 107
* # ┇ 0x81, 0x01, // Input (Cnst,Arr,Abs) 109
* # 0xc0, // End Collection 111
* # 0xc0, // End Collection 112
* # 0xc0, // End Collection 113
* R: 114 05 01 09 0e a1 01 85 03 05 0d 75 08 95 01 81 01 09 21 a1 02 15 00 25 01 75 01 95 01 a1 00 05 09 09 01 81 02 05 0d 09 33 81 02 95 06 81 03 a1 02 05 01 09 37 16 00 80 26 ff 7f 75 10 95 01 81 06 35 00 46 10 0e 15 00 26 10 0e 09 48 b1 02 45 00 c0 75 08 95 01 81 01 75 08 95 01 81 01 75 08 95 01 81 01 75 08 95 01 81 01 75 08 95 01 81 01 c0 c0 c0
* N: Keydial mini-050
* I: 5 256c 8251
*
* The second hidraw node is what sends events:
*
* # Keydial mini-050
* # Report descriptor length: 160 bytes
* # Bytes // Field Name Offset
* # ----------------------------------------------------------------------------------
* # 🮥 0x05, 0x01, // Usage Page (Generic Desktop) 0
* # 🭬 0x09, 0x06, // Usage (Keyboard) 2
* # 0xa1, 0x01, // Collection (Application) 4
* # ┅ 0x85, 0x01, // Report ID (1) 6
* # 🮥 0x05, 0x07, // Usage Page (Keyboard/Keypad) 8
* # 🭬 0x19, 0xe0, // Usage Minimum (224) 10
* # 🭬 0x29, 0xe7, // Usage Maximum (231) 12
* # 0x15, 0x00, // Logical Minimum (0) 14
* # 0x25, 0x01, // Logical Maximum (1) 16
* # 0x75, 0x01, // Report Size (1) 18
* # 0x95, 0x08, // Report Count (8) 20
* # ┇ 0x81, 0x02, // Input (Data,Var,Abs) 22
* # 0x95, 0x01, // Report Count (1) 24
* # 0x75, 0x08, // Report Size (8) 26
* # ┇ 0x81, 0x01, // Input (Cnst,Arr,Abs) 28
* # 0x95, 0x05, // Report Count (5) 30
* # 0x75, 0x01, // Report Size (1) 32
* # 🮥 0x05, 0x08, // Usage Page (LED) 34
* # 🭬 0x19, 0x01, // Usage Minimum (1) 36
* # 🭬 0x29, 0x05, // Usage Maximum (5) 38
* # ┊ 0x91, 0x02, // Output (Data,Var,Abs) 40
* # 0x95, 0x01, // Report Count (1) 42
* # 0x75, 0x03, // Report Size (3) 44
* # ┊ 0x91, 0x01, // Output (Cnst,Arr,Abs) 46
* # 0x95, 0x06, // Report Count (6) 48
* # 0x75, 0x08, // Report Size (8) 50
* # 0x15, 0x00, // Logical Minimum (0) 52
* # 0x25, 0xf1, // Logical Maximum (241) 54
* # 🮥 0x05, 0x07, // Usage Page (Keyboard/Keypad) 56
* # 🭬 0x19, 0x00, // Usage Minimum (0) 58
* # 🭬 0x29, 0xf1, // Usage Maximum (241) 60
* # ┇ 0x81, 0x00, // Input (Data,Arr,Abs) 62
* # 0xc0, // End Collection 64
* # 🮥 0x05, 0x0c, // Usage Page (Consumer) 65
* # 🭬 0x09, 0x01, // Usage (Consumer Control) 67
* # 0xa1, 0x01, // Collection (Application) 69
* # ┅ 0x85, 0x02, // Report ID (2) 71
* # 🮥 0x05, 0x0c, // Usage Page (Consumer) 73
* # 🭬 0x19, 0x00, // Usage Minimum (0) 75
* # 🭬 0x2a, 0x80, 0x03, // Usage Maximum (896) 77
* # 0x15, 0x00, // Logical Minimum (0) 80
* # 0x26, 0x80, 0x03, // Logical Maximum (896) 82
* # 0x75, 0x10, // Report Size (16) 85
* # 0x95, 0x01, // Report Count (1) 87
* # ┇ 0x81, 0x00, // Input (Data,Arr,Abs) 89
* # 0xc0, // End Collection 91
* # 🮥 0x05, 0x01, // Usage Page (Generic Desktop) 92
* # 🭬 0x09, 0x02, // Usage (Mouse) 94
* # 0xa1, 0x01, // Collection (Application) 96
* # 🭬 0x09, 0x01, // Usage (Pointer) 98
* # ┅ 0x85, 0x05, // Report ID (5) 100
* # 0xa1, 0x00, // Collection (Physical) 102
* # 🮥 0x05, 0x09, // Usage Page (Button) 104
* # 🭬 0x19, 0x01, // Usage Minimum (1) 106
* # 🭬 0x29, 0x05, // Usage Maximum (5) 108
* # 0x15, 0x00, // Logical Minimum (0) 110
* # 0x25, 0x01, // Logical Maximum (1) 112
* # 0x95, 0x05, // Report Count (5) 114
* # 0x75, 0x01, // Report Size (1) 116
* # ┇ 0x81, 0x02, // Input (Data,Var,Abs) 118
* # 0x95, 0x01, // Report Count (1) 120
* # 0x75, 0x03, // Report Size (3) 122
* # ┇ 0x81, 0x01, // Input (Cnst,Arr,Abs) 124
* # 🮥 0x05, 0x01, // Usage Page (Generic Desktop) 126
* # 🭬 0x09, 0x30, // Usage (X) 128
* # 🭬 0x09, 0x31, // Usage (Y) 130
* # 0x16, 0x01, 0x80, // Logical Minimum (32769) 132
* # 0x26, 0xff, 0x7f, // Logical Maximum (32767) 135
* # 0x75, 0x10, // Report Size (16) 138
* # 0x95, 0x02, // Report Count (2) 140
* # ┇ 0x81, 0x06, // Input (Data,Var,Rel) 142
* # 🮥 0x05, 0x01, // Usage Page (Generic Desktop) 144
* # 🭬 0x09, 0x38, // Usage (Wheel) 146
* # 0x15, 0x81, // Logical Minimum (129) 148
* # 0x25, 0x7f, // Logical Maximum (127) 150
* # 0x95, 0x01, // Report Count (1) 152
* # 0x75, 0x08, // Report Size (8) 154
* # ┇ 0x81, 0x06, // Input (Data,Var,Rel) 156
* # 0xc0, // End Collection 158
* # 0xc0, // End Collection 159
* R: 160 05 01 09 06 a1 01 85 01 05 07 19 e0 29 e7 15 00 25 01 75 01 95 08 81 02 95 01 75 08 81 01 95 05 75 01 05 08 19 01 29 05 91 02 95 01 75 03 91 01 95 06 75 08 15 00 25 f1 05 07 19 00 29 f1 81 00 c0 05 0c 09 01 a1 01 85 02 05 0c 19 00 2a 80 03 15 00 26 80 03 75 10 95 01 81 00 c0 05 01 09 02 a1 01 09 01 85 05 a1 00 05 09 19 01 29 05 15 00 25 01 95 05 75 01 81 02 95 01 75 03 81 01 05 01 09 30 09 31 16 01 80 26 ff 7f 75 10 95 02 81 06 05 01 09 38 15 81 25 7f 95 01 75 08 81 06 c0 c0
* N: Keydial mini-050
* I: 5 256c 8251
* # Report descriptor:
* # ------- Input Report -------
* # ░ Report ID: 1
* # ░ | Report size: 72 bits
* # ░ Bit: 8 Usage: 0007/00e0: Keyboard/Keypad / Keyboard LeftControl Logical Range: 0..=1
* # ░ Bit: 9 Usage: 0007/00e1: Keyboard/Keypad / Keyboard LeftShift Logical Range: 0..=1
* # ░ Bit: 10 Usage: 0007/00e2: Keyboard/Keypad / Keyboard LeftAlt Logical Range: 0..=1
* # ░ Bit: 11 Usage: 0007/00e3: Keyboard/Keypad / Keyboard Left GUI Logical Range: 0..=1
* # ░ Bit: 12 Usage: 0007/00e4: Keyboard/Keypad / Keyboard RightControl Logical Range: 0..=1
* # ░ Bit: 13 Usage: 0007/00e5: Keyboard/Keypad / Keyboard RightShift Logical Range: 0..=1
* # ░ Bit: 14 Usage: 0007/00e6: Keyboard/Keypad / Keyboard RightAlt Logical Range: 0..=1
* # ░ Bit: 15 Usage: 0007/00e7: Keyboard/Keypad / Keyboard Right GUI Logical Range: 0..=1
* # ░ Bits: 16..=23 ######### Padding
* # ░ Bits: 24..=71 Usages: Logical Range: 0..=241
* # ░ 0007/0000: <unknown>
* # ░ 0007/0001: Keyboard/Keypad / ErrorRollOver
* # ░ 0007/0002: Keyboard/Keypad / POSTFail
* # ░ 0007/0003: Keyboard/Keypad / ErrorUndefined
* # ░ 0007/0004: Keyboard/Keypad / Keyboard A
* # ░ ... use --full to see all usages
* # ------- Input Report -------
* # ▒ Report ID: 2
* # ▒ | Report size: 24 bits
* # ▒ Bits: 8..=23 Usages: Logical Range: 0..=896
* # ▒ 000c/0000: <unknown>
* # ▒ 000c/0001: Consumer / Consumer Control
* # ▒ 000c/0002: Consumer / Numeric Key Pad
* # ▒ 000c/0003: Consumer / Programmable Buttons
* # ▒ 000c/0004: Consumer / Microphone
* # ▒ ... use --full to see all usages
* # ------- Input Report -------
* # ▞ Report ID: 5
* # ▞ | Report size: 56 bits
* # ▞ Bit: 8 Usage: 0009/0001: Button / Button 1 Logical Range: 0..=1
* # ▞ Bit: 9 Usage: 0009/0002: Button / Button 2 Logical Range: 0..=1
* # ▞ Bit: 10 Usage: 0009/0003: Button / Button 3 Logical Range: 0..=1
* # ▞ Bit: 11 Usage: 0009/0004: Button / Button 4 Logical Range: 0..=1
* # ▞ Bit: 12 Usage: 0009/0005: Button / Button 5 Logical Range: 0..=1
* # ▞ Bits: 13..=15 ######### Padding
* # ▞ Bits: 16..=31 Usage: 0001/0030: Generic Desktop / X Logical Range: 32769..=32767
* # ▞ Bits: 32..=47 Usage: 0001/0031: Generic Desktop / Y Logical Range: 32769..=32767
* # ▞ Bits: 48..=55 Usage: 0001/0038: Generic Desktop / Wheel Logical Range: 129..=127
* # ------- Output Report -------
* # ░ Report ID: 1
* # ░ | Report size: 16 bits
* # ░ Bit: 8 Usage: 0008/0001: LED / Num Lock Logical Range: 0..=1
* # ░ Bit: 9 Usage: 0008/0002: LED / Caps Lock Logical Range: 0..=1
* # ░ Bit: 10 Usage: 0008/0003: LED / Scroll Lock Logical Range: 0..=1
* # ░ Bit: 11 Usage: 0008/0004: LED / Compose Logical Range: 0..=1
* # ░ Bit: 12 Usage: 0008/0005: LED / Kana Logical Range: 0..=1
* # ░ Bits: 13..=15 ######### Padding
* ##############################################################################
* # Event nodes:
* # - /dev/input/event12: "Keydial mini-050 Keyboard"
* # - /dev/input/event14: "Keydial mini-050 Mouse"
* ##############################################################################
* # Recorded events below in format:
* # E: <seconds>.<microseconds> <length-in-bytes> [bytes ...]
* #
*
* - Report ID 1 sends keyboard shortcuts when pressing the buttons, e.g.
*
* # ░ Report ID: 1 /
* # ░ Keyboard LeftControl: 0 |Keyboard LeftShift: 0 |Keyboard LeftAlt: 0 |Keyboard Left GUI: 0 |Keyboard RightControl: 0 |Keyboard RightShift: 0 |Keyboard RightAlt: 0 |Keyboard Right GUI: 0 |<8 bits padding> |0007/0000: 0| Keyboard K: 14| 0007/0000: 0| 0007/0000: 0| 0007/0000: 0| 0007/0000: 0
* E: 000000.000292 9 01 00 00 00 0e 00 00 00 00
*
* - Report ID 2 sends the button inside the wheel/dial thing
* # ▒ Report ID: 2 /
* # ▒ Play/Pause: 205
* E: 000134.347845 3 02 cd 00
* # ▒ Report ID: 2 /
* # ▒ 000c/0000: 0
* E: 000134.444965 3 02 00 00
*
* - Report ID 5 sends the wheel relative events (always a double-event with the second as zero)
* # ▞ Report ID: 5 /
* # ▞ Button 1: 0 |Button 2: 0 |Button 3: 0 |Button 4: 0 |Button 5: 0 |<3 bits padding> |X: 0 |Y: 0 |Wheel: 255
* E: 000064.859915 7 05 00 00 00 00 00 ff
* # ▞ Report ID: 5 /
* # ▞ Button 1: 0 |Button 2: 0 |Button 3: 0 |Button 4: 0 |Button 5: 0 |<3 bits padding> |X: 0 |Y: 0 |Wheel: 0
* E: 000064.882009 7 05 00 00 00 00 00 00
*/
#define BT_PAD_REPORT_DESCRIPTOR_LENGTH 160
#define BT_PUCK_REPORT_DESCRIPTOR_LENGTH 114 // This one doesn't send events
#define BT_PAD_KBD_REPORT_ID 1
#define BT_PAD_CC_REPORT_ID 2
#define BT_PAD_MOUSE_REPORT_ID 5
#define BT_PAD_KBD_REPORT_LENGTH 9
#define BT_PAD_CC_REPORT_LENGTH 3
#define BT_PAD_MOUSE_REPORT_LENGTH 7
#define OUR_REPORT_ID 11 /* "randomly" picked report ID for our reports */
__u32 last_button_state = 0;
static const __u8 disabled_rdesc_puck[] = {
FixedSizeVendorReport(BT_PUCK_REPORT_DESCRIPTOR_LENGTH)
};
static const __u8 fixed_rdesc_pad[] = {
UsagePage_GenericDesktop
Usage_GD_Keypad
CollectionApplication(
// Byte 0
ReportId(OUR_REPORT_ID)
UsagePage_Digitizers
Usage_Dig_TabletFunctionKeys
CollectionPhysical(
// Byte 1 is a button so we look like a tablet
Usage_Dig_BarrelSwitch // BTN_STYLUS, needed so we get to be a tablet pad
ReportCount(1)
ReportSize(1)
Input(Var|Abs)
ReportCount(7) // Padding
Input(Const)
// Bytes 2/3 - x/y just exist so we get to be a tablet pad
UsagePage_GenericDesktop
Usage_GD_X
Usage_GD_Y
LogicalMinimum_i8(0x0)
LogicalMaximum_i8(0x1)
ReportCount(2)
ReportSize(8)
Input(Var|Abs)
// Bytes 4-7 are the button state for 19 buttons + pad out to u32
// We send the first 10 buttons as buttons 1-10 which is BTN_0 -> BTN_9
UsagePage_Button
UsageMinimum_i8(1)
UsageMaximum_i8(10)
LogicalMinimum_i8(0x0)
LogicalMaximum_i8(0x1)
ReportCount(10)
ReportSize(1)
Input(Var|Abs)
// We send the other 9 buttons as buttons 0x31 and above -> BTN_A - BTN_TL2
UsageMinimum_i8(0x31)
UsageMaximum_i8(0x3a)
ReportCount(9)
ReportSize(1)
Input(Var|Abs)
ReportCount(13)
ReportSize(1)
Input(Const) // padding
// Byte 8 is the wheel
UsagePage_GenericDesktop
Usage_GD_Wheel
LogicalMinimum_i8(-1)
LogicalMaximum_i8(1)
ReportCount(1)
ReportSize(8)
Input(Var|Rel)
)
// Make sure we match our original report length
FixedSizeVendorReport(BT_PAD_KBD_REPORT_LENGTH)
)
};
SEC(HID_BPF_RDESC_FIXUP)
int BPF_PROG(k20_bt_fix_rdesc, struct hid_bpf_ctx *hctx)
{
__u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, HID_MAX_DESCRIPTOR_SIZE /* size */);
__s32 rdesc_size = hctx->size;
if (!data)
return 0; /* EPERM check */
if (rdesc_size == BT_PAD_REPORT_DESCRIPTOR_LENGTH) {
__builtin_memcpy(data, fixed_rdesc_pad, sizeof(fixed_rdesc_pad));
return sizeof(fixed_rdesc_pad);
}
if (rdesc_size == BT_PUCK_REPORT_DESCRIPTOR_LENGTH) {
// This hidraw node doesn't send anything and can be ignored
__builtin_memcpy(data, disabled_rdesc_puck, sizeof(disabled_rdesc_puck));
return sizeof(disabled_rdesc_puck);
}
return 0;
}
SEC(HID_BPF_DEVICE_EVENT)
int BPF_PROG(k20_bt_fix_events, struct hid_bpf_ctx *hctx)
{
__u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, 12 /* size */);
struct pad_report {
__u8 report_id;
__u8 btn_stylus:1;
__u8 pad:7;
__u8 x;
__u8 y;
__u32 buttons;
__u8 wheel;
} __packed * pad_report = (struct pad_report *)data;
if (!data)
return 0; /* EPERM check */
/* Report ID 1 - Keyboard events (button presses) */
if (data[0] == BT_PAD_KBD_REPORT_ID) {
const __u8 button_mapping[] = {
0x0e, /* Button 1: K */
0x0a, /* Button 2: G */
0x0f, /* Button 3: L */
0x4c, /* Button 4: Delete */
0x0c, /* Button 5: I */
0x07, /* Button 6: D */
0x05, /* Button 7: B */
0x08, /* Button 8: E */
0x16, /* Button 9: S */
0x1d, /* Button 10: Z */
0x06, /* Button 11: C */
0x19, /* Button 12: V */
0xff, /* Button 13: LeftControl */
0xff, /* Button 14: LeftAlt */
0xff, /* Button 15: LeftShift */
0x28, /* Button 16: Return Enter */
0x2c, /* Button 17: Spacebar */
0x11, /* Button 18: N */
};
__u8 modifiers = data[1];
__u32 buttons = 0;
if (modifiers & 0x01) { /* Control */
buttons |= BIT(12);
}
if (modifiers & 0x02) { /* Shift */
buttons |= BIT(14);
}
if (modifiers & 0x04) { /* Alt */
buttons |= BIT(13);
}
for (int i = 4; i < BT_PAD_KBD_REPORT_LENGTH; i++) {
if (!data[i])
break;
for (size_t b = 0; b < ARRAY_SIZE(button_mapping); b++) {
if (data[i] != 0xff && data[i] == button_mapping[b]) {
buttons |= BIT(b);
break;
}
}
}
last_button_state = buttons;
pad_report->report_id = OUR_REPORT_ID;
pad_report->btn_stylus = 0;
pad_report->x = 0;
pad_report->y = 0;
pad_report->buttons = buttons;
pad_report->wheel = 0;
return sizeof(struct pad_report);
}
/* Report ID 2 - Consumer control events (the button inside the wheel) */
if (data[0] == BT_PAD_CC_REPORT_ID) {
const __u8 PlayPause = 0xcd;
if (data[1] == PlayPause)
last_button_state |= BIT(18);
else
last_button_state &= ~BIT(18);
pad_report->report_id = OUR_REPORT_ID;
pad_report->btn_stylus = 0;
pad_report->x = 0;
pad_report->y = 0;
pad_report->buttons = last_button_state;
pad_report->wheel = 0;
return sizeof(struct pad_report);
}
/* Report ID 5 - Mouse events (wheel rotation) */
if (data[0] == BT_PAD_MOUSE_REPORT_ID) {
__u8 wheel_delta = data[6];
pad_report->report_id = OUR_REPORT_ID;
pad_report->btn_stylus = 0;
pad_report->x = 0;
pad_report->y = 0;
pad_report->buttons = last_button_state;
pad_report->wheel = wheel_delta;
return sizeof(struct pad_report);
}
return 0;
}
HID_BPF_OPS(keydial_k20_bluetooth) = {
.hid_device_event = (void *)k20_bt_fix_events,
.hid_rdesc_fixup = (void *)k20_bt_fix_rdesc,
};
SEC("syscall")
int probe(struct hid_bpf_probe_args *ctx)
{
switch (ctx->rdesc_size) {
case BT_PAD_REPORT_DESCRIPTOR_LENGTH:
case BT_PUCK_REPORT_DESCRIPTOR_LENGTH:
ctx->retval = 0;
break;
default:
ctx->retval = -EINVAL;
}
return 0;
}
char _license[] SEC("license") = "GPL";
@@ -0,0 +1,532 @@
// SPDX-License-Identifier: GPL-2.0-only
/* Copyright (c) 2024 Red Hat, Inc
*/
#include "vmlinux.h"
#include "hid_bpf.h"
#include "hid_bpf_helpers.h"
#include "hid_report_helpers.h"
#include <bpf/bpf_tracing.h>
#define VID_HUION 0x256C
#define PID_KEYDIAL_K20 0x0069
HID_BPF_CONFIG(
HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_HUION, PID_KEYDIAL_K20),
);
/* Filled in by udev-hid-bpf */
char UDEV_PROP_HUION_FIRMWARE_ID[64];
/* The prefix of the firmware ID we expect for this device. The full firmware
* string has a date suffix, e.g. HUION_T21h_230511
*/
char EXPECTED_FIRMWARE_ID[] = "HUION_T21h_";
/* How this BPF program works: the tablet has two modes, firmware mode and
* tablet mode. In firmware mode (out of the box) the tablet sends button events
* as keyboard shortcuts and the dial as wheel but it's not forwarded by the kernel.
* In tablet mode it uses a vendor specific hid report to report everything instead.
* Depending on the mode some hid reports are never sent and the corresponding
* devices are mute.
*
* To switch the tablet use e.g. https://github.com/whot/huion-switcher
* or one of the tools from the digimend project
*
* This BPF currently works for both modes only. The huion-switcher tool sets the
* HUION_FIRMWARE_ID udev property - if that is set then we disable the firmware
* pad and pen reports (by making them vendor collections that are ignored).
* If that property is not set we fix all hidraw nodes so the tablet works in
* either mode though the drawback is that the device will show up twice if
* you bind it to all event nodes
*
* Default report descriptor for the first exposed hidraw node:
*
* # HUION Huion Keydial_K20
* # Report descriptor length: 18 bytes
* # 0x06, 0x00, 0xff, // Usage Page (Vendor Defined Page 0xFF00) 0
* # 0x09, 0x01, // Usage (Vendor Usage 0x01) 3
* # 0xa1, 0x01, // Collection (Application) 5
* # 0x85, 0x08, // Report ID (8) 7
* # 0x75, 0x58, // Report Size (88) 9
* # 0x95, 0x01, // Report Count (1) 11
* # 0x09, 0x01, // Usage (Vendor Usage 0x01) 13
* # 0x81, 0x02, // Input (Data,Var,Abs) 15
* # 0xc0, // End Collection 17
* R: 18 06 00 ff 09 01 a1 01 85 08 75 58 95 01 09 01 81 02 c0
*
* This report descriptor appears to be identical for all Huion devices.
*
* Second hidraw node is the Pad. This one sends the button events until the tablet is
* switched to raw mode, then it's mute.
*
* # HUION Huion Keydial_K20
* # Report descriptor length: 135 bytes
* # 0x05, 0x01, // Usage Page (Generic Desktop) 0
* # 0x09, 0x06, // Usage (Keyboard) 2
* # 0xa1, 0x01, // Collection (Application) 4
* # 0x85, 0x03, // Report ID (3) 6
* # 0x05, 0x07, // Usage Page (Keyboard/Keypad) 8
* # 0x19, 0xe0, // UsageMinimum (224) 10
* # 0x29, 0xe7, // UsageMaximum (231) 12
* # 0x15, 0x00, // Logical Minimum (0) 14
* # 0x25, 0x01, // Logical Maximum (1) 16
* # 0x75, 0x01, // Report Size (1) 18
* # 0x95, 0x08, // Report Count (8) 20
* # 0x81, 0x02, // Input (Data,Var,Abs) 22
* # 0x05, 0x07, // Usage Page (Keyboard/Keypad) 24
* # 0x19, 0x00, // UsageMinimum (0) 26
* # 0x29, 0xff, // UsageMaximum (255) 28
* # 0x26, 0xff, 0x00, // Logical Maximum (255) 30
* # 0x75, 0x08, // Report Size (8) 33
* # 0x95, 0x06, // Report Count (6) 35
* # 0x81, 0x00, // Input (Data,Arr,Abs) 37
* # 0xc0, // End Collection 39
* # 0x05, 0x0c, // Usage Page (Consumer) 40
* # 0x09, 0x01, // Usage (Consumer Control) 42
* # 0xa1, 0x01, // Collection (Application) 44
* # 0x85, 0x04, // Report ID (4) 46
* # 0x05, 0x0c, // Usage Page (Consumer) 48
* # 0x19, 0x00, // UsageMinimum (0) 50
* # 0x2a, 0x80, 0x03, // UsageMaximum (896) 52
* # 0x15, 0x00, // Logical Minimum (0) 55
* # 0x26, 0x80, 0x03, // Logical Maximum (896) 57
* # 0x75, 0x10, // Report Size (16) 60
* # 0x95, 0x01, // Report Count (1) 62
* # 0x81, 0x00, // Input (Data,Arr,Abs) 64
* # 0xc0, // End Collection 66
* # 0x05, 0x01, // Usage Page (Generic Desktop) 67
* # 0x09, 0x02, // Usage (Mouse) 69
* # 0xa1, 0x01, // Collection (Application) 71
* # 0x09, 0x01, // Usage (Pointer) 73
* # 0x85, 0x05, // Report ID (5) 75
* # 0xa1, 0x00, // Collection (Physical) 77
* # 0x05, 0x09, // Usage Page (Button) 79
* # 0x19, 0x01, // UsageMinimum (1) 81
* # 0x29, 0x05, // UsageMaximum (5) 83
* # 0x15, 0x00, // Logical Minimum (0) 85
* # 0x25, 0x01, // Logical Maximum (1) 87
* # 0x95, 0x05, // Report Count (5) 89
* # 0x75, 0x01, // Report Size (1) 91
* # 0x81, 0x02, // Input (Data,Var,Abs) 93
* # 0x95, 0x01, // Report Count (1) 95
* # 0x75, 0x03, // Report Size (3) 97
* # 0x81, 0x01, // Input (Cnst,Arr,Abs) 99
* # 0x05, 0x01, // Usage Page (Generic Desktop) 101
* # 0x09, 0x30, // Usage (X) 103
* # 0x09, 0x31, // Usage (Y) 105
* # 0x16, 0x00, 0x80, // Logical Minimum (-32768) 107
* # 0x26, 0xff, 0x7f, // Logical Maximum (32767) 110
* # 0x75, 0x10, // Report Size (16) 113
* # 0x95, 0x02, // Report Count (2) 115
* # 0x81, 0x06, // Input (Data,Var,Rel) 117
* # 0x95, 0x01, // Report Count (1) 119
* # 0x75, 0x08, // Report Size (8) 121
* # 0x05, 0x01, // Usage Page (Generic Desktop) 123
* # 0x09, 0x38, // Usage (Wheel) 125
* # 0x15, 0x81, // Logical Minimum (-127) 127
* # 0x25, 0x7f, // Logical Maximum (127) 129
* # 0x81, 0x06, // Input (Data,Var,Rel) 131
* # 0xc0, // End Collection 133
* # 0xc0, // End Collection 134
* R: 135 05 01 09 06 a1 01 85 03 05 07 19 e0 29 e7 15 00 25 01 75 01 95 08 81 02 05 07 19 00 29 ff 26 ff 00 75 08 95 06 81 00 c0 05 0c 09 01 a1 01 85 04 05 0c 19 00 2a 80 03 15 00 26 80 03 75 10 95 01 81 00 c0 05 01 09 02 a1 01 09 01 85 05 a1 00 05 09 19 01 29 05 15 00 25 01 95 05 75 01 81 02 95 01 75 03 81 01 05 01 09 30 09 31 16 00 80 26 ff 7f 7510 95 02 81 06 95 01 75 08 05 01 09 38 15 81 25 7f 81 06 c0 c0
*
* Third hidraw node is a multi-axis controller which sends the dial events
* and the button inside the dial. If the tablet is switched to raw mode it is mute.
*
* # HUION Huion Keydial_K20
* # Report descriptor length: 108 bytes
* # 0x05, 0x01, // Usage Page (Generic Desktop) 0
* # 0x09, 0x0e, // Usage (System Multi-Axis Controller) 2
* # 0xa1, 0x01, // Collection (Application) 4
* # 0x85, 0x11, // Report ID (17) 6
* # 0x05, 0x0d, // Usage Page (Digitizers) 8
* # 0x09, 0x21, // Usage (Puck) 10
* # 0xa1, 0x02, // Collection (Logical) 12
* # 0x15, 0x00, // Logical Minimum (0) 14
* # 0x25, 0x01, // Logical Maximum (1) 16
* # 0x75, 0x01, // Report Size (1) 18
* # 0x95, 0x01, // Report Count (1) 20
* # 0xa1, 0x00, // Collection (Physical) 22
* # 0x05, 0x09, // Usage Page (Button) 24
* # 0x09, 0x01, // Usage (Button 1) 26
* # 0x81, 0x02, // Input (Data,Var,Abs) 28
* # 0x05, 0x0d, // Usage Page (Digitizers) 30
* # 0x09, 0x33, // Usage (Touch) 32
* # 0x81, 0x02, // Input (Data,Var,Abs) 34
* # 0x95, 0x06, // Report Count (6) 36
* # 0x81, 0x03, // Input (Cnst,Var,Abs) 38
* # 0xa1, 0x02, // Collection (Logical) 40
* # 0x05, 0x01, // Usage Page (Generic Desktop) 42
* # 0x09, 0x37, // Usage (Dial) 44
* # 0x16, 0x00, 0x80, // Logical Minimum (-32768) 46
* # 0x26, 0xff, 0x7f, // Logical Maximum (32767) 49
* # 0x75, 0x10, // Report Size (16) 52
* # 0x95, 0x01, // Report Count (1) 54
* # 0x81, 0x06, // Input (Data,Var,Rel) 56
* # 0x35, 0x00, // Physical Minimum (0) 58
* # 0x46, 0x10, 0x0e, // Physical Maximum (3600) 60
* # 0x15, 0x00, // Logical Minimum (0) 63
* # 0x26, 0x10, 0x0e, // Logical Maximum (3600) 65
* # 0x09, 0x48, // Usage (Resolution Multiplier) 68
* # 0xb1, 0x02, // Feature (Data,Var,Abs) 70
* # 0x45, 0x00, // Physical Maximum (0) 72
* # 0xc0, // End Collection 74
* # 0x75, 0x08, // Report Size (8) 75
* # 0x95, 0x01, // Report Count (1) 77
* # 0x81, 0x01, // Input (Cnst,Arr,Abs) 79
* # 0x75, 0x08, // Report Size (8) 81
* # 0x95, 0x01, // Report Count (1) 83
* # 0x81, 0x01, // Input (Cnst,Arr,Abs) 85
* # 0x75, 0x08, // Report Size (8) 87
* # 0x95, 0x01, // Report Count (1) 89
* # 0x81, 0x01, // Input (Cnst,Arr,Abs) 91
* # 0x75, 0x08, // Report Size (8) 93
* # 0x95, 0x01, // Report Count (1) 95
* # 0x81, 0x01, // Input (Cnst,Arr,Abs) 97
* # 0x75, 0x08, // Report Size (8) 99
* # 0x95, 0x01, // Report Count (1) 101
* # 0x81, 0x01, // Input (Cnst,Arr,Abs) 103
* # 0xc0, // End Collection 105
* # 0xc0, // End Collection 106
* # 0xc0, // End Collection 107
* R: 108 05 01 09 0e a1 01 85 11 05 0d 09 21 a1 02 15 00 25 01 75 01 95 01 a1 00 05 09 09 01 81 02 05 0d 09 33 81 02 95 06 81 03 a1 02 05 01 09 37 16 00 80 26 ff 7f 75 10 95 01 81 06 35 00 46 10 0e 15 00 26 10 0e 09 48 b1 02 45 00 c0 75 08 95 01 81 01 75 08 95 01 81 01 75 08 95 01 81 01 75 08 95 01 81 01 75 08 95 01 81 01 c0 c0 c0
*
*/
#define PAD_REPORT_DESCRIPTOR_LENGTH 135
#define PUCK_REPORT_DESCRIPTOR_LENGTH 108
#define VENDOR_REPORT_DESCRIPTOR_LENGTH 18
#define PAD_KBD_REPORT_ID 3
#define PAD_CC_REPORT_ID 3 // never sends events
#define PAD_MOUSE_REPORT_ID 4 // never sends events
#define PUCK_REPORT_ID 17
#define VENDOR_REPORT_ID 8
#define PAD_KBD_REPORT_LENGTH 8
#define PAD_CC_REPORT_LENGTH 3
#define PAD_MOUSE_REPORT_LENGTH 7
#define PUCK_REPORT_LENGTH 9
#define VENDOR_REPORT_LENGTH 12
__u32 last_button_state;
static const __u8 disabled_rdesc_puck[] = {
FixedSizeVendorReport(PUCK_REPORT_LENGTH)
};
static const __u8 disabled_rdesc_pad[] = {
FixedSizeVendorReport(PAD_KBD_REPORT_LENGTH)
FixedSizeVendorReport(PAD_CC_REPORT_LENGTH)
FixedSizeVendorReport(PAD_MOUSE_REPORT_LENGTH)
};
static const __u8 fixed_rdesc_vendor[] = {
UsagePage_GenericDesktop
Usage_GD_Keypad
CollectionApplication(
// Byte 0
// We send our pad events on the vendor report id because why not
ReportId(VENDOR_REPORT_ID)
UsagePage_Digitizers
Usage_Dig_TabletFunctionKeys
CollectionPhysical(
// Byte 1 is a button so we look like a tablet
Usage_Dig_BarrelSwitch // BTN_STYLUS, needed so we get to be a tablet pad
ReportCount(1)
ReportSize(1)
Input(Var|Abs)
ReportCount(7) // Padding
Input(Const)
// Bytes 2/3 - x/y just exist so we get to be a tablet pad
UsagePage_GenericDesktop
Usage_GD_X
Usage_GD_Y
LogicalMinimum_i8(0x0)
LogicalMaximum_i8(0x1)
ReportCount(2)
ReportSize(8)
Input(Var|Abs)
// Bytes 4-7 are the button state for 19 buttons + pad out to u32
// We send the first 10 buttons as buttons 1-10 which is BTN_0 -> BTN_9
UsagePage_Button
UsageMinimum_i8(1)
UsageMaximum_i8(10)
LogicalMinimum_i8(0x0)
LogicalMaximum_i8(0x1)
ReportCount(10)
ReportSize(1)
Input(Var|Abs)
// We send the other 9 buttons as buttons 0x31 and above -> BTN_A - BTN_TL2
UsageMinimum_i8(0x31)
UsageMaximum_i8(0x3a)
ReportCount(9)
ReportSize(1)
Input(Var|Abs)
ReportCount(13)
ReportSize(1)
Input(Const) // padding
// Byte 6 is the wheel
UsagePage_GenericDesktop
Usage_GD_Wheel
LogicalMinimum_i8(-1)
LogicalMaximum_i8(1)
ReportCount(1)
ReportSize(8)
Input(Var|Rel)
)
// Make sure we match our original report length
FixedSizeVendorReport(VENDOR_REPORT_LENGTH)
)
};
/* Identical to fixed_rdesc_pad but with different FixedSizeVendorReport */
static const __u8 fixed_rdesc_pad[] = {
UsagePage_GenericDesktop
Usage_GD_Keypad
CollectionApplication(
// Byte 0
// We send our pad events on the vendor report id because why not
ReportId(VENDOR_REPORT_ID)
UsagePage_Digitizers
Usage_Dig_TabletFunctionKeys
CollectionPhysical(
// Byte 1 is a button so we look like a tablet
Usage_Dig_BarrelSwitch // BTN_STYLUS, needed so we get to be a tablet pad
ReportCount(1)
ReportSize(1)
Input(Var|Abs)
ReportCount(7) // Padding
Input(Const)
// Bytes 2/3 - x/y just exist so we get to be a tablet pad
UsagePage_GenericDesktop
Usage_GD_X
Usage_GD_Y
LogicalMinimum_i8(0x0)
LogicalMaximum_i8(0x1)
ReportCount(2)
ReportSize(8)
Input(Var|Abs)
// Bytes 4-7 are the button state for 19 buttons + pad out to u32
// We send the first 10 buttons as buttons 1-10 which is BTN_0 -> BTN_9
UsagePage_Button
UsageMinimum_i8(1)
UsageMaximum_i8(10)
LogicalMinimum_i8(0x0)
LogicalMaximum_i8(0x1)
ReportCount(10)
ReportSize(1)
Input(Var|Abs)
// We send the other 9 buttons as buttons 0x31 and above -> BTN_A - BTN_TL2
UsageMinimum_i8(0x31)
UsageMaximum_i8(0x3a)
ReportCount(9)
ReportSize(1)
Input(Var|Abs)
ReportCount(13)
ReportSize(1)
Input(Const) // padding
// Byte 6 is the wheel
UsagePage_GenericDesktop
Usage_GD_Wheel
LogicalMinimum_i8(-1)
LogicalMaximum_i8(1)
ReportCount(1)
ReportSize(8)
Input(Var|Rel)
)
// Make sure we match our original report lengths
FixedSizeVendorReport(PAD_KBD_REPORT_LENGTH)
FixedSizeVendorReport(PAD_CC_REPORT_LENGTH)
FixedSizeVendorReport(PAD_MOUSE_REPORT_LENGTH)
)
};
SEC(HID_BPF_RDESC_FIXUP)
int BPF_PROG(k20_fix_rdesc, struct hid_bpf_ctx *hctx)
{
__u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, HID_MAX_DESCRIPTOR_SIZE /* size */);
__s32 rdesc_size = hctx->size;
__u8 have_fw_id;
if (!data)
return 0; /* EPERM check */
/* If we have a firmware ID and it matches our expected prefix, we
* disable the default pad/puck nodes. They won't send events
* but cause duplicate devices.
*/
have_fw_id = __builtin_memcmp(UDEV_PROP_HUION_FIRMWARE_ID,
EXPECTED_FIRMWARE_ID,
sizeof(EXPECTED_FIRMWARE_ID) - 1) == 0;
if (rdesc_size == PAD_REPORT_DESCRIPTOR_LENGTH) {
if (have_fw_id) {
__builtin_memcpy(data, disabled_rdesc_pad, sizeof(disabled_rdesc_pad));
return sizeof(disabled_rdesc_pad);
} else {
__builtin_memcpy(data, fixed_rdesc_pad, sizeof(fixed_rdesc_pad));
return sizeof(fixed_rdesc_pad);
}
}
if (rdesc_size == PUCK_REPORT_DESCRIPTOR_LENGTH) {
if (have_fw_id) {
__builtin_memcpy(data, disabled_rdesc_puck, sizeof(disabled_rdesc_puck));
return sizeof(disabled_rdesc_puck);
}
}
/* Always fix the vendor mode so the tablet will work even if nothing sets
* the udev property (e.g. huion-switcher run manually)
*/
if (rdesc_size == VENDOR_REPORT_DESCRIPTOR_LENGTH) {
__builtin_memcpy(data, fixed_rdesc_vendor, sizeof(fixed_rdesc_vendor));
return sizeof(fixed_rdesc_vendor);
}
return 0;
}
SEC(HID_BPF_DEVICE_EVENT)
int BPF_PROG(k20_fix_events, struct hid_bpf_ctx *hctx)
{
__u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, 10 /* size */);
if (!data)
return 0; /* EPERM check */
/* Only sent if tablet is in raw mode */
if (data[0] == VENDOR_REPORT_ID) {
/* See fixed_rdesc_pad */
struct pad_report {
__u8 report_id;
__u8 btn_stylus:1;
__u8 pad:7;
__u8 x;
__u8 y;
__u32 buttons;
__u8 wheel;
} __attribute__((packed)) *pad_report;
__u8 wheel = 0;
/* Wheel report */
if (data[1] == 0xf1) {
if (data[5] == 2)
wheel = 0xff;
else
wheel = data[5];
} else {
/* data[4..6] are the buttons, mapped correctly */
last_button_state = data[4] | (data[5] << 8) | (data[6] << 16);
wheel = 0; // wheel
}
pad_report = (struct pad_report *)data;
pad_report->report_id = VENDOR_REPORT_ID;
pad_report->btn_stylus = 0;
pad_report->x = 0;
pad_report->y = 0;
pad_report->buttons = last_button_state;
pad_report->wheel = wheel;
return sizeof(struct pad_report);
}
if (data[0] == PAD_KBD_REPORT_ID) {
const __u8 button_mapping[] = {
0x0e, /* Button 1: K */
0x0a, /* Button 2: G */
0x0f, /* Button 3: L */
0x4c, /* Button 4: Delete */
0x0c, /* Button 5: I */
0x07, /* Button 6: D */
0x05, /* Button 7: B */
0x08, /* Button 8: E */
0x16, /* Button 9: S */
0x1d, /* Button 10: Z */
0x06, /* Button 11: C */
0x19, /* Button 12: V */
0xff, /* Button 13: LeftControl */
0xff, /* Button 14: LeftAlt */
0xff, /* Button 15: LeftShift */
0x28, /* Button 16: Return Enter */
0x2c, /* Button 17: Spacebar */
0x11, /* Button 18: N */
};
/* See fixed_rdesc_pad */
struct pad_report {
__u8 report_id;
__u8 btn_stylus:1;
__u8 pad:7;
__u8 x;
__u8 y;
__u32 buttons;
__u8 wheel;
} __attribute__((packed)) *pad_report;
int i;
size_t b;
__u8 modifiers = data[1];
__u32 buttons = 0;
if (modifiers & 0x01) { /* Control */
buttons |= BIT(12);
}
if (modifiers & 0x02) { /* Shift */
buttons |= BIT(14);
}
if (modifiers & 0x04) { /* Alt */
buttons |= BIT(13);
}
for (i = 2; i < PAD_KBD_REPORT_LENGTH; i++) {
if (!data[i])
break;
for (b = 0; b < ARRAY_SIZE(button_mapping); b++) {
if (data[i] == button_mapping[b]) {
buttons |= BIT(b);
break;
}
}
data[i] = 0;
}
pad_report = (struct pad_report *)data;
pad_report->report_id = VENDOR_REPORT_ID;
pad_report->btn_stylus = 0;
pad_report->x = 0;
pad_report->y = 0;
pad_report->buttons = buttons;
// The wheel happens on a different hidraw node but its
// values are unreliable (as is the button inside the wheel).
// So the wheel is simply always zero, if you want the wheel
// to work reliably, use the tablet mode.
pad_report->wheel = 0;
return sizeof(struct pad_report);
}
return 0;
}
HID_BPF_OPS(keydial_k20) = {
.hid_device_event = (void *)k20_fix_events,
.hid_rdesc_fixup = (void *)k20_fix_rdesc,
};
SEC("syscall")
int probe(struct hid_bpf_probe_args *ctx)
{
switch (ctx->rdesc_size) {
case PAD_REPORT_DESCRIPTOR_LENGTH:
case PUCK_REPORT_DESCRIPTOR_LENGTH:
case VENDOR_REPORT_DESCRIPTOR_LENGTH:
ctx->retval = 0;
break;
default:
ctx->retval = -EINVAL;
}
return 0;
}
char _license[] SEC("license") = "GPL";
@@ -0,0 +1,63 @@
// 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(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 (size_t 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;
}
HID_BPF_OPS(iogear_kaliber_momentum) = {
.hid_rdesc_fixup = (void *)hid_fix_rdesc,
};
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";
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: GPL-2.0-only
/* Copyright (c) 2025 Curran Muhlberger
*/
#include "vmlinux.h"
#include "hid_bpf.h"
#include "hid_bpf_helpers.h"
#include <bpf/bpf_tracing.h>
#define VID_LOGITECH 0x046D
#define PID_SPACENAVIGATOR 0xC626
HID_BPF_CONFIG(
HID_DEVICE(BUS_USB, HID_GROUP_ANY, VID_LOGITECH, PID_SPACENAVIGATOR)
);
/*
* The 3Dconnexion SpaceNavigator 3D Mouse is a multi-axis controller with 6
* axes (grouped as X,Y,Z and Rx,Ry,Rz). Axis data is absolute, but the report
* descriptor erroneously declares it to be relative. We fix the report
* descriptor to mark both axis collections as absolute.
*
* The kernel attempted to fix this in commit 24985cf68612 (HID: support
* Logitech/3DConnexion SpaceTraveler and SpaceNavigator), but the descriptor
* data offsets are incorrect for at least some SpaceNavigator units.
*/
SEC(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 */
/* Offset of Input item in X,Y,Z and Rx,Ry,Rz collections for all known
* firmware variants.
* - 2009 model: X,Y,Z @ 32-33, Rx,Ry,Rz @ 49-50 (fixup originally
* applied in kernel)
* - 2016 model (size==228): X,Y,Z @ 36-37, Rx,Ry,Rz @ 53-54
*
* The descriptor size of the 2009 model is not known, and there is evidence
* for at least two other variants (with sizes 202 & 217) besides the 2016
* model, so we try all known offsets regardless of descriptor size.
*/
const u8 offsets[] = {32, 36, 49, 53};
for (size_t idx = 0; idx < ARRAY_SIZE(offsets); idx++) {
u8 offset = offsets[idx];
/* if Input (Data,Var,Rel) , make it Input (Data,Var,Abs) */
if (data[offset] == 0x81 && data[offset + 1] == 0x06)
data[offset + 1] = 0x02;
}
return 0;
}
HID_BPF_OPS(logitech_spacenavigator) = {
.hid_rdesc_fixup = (void *)hid_fix_rdesc,
};
SEC("syscall")
int probe(struct hid_bpf_probe_args *ctx)
{
/* Ensure report descriptor size matches one of the known variants. */
if (ctx->rdesc_size != 202 &&
ctx->rdesc_size != 217 &&
ctx->rdesc_size != 228) {
ctx->retval = -EINVAL;
return 0;
}
/* Check whether the kernel has already applied the fix. */
if ((ctx->rdesc[32] == 0x81 && ctx->rdesc[33] == 0x02 &&
ctx->rdesc[49] == 0x81 && ctx->rdesc[50] == 0x02) ||
(ctx->rdesc[36] == 0x81 && ctx->rdesc[37] == 0x02 &&
ctx->rdesc[53] == 0x81 && ctx->rdesc[54] == 0x02))
ctx->retval = -EINVAL;
else
ctx->retval = 0;
return 0;
}
char _license[] SEC("license") = "GPL";
@@ -0,0 +1,93 @@
# 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 -Wall -Werror $(INCLUDES) \
-Wno-microsoft-anon-tag \
-fms-extensions \
-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
@@ -0,0 +1,136 @@
// 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 paddles on the back of the device as a single
* bitfield value of usage "Assign Selection".
*
* The kernel doesn't process the paddles usage properly and reports KEY_UNKNOWN.
*
* SDL doesn't know how to interpret 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 interpret 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(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;
}
HID_BPF_OPS(xbox_elite_2) = {
.hid_rdesc_fixup = (void *)hid_fix_rdesc,
};
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";
@@ -0,0 +1,154 @@
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2024 Tatsuyuki Ishi
*/
#include "vmlinux.h"
#include "hid_bpf.h"
#include "hid_bpf_helpers.h"
#include <bpf/bpf_tracing.h>
#define VID_HOLTEK 0x04D9
#define PID_MD770 0x0339
#define RDESC_SIZE 203
HID_BPF_CONFIG(
HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_HOLTEK, PID_MD770)
);
/*
* The Mistel MD770 keyboard reports the first 6 simultaneous key presses
* through the first interface, and anything beyond that through a second
* interface. Unfortunately, the second interface's report descriptor has an
* error, causing events to be malformed and ignored. This HID-BPF driver
* fixes the descriptor to allow NKRO to work again.
*
* For reference, this is the original report descriptor:
*
* 0x05, 0x01, // Usage Page (Generic Desktop) 0
* 0x09, 0x80, // Usage (System Control) 2
* 0xa1, 0x01, // Collection (Application) 4
* 0x85, 0x01, // Report ID (1) 6
* 0x19, 0x81, // Usage Minimum (129) 8
* 0x29, 0x83, // Usage Maximum (131) 10
* 0x15, 0x00, // Logical Minimum (0) 12
* 0x25, 0x01, // Logical Maximum (1) 14
* 0x95, 0x03, // Report Count (3) 16
* 0x75, 0x01, // Report Size (1) 18
* 0x81, 0x02, // Input (Data,Var,Abs) 20
* 0x95, 0x01, // Report Count (1) 22
* 0x75, 0x05, // Report Size (5) 24
* 0x81, 0x01, // Input (Cnst,Arr,Abs) 26
* 0xc0, // End Collection 28
* 0x05, 0x0c, // Usage Page (Consumer Devices) 29
* 0x09, 0x01, // Usage (Consumer Control) 31
* 0xa1, 0x01, // Collection (Application) 33
* 0x85, 0x02, // Report ID (2) 35
* 0x15, 0x00, // Logical Minimum (0) 37
* 0x25, 0x01, // Logical Maximum (1) 39
* 0x95, 0x12, // Report Count (18) 41
* 0x75, 0x01, // Report Size (1) 43
* 0x0a, 0x83, 0x01, // Usage (AL Consumer Control Config) 45
* 0x0a, 0x8a, 0x01, // Usage (AL Email Reader) 48
* 0x0a, 0x92, 0x01, // Usage (AL Calculator) 51
* 0x0a, 0x94, 0x01, // Usage (AL Local Machine Browser) 54
* 0x09, 0xcd, // Usage (Play/Pause) 57
* 0x09, 0xb7, // Usage (Stop) 59
* 0x09, 0xb6, // Usage (Scan Previous Track) 61
* 0x09, 0xb5, // Usage (Scan Next Track) 63
* 0x09, 0xe2, // Usage (Mute) 65
* 0x09, 0xea, // Usage (Volume Down) 67
* 0x09, 0xe9, // Usage (Volume Up) 69
* 0x0a, 0x21, 0x02, // Usage (AC Search) 71
* 0x0a, 0x23, 0x02, // Usage (AC Home) 74
* 0x0a, 0x24, 0x02, // Usage (AC Back) 77
* 0x0a, 0x25, 0x02, // Usage (AC Forward) 80
* 0x0a, 0x26, 0x02, // Usage (AC Stop) 83
* 0x0a, 0x27, 0x02, // Usage (AC Refresh) 86
* 0x0a, 0x2a, 0x02, // Usage (AC Bookmarks) 89
* 0x81, 0x02, // Input (Data,Var,Abs) 92
* 0x95, 0x01, // Report Count (1) 94
* 0x75, 0x0e, // Report Size (14) 96
* 0x81, 0x01, // Input (Cnst,Arr,Abs) 98
* 0xc0, // End Collection 100
* 0x05, 0x01, // Usage Page (Generic Desktop) 101
* 0x09, 0x02, // Usage (Mouse) 103
* 0xa1, 0x01, // Collection (Application) 105
* 0x09, 0x01, // Usage (Pointer) 107
* 0xa1, 0x00, // Collection (Physical) 109
* 0x85, 0x03, // Report ID (3) 111
* 0x05, 0x09, // Usage Page (Button) 113
* 0x19, 0x01, // Usage Minimum (1) 115
* 0x29, 0x08, // Usage Maximum (8) 117
* 0x15, 0x00, // Logical Minimum (0) 119
* 0x25, 0x01, // Logical Maximum (1) 121
* 0x75, 0x01, // Report Size (1) 123
* 0x95, 0x08, // Report Count (8) 125
* 0x81, 0x02, // Input (Data,Var,Abs) 127
* 0x05, 0x01, // Usage Page (Generic Desktop) 129
* 0x09, 0x30, // Usage (X) 131
* 0x09, 0x31, // Usage (Y) 133
* 0x16, 0x01, 0x80, // Logical Minimum (-32767) 135
* 0x26, 0xff, 0x7f, // Logical Maximum (32767) 138
* 0x75, 0x10, // Report Size (16) 141
* 0x95, 0x02, // Report Count (2) 143
* 0x81, 0x06, // Input (Data,Var,Rel) 145
* 0x09, 0x38, // Usage (Wheel) 147
* 0x15, 0x81, // Logical Minimum (-127) 149
* 0x25, 0x7f, // Logical Maximum (127) 151
* 0x75, 0x08, // Report Size (8) 153
* 0x95, 0x01, // Report Count (1) 155
* 0x81, 0x06, // Input (Data,Var,Rel) 157
* 0x05, 0x0c, // Usage Page (Consumer Devices) 159
* 0x0a, 0x38, 0x02, // Usage (AC Pan) 161
* 0x95, 0x01, // Report Count (1) 164
* 0x81, 0x06, // Input (Data,Var,Rel) 166
* 0xc0, // End Collection 168
* 0xc0, // End Collection 169
* 0x05, 0x01, // Usage Page (Generic Desktop) 170
* 0x09, 0x06, // Usage (Keyboard) 172
* 0xa1, 0x01, // Collection (Application) 174
* 0x85, 0x04, // Report ID (4) 176
* 0x05, 0x07, // Usage Page (Keyboard) 178
* 0x95, 0x01, // Report Count (1) 180
* 0x75, 0x08, // Report Size (8) 182
* 0x81, 0x03, // Input (Cnst,Var,Abs) 184
* 0x95, 0xe8, // Report Count (232) 186
* 0x75, 0x01, // Report Size (1) 188
* 0x15, 0x00, // Logical Minimum (0) 190
* 0x25, 0x01, // Logical Maximum (1) 192
* 0x05, 0x07, // Usage Page (Keyboard) 194
* 0x19, 0x00, // Usage Minimum (0) 196
* 0x29, 0xe7, // Usage Maximum (231) 198
* 0x81, 0x00, // Input (Data,Arr,Abs) 200 <- change to 0x81, 0x02 (Data,Var,Abs)
* 0xc0, // End Collection 202
*/
SEC(HID_BPF_RDESC_FIXUP)
int BPF_PROG(hid_rdesc_fixup_mistel_md770, struct hid_bpf_ctx *hctx)
{
__u8 *data = hid_bpf_get_data(hctx, 0, HID_MAX_DESCRIPTOR_SIZE);
if (!data)
return 0; /* EPERM check */
if (data[201] == 0x00)
data[201] = 0x02;
return 0;
}
HID_BPF_OPS(mistel_md770) = {
.hid_rdesc_fixup = (void *)hid_rdesc_fixup_mistel_md770,
};
SEC("syscall")
int probe(struct hid_bpf_probe_args *ctx)
{
ctx->retval = ctx->rdesc_size != RDESC_SIZE;
if (ctx->retval)
ctx->retval = -EINVAL;
return 0;
}
char _license[] SEC("license") = "GPL";
+102
View 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
```
@@ -0,0 +1,148 @@
// SPDX-License-Identifier: GPL-2.0-only
/* Copyright (c) 2024 José Expósito
*/
#include "vmlinux.h"
#include "hid_bpf.h"
#include "hid_bpf_helpers.h"
#include <bpf/bpf_tracing.h>
#define VID_RAPOO 0x24AE
#define PID_M50 0x2015
#define RDESC_SIZE 186
HID_BPF_CONFIG(
HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_RAPOO, PID_M50)
);
/*
* The Rapoo M50 Plus Silent mouse has 2 side buttons in addition to the left,
* right and middle buttons. However, its original HID descriptor has a Usage
* Maximum of 3, preventing the side buttons to work. This HID-BPF driver
* changes that usage to 5.
*
* For reference, this is the original report descriptor:
*
* 0x05, 0x01, // Usage Page (Generic Desktop) 0
* 0x09, 0x02, // Usage (Mouse) 2
* 0xa1, 0x01, // Collection (Application) 4
* 0x85, 0x01, // Report ID (1) 6
* 0x09, 0x01, // Usage (Pointer) 8
* 0xa1, 0x00, // Collection (Physical) 10
* 0x05, 0x09, // Usage Page (Button) 12
* 0x19, 0x01, // Usage Minimum (1) 14
* 0x29, 0x03, // Usage Maximum (3) 16 <- change to 0x05
* 0x15, 0x00, // Logical Minimum (0) 18
* 0x25, 0x01, // Logical Maximum (1) 20
* 0x75, 0x01, // Report Size (1) 22
* 0x95, 0x05, // Report Count (5) 24
* 0x81, 0x02, // Input (Data,Var,Abs) 26
* 0x75, 0x03, // Report Size (3) 28
* 0x95, 0x01, // Report Count (1) 30
* 0x81, 0x01, // Input (Cnst,Arr,Abs) 32
* 0x05, 0x01, // Usage Page (Generic Desktop) 34
* 0x09, 0x30, // Usage (X) 36
* 0x09, 0x31, // Usage (Y) 38
* 0x16, 0x01, 0x80, // Logical Minimum (-32767) 40
* 0x26, 0xff, 0x7f, // Logical Maximum (32767) 43
* 0x75, 0x10, // Report Size (16) 46
* 0x95, 0x02, // Report Count (2) 48
* 0x81, 0x06, // Input (Data,Var,Rel) 50
* 0x09, 0x38, // Usage (Wheel) 52
* 0x15, 0x81, // Logical Minimum (-127) 54
* 0x25, 0x7f, // Logical Maximum (127) 56
* 0x75, 0x08, // Report Size (8) 58
* 0x95, 0x01, // Report Count (1) 60
* 0x81, 0x06, // Input (Data,Var,Rel) 62
* 0xc0, // End Collection 64
* 0xc0, // End Collection 65
* 0x05, 0x0c, // Usage Page (Consumer Devices) 66
* 0x09, 0x01, // Usage (Consumer Control) 68
* 0xa1, 0x01, // Collection (Application) 70
* 0x85, 0x02, // Report ID (2) 72
* 0x75, 0x10, // Report Size (16) 74
* 0x95, 0x01, // Report Count (1) 76
* 0x15, 0x01, // Logical Minimum (1) 78
* 0x26, 0x8c, 0x02, // Logical Maximum (652) 80
* 0x19, 0x01, // Usage Minimum (1) 83
* 0x2a, 0x8c, 0x02, // Usage Maximum (652) 85
* 0x81, 0x00, // Input (Data,Arr,Abs) 88
* 0xc0, // End Collection 90
* 0x05, 0x01, // Usage Page (Generic Desktop) 91
* 0x09, 0x80, // Usage (System Control) 93
* 0xa1, 0x01, // Collection (Application) 95
* 0x85, 0x03, // Report ID (3) 97
* 0x09, 0x82, // Usage (System Sleep) 99
* 0x09, 0x81, // Usage (System Power Down) 101
* 0x09, 0x83, // Usage (System Wake Up) 103
* 0x15, 0x00, // Logical Minimum (0) 105
* 0x25, 0x01, // Logical Maximum (1) 107
* 0x19, 0x01, // Usage Minimum (1) 109
* 0x29, 0x03, // Usage Maximum (3) 111
* 0x75, 0x01, // Report Size (1) 113
* 0x95, 0x03, // Report Count (3) 115
* 0x81, 0x02, // Input (Data,Var,Abs) 117
* 0x95, 0x05, // Report Count (5) 119
* 0x81, 0x01, // Input (Cnst,Arr,Abs) 121
* 0xc0, // End Collection 123
* 0x05, 0x01, // Usage Page (Generic Desktop) 124
* 0x09, 0x00, // Usage (Undefined) 126
* 0xa1, 0x01, // Collection (Application) 128
* 0x85, 0x05, // Report ID (5) 130
* 0x06, 0x00, 0xff, // Usage Page (Vendor Defined Page 1) 132
* 0x09, 0x01, // Usage (Vendor Usage 1) 135
* 0x15, 0x81, // Logical Minimum (-127) 137
* 0x25, 0x7f, // Logical Maximum (127) 139
* 0x75, 0x08, // Report Size (8) 141
* 0x95, 0x07, // Report Count (7) 143
* 0xb1, 0x02, // Feature (Data,Var,Abs) 145
* 0xc0, // End Collection 147
* 0x06, 0x00, 0xff, // Usage Page (Vendor Defined Page 1) 148
* 0x09, 0x0e, // Usage (Vendor Usage 0x0e) 151
* 0xa1, 0x01, // Collection (Application) 153
* 0x85, 0xba, // Report ID (186) 155
* 0x95, 0x1f, // Report Count (31) 157
* 0x75, 0x08, // Report Size (8) 159
* 0x26, 0xff, 0x00, // Logical Maximum (255) 161
* 0x15, 0x00, // Logical Minimum (0) 164
* 0x09, 0x01, // Usage (Vendor Usage 1) 166
* 0x91, 0x02, // Output (Data,Var,Abs) 168
* 0x85, 0xba, // Report ID (186) 170
* 0x95, 0x1f, // Report Count (31) 172
* 0x75, 0x08, // Report Size (8) 174
* 0x26, 0xff, 0x00, // Logical Maximum (255) 176
* 0x15, 0x00, // Logical Minimum (0) 179
* 0x09, 0x01, // Usage (Vendor Usage 1) 181
* 0x81, 0x02, // Input (Data,Var,Abs) 183
* 0xc0, // End Collection 185
*/
SEC(HID_BPF_RDESC_FIXUP)
int BPF_PROG(hid_rdesc_fixup_rapoo_m50, struct hid_bpf_ctx *hctx)
{
__u8 *data = hid_bpf_get_data(hctx, 0, HID_MAX_DESCRIPTOR_SIZE);
if (!data)
return 0; /* EPERM check */
if (data[17] == 0x03)
data[17] = 0x05;
return 0;
}
HID_BPF_OPS(rapoo_m50) = {
.hid_rdesc_fixup = (void *)hid_rdesc_fixup_rapoo_m50,
};
SEC("syscall")
int probe(struct hid_bpf_probe_args *ctx)
{
ctx->retval = ctx->rdesc_size != RDESC_SIZE;
if (ctx->retval)
ctx->retval = -EINVAL;
return 0;
}
char _license[] SEC("license") = "GPL";
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/* Copyright (c) 2025 TUXEDO Computers GmbH
*/
#include "vmlinux.h"
#include "hid_bpf.h"
#include "hid_bpf_helpers.h"
#include <bpf/bpf_tracing.h>
HID_BPF_CONFIG(
HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, 0x048D, 0x8910)
);
SEC(HID_BPF_DEVICE_EVENT)
int BPF_PROG(ignore_key_fix_event, struct hid_bpf_ctx *hid_ctx)
{
const int expected_length = 37;
const int expected_report_id = 1;
__u8 *data;
int i;
if (hid_ctx->size < expected_length)
return 0;
data = hid_bpf_get_data(hid_ctx, 0, expected_length);
if (!data || data[0] != expected_report_id)
return 0;
// Zero out F13 (HID usage ID: 0x68) key press.
// The first 6 parallel key presses (excluding modifier keys) are
// encoded in an array containing usage IDs.
for (i = 3; i < 9; ++i)
if (data[i] == 0x68)
data[i] = 0x00;
// Additional parallel key presses starting with the 7th (excluding
// modifier keys) are encoded as a bit flag with the offset being
// the usage ID.
data[22] &= 0xfe;
return 0;
}
HID_BPF_OPS(ignore_button) = {
.hid_device_event = (void *)ignore_key_fix_event,
};
char _license[] SEC("license") = "GPL";
@@ -0,0 +1,144 @@
// SPDX-License-Identifier: GPL-2.0-only
/* Copyright (c) 2024 Kumar Swarnam Iyer (kumar.s.iyer65@gmail.com)
*/
#include "vmlinux.h"
#include "hid_bpf.h"
#include "hid_bpf_helpers.h"
#include <bpf/bpf_tracing.h>
#define VID_THRUSTMASTER 0x044F
#define PID_TCA_YOKE_BOEING 0x0409
HID_BPF_CONFIG(
HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_THRUSTMASTER, PID_TCA_YOKE_BOEING)
);
/* The original HID descriptor of the Thrustmaster TCA Yoke Boeing joystick contains
* an Input field that shows up as an axis, ABS_MISC in Linux. But it is not possible
* to assign an actual physical control to this axis as they're all taken up. There
* are 2 vendor-defined inputs where the Input type appears to be defined wrongly.
* This bpf attempts to fix this by changing the Inputs so that it doesn't show up in
* Linux at all.
* This version is the short version fix that only changes 2 fields in the descriptor
* instead of the whole report descriptor.
* For reference, this is the original report descriptor:
*
* 0x05, 0x01, // Usage Page (Generic Desktop) 0
* 0x09, 0x04, // Usage (Joystick) 2
* 0xa1, 0x01, // Collection (Application) 4
* 0x85, 0x01, // Report ID (1) 6
* 0x09, 0x39, // Usage (Hat switch) 8
* 0x15, 0x00, // Logical Minimum (0) 10
* 0x25, 0x07, // Logical Maximum (7) 12
* 0x35, 0x00, // Physical Minimum (0) 14
* 0x46, 0x3b, 0x01, // Physical Maximum (315) 16
* 0x65, 0x14, // Unit (EnglishRotation: deg) 19
* 0x75, 0x04, // Report Size (4) 21
* 0x95, 0x01, // Report Count (1) 23
* 0x81, 0x42, // Input (Data,Var,Abs,Null) 25
* 0x65, 0x00, // Unit (None) 27
* 0x05, 0x09, // Usage Page (Button) 29
* 0x19, 0x01, // Usage Minimum (1) 31
* 0x29, 0x12, // Usage Maximum (18) 33
* 0x15, 0x00, // Logical Minimum (0) 35
* 0x25, 0x01, // Logical Maximum (1) 37
* 0x75, 0x01, // Report Size (1) 39
* 0x95, 0x12, // Report Count (18) 41
* 0x81, 0x02, // Input (Data,Var,Abs) 43
* 0x95, 0x02, // Report Count (2) 45
* 0x81, 0x03, // Input (Cnst,Var,Abs) 47
* 0x05, 0x01, // Usage Page (Generic Desktop) 49
* 0x09, 0x31, // Usage (Y) 51
* 0x09, 0x30, // Usage (X) 53
* 0x09, 0x32, // Usage (Z) 55
* 0x09, 0x34, // Usage (Ry) 57
* 0x09, 0x33, // Usage (Rx) 59
* 0x09, 0x35, // Usage (Rz) 61
* 0x15, 0x00, // Logical Minimum (0) 63
* 0x27, 0xff, 0xff, 0x00, 0x00, // Logical Maximum (65535) 65
* 0x75, 0x10, // Report Size (16) 70
* 0x95, 0x06, // Report Count (6) 72
* 0x81, 0x02, // Input (Data,Var,Abs) 74
* 0x06, 0xf0, 0xff, // Usage Page (Vendor Usage Page 0xfff0) 76
* 0x09, 0x59, // Usage (Vendor Usage 0x59) 79
* 0x15, 0x00, // Logical Minimum (0) 81
* 0x26, 0xff, 0x00, // Logical Maximum (255) 83
* 0x75, 0x08, // Report Size (8) 86
* 0x95, 0x01, // Report Count (1) 88
* 0x81, 0x02, // Input (Data,Var,Abs) 90 --> Needs to be changed
* 0x09, 0x51, // Usage (Vendor Usage 0x51) 92
* 0x15, 0x00, // Logical Minimum (0) 94
* 0x26, 0xff, 0x00, // Logical Maximum (255) 96
* 0x75, 0x08, // Report Size (8) 99
* 0x95, 0x20, // Report Count (32) 101 --> Needs to be changed
* 0x81, 0x02, // Input (Data,Var,Abs) 103
* 0x09, 0x50, // Usage (Vendor Usage 0x50) 105
* 0x15, 0x00, // Logical Minimum (0) 107
* 0x26, 0xff, 0x00, // Logical Maximum (255) 109
* 0x75, 0x08, // Report Size (8) 112
* 0x95, 0x0f, // Report Count (15) 114
* 0x81, 0x03, // Input (Cnst,Var,Abs) 116
* 0x09, 0x47, // Usage (Vendor Usage 0x47) 118
* 0x85, 0xf2, // Report ID (242) 120
* 0x15, 0x00, // Logical Minimum (0) 122
* 0x26, 0xff, 0x00, // Logical Maximum (255) 124
* 0x75, 0x08, // Report Size (8) 127
* 0x95, 0x3f, // Report Count (63) 129
* 0xb1, 0x02, // Feature (Data,Var,Abs) 131
* 0x09, 0x48, // Usage (Vendor Usage 0x48) 133
* 0x85, 0xf3, // Report ID (243) 135
* 0x15, 0x00, // Logical Minimum (0) 137
* 0x26, 0xff, 0x00, // Logical Maximum (255) 139
* 0x75, 0x08, // Report Size (8) 142
* 0x95, 0x3f, // Report Count (63) 144
* 0xb1, 0x02, // Feature (Data,Var,Abs) 146
* 0xc0, // End Collection 148
*/
SEC(HID_BPF_RDESC_FIXUP)
int BPF_PROG(hid_fix_rdesc_tca_yoke, struct hid_bpf_ctx *hctx)
{
const int expected_length = 148;
if (hctx->size != expected_length)
return 0;
__u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, HID_MAX_DESCRIPTOR_SIZE /* size */);
if (!data)
return 0; /* EPERM */
/* Safety check, our probe() should take care of this though */
if (data[1] != 0x01 /* Generic Desktop */ || data[3] != 0x04 /* Joystick */)
return 0;
/* The report descriptor sets incorrect Input items in 2 places, resulting in a
* non-existing axis showing up.
* This change sets the correct Input which prevents the axis from showing up in Linux.
*/
if (data[90] == 0x81 && /* Input */
data[103] == 0x81) { /* Input */
data[91] = 0x03; /* Input set to 0x03 Constant, Variable Absolute */
data[104] = 0x03; /* Input set to 0X03 Constant, Variable Absolute */
}
return 0;
}
HID_BPF_OPS(tca_yoke) = {
.hid_rdesc_fixup = (void *)hid_fix_rdesc_tca_yoke,
};
SEC("syscall")
int probe(struct hid_bpf_probe_args *ctx)
{
/* ensure the kernel isn't fixed already */
if (ctx->rdesc[91] != 0x02) /* Input for 0x59 Usage type has changed */
ctx->retval = -EINVAL;
return 0;
}
char _license[] SEC("license") = "GPL";
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: GPL-2.0-only
/* Fix for Trust Philips SPK6327 (145f:024b)
* Modifier keys report as Array (0x00) instead of Variable (0x02)
* causing LCtrl, LAlt, Super etc. to all act as LShift
*/
#include "vmlinux.h"
#include "hid_bpf.h"
#include "hid_bpf_helpers.h"
#include <bpf/bpf_tracing.h>
#define VID_TRUST 0x145F
#define PID_SPK6327 0x024B
HID_BPF_CONFIG(
HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_TRUST, PID_SPK6327)
);
SEC(HID_BPF_RDESC_FIXUP)
int BPF_PROG(hid_fix_rdesc, struct hid_bpf_ctx *hctx)
{
__u8 *data = hid_bpf_get_data(hctx, 0, 4096);
if (!data)
return 0;
/* Fix modifier keys: Input Array (0x00) -> Input Variable (0x02) */
if (data[101] == 0x00)
data[101] = 0x02;
return 0;
}
HID_BPF_OPS(trust_spk6327) = {
.hid_rdesc_fixup = (void *)hid_fix_rdesc,
};
SEC("syscall")
int probe(struct hid_bpf_probe_args *ctx)
{
/* Only apply to interface 1 (169 bytes) not interface 0 (62 bytes) */
if (ctx->rdesc_size == 169)
ctx->retval = 0;
else
ctx->retval = -EINVAL;
return 0;
}
char _license[] SEC("license") = "GPL";
@@ -0,0 +1,321 @@
// SPDX-License-Identifier: GPL-2.0-only
/* Copyright (c) 2025 Red Hat
*/
#include "vmlinux.h"
#include "hid_bpf.h"
#include "hid_bpf_helpers.h"
#include <bpf/bpf_tracing.h>
#define VID_WALTOP 0x172F
#define PID_BATTERYLESS_TABLET 0x0505
HID_BPF_CONFIG(
HID_DEVICE(BUS_USB, HID_GROUP_ANY, VID_WALTOP, PID_BATTERYLESS_TABLET)
);
#define EXPECTED_RDESC_SIZE 335
#define PEN_REPORT_ID 16
#define TIP_SWITCH BIT(0)
#define BARREL_SWITCH BIT(1)
#define SECONDARY_BARREL_SWITCH BIT(5)
static __u8 last_button_state;
static const __u8 fixed_rdesc[] = {
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x02, // Usage (Mouse)
0xa1, 0x01, // Collection (Application)
0x85, 0x01, // Report ID (1)
0x09, 0x01, // Usage (Pointer)
0xa1, 0x00, // Collection (Physical)
0x05, 0x09, // Usage Page (Button)
0x19, 0x01, // Usage Minimum (1)
0x29, 0x05, // Usage Maximum (5)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x05, // Report Count (5)
0x81, 0x02, // Input (Data,Var,Abs)
0x75, 0x03, // Report Size (3)
0x95, 0x01, // Report Count (1)
0x81, 0x03, // Input (Cnst,Var,Abs)
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x30, // Usage (X)
0x09, 0x31, // Usage (Y)
0x09, 0x38, // Usage (Wheel)
0x15, 0x81, // Logical Minimum (-127)
0x25, 0x7f, // Logical Maximum (127)
0x75, 0x08, // Report Size (8)
0x95, 0x03, // Report Count (3)
0x81, 0x06, // Input (Data,Var,Rel)
0x05, 0x0c, // Usage Page (Consumer)
0x15, 0x81, // Logical Minimum (-127)
0x25, 0x7f, // Logical Maximum (127)
0x75, 0x08, // Report Size (8)
0x95, 0x01, // Report Count (1)
0x0a, 0x38, 0x02, // Usage (AC Pan)
0x81, 0x06, // Input (Data,Var,Rel)
0xc0, // End Collection
0xc0, // End Collection
0x05, 0x0d, // Usage Page (Digitizers)
0x09, 0x02, // Usage (Pen)
0xa1, 0x01, // Collection (Application)
0x85, 0x02, // Report ID (2)
0x09, 0x20, // Usage (Stylus)
0xa1, 0x00, // Collection (Physical)
0x09, 0x00, // Usage (0x0000)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xff, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Size (8)
0x95, 0x09, // Report Count (9)
0x81, 0x02, // Input (Data,Var,Abs)
0x09, 0x3f, // Usage (Azimuth)
0x09, 0x40, // Usage (Altitude)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xff, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Size (8)
0x95, 0x02, // Report Count (2)
0xb1, 0x02, // Feature (Data,Var,Abs)
0xc0, // End Collection
0x85, 0x05, // Report ID (5)
0x05, 0x0d, // Usage Page (Digitizers)
0x09, 0x20, // Usage (Stylus)
0xa1, 0x00, // Collection (Physical)
0x09, 0x00, // Usage (0x0000)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xff, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Size (8)
0x95, 0x07, // Report Count (7)
0x81, 0x02, // Input (Data,Var,Abs)
0xc0, // End Collection
0x85, 0x0a, // Report ID (10)
0x05, 0x0d, // Usage Page (Digitizers)
0x09, 0x20, // Usage (Stylus)
0xa1, 0x00, // Collection (Physical)
0x09, 0x00, // Usage (0x0000)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xff, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Size (8)
0x95, 0x07, // Report Count (7)
0x81, 0x02, // Input (Data,Var,Abs)
0xc0, // End Collection
0x85, 0x10, // Report ID (16)
0x09, 0x20, // Usage (Stylus)
0xa1, 0x00, // Collection (Physical)
0x09, 0x42, // Usage (Tip Switch)
0x09, 0x44, // Usage (Barrel Switch)
0x09, 0x3c, // Usage (Invert)
0x09, 0x45, // Usage (Eraser)
0x09, 0x32, // Usage (In Range)
0x09, 0x5a, // Usage (Secondary Barrel Switch) <-- added
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x06, // Report Count (6) <--- changed from 5
0x81, 0x02, // Input (Data,Var,Abs)
0x95, 0x02, // Report Count (2) <--- changed from 3
0x81, 0x03, // Input (Cnst,Var,Abs)
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x30, // Usage (X)
0x75, 0x10, // Report Size (16)
0x95, 0x01, // Report Count (1)
0xa4, // Push
0x55, 0x0d, // Unit Exponent (-3)
0x65, 0x33, // Unit (EnglishLinear: in³)
0x15, 0x00, // Logical Minimum (0)
0x26, 0x00, 0x7d, // Logical Maximum (32000)
0x35, 0x00, // Physical Minimum (0)
0x46, 0x00, 0x7d, // Physical Maximum (32000)
0x81, 0x02, // Input (Data,Var,Abs)
0x09, 0x31, // Usage (Y)
0x15, 0x00, // Logical Minimum (0)
0x26, 0x20, 0x4e, // Logical Maximum (20000)
0x35, 0x00, // Physical Minimum (0)
0x46, 0x20, 0x4e, // Physical Maximum (20000)
0x81, 0x02, // Input (Data,Var,Abs)
0x05, 0x0d, // Usage Page (Digitizers)
0x09, 0x30, // Usage (Tip Pressure)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xff, 0x07, // Logical Maximum (2047)
0x35, 0x00, // Physical Minimum (0)
0x46, 0xff, 0x07, // Physical Maximum (2047)
0x81, 0x02, // Input (Data,Var,Abs)
0x05, 0x0d, // Usage Page (Digitizers)
0x09, 0x3d, // Usage (X Tilt)
0x09, 0x3e, // Usage (Y Tilt)
0x15, 0xc4, // Logical Minimum (-60) <- changed from -127
0x25, 0x3c, // Logical Maximum (60) <- changed from 127
0x75, 0x08, // Report Size (8)
0x95, 0x02, // Report Count (2)
0x81, 0x02, // Input (Data,Var,Abs)
0xc0, // End Collection
0xc0, // End Collection
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x06, // Usage (Keyboard)
0xa1, 0x01, // Collection (Application)
0x85, 0x0d, // Report ID (13)
0x05, 0x07, // Usage Page (Keyboard/Keypad)
0x19, 0xe0, // Usage Minimum (224)
0x29, 0xe7, // Usage Maximum (231)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x08, // Report Count (8)
0x81, 0x02, // Input (Data,Var,Abs)
0x75, 0x08, // Report Size (8)
0x95, 0x01, // Report Count (1)
0x81, 0x01, // Input (Cnst,Arr,Abs)
0x05, 0x07, // Usage Page (Keyboard/Keypad)
0x19, 0x00, // Usage Minimum (0)
0x29, 0x65, // Usage Maximum (101)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x65, // Logical Maximum (101)
0x75, 0x08, // Report Size (8)
0x95, 0x05, // Report Count (5)
0x81, 0x00, // Input (Data,Arr,Abs)
0xc0, // End Collection
0x05, 0x0c, // Usage Page (Consumer)
0x09, 0x01, // Usage (Consumer Control)
0xa1, 0x01, // Collection (Application)
0x85, 0x0c, // Report ID (12)
0x09, 0xe9, // Usage (Volume Increment)
0x09, 0xea, // Usage (Volume Decrement)
0x09, 0xe2, // Usage (Mute)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x03, // Report Count (3)
0x81, 0x06, // Input (Data,Var,Rel)
0x75, 0x05, // Report Size (5)
0x95, 0x01, // Report Count (1)
0x81, 0x07, // Input (Cnst,Var,Rel)
0xc0, // End Collection
};
static inline unsigned int bitwidth32(__u32 x)
{
return 32 - __builtin_clzg(x, 32);
}
static inline unsigned int floor_log2_32(__u32 x)
{
return bitwidth32(x) - 1;
}
/* Maps the interval [0, 2047] to itself using a scaled
* approximation of the function log2(x+1).
*/
static unsigned int scaled_log2(__u16 v)
{
const unsigned int XMAX = 2047;
const unsigned int YMAX = 11; /* log2(2048) = 11 */
unsigned int x = v + 1;
unsigned int n = floor_log2_32(x);
unsigned int b = 1 << n;
/* Fixed-point fraction in [0, 1), linearly
* interpolated using delta-y = 1 and
* delta-x = (2b - b) = b.
*/
unsigned int frac = (x - b) << YMAX;
unsigned int lerp = frac / b;
unsigned int log2 = (n << YMAX) + lerp;
return ((log2 * XMAX) / YMAX) >> YMAX;
}
SEC(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 */
__builtin_memcpy(data, fixed_rdesc, sizeof(fixed_rdesc));
return sizeof(fixed_rdesc);
}
SEC(HID_BPF_DEVICE_EVENT)
int BPF_PROG(waltop_fix_events, struct hid_bpf_ctx *hctx)
{
__u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, 10 /* size */);
if (!data)
return 0; /* EPERM check */
__u8 report_id = data[0];
if (report_id != PEN_REPORT_ID)
return 0;
/* On this tablet if the secondary barrel switch is pressed,
* the tablet sends tip down and barrel down. Change this to
* just secondary barrel down when there is no ambiguity.
*
* It's possible that there is a bug in the firmware and the
* device intends to set invert + eraser instead (i.e. the
* pysical button is an eraser button) but since
* the pressure is always zero, said eraser button
* would be useless anyway.
*
* So let's just change the button to secondary barrel down.
*/
__u8 tip_switch = data[1] & TIP_SWITCH;
__u8 barrel_switch = data[1] & BARREL_SWITCH;
__u8 tip_held = last_button_state & TIP_SWITCH;
__u8 barrel_held = last_button_state & BARREL_SWITCH;
if (tip_switch && barrel_switch && !tip_held && !barrel_held) {
data[1] &= ~(TIP_SWITCH | BARREL_SWITCH); /* release tip and barrel */
data[1] |= SECONDARY_BARREL_SWITCH; /* set secondary barrel switch */
}
last_button_state = data[1];
/* The pressure sensor on this tablet maps around half of the
* logical pressure range into the interval [0-100]. Further
* pressure causes the sensor value to increase exponentially
* up to a maximum value of 2047.
*
* The values 12 and 102 were chosen to have an integer slope
* with smooth transition between the two curves around the
* value 100.
*/
__u16 pressure = (((__u16)data[6]) << 0) | (((__u16)data[7]) << 8);
if (pressure <= 102)
pressure *= 12;
else
pressure = scaled_log2(pressure);
data[6] = pressure >> 0;
data[7] = pressure >> 8;
return 0;
}
HID_BPF_OPS(waltop_batteryless) = {
.hid_device_event = (void *)waltop_fix_events,
.hid_rdesc_fixup = (void *)hid_fix_rdesc,
};
SEC("syscall")
int probe(struct hid_bpf_probe_args *ctx)
{
if (ctx->rdesc_size == EXPECTED_RDESC_SIZE)
ctx->retval = 0;
else
ctx->retval = -EINVAL;
return 0;
}
char _license[] SEC("license") = "GPL";
@@ -0,0 +1,177 @@
// 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(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;
}
HID_BPF_OPS(wacom_artpen) = {
.hid_device_event = (void *)artpen_pressure_interpolate,
};
SEC("syscall")
int probe(struct hid_bpf_probe_args *ctx)
{
struct hid_bpf_ctx *hid_ctx;
__u16 pid;
size_t 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";
@@ -0,0 +1,331 @@
// SPDX-License-Identifier: GPL-2.0-only
/* Copyright (c) 2024 Red Hat, Inc
*/
#include "vmlinux.h"
#include "hid_bpf.h"
#include "hid_bpf_helpers.h"
#include "hid_report_helpers.h"
#include <bpf/bpf_tracing.h>
#define HID_BPF_ASYNC_MAX_CTX 1
#include "hid_bpf_async.h"
#define VID_UGEE 0x28BD
/* same PID whether connected directly or through the provided dongle: */
#define PID_ACK05_REMOTE 0x0202
HID_BPF_CONFIG(
HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_UGEE, PID_ACK05_REMOTE),
);
/*
* By default, the pad reports the buttons through a set of key sequences.
*
* The pad reports a classic keyboard report descriptor:
* # HANVON UGEE Shortcut Remote
* Report descriptor length: 102 bytes
* 0x05, 0x01, // Usage Page (Generic Desktop) 0
* 0x09, 0x02, // Usage (Mouse) 2
* 0xa1, 0x01, // Collection (Application) 4
* 0x85, 0x09, // Report ID (9) 6
* 0x09, 0x01, // Usage (Pointer) 8
* 0xa1, 0x00, // Collection (Physical) 10
* 0x05, 0x09, // Usage Page (Button) 12
* 0x19, 0x01, // UsageMinimum (1) 14
* 0x29, 0x03, // UsageMaximum (3) 16
* 0x15, 0x00, // Logical Minimum (0) 18
* 0x25, 0x01, // Logical Maximum (1) 20
* 0x95, 0x03, // Report Count (3) 22
* 0x75, 0x01, // Report Size (1) 24
* 0x81, 0x02, // Input (Data,Var,Abs) 26
* 0x95, 0x05, // Report Count (5) 28
* 0x81, 0x01, // Input (Cnst,Arr,Abs) 30
* 0x05, 0x01, // Usage Page (Generic Desktop) 32
* 0x09, 0x30, // Usage (X) 34
* 0x09, 0x31, // Usage (Y) 36
* 0x26, 0xff, 0x7f, // Logical Maximum (32767) 38
* 0x95, 0x02, // Report Count (2) 41
* 0x75, 0x10, // Report Size (16) 43
* 0x81, 0x02, // Input (Data,Var,Abs) 45
* 0x05, 0x0d, // Usage Page (Digitizers) 47
* 0x09, 0x30, // Usage (Tip Pressure) 49
* 0x26, 0xff, 0x07, // Logical Maximum (2047) 51
* 0x95, 0x01, // Report Count (1) 54
* 0x75, 0x10, // Report Size (16) 56
* 0x81, 0x02, // Input (Data,Var,Abs) 58
* 0xc0, // End Collection 60
* 0xc0, // End Collection 61
* 0x05, 0x01, // Usage Page (Generic Desktop) 62
* 0x09, 0x06, // Usage (Keyboard) 64
* 0xa1, 0x01, // Collection (Application) 66
* 0x85, 0x06, // Report ID (6) 68
* 0x05, 0x07, // Usage Page (Keyboard/Keypad) 70
* 0x19, 0xe0, // UsageMinimum (224) 72
* 0x29, 0xe7, // UsageMaximum (231) 74
* 0x15, 0x00, // Logical Minimum (0) 76
* 0x25, 0x01, // Logical Maximum (1) 78
* 0x75, 0x01, // Report Size (1) 80
* 0x95, 0x08, // Report Count (8) 82
* 0x81, 0x02, // Input (Data,Var,Abs) 84
* 0x05, 0x07, // Usage Page (Keyboard/Keypad) 86
* 0x19, 0x00, // UsageMinimum (0) 88
* 0x29, 0xff, // UsageMaximum (255) 90
* 0x26, 0xff, 0x00, // Logical Maximum (255) 92
* 0x75, 0x08, // Report Size (8) 95
* 0x95, 0x06, // Report Count (6) 97
* 0x81, 0x00, // Input (Data,Arr,Abs) 99
* 0xc0, // End Collection 101
*
* Each button gets assigned the following events:
*
* Buttons released: 06 00 00 00 00 00 00 00
* Button 1: 06 01 12 00 00 00 00 00 -> LControl + o
* Button 2: 06 01 11 00 00 00 00 00 -> LControl + n
* Button 3: 06 00 3e 00 00 00 00 00 -> F5
* Button 4: 06 02 00 00 00 00 00 00 -> LShift
* Button 5: 06 01 00 00 00 00 00 00 -> LControl
* Button 6: 06 04 00 00 00 00 00 00 -> LAlt
* Button 7: 06 01 16 00 00 00 00 00 -> LControl + s
* Button 8: 06 01 1d 00 00 00 00 00 -> LControl + z
* Button 9: 06 00 2c 00 00 00 00 00 -> Space
* Button 10: 06 03 1d 00 00 00 00 00 -> LControl + LShift + z
* Wheel: 06 01 57 00 00 00 00 00 -> clockwise rotation (LControl + Keypad Plus)
* Wheel: 06 01 56 00 00 00 00 00 -> counter-clockwise rotation
* (LControl + Keypad Minus)
*
* However, multiple buttons can be pressed at the same time, and when this happens,
* each button gets assigned a new slot in the Input (Data,Arr,Abs):
*
* Button 1 + 3: 06 01 12 3e 00 00 00 00 -> LControl + o + F5
*
* When a modifier is pressed (Button 4, 5, or 6), the assigned key is set to 00:
*
* Button 5 + 7: 06 01 00 16 00 00 00 00 -> LControl + s
*
* This is mostly fine, but with Button 8 and Button 10 sharing the same
* key value ("z"), there are cases where we can not know which is which.
*
*/
#define PAD_WIRED_DESCRIPTOR_LENGTH 102
#define PAD_DONGLE_DESCRIPTOR_LENGTH 177
#define STYLUS_DESCRIPTOR_LENGTH 109
#define VENDOR_DESCRIPTOR_LENGTH 36
#define PAD_REPORT_ID 6
#define RAW_PAD_REPORT_ID 0xf0
#define RAW_BATTERY_REPORT_ID 0xf2
#define VENDOR_REPORT_ID 2
#define PAD_REPORT_LENGTH 8
#define VENDOR_REPORT_LENGTH 12
__u16 last_button_state;
static const __u8 disabled_rdesc[] = {
// Make sure we match our original report length
FixedSizeVendorReport(VENDOR_REPORT_LENGTH)
};
static const __u8 fixed_rdesc_vendor[] = {
UsagePage_GenericDesktop
Usage_GD_Keypad
CollectionApplication(
// -- Byte 0 in report
ReportId(RAW_PAD_REPORT_ID)
// Byte 1 in report - same than report ID
ReportCount(1)
ReportSize(8)
Input(Const) // padding (internal report ID)
LogicalMaximum_i8(0)
LogicalMaximum_i8(1)
UsagePage_Digitizers
Usage_Dig_TabletFunctionKeys
CollectionPhysical(
// Byte 2-3 is the button state
UsagePage_Button
UsageMinimum_i8(0x01)
UsageMaximum_i8(0x0a)
LogicalMinimum_i8(0x0)
LogicalMaximum_i8(0x1)
ReportCount(10)
ReportSize(1)
Input(Var|Abs)
Usage_i8(0x31) // will be mapped as BTN_A / BTN_SOUTH
ReportCount(1)
Input(Var|Abs)
ReportCount(5) // padding
Input(Const)
// Byte 4 in report - just exists so we get to be a tablet pad
UsagePage_Digitizers
Usage_Dig_BarrelSwitch // BTN_STYLUS
ReportCount(1)
ReportSize(1)
Input(Var|Abs)
ReportCount(7) // padding
Input(Const)
// Bytes 5/6 in report - just exists so we get to be a tablet pad
UsagePage_GenericDesktop
Usage_GD_X
Usage_GD_Y
ReportCount(2)
ReportSize(8)
Input(Var|Abs)
// Byte 7 in report is the dial
Usage_GD_Wheel
LogicalMinimum_i8(-1)
LogicalMaximum_i8(1)
ReportCount(1)
ReportSize(8)
Input(Var|Rel)
)
// -- Byte 0 in report
ReportId(RAW_BATTERY_REPORT_ID)
// Byte 1 in report - same than report ID
ReportCount(1)
ReportSize(8)
Input(Const) // padding (internal report ID)
// Byte 2 in report - always 0x01
Input(Const) // padding (internal report ID)
UsagePage_Digitizers
/*
* We represent the device as a stylus to force the kernel to not
* directly query its battery state. Instead the kernel will rely
* only on the provided events.
*/
Usage_Dig_Stylus
CollectionPhysical(
// Byte 3 in report - battery value
UsagePage_BatterySystem
Usage_BS_AbsoluteStateOfCharge
LogicalMinimum_i8(0)
LogicalMaximum_i8(100)
ReportCount(1)
ReportSize(8)
Input(Var|Abs)
// Byte 4 in report - charging state
Usage_BS_Charging
LogicalMinimum_i8(0)
LogicalMaximum_i8(1)
ReportCount(1)
ReportSize(8)
Input(Var|Abs)
)
)
};
SEC(HID_BPF_RDESC_FIXUP)
int BPF_PROG(ack05_fix_rdesc, struct hid_bpf_ctx *hctx)
{
__u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, HID_MAX_DESCRIPTOR_SIZE /* size */);
__s32 rdesc_size = hctx->size;
if (!data)
return 0; /* EPERM check */
if (rdesc_size == VENDOR_DESCRIPTOR_LENGTH) {
/*
* The vendor fixed rdesc is appended after the current one,
* to keep the output reports working.
*/
__builtin_memcpy(data + rdesc_size, fixed_rdesc_vendor, sizeof(fixed_rdesc_vendor));
return sizeof(fixed_rdesc_vendor) + rdesc_size;
}
hid_set_name(hctx->hid, "Disabled by HID-BPF Hanvon Ugee Shortcut Remote");
__builtin_memcpy(data, disabled_rdesc, sizeof(disabled_rdesc));
return sizeof(disabled_rdesc);
}
static int HID_BPF_ASYNC_FUN(switch_to_raw_mode)(struct hid_bpf_ctx *hid)
{
static __u8 magic_0[32] = {0x02, 0xb0, 0x04, 0x00, 0x00};
int err;
/*
* The proprietary driver sends the 3 following packets after the
* above one.
* These don't seem to have any effect, so we don't send them to save
* some processing time.
*
* static __u8 magic_1[32] = {0x02, 0xb4, 0x01, 0x00, 0x01};
* static __u8 magic_2[32] = {0x02, 0xb4, 0x01, 0x00, 0xff};
* static __u8 magic_3[32] = {0x02, 0xb8, 0x04, 0x00, 0x00};
*/
err = hid_bpf_hw_output_report(hid, magic_0, sizeof(magic_0));
if (err < 0)
return err;
return 0;
}
SEC(HID_BPF_DEVICE_EVENT)
int BPF_PROG(ack05_fix_events, struct hid_bpf_ctx *hctx)
{
__u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, PAD_REPORT_LENGTH);
int ret = 0;
if (!data)
return 0; /* EPERM check */
if (data[0] != VENDOR_REPORT_ID)
return 0;
/* reconnect event */
if (data[1] == 0xf8 && data[2] == 02 && data[3] == 0x01)
HID_BPF_ASYNC_DELAYED_CALL(switch_to_raw_mode, hctx, 10);
/* button event */
if (data[1] == RAW_PAD_REPORT_ID) {
data[0] = data[1];
if (data[7] == 0x02)
data[7] = 0xff;
ret = 8;
} else if (data[1] == RAW_BATTERY_REPORT_ID) {
data[0] = data[1];
ret = 5;
}
return ret;
}
HID_BPF_OPS(xppen_ack05_remote) = {
.hid_device_event = (void *)ack05_fix_events,
.hid_rdesc_fixup = (void *)ack05_fix_rdesc,
};
SEC("syscall")
int probe(struct hid_bpf_probe_args *ctx)
{
switch (ctx->rdesc_size) {
case PAD_WIRED_DESCRIPTOR_LENGTH:
case PAD_DONGLE_DESCRIPTOR_LENGTH:
case STYLUS_DESCRIPTOR_LENGTH:
case VENDOR_DESCRIPTOR_LENGTH:
ctx->retval = 0;
break;
default:
ctx->retval = -EINVAL;
break;
}
if (ctx->rdesc_size == VENDOR_DESCRIPTOR_LENGTH) {
struct hid_bpf_ctx *hctx = hid_bpf_allocate_context(ctx->hid);
if (!hctx) {
ctx->retval = -EINVAL;
return 0;
}
ctx->retval = HID_BPF_ASYNC_INIT(switch_to_raw_mode) ||
switch_to_raw_mode(hctx);
hid_bpf_release_context(hctx);
}
return 0;
}
char _license[] SEC("license") = "GPL";
@@ -0,0 +1,231 @@
// 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 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(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(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;
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;
}
HID_BPF_OPS(xppen_artist_24) = {
.hid_rdesc_fixup = (void *)hid_fix_rdesc_xppen_artist24,
.hid_device_event = (void *)xppen_24_fix_eraser,
};
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";
@@ -0,0 +1,324 @@
// 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
#define PID_ARTIST_PRO19_GEN2 0x096A
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),
HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_UGEE, PID_ARTIST_PRO19_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 the 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(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;
} else if (hctx->hid->product == PID_ARTIST_PRO19_GEN2) {
/* 19" screen reports 16.101" x 9.057" */
data[63] = 0x3e;
data[62] = 0xe5;
data[73] = 0x23;
data[72] = 0x61;
}
return sizeof(fixed_rdesc);
}
static int 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
};
/* 19" inch screen 16.101" x 9.057" */
static const __u16 angle_offsets_horizontal_19[128] = {
0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 25, 27, 29, 31, 33, 35, 37, 39, 41,
42, 44, 46, 48, 50, 51, 53, 55, 57, 58, 60, 62, 63, 65, 67, 68, 70, 71, 73, 74, 76,
77, 79, 80, 82, 83, 84, 86, 87, 88, 89, 90, 92, 93, 94, 95, 96, 97, 98, 99, 100,
101, 102, 103, 104, 104, 105, 106, 106, 107, 108, 108, 109, 109, 110, 110, 111,
111, 112, 112, 112, 112, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113,
113, 113, 112, 112, 112, 112, 111, 111, 110, 110, 109, 109, 108, 108, 107, 106,
106, 105, 104, 104, 103, 102, 101, 100, 99, 98, 97, 96, 95, 94, 93, 92, 90
};
static const __u16 angle_offsets_vertical_19[128] = {
0, 4, 7, 11, 14, 18, 21, 25, 28, 32, 35, 38, 42, 45, 49, 52, 56, 59, 62, 66, 69, 72,
75, 79, 82, 85, 88, 91, 95, 98, 101, 104, 107, 110, 113, 116, 118, 121, 124, 127,
129, 132, 135, 137, 140, 142, 145, 147, 150, 152, 154, 157, 159, 161, 163, 165, 167,
169, 171, 173, 174, 176, 178, 179, 181, 183, 184, 185, 187, 188, 189, 190, 192, 193,
194, 195, 195, 196, 197, 198, 198, 199, 199, 200, 200, 201, 201, 201, 201, 201, 201,
201, 201, 201, 201, 201, 200, 200, 199, 199, 198, 198, 197, 196, 195, 195, 194, 193,
192, 190, 189, 188, 187, 185, 184, 183, 181, 179, 178, 176, 174, 173, 171, 169, 167,
165, 163, 161
};
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;
}
static int 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];
switch (hctx->hid->product) {
case 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);
break;
case 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);
break;
case PID_ARTIST_PRO19_GEN2:
compensate_coordinates_by_tilt(data, 2, tilt_x, &angle_offsets_horizontal_19);
compensate_coordinates_by_tilt(data, 4, tilt_y, &angle_offsets_vertical_19);
break;
}
return 0;
}
SEC(HID_BPF_DEVICE_EVENT)
int BPF_PROG(xppen_artist_pro_16_device_event, struct hid_bpf_ctx *hctx)
{
int ret = xppen_16_fix_angle_offset(hctx);
if (ret)
return ret;
return xppen_16_fix_eraser(hctx);
}
HID_BPF_OPS(xppen_artist_pro_16) = {
.hid_rdesc_fixup = (void *)hid_fix_rdesc_xppen_artistpro16gen2,
.hid_device_event = (void *)xppen_artist_pro_16_device_event,
};
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";
@@ -0,0 +1,305 @@
// SPDX-License-Identifier: GPL-2.0-only
/* Copyright (c) 2025 Red Hat
*/
#include "vmlinux.h"
#include "hid_bpf.h"
#include "hid_bpf_helpers.h"
#include "hid_report_helpers.h"
#include <bpf/bpf_tracing.h>
#define VID_UGEE 0x28BD /* VID is shared with SinoWealth and Glorious and prob others */
#define PID_DECO_01_V3 0x0947
HID_BPF_CONFIG(
HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_UGEE, PID_DECO_01_V3),
);
/*
* Default report descriptor reports:
* - a report descriptor for the pad buttons, reported as key sequences
* - a report descriptor for the pen
* - a vendor-specific report descriptor
*
* The Pad report descriptor, see
* https://gitlab.freedesktop.org/libevdev/udev-hid-bpf/-/issues/54
*
* # Report descriptor length: 102 bytes
* 0x05, 0x01, // Usage Page (Generic Desktop) 0
* 0x09, 0x02, // Usage (Mouse) 2
* 0xa1, 0x01, // Collection (Application) 4
* 0x85, 0x09, // Report ID (9) 6
* 0x09, 0x01, // Usage (Pointer) 8
* 0xa1, 0x00, // Collection (Physical) 10
* 0x05, 0x09, // Usage Page (Button) 12
* 0x19, 0x01, // UsageMinimum (1) 14
* 0x29, 0x03, // UsageMaximum (3) 16
* 0x15, 0x00, // Logical Minimum (0) 18
* 0x25, 0x01, // Logical Maximum (1) 20
* 0x95, 0x03, // Report Count (3) 22
* 0x75, 0x01, // Report Size (1) 24
* 0x81, 0x02, // Input (Data,Var,Abs) 26
* 0x95, 0x05, // Report Count (5) 28
* 0x81, 0x01, // Input (Cnst,Arr,Abs) 30
* 0x05, 0x01, // Usage Page (Generic Desktop) 32
* 0x09, 0x30, // Usage (X) 34
* 0x09, 0x31, // Usage (Y) 36
* 0x26, 0xff, 0x7f, // Logical Maximum (32767) 38
* 0x95, 0x02, // Report Count (2) 41
* 0x75, 0x10, // Report Size (16) 43
* 0x81, 0x02, // Input (Data,Var,Abs) 45
* 0x05, 0x0d, // Usage Page (Digitizers) 47
* 0x09, 0x30, // Usage (Tip Pressure) 49
* 0x26, 0xff, 0x07, // Logical Maximum (2047) 51
* 0x95, 0x01, // Report Count (1) 54
* 0x75, 0x10, // Report Size (16) 56
* 0x81, 0x02, // Input (Data,Var,Abs) 58
* 0xc0, // End Collection 60
* 0xc0, // End Collection 61
* 0x05, 0x01, // Usage Page (Generic Desktop) 62
* 0x09, 0x06, // Usage (Keyboard) 64
* 0xa1, 0x01, // Collection (Application) 66
* 0x85, 0x06, // Report ID (6) 68
* 0x05, 0x07, // Usage Page (Keyboard/Keypad) 70
* 0x19, 0xe0, // UsageMinimum (224) 72
* 0x29, 0xe7, // UsageMaximum (231) 74
* 0x15, 0x00, // Logical Minimum (0) 76
* 0x25, 0x01, // Logical Maximum (1) 78
* 0x75, 0x01, // Report Size (1) 80
* 0x95, 0x08, // Report Count (8) 82
* 0x81, 0x02, // Input (Data,Var,Abs) 84
* 0x05, 0x07, // Usage Page (Keyboard/Keypad) 86
* 0x19, 0x00, // UsageMinimum (0) 88
* 0x29, 0xff, // UsageMaximum (255) 90
* 0x26, 0xff, 0x00, // Logical Maximum (255) 92
* 0x75, 0x08, // Report Size (8) 95
* 0x95, 0x06, // Report Count (6) 97
* 0x81, 0x00, // Input (Data,Arr,Abs) 99
* 0xc0, // End Collection 101
*
* And key events for buttons top->bottom are:
* Buttons released: 06 00 00 00 00 00 00 00
* Button1: 06 00 05 00 00 00 00 00 -> b
* Button2: 06 00 08 00 00 00 00 00 -> e
* Button3: 06 04 00 00 00 00 00 00 -> LAlt
* Button4: 06 00 2c 00 00 00 00 00 -> Space
* Button5: 06 01 16 00 00 00 00 00 -> LControl + s
* Button6: 06 01 1d 00 00 00 00 00 -> LControl + z
* Button7: 06 01 57 00 00 00 00 00 -> LControl + Keypad Plus
* Button8: 06 01 56 00 00 00 00 00 -> LControl + Keypad Dash
*
* When multiple buttons are pressed at the same time, the values used to
* identify the buttons are identical, but they appear in different bytes of the
* record. For example, when button 2 (0x08) and button 1 (0x05) are pressed,
* this is the report:
*
* Buttons 2 and 1: 06 00 08 05 00 00 00 00 -> e + b
*
* Buttons 1, 2, 4, 5 and 6 can be matched by finding their values in the
* report.
*
* Button 3 is pressed when the 3rd bit is 1. For example, pressing buttons 3
* and 5 generates this report:
*
* Buttons 3 and 5: 06 05 16 00 00 00 00 00 -> LControl + LAlt + s
* -- --
* | |
* | `- Button 5 (0x16)
* `- 0x05 = 0101. Button 3 is pressed
* ^
*
* pad_buttons contains a list of buttons that can be matched in
* HID_BPF_DEVICE_EVENT. Button 3 as it has a dedicated bit.
*
*
* The Pen report descriptor announces a wrong tilt range:
*
* Report descriptor length: 109 bytes
* 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, 0x01, // Collection (Application) 10
* 0x09, 0x42, // Usage (Tip Switch) 12
* 0x09, 0x44, // Usage (Barrel Switch) 14
* 0x09, 0x45, // Usage (Eraser) 16
* 0x09, 0x3c, // Usage (Invert) 18
* 0x15, 0x00, // Logical Minimum (0) 20
* 0x25, 0x01, // Logical Maximum (1) 22
* 0x75, 0x01, // Report Size (1) 24
* 0x95, 0x04, // Report Count (4) 26
* 0x81, 0x02, // Input (Data,Var,Abs) 28
* 0x95, 0x01, // Report Count (1) 30
* 0x81, 0x03, // Input (Cnst,Var,Abs) 32
* 0x09, 0x32, // Usage (In Range) 34
* 0x95, 0x01, // Report Count (1) 36
* 0x81, 0x02, // Input (Data,Var,Abs) 38
* 0x95, 0x02, // Report Count (2) 40
* 0x81, 0x03, // Input (Cnst,Var,Abs) 42
* 0x75, 0x10, // Report Size (16) 44
* 0x95, 0x01, // Report Count (1) 46
* 0x35, 0x00, // Physical Minimum (0) 48
* 0xa4, // Push 50
* 0x05, 0x01, // Usage Page (Generic Desktop) 51
* 0x09, 0x30, // Usage (X) 53
* 0x65, 0x13, // Unit (EnglishLinear: in) 55
* 0x55, 0x0d, // Unit Exponent (-3) 57
* 0x46, 0x10, 0x27, // Physical Maximum (10000) 59
* 0x26, 0xff, 0x7f, // Logical Maximum (32767) 62
* 0x81, 0x02, // Input (Data,Var,Abs) 65
* 0x09, 0x31, // Usage (Y) 67
* 0x46, 0x6a, 0x18, // Physical Maximum (6250) 69
* 0x26, 0xff, 0x7f, // Logical Maximum (32767) 72
* 0x81, 0x02, // Input (Data,Var,Abs) 75
* 0xb4, // Pop 77
* 0x09, 0x30, // Usage (X) 78
* 0x45, 0x00, // Physical Maximum (0) 80
* 0x26, 0xff, 0x3f, // Logical Maximum (16383) 82
* 0x81, 0x42, // Input (Data,Var,Abs,Null) 85
* 0x09, 0x3d, // Usage (Start) 87
* 0x15, 0x81, // Logical Minimum (-127) 89 <- Change from -127 to -60
* 0x25, 0x7f, // Logical Maximum (127) 91 <- Change from 127 to 60
* 0x75, 0x08, // Report Size (8) 93
* 0x95, 0x01, // Report Count (1) 95
* 0x81, 0x02, // Input (Data,Var,Abs) 97
* 0x09, 0x3e, // Usage (Select) 99
* 0x15, 0x81, // Logical Minimum (-127) 101 <- Change from -127 to -60
* 0x25, 0x7f, // Logical Maximum (127) 103 <- Change from 127 to 60
* 0x81, 0x02, // Input (Data,Var,Abs) 105
* 0xc0, // End Collection 107
* 0xc0, // End Collection 108
*/
#define PEN_REPORT_DESCRIPTOR_LENGTH 109
#define PAD_REPORT_DESCRIPTOR_LENGTH 102
#define PAD_REPORT_LENGTH 8
#define PAD_REPORT_ID 6
#define PAD_NUM_BUTTONS 8
static const __u8 fixed_rdesc_pad[] = {
UsagePage_GenericDesktop
Usage_GD_Keypad
CollectionApplication(
// Byte 0 in report is the report ID
ReportId(PAD_REPORT_ID)
ReportCount(1)
ReportSize(8)
UsagePage_Digitizers
Usage_Dig_TabletFunctionKeys
CollectionPhysical(
// Byte 1 is the button state
UsagePage_Button
UsageMinimum_i8(0x01)
UsageMaximum_i8(PAD_NUM_BUTTONS)
LogicalMinimum_i8(0x0)
LogicalMaximum_i8(0x1)
ReportCount(PAD_NUM_BUTTONS)
ReportSize(1)
Input(Var|Abs)
// Byte 2 in report - just exists so we get to be a tablet pad
UsagePage_Digitizers
Usage_Dig_BarrelSwitch // BTN_STYLUS
ReportCount(1)
ReportSize(1)
Input(Var|Abs)
ReportCount(7) // padding
Input(Const)
// Bytes 3/4 in report - just exists so we get to be a tablet pad
UsagePage_GenericDesktop
Usage_GD_X
Usage_GD_Y
ReportCount(2)
ReportSize(8)
Input(Var|Abs)
// Byte 5-7 are padding so we match the original report lengtth
ReportCount(3)
ReportSize(8)
Input(Const)
)
)
};
SEC(HID_BPF_RDESC_FIXUP)
int BPF_PROG(xppen_deco01v3_rdesc_fixup, struct hid_bpf_ctx *hctx)
{
__u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, HID_MAX_DESCRIPTOR_SIZE /* size */);
const __u8 wrong_logical_range[] = {0x15, 0x81, 0x25, 0x7f};
const __u8 correct_logical_range[] = {0x15, 0xc4, 0x25, 0x3c};
if (!data)
return 0; /* EPERM check */
switch (hctx->size) {
case PAD_REPORT_DESCRIPTOR_LENGTH:
__builtin_memcpy(data, fixed_rdesc_pad, sizeof(fixed_rdesc_pad));
return sizeof(fixed_rdesc_pad);
case PEN_REPORT_DESCRIPTOR_LENGTH:
if (__builtin_memcmp(&data[89], wrong_logical_range,
sizeof(wrong_logical_range)) == 0)
__builtin_memcpy(&data[89], correct_logical_range,
sizeof(correct_logical_range));
if (__builtin_memcmp(&data[101], wrong_logical_range,
sizeof(wrong_logical_range)) == 0)
__builtin_memcpy(&data[101], correct_logical_range,
sizeof(correct_logical_range));
break;
}
return 0;
}
SEC(HID_BPF_DEVICE_EVENT)
int BPF_PROG(xppen_deco01v3_device_event, struct hid_bpf_ctx *hctx)
{
static const __u8 pad_buttons[] = { 0x05, 0x08, 0x00, 0x2c, 0x16, 0x1d, 0x57, 0x56 };
__u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, PAD_REPORT_LENGTH /* size */);
if (!data)
return 0; /* EPERM check */
if (data[0] == PAD_REPORT_ID) {
__u8 button_mask = 0;
size_t d, b;
/* data[1] stores the status of BTN_2 in the 3rd bit*/
if (data[1] & BIT(2))
button_mask |= BIT(2);
/* The rest of the descriptor stores the buttons as in pad_buttons */
for (d = 2; d < 8; d++) {
for (b = 0; b < sizeof(pad_buttons); b++) {
if (data[d] != 0 && data[d] == pad_buttons[b])
button_mask |= BIT(b);
}
}
__u8 report[8] = {PAD_REPORT_ID, button_mask, 0x00};
__builtin_memcpy(data, report, sizeof(report));
}
return 0;
}
HID_BPF_OPS(xppen_deco01v3) = {
.hid_rdesc_fixup = (void *)xppen_deco01v3_rdesc_fixup,
.hid_device_event = (void *)xppen_deco01v3_device_event,
};
SEC("syscall")
int probe(struct hid_bpf_probe_args *ctx)
{
switch (ctx->rdesc_size) {
case PAD_REPORT_DESCRIPTOR_LENGTH:
case PEN_REPORT_DESCRIPTOR_LENGTH:
ctx->retval = 0;
break;
default:
ctx->retval = -EINVAL;
}
return 0;
}
char _license[] SEC("license") = "GPL";
@@ -0,0 +1,359 @@
// SPDX-License-Identifier: GPL-2.0-only
#include "vmlinux.h"
#include "hid_bpf.h"
#include "hid_bpf_helpers.h"
#include "hid_report_helpers.h"
#include <bpf/bpf_tracing.h>
#define VID_UGEE 0x28BD
#define PID_DECO_02 0x0803
HID_BPF_CONFIG(
HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_UGEE, PID_DECO_02),
);
/*
* Devices are:
* - Pad input, including pen (This is the only one we are interested in)
* - Pen input as mouse
* - Vendor
*
* Descriptors on main device are:
* - 7: Pen
* - 6: Vendor settings? Unclear
* - 3: Keyboard (This is what we want to modify)
* - 5: Feature report
*
* This creates three event nodes:
* - XP-PEN DECO 02 Stylus
* - XP-PEN DECO 02
* - XP-PEN DECO 02 Keyboard (Again, what we want to modify)
*
* # Report descriptor length: 188 bytes
* # 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, 0x45, // Usage (Eraser) 16
* # 0x09, 0x3c, // Usage (Invert) 18
* # 0x09, 0x32, // Usage (In Range) 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
* # 0x81, 0x02, // Input (Data,Var,Abs) 30
* # 0x95, 0x03, // Report Count (3) 32
* # 0x81, 0x03, // Input (Cnst,Var,Abs) 34
* # 0x05, 0x01, // Usage Page (Generic Desktop) 36
* # 0x09, 0x30, // Usage (X) 38
* # 0x15, 0x00, // Logical Minimum (0) 40
* # 0x26, 0x50, 0x57, // Logical Maximum (22352) 42
* # 0x55, 0x0d, // Unit Exponent (-3) 45
* # 0x65, 0x13, // Unit (EnglishLinear: in) 47
* # 0x35, 0x00, // Physical Minimum (0) 49
* # 0x46, 0x50, 0x57, // Physical Maximum (22352) 51
* # 0x75, 0x10, // Report Size (16) 54
* # 0x95, 0x01, // Report Count (1) 56
* # 0x81, 0x02, // Input (Data,Var,Abs) 58
* # 0x09, 0x31, // Usage (Y) 60
* # 0x15, 0x00, // Logical Minimum (0) 62
* # 0x26, 0x92, 0x36, // Logical Maximum (13970) 64
* # 0x55, 0x0d, // Unit Exponent (-3) 67
* # 0x65, 0x13, // Unit (EnglishLinear: in) 69
* # 0x35, 0x00, // Physical Minimum (0) 71
* # 0x46, 0x92, 0x36, // Physical Maximum (13970) 73
* # 0x75, 0x10, // Report Size (16) 76
* # 0x95, 0x01, // Report Count (1) 78
* # 0x81, 0x02, // Input (Data,Var,Abs) 80
* # 0x05, 0x0d, // Usage Page (Digitizers) 82
* # 0x09, 0x30, // Usage (Tip Pressure) 84
* # 0x15, 0x00, // Logical Minimum (0) 86
* # 0x26, 0xff, 0x1f, // Logical Maximum (8191) 88
* # 0x75, 0x10, // Report Size (16) 91
* # 0x95, 0x01, // Report Count (1) 93
* # 0x81, 0x02, // Input (Data,Var,Abs) 95
* # 0xc0, // End Collection 97
* # 0xc0, // End Collection 98
* # 0x09, 0x0e, // Usage (Device Configuration) 99
* # 0xa1, 0x01, // Collection (Application) 101
* # 0x85, 0x05, // Report ID (5) 103
* # 0x09, 0x23, // Usage (Device Settings) 105
* # 0xa1, 0x02, // Collection (Logical) 107
* # 0x09, 0x52, // Usage (Inputmode) 109
* # 0x09, 0x53, // Usage (Device Index) 111
* # 0x25, 0x0a, // Logical Maximum (10) 113
* # 0x75, 0x08, // Report Size (8) 115
* # 0x95, 0x02, // Report Count (2) 117
* # 0xb1, 0x02, // Feature (Data,Var,Abs) 119
* # 0xc0, // End Collection 121
* # 0xc0, // End Collection 122
* # 0x05, 0x0c, // Usage Page (Consumer Devices) 123
* # 0x09, 0x36, // Usage (Function Buttons) 125
* # 0xa1, 0x00, // Collection (Physical) 127
* # 0x85, 0x06, // Report ID (6) 129
* # 0x05, 0x09, // Usage Page (Button) 131
* # 0x19, 0x01, // Usage Minimum (1) 133
* # 0x29, 0x20, // Usage Maximum (32) 135
* # 0x15, 0x00, // Logical Minimum (0) 137
* # 0x25, 0x01, // Logical Maximum (1) 139
* # 0x95, 0x20, // Report Count (32) 141
* # 0x75, 0x01, // Report Size (1) 143
* # 0x81, 0x02, // Input (Data,Var,Abs) 145
* # 0xc0, // End Collection 147
* # 0x05, 0x01, // Usage Page (Generic Desktop) 148
* # 0x09, 0x06, // Usage (Keyboard) 150
* # 0xa1, 0x01, // Collection (Application) 152
* # 0x85, 0x03, // Report ID (3) 154
* # 0x05, 0x07, // Usage Page (Keyboard) 156
* # 0x19, 0xe0, // Usage Minimum (224) 158
* # 0x29, 0xe7, // Usage Maximum (231) 160
* # 0x15, 0x00, // Logical Minimum (0) 162
* # 0x25, 0x01, // Logical Maximum (1) 164
* # 0x75, 0x01, // Report Size (1) 166
* # 0x95, 0x08, // Report Count (8) 168
* # 0x81, 0x02, // Input (Data,Var,Abs) 170
* # 0x05, 0x07, // Usage Page (Keyboard) 172
* # 0x19, 0x00, // Usage Minimum (0) 174
* # 0x29, 0xff, // Usage Maximum (255) 176
* # 0x26, 0xff, 0x00, // Logical Maximum (255) 178
* # 0x75, 0x08, // Report Size (8) 181
* # 0x95, 0x06, // Report Count (6) 183
* # 0x81, 0x00, // Input (Data,Arr,Abs) 185
* # 0xc0, // End Collection 187
*
* Key events; top to bottom:
* Buttons released: 03 00 00 00 00 00 00 00
* Button1: 03 00 05 00 00 00 00 00 -> 'b and B'
* Button2: 03 00 2c 00 00 00 00 00 -> 'Spacebar'
* Button3: 03 00 08 00 00 00 00 00 -> 'e and E'
* Button4: 03 00 0c 00 00 00 00 00 -> 'i and I'
* Button5: 03 05 1d 00 00 00 00 00 -> LeftControl + LeftAlt + 'z and Z'
* Button6: 03 01 16 00 00 00 00 00 -> LeftControl + 's and S'
*
* Dial Events:
* Clockwise: 03 01 2e 00 00 00 00 00 -> LeftControl + '= and +'
* Anticlockwise: 03 01 2d 00 00 00 00 00 -> LeftControl + '- and (underscore)'
*
* NOTE: Input event descriptions begin at byte 2, and progressively build
* towards byte 7 as each new key is pressed maintaining the press order.
* For example:
* BTN1 followed by BTN2 is 03 00 05 2c 00 00 00 00
* BTN2 followed by BTN1 is 03 00 2c 05 00 00 00 00
*
* Releasing a button causes its byte to be freed, and the next item in the list
* is pushed forwards. Dial events are released immediately after an event is
* registered (i.e. after each "click"), so will continually appear pushed
* backwards in the report.
*
* When a button with a modifier key is pressed, the button identifier stacks in
* an abnormal way, where the highest modifier byte always supersedes others.
* In these cases, the button with the higher modifier is always last.
* For example:
* BTN6 followed by BTN5 is 03 05 1d 16 00 00 00 00
* BTN5 followed by BTN6 is 03 05 1d 16 00 00 00 00
* BTN5 followed by BTN1 is 03 05 05 1d 00 00 00 00
*
* For three button presses in order, demonstrating strictly above rules:
* BTN6, BTN1, BTN5 is 03 05 05 1d 16 00 00 00
* BTN5, BTN1, BTN6 is 03 05 05 1d 16 00 00 00
*
* In short, when BTN5/6 are pressed, the order of operations is lost, as they
* will always float to the end when pressed in combination with others.
*
* Fortunately, all states are recorded in the same way, with no overlaps.
* Byte 1 can be used as a spare for the wheel, since this is for mod keys.
*/
#define RDESC_SIZE_PAD 188
#define REPORT_SIZE_PAD 8
#define REPORT_ID_BUTTONS 3
#define PAD_BUTTON_COUNT 6
#define RDESC_KEYBOARD_OFFSET 148
static const __u8 fixed_rdesc_pad[] = {
/* Copy of pen descriptor to avoid losing functionality */
UsagePage_Digitizers
Usage_Dig_Pen
CollectionApplication(
ReportId(7)
Usage_Dig_Stylus
CollectionPhysical(
Usage_Dig_TipSwitch
Usage_Dig_BarrelSwitch
Usage_Dig_Eraser
Usage_Dig_Invert
Usage_Dig_InRange
LogicalMinimum_i8(0)
LogicalMaximum_i8(1)
ReportSize(1)
ReportCount(5)
Input(Var|Abs)
ReportCount(3)
Input(Const) /* Input (Const, Var, Abs) */
UsagePage_GenericDesktop
Usage_GD_X
LogicalMinimum_i16(0)
LogicalMaximum_i16(22352)
UnitExponent(-3)
Unit(in) /* (EnglishLinear: in) */
PhysicalMinimum_i16(0)
PhysicalMaximum_i16(22352)
ReportSize(16)
ReportCount(1)
Input(Var|Abs)
Usage_GD_Y
LogicalMinimum_i16(0)
LogicalMaximum_i16(13970)
UnitExponent(-3)
Unit(in) /* (EnglishLinear: in) */
PhysicalMinimum_i16(0)
PhysicalMaximum_i16(13970)
ReportSize(16)
ReportCount(1)
Input(Var|Abs)
UsagePage_Digitizers
Usage_Dig_TipPressure
LogicalMinimum_i16(0)
LogicalMaximum_i16(8191)
ReportSize(16)
ReportCount(1)
Input(Var|Abs)
)
)
/* FIXES BEGIN */
UsagePage_GenericDesktop
Usage_GD_Keypad
CollectionApplication(
ReportId(REPORT_ID_BUTTONS) /* Retain original ID on byte 0 */
ReportCount(1)
ReportSize(REPORT_SIZE_PAD)
UsagePage_Digitizers
Usage_Dig_TabletFunctionKeys
CollectionPhysical(
/* Byte 1: Dial state */
UsagePage_GenericDesktop
Usage_GD_Dial
LogicalMinimum_i8(-1)
LogicalMaximum_i8(1)
ReportCount(1)
ReportSize(REPORT_SIZE_PAD)
Input(Var|Rel)
/* Byte 2: Button state */
UsagePage_Button
ReportSize(1)
ReportCount(PAD_BUTTON_COUNT)
UsageMinimum_i8(0x01)
UsageMaximum_i8(PAD_BUTTON_COUNT) /* Number of buttons */
LogicalMinimum_i8(0x0)
LogicalMaximum_i8(0x1)
Input(Var|Abs)
/* Byte 3: Exists to be tablet pad */
UsagePage_Digitizers
Usage_Dig_BarrelSwitch
ReportCount(1)
ReportSize(1)
Input(Var|Abs)
ReportCount(7) /* Padding, to fill full report space */
Input(Const)
/* Byte 4/5: Exists to be a tablet pad */
UsagePage_GenericDesktop
Usage_GD_X
Usage_GD_Y
ReportCount(2)
ReportSize(8)
Input(Var|Abs)
/* Bytes 6/7: Padding, to match original length */
ReportCount(2)
ReportSize(8)
Input(Const)
)
FixedSizeVendorReport(RDESC_SIZE_PAD)
)
};
SEC(HID_BPF_RDESC_FIXUP)
int BPF_PROG(xppen_deco02_rdesc_fixup, struct hid_bpf_ctx *hctx)
{
__u8 *data = hid_bpf_get_data(hctx, 0, HID_MAX_DESCRIPTOR_SIZE);
if (!data)
return 0; /* EPERM Check */
if (hctx->size == RDESC_SIZE_PAD) {
__builtin_memcpy(data, fixed_rdesc_pad, sizeof(fixed_rdesc_pad));
return sizeof(fixed_rdesc_pad);
}
return 0;
}
SEC(HID_BPF_DEVICE_EVENT)
int BPF_PROG(xppen_deco02_device_event, struct hid_bpf_ctx *hctx)
{
__u8 *data = hid_bpf_get_data(hctx, 0, REPORT_SIZE_PAD);
if (!data || data[0] != REPORT_ID_BUTTONS)
return 0; /* EPERM or wrong report */
__u8 dial_code = 0;
__u8 button_mask = 0;
size_t d;
/* Start from 2; 0 is report ID, 1 is modifier keys, replaced by dial */
for (d = 2; d < 8; d++) {
switch (data[d]) {
case 0x2e:
dial_code = 1;
break;
case 0x2d:
dial_code = -1;
break;
/* below are buttons, top to bottom */
case 0x05:
button_mask |= BIT(0);
break;
case 0x2c:
button_mask |= BIT(1);
break;
case 0x08:
button_mask |= BIT(2);
break;
case 0x0c:
button_mask |= BIT(3);
break;
case 0x1d:
button_mask |= BIT(4);
break;
case 0x16:
button_mask |= BIT(05);
break;
default:
break;
}
}
__u8 report[8] = { REPORT_ID_BUTTONS, dial_code, button_mask, 0x00 };
__builtin_memcpy(data, report, sizeof(report));
return 0;
}
HID_BPF_OPS(xppen_deco02) = {
.hid_rdesc_fixup = (void *)xppen_deco02_rdesc_fixup,
.hid_device_event = (void *)xppen_deco02_device_event,
};
SEC("syscall")
int probe(struct hid_bpf_probe_args *ctx)
{
ctx->retval = ctx->rdesc_size != RDESC_SIZE_PAD ? -EINVAL : 0;
return 0;
}
char _license[] SEC("license") = "GPL";
@@ -0,0 +1,231 @@
// SPDX-License-Identifier: GPL-2.0-only
/* Copyright (c) 2024 José Expósito
*/
#include "vmlinux.h"
#include "hid_bpf.h"
#include "hid_bpf_helpers.h"
#include <bpf/bpf_tracing.h>
#define VID_UGEE 0x28BD
#define PID_DECO_MINI_4 0x0929
#define RDESC_SIZE_PAD 177
#define RDESC_SIZE_PEN 109
#define PAD_REPORT_ID 0x06
/*
* XP-Pen devices return a descriptor with the values the driver should use when
* one of its interfaces is queried. For this device the descriptor is:
*
* 0E 03 60 4F 88 3B 06 00 FF 1F D8 13
* ----- ----- ----- -----
* | | | |
* | | | `- Resolution: 5080 (13d8)
* | | `- Maximum pressure: 8191 (1FFF)
* | `- Logical maximum Y: 15240 (3B88)
* `- Logical maximum X: 20320 (4F60)
*
* The physical maximum is calculated as (logical_max * 1000) / resolution.
*/
#define LOGICAL_MAX_X 0x60, 0x4F
#define LOGICAL_MAX_Y 0x88, 0x3B
#define PHYSICAL_MAX_X 0xA0, 0x0F
#define PHYSICAL_MAX_Y 0xB8, 0x0B
#define PRESSURE_MAX 0xFF, 0x1F
HID_BPF_CONFIG(
HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_UGEE, PID_DECO_MINI_4)
);
/*
* The tablet send these values when the pad buttons are pressed individually:
*
* Buttons released: 06 00 00 00 00 00 00 00
* Button 1: 06 00 05 00 00 00 00 00 -> b
* Button 2: 06 00 08 00 00 00 00 00 -> e
* Button 3: 06 04 00 00 00 00 00 00 -> LAlt
* Button 4: 06 00 2c 00 00 00 00 00 -> Space
* Button 5: 06 01 16 00 00 00 00 00 -> LControl + s
* Button 6: 06 01 1d 00 00 00 00 00 -> LControl + z
*
* When multiple buttons are pressed at the same time, the values used to
* identify the buttons are identical, but they appear in different bytes of the
* record. For example, when button 2 (0x08) and button 1 (0x05) are pressed,
* this is the report:
*
* Buttons 2 and 1: 06 00 08 05 00 00 00 00 -> e + b
*
* Buttons 1, 2, 4, 5 and 6 can be matched by finding their values in the
* report.
*
* Button 3 is pressed when the 3rd bit is 1. For example, pressing buttons 3
* and 5 generates this report:
*
* Buttons 3 and 5: 06 05 16 00 00 00 00 00 -> LControl + LAlt + s
* -- --
* | |
* | `- Button 5 (0x16)
* `- 0x05 = 0101. Button 3 is pressed
* ^
*
* pad_buttons contains a list of buttons that can be matched in
* HID_BPF_DEVICE_EVENT. Button 3 as it has a dedicated bit.
*/
static const __u8 pad_buttons[] = { 0x05, 0x08, 0x00, 0x2C, 0x16, 0x1D };
static const __u8 fixed_pad_rdesc[] = {
0x05, 0x01, /* Usage Page (Desktop), */
0x09, 0x07, /* Usage (Keypad), */
0xA1, 0x01, /* Collection (Application), */
0x85, 0x06, /* Report ID (6), */
0x05, 0x0D, /* Usage Page (Digitizer), */
0x09, 0x39, /* Usage (Tablet Function Keys), */
0xA0, /* Collection (Physical), */
0x05, 0x09, /* Usage Page (Button), */
0x75, 0x01, /* Report Size (1), */
0x95, 0x06, /* Report Count (6), */
0x19, 0x01, /* Usage Minimum (01h), */
0x29, 0x06, /* Usage Maximum (06h), */
0x14, /* Logical Minimum (0), */
0x25, 0x01, /* Logical Maximum (1), */
0x81, 0x02, /* Input (Variable), */
0x95, 0x32, /* Report Count (50), */
0x81, 0x01, /* Input (Constant), */
0xC0, /* End Collection, */
0xC0 /* End Collection */
};
static const __u8 fixed_pen_rdesc[] = {
0x05, 0x0d, /* Usage Page (Digitizers), */
0x09, 0x01, /* Usage (Digitizer), */
0xa1, 0x01, /* Collection (Application), */
0x85, 0x07, /* Report ID (7), */
0x09, 0x20, /* Usage (Stylus), */
0xa1, 0x00, /* Collection (Physical), */
0x09, 0x42, /* Usage (Tip Switch), */
0x09, 0x44, /* Usage (Barrel Switch), */
0x09, 0x46, /* Usage (Tablet Pick), */
0x75, 0x01, /* Report Size (1), */
0x95, 0x03, /* Report Count (3), */
0x14, /* Logical Minimum (0), */
0x25, 0x01, /* Logical Maximum (1), */
0x81, 0x02, /* Input (Variable), */
0x95, 0x02, /* Report Count (2), */
0x81, 0x03, /* Input (Constant, Variable), */
0x09, 0x32, /* Usage (In Range), */
0x95, 0x01, /* Report Count (1), */
0x81, 0x02, /* Input (Variable), */
0x95, 0x02, /* Report Count (2), */
0x81, 0x03, /* Input (Constant, Variable), */
0x75, 0x10, /* Report Size (16), */
0x95, 0x01, /* Report Count (1), */
0x35, 0x00, /* Physical Minimum (0), */
0xa4, /* Push, */
0x05, 0x01, /* Usage Page (Desktop), */
0x09, 0x30, /* Usage (X), */
0x65, 0x13, /* Unit (Inch), */
0x55, 0x0d, /* Unit Exponent (-3), */
0x26, LOGICAL_MAX_X, /* Logical Maximum, */
0x46, PHYSICAL_MAX_X, /* Physical Maximum, */
0x81, 0x02, /* Input (Variable), */
0x09, 0x31, /* Usage (Y), */
0x26, LOGICAL_MAX_Y, /* Logical Maximum, */
0x46, PHYSICAL_MAX_Y, /* Physical Maximum, */
0x81, 0x02, /* Input (Variable), */
0xb4, /* Pop, */
0x09, 0x30, /* Usage (Tip Pressure), */
0x45, 0x00, /* Physical Maximum (0), */
0x26, PRESSURE_MAX, /* Logical Maximum, */
0x75, 0x0D, /* Report Size (13), */
0x95, 0x01, /* Report Count (1), */
0x81, 0x02, /* Input (Variable), */
0x75, 0x01, /* Report Size (1), */
0x95, 0x13, /* Report Count (19), */
0x81, 0x01, /* Input (Constant), */
0xc0, /* End Collection, */
0xc0, /* End Collection */
};
static const size_t fixed_pad_rdesc_size = sizeof(fixed_pad_rdesc);
static const size_t fixed_pen_rdesc_size = sizeof(fixed_pen_rdesc);
SEC(HID_BPF_RDESC_FIXUP)
int BPF_PROG(hid_rdesc_fixup_xppen_deco_mini_4, struct hid_bpf_ctx *hctx)
{
__u8 *data = hid_bpf_get_data(hctx, 0, HID_MAX_DESCRIPTOR_SIZE);
if (!data)
return 0; /* EPERM check */
if (hctx->size == RDESC_SIZE_PAD) {
__builtin_memcpy(data, fixed_pad_rdesc, fixed_pad_rdesc_size);
return fixed_pad_rdesc_size;
} else if (hctx->size == RDESC_SIZE_PEN) {
__builtin_memcpy(data, fixed_pen_rdesc, fixed_pen_rdesc_size);
return fixed_pen_rdesc_size;
}
return 0;
}
SEC(HID_BPF_DEVICE_EVENT)
int BPF_PROG(hid_device_event_xppen_deco_mini_4, struct hid_bpf_ctx *hctx)
{
__u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, 8 /* size */);
__u8 button_mask = 0;
size_t d, b;
if (!data)
return 0; /* EPERM check */
if (data[0] != PAD_REPORT_ID)
return 0;
/* data[1] stores the status of BTN_2 in the 3rd bit*/
if (data[1] & BIT(2))
button_mask |= BIT(2);
/* The rest of the descriptor stores the buttons as in pad_buttons */
for (d = 2; d < 8; d++) {
for (b = 0; b < sizeof(pad_buttons); b++) {
if (data[d] != 0 && data[d] == pad_buttons[b])
button_mask |= BIT(b);
}
}
__u8 report[8] = {PAD_REPORT_ID, button_mask, 0x00};
__builtin_memcpy(data, report, sizeof(report));
return 0;
}
HID_BPF_OPS(deco_mini_4) = {
.hid_device_event = (void *)hid_device_event_xppen_deco_mini_4,
.hid_rdesc_fixup = (void *)hid_rdesc_fixup_xppen_deco_mini_4,
};
SEC("syscall")
int probe(struct hid_bpf_probe_args *ctx)
{
/*
* The device has 2 modes: The compatibility mode, enabled by default,
* and the raw mode, that can be activated by sending a buffer of magic
* data to a certain USB endpoint.
*
* Depending on the mode, different interfaces of the device are used:
* - First interface: Pad in compatibility mode
* - Second interface: Pen in compatibility mode
* - Third interface: Only used in raw mode
*
* We'll use the device in compatibility mode.
*/
ctx->retval = ctx->rdesc_size != RDESC_SIZE_PAD &&
ctx->rdesc_size != RDESC_SIZE_PEN;
if (ctx->retval)
ctx->retval = -EINVAL;
return 0;
}
char _license[] SEC("license") = "GPL";
@@ -0,0 +1,21 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/* Copyright (c) 2022 Benjamin Tissoires
*/
#ifndef ____HID_BPF__H
#define ____HID_BPF__H
#define HID_BPF_DEVICE_EVENT "struct_ops/hid_device_event"
#define HID_BPF_RDESC_FIXUP "struct_ops/hid_rdesc_fixup"
#define HID_BPF_OPS(name) SEC(".struct_ops.link") \
struct hid_bpf_ops name
#define hid_set_name(_hdev, _name) __builtin_memcpy(_hdev->name, _name, sizeof(_name))
struct hid_bpf_probe_args {
unsigned int hid;
unsigned int rdesc_size;
unsigned char rdesc[4096];
int retval;
};
#endif /* ____HID_BPF__H */
@@ -0,0 +1,219 @@
/* SPDX-License-Identifier: GPL-2.0-only
* Copyright (c) 2024 Benjamin Tissoires
*/
#ifndef __HID_BPF_ASYNC_H__
#define __HID_BPF_ASYNC_H__
#ifndef HID_BPF_ASYNC_MAX_CTX
#error "HID_BPF_ASYNC_MAX_CTX should be set to the maximum number of concurrent async functions"
#endif /* HID_BPF_ASYNC_MAX_CTX */
#define CLOCK_MONOTONIC 1
typedef int (*hid_bpf_async_callback_t)(void *map, int *key, void *value);
enum hid_bpf_async_state {
HID_BPF_ASYNC_STATE_UNSET = 0,
HID_BPF_ASYNC_STATE_INITIALIZING,
HID_BPF_ASYNC_STATE_INITIALIZED,
HID_BPF_ASYNC_STATE_STARTING,
HID_BPF_ASYNC_STATE_RUNNING,
};
struct hid_bpf_async_map_elem {
struct bpf_spin_lock lock;
enum hid_bpf_async_state state;
struct bpf_timer t;
struct bpf_wq wq;
u32 hid;
};
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, HID_BPF_ASYNC_MAX_CTX);
__type(key, u32);
__type(value, struct hid_bpf_async_map_elem);
} hid_bpf_async_ctx_map SEC(".maps");
/**
* HID_BPF_ASYNC_CB: macro to define an async callback used in a bpf_wq
*
* The caller is responsible for allocating a key in the async map
* with hid_bpf_async_get_ctx().
*/
#define HID_BPF_ASYNC_CB(cb) \
cb(void *map, int *key, void *value); \
static __always_inline int \
____##cb(struct hid_bpf_ctx *ctx); \
typeof(cb(0, 0, 0)) cb(void *map, int *key, void *value) \
{ \
struct hid_bpf_async_map_elem *e; \
struct hid_bpf_ctx *ctx; \
\
e = (struct hid_bpf_async_map_elem *)value; \
ctx = hid_bpf_allocate_context(e->hid); \
if (!ctx) \
return 0; /* EPERM check */ \
\
e->state = HID_BPF_ASYNC_STATE_RUNNING; \
\
____##cb(ctx); \
\
e->state = HID_BPF_ASYNC_STATE_INITIALIZED; \
hid_bpf_release_context(ctx); \
return 0; \
} \
static __always_inline int \
____##cb
/**
* ASYNC: macro to automatically handle async callbacks contexts
*
* Needs to be used in conjunction with HID_BPF_ASYNC_INIT and HID_BPF_ASYNC_DELAYED_CALL
*/
#define HID_BPF_ASYNC_FUN(fun) \
fun(struct hid_bpf_ctx *ctx); \
int ____key__##fun; \
static int ____async_init_##fun(void) \
{ \
____key__##fun = hid_bpf_async_get_ctx(); \
if (____key__##fun < 0) \
return ____key__##fun; \
return 0; \
} \
static int HID_BPF_ASYNC_CB(____##fun##_cb)(struct hid_bpf_ctx *hctx) \
{ \
return fun(hctx); \
} \
typeof(fun(0)) fun
#define HID_BPF_ASYNC_INIT(fun) ____async_init_##fun()
#define HID_BPF_ASYNC_DELAYED_CALL(fun, ctx, delay) \
hid_bpf_async_delayed_call(ctx, delay, ____key__##fun, ____##fun##_cb)
/*
* internal cb for starting the delayed work callback in a workqueue.
*/
static int __start_wq_timer_cb(void *map, int *key, void *value)
{
struct hid_bpf_async_map_elem *e = (struct hid_bpf_async_map_elem *)value;
bpf_wq_start(&e->wq, 0);
return 0;
}
static int hid_bpf_async_find_empty_key(void)
{
int i;
bpf_for(i, 0, HID_BPF_ASYNC_MAX_CTX) {
struct hid_bpf_async_map_elem *elem;
int key = i;
elem = bpf_map_lookup_elem(&hid_bpf_async_ctx_map, &key);
if (!elem)
return -ENOMEM; /* should never happen */
{
guard(bpf_spin)(&elem->lock);
if (elem->state == HID_BPF_ASYNC_STATE_UNSET) {
elem->state = HID_BPF_ASYNC_STATE_INITIALIZING;
return i;
}
}
}
return -EINVAL;
}
static int hid_bpf_async_get_ctx(void)
{
int key = hid_bpf_async_find_empty_key();
struct hid_bpf_async_map_elem *elem;
int err;
if (key < 0)
return key;
elem = bpf_map_lookup_elem(&hid_bpf_async_ctx_map, &key);
if (!elem)
return -EINVAL;
err = bpf_timer_init(&elem->t, &hid_bpf_async_ctx_map, CLOCK_MONOTONIC);
if (err)
return err;
err = bpf_timer_set_callback(&elem->t, __start_wq_timer_cb);
if (err)
return err;
err = bpf_wq_init(&elem->wq, &hid_bpf_async_ctx_map, 0);
if (err)
return err;
elem->state = HID_BPF_ASYNC_STATE_INITIALIZED;
return key;
}
static inline u64 ms_to_ns(u64 milliseconds)
{
return (u64)milliseconds * 1000UL * 1000UL;
}
static int hid_bpf_async_delayed_call(struct hid_bpf_ctx *hctx, u64 milliseconds, int key,
hid_bpf_async_callback_t wq_cb)
{
struct hid_bpf_async_map_elem *elem;
int err;
elem = bpf_map_lookup_elem(&hid_bpf_async_ctx_map, &key);
if (!elem)
return -EINVAL;
{
guard(bpf_spin)(&elem->lock);
/* The wq must be:
* - HID_BPF_ASYNC_STATE_INITIALIZED -> it's been initialized and ready to be called
* - HID_BPF_ASYNC_STATE_RUNNING -> possible re-entry from the wq itself
*/
if (elem->state != HID_BPF_ASYNC_STATE_INITIALIZED &&
elem->state != HID_BPF_ASYNC_STATE_RUNNING)
return -EINVAL;
elem->state = HID_BPF_ASYNC_STATE_STARTING;
}
elem->hid = hctx->hid->id;
err = bpf_wq_set_callback(&elem->wq, wq_cb, 0);
if (err)
return err;
if (milliseconds) {
/* needed for every call because a cancel might unset this */
err = bpf_timer_set_callback(&elem->t, __start_wq_timer_cb);
if (err)
return err;
err = bpf_timer_start(&elem->t, ms_to_ns(milliseconds), 0);
if (err)
return err;
return 0;
}
return bpf_wq_start(&elem->wq, 0);
}
static inline int hid_bpf_async_call(struct hid_bpf_ctx *ctx, int key,
hid_bpf_async_callback_t wq_cb)
{
return hid_bpf_async_delayed_call(ctx, 0, key, wq_cb);
}
#endif /* __HID_BPF_ASYNC_H__ */
@@ -0,0 +1,507 @@
/* 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 <bpf/bpf_endian.h>
#include <linux/errno.h>
#include "hid_report_descriptor_helpers.h"
/* Compiler attributes */
#ifndef __packed
#define __packed __attribute__((packed))
#endif
#ifndef __maybe_unused
#define __maybe_unused __attribute__((__unused__))
#endif
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;
extern int hid_bpf_hw_output_report(struct hid_bpf_ctx *ctx,
__u8 *buf, size_t buf__sz) __weak __ksym;
extern int hid_bpf_input_report(struct hid_bpf_ctx *ctx,
enum hid_report_type type,
__u8 *data,
size_t buf__sz) __weak __ksym;
extern int hid_bpf_try_input_report(struct hid_bpf_ctx *ctx,
enum hid_report_type type,
__u8 *data,
size_t buf__sz) __weak __ksym;
/* bpf_wq implementation */
extern int bpf_wq_init(struct bpf_wq *wq, void *p__map, unsigned int flags) __weak __ksym;
extern int bpf_wq_start(struct bpf_wq *wq, unsigned int flags) __weak __ksym;
extern int bpf_wq_set_callback(struct bpf_wq *wq,
int (*callback_fn)(void *, int *, void *),
unsigned int flags) __weak __ksym;
#define HID_MAX_DESCRIPTOR_SIZE 4096
#define HID_IGNORE_EVENT -1
/**
* Use: _cleanup_(somefunction) struct foo *bar;
*/
#define _cleanup_(_x) __attribute__((cleanup(_x)))
/**
* Use: _release_(foo) *bar;
*
* This requires foo_releasep() to be present, use DEFINE_RELEASE_CLEANUP_FUNC.
*/
#define _release_(_type) struct _type __attribute__((cleanup(_type##_releasep)))
/**
* Define a cleanup function for the struct type foo with a matching
* foo_release(). Use:
* DEFINE_RELEASE_CLEANUP_FUNC(foo)
* _unref_(foo) struct foo *bar;
*/
#define DEFINE_RELEASE_CLEANUP_FUNC(_type) \
static inline void _type##_releasep(struct _type **_p) { \
if (*_p) \
_type##_release(*_p); \
} \
struct __useless_struct_to_allow_trailing_semicolon__
/* for being able to have a cleanup function */
#define hid_bpf_ctx_release hid_bpf_release_context
DEFINE_RELEASE_CLEANUP_FUNC(hid_bpf_ctx);
/*
* Kernel-style guard macros adapted for BPF
* Based on include/linux/cleanup.h from the Linux kernel
*
* These provide automatic lock/unlock using __attribute__((cleanup))
* similar to how _release_() works for contexts.
*/
/**
* DEFINE_GUARD(name, type, lock, unlock):
* Define a guard for automatic lock/unlock using the same pattern as _release_()
* @name: identifier for the guard (e.g., bpf_spin)
* @type: lock variable type (e.g., struct bpf_spin_lock)
* @lock: lock function name (e.g., bpf_spin_lock)
* @unlock: unlock function name (e.g., bpf_spin_unlock)
*
* guard(name):
* Declare and lock in one statement - lock held until end of scope
*
* Example:
* DEFINE_GUARD(bpf_spin, struct bpf_spin_lock, bpf_spin_lock, bpf_spin_unlock)
*
* void foo(struct bpf_spin_lock *lock) {
* guard(bpf_spin)(lock);
* // lock held until end of scope
* }
*/
/* Guard helper struct - stores lock pointer for cleanup */
#define DEFINE_GUARD(_name, _type, _lock, _unlock) \
struct _name##_guard { \
_type *lock; \
}; \
static inline void _name##_guard_cleanup(struct _name##_guard *g) { \
if (g && g->lock) \
_unlock(g->lock); \
} \
static inline struct _name##_guard _name##_guard_init(_type *l) { \
if (l) \
_lock(l); \
return (struct _name##_guard){.lock = l}; \
} \
struct __useless_struct_to_allow_trailing_semicolon__
#define guard(_name) \
struct _name##_guard COMBINE(guard, __LINE__) __attribute__((cleanup(_name##_guard_cleanup))) = \
_name##_guard_init
/* Define BPF spinlock guard */
DEFINE_GUARD(bpf_spin, struct bpf_spin_lock, bpf_spin_lock, bpf_spin_unlock);
/* 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 BIT(n) (1UL << (n))
#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")
/* Equivalency macros for bpf_htons and friends which are
* Big Endian only - HID needs little endian so these are the
* corresponding macros for that. See bpf/bpf_endian.h
*/
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
# define __hid_bpf_le16_to_cpu(x) (x)
# define __hid_bpf_le32_to_cpu(x) (x)
# define __hid_bpf_le64_to_cpu(x) (x)
# define __hid_bpf_cpu_to_le16(x) (x)
# define __hid_bpf_cpu_to_le32(x) (x)
# define __hid_bpf_cpu_to_le64(x) (x)
# define __hid_bpf_constant_le16_to_cpu(x) (x)
# define __hid_bpf_constant_le32_to_cpu(x) (x)
# define __hid_bpf_constant_le64_to_cpu(x) (x)
# define __hid_bpf_constant_cpu_to_le16(x) (x)
# define __hid_bpf_constant_cpu_to_le32(x) (x)
# define __hid_bpf_constant_cpu_to_le64(x) (x)
#elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
# define __hid_bpf_le16_to_cpu(x) __builtin_bswap16(x)
# define __hid_bpf_le32_to_cpu(x) __builtin_bswap32(x)
# define __hid_bpf_le64_to_cpu(x) __builtin_bswap64(x)
# define __hid_bpf_cpu_to_le16(x) __builtin_bswap16(x)
# define __hid_bpf_cpu_to_le32(x) __builtin_bswap32(x)
# define __hid_bpf_cpu_to_le64(x) __builtin_bswap64(x)
# define __hid_bpf_constant_le16_to_cpu(x) __bpf_swab16(x)
# define __hid_bpf_constant_le32_to_cpu(x) __bpf_swab32(x)
# define __hid_bpf_constant_le64_to_cpu(x) __bpf_swab64(x)
# define __hid_bpf_constant_cpu_to_le16(x) __bpf_swab16(x)
# define __hid_bpf_constant_cpu_to_le32(x) __bpf_swab32(x)
# define __hid_bpf_constant_cpu_to_le64(x) __bpf_swab64(x)
#else
# error "Invalid __BYTE_ORDER__"
#endif
#define hid_bpf_le16_to_cpu(x) \
(__builtin_constant_p(x) ? \
__hid_bpf_constant_le16_to_cpu(x) : __hid_bpf_le16_to_cpu(x))
#define hid_bpf_le32_to_cpu(x) \
(__builtin_constant_p(x) ? \
__hid_bpf_constant_le32_to_cpu(x) : __hid_bpf_le32_to_cpu(x))
#define hid_bpf_le64_to_cpu(x) \
(__builtin_constant_p(x) ? \
__hid_bpf_constant_le64_to_cpu(x) : __hid_bpf_le64_to_cpu(x))
#define hid_bpf_cpu_to_le16(x) \
(__builtin_constant_p(x) ? \
__hid_bpf_constant_cpu_to_le16(x) : __hid_bpf_cpu_to_le16(x))
#define hid_bpf_cpu_to_le32(x) \
(__builtin_constant_p(x) ? \
__hid_bpf_constant_cpu_to_le32(x) : __hid_bpf_cpu_to_le32(x))
#define hid_bpf_cpu_to_le64(x) \
(__builtin_constant_p(x) ? \
__hid_bpf_constant_cpu_to_le64(x) : __hid_bpf_cpu_to_le64(x))
#define hid_bpf_be16_to_cpu(x) bpf_ntohs(x)
#define hid_bpf_be32_to_cpu(x) bpf_ntohl(x)
#define hid_bpf_be64_to_cpu(x) bpf_be64_to_cpu(x)
#define hid_bpf_cpu_to_be16(x) bpf_htons(x)
#define hid_bpf_cpu_to_be32(x) bpf_htonl(x)
#define hid_bpf_cpu_to_be64(x) bpf_cpu_to_be64(x)
/*
* The following macros are helpers for exporting udev properties:
*
* EXPORT_UDEV_PROP(name, len) generates:
* - a map with a single element UDEV_PROP_##name, of size len
* - a const global declaration of that len: SIZEOF_##name
*
* udev_prop_ptr(name) retrieves the data pointer behind the map.
*
* UDEV_PROP_SPRINTF(name, fmt, ...) writes data into the udev property.
*
* Can be used as such:
* EXPORT_UDEV_PROP(HID_FOO, 32);
*
* SEC("syscall")
* int probe(struct hid_bpf_probe_args *ctx)
* {
* const char *foo = "foo";
* UDEV_PROP_SPRINTF(HID_FOO, "%s", foo);
*
* return 0;
* }
*/
#define EXPORT_UDEV_PROP(name, len) \
const __u32 SIZEOF_##name = len; \
struct COMBINE(udev_prop, __LINE__) { \
__uint(type, BPF_MAP_TYPE_ARRAY); \
__uint(max_entries, 1); \
__type(key, __u32); \
__type(value, __u8[len]); \
} UDEV_PROP_##name SEC(".maps");
#define udev_prop_ptr(name) \
bpf_map_lookup_elem(&UDEV_PROP_##name, &(__u32){0})
#define UDEV_PROP_SPRINTF(name, fmt, ...) \
BPF_SNPRINTF(udev_prop_ptr(name), SIZEOF_##name, fmt, ##__VA_ARGS__)
static inline __maybe_unused __u16 field_start_byte(struct hid_rdesc_field *field)
{
return field->bits_start / 8;
}
static inline __maybe_unused __u16 field_end_byte(struct hid_rdesc_field *field)
{
if (!field->bits_end)
return 0;
return (__u16)(field->bits_end - 1) / 8;
}
static __maybe_unused __u32 extract_bits(__u8 *buffer, const size_t size, struct hid_rdesc_field *field)
{
__s32 nbits = field->bits_end - field->bits_start;
__u32 start = field_start_byte(field);
__u32 end = field_end_byte(field);
__u8 base_shift = field->bits_start % 8;
if (nbits <= 0 || nbits > 32 || start >= size || end >= size)
return 0;
/* Fast path for byte-aligned standard-sized reads */
if (base_shift == 0) {
/* 8-bit aligned read */
if (nbits == 8 && start < size)
return buffer[start];
/* 16-bit aligned read - use separate variables for verifier */
if (nbits == 16) {
__u32 off0 = start;
__u32 off1 = start + 1;
if (off0 < size && off1 < size) {
return buffer[off0] |
((__u32)buffer[off1] << 8);
}
}
/* 32-bit aligned read - use separate variables for verifier */
if (nbits == 32) {
__u32 off0 = start;
__u32 off1 = start + 1;
__u32 off2 = start + 2;
__u32 off3 = start + 3;
if (off0 < size && off1 < size &&
off2 < size && off3 < size) {
return buffer[off0] |
((__u32)buffer[off1] << 8) |
((__u32)buffer[off2] << 16) |
((__u32)buffer[off3] << 24);
}
}
}
/* General case: bit manipulation for unaligned or non-standard sizes */
int mask = 0xffffffff >> (32 - nbits);
__u64 value = 0;
__u32 i;
bpf_for (i, start, end + 1) {
value |= (__u64)buffer[i] << ((i - start) * 8);
}
return (value >> base_shift) & mask;
}
#define EXTRACT_BITS(buffer, field) extract_bits(buffer, sizeof(buffer), field)
/* Base macro for iterating over HID arrays with bounds checking.
* Follows the bpf_for pattern from libbpf.
*/
#define __hid_bpf_for_each_array(array, num_elements, max_elements, var) \
for ( \
/* initialize and define destructor */ \
struct bpf_iter_num ___it __attribute__((aligned(8), \
cleanup(bpf_iter_num_destroy))), \
/* ___p pointer is necessary to call bpf_iter_num_new() *once* */ \
*___p __attribute__((unused)) = ( \
/* always initialize iterator; if bounds fail, iterate 0 times */ \
bpf_iter_num_new(&___it, 0, \
(num_elements) > (max_elements) ? \
0 : (num_elements)), \
/* workaround for Clang bug */ \
(void)bpf_iter_num_destroy, (void *)0); \
({ \
/* iteration step */ \
int *___t = bpf_iter_num_next(&___it); \
int ___i; \
/* termination and bounds check, assign var */ \
(___t && (___i = *___t, ___i >= 0 && ___i < (num_elements)) && \
((num_elements) <= (max_elements)) && \
(var = &(array)[___i], 1)); \
}); \
)
/* Iterate over input reports in a descriptor */
#define hid_bpf_for_each_input_report(descriptor, report_var) \
__hid_bpf_for_each_array((descriptor)->input_reports, \
(descriptor)->num_input_reports, \
HID_MAX_REPORTS, report_var)
/* Iterate over feature reports in a descriptor */
#define hid_bpf_for_each_feature_report(descriptor, report_var) \
__hid_bpf_for_each_array((descriptor)->feature_reports, \
(descriptor)->num_feature_reports, \
HID_MAX_REPORTS, report_var)
/* Iterate over output reports in a descriptor */
#define hid_bpf_for_each_output_report(descriptor, report_var) \
__hid_bpf_for_each_array((descriptor)->output_reports, \
(descriptor)->num_output_reports, \
HID_MAX_REPORTS, report_var)
/* Iterate over fields in a report */
#define hid_bpf_for_each_field(report, field_var) \
__hid_bpf_for_each_array((report)->fields, (report)->num_fields, \
HID_MAX_FIELDS, field_var)
/* Iterate over collections in a field */
#define hid_bpf_for_each_collection(field, collection_var) \
__hid_bpf_for_each_array((field)->collections, (field)->num_collections, \
HID_MAX_COLLECTIONS, collection_var)
#endif /* __HID_BPF_HELPERS_H */
@@ -0,0 +1,80 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/* Copyright (c) 2022 Benjamin Tissoires
*/
#ifndef __HID_REPORT_DESCRIPTOR_HELPERS_H
#define __HID_REPORT_DESCRIPTOR_HELPERS_H
#include "vmlinux.h"
/* Compiler attributes */
#ifndef __packed
#define __packed __attribute__((packed))
#endif
#ifndef __maybe_unused
#define __maybe_unused __attribute__((__unused__))
#endif
/* Report Descriptor Structures */
#define HID_MAX_COLLECTIONS 32
#define HID_MAX_FIELDS 64
#define HID_MAX_REPORTS 16
enum hid_rdesc_field_type {
HID_FIELD_VARIABLE = 0,
HID_FIELD_ARRAY = 1,
HID_FIELD_CONSTANT = 2,
};
struct hid_rdesc_collection {
__u16 usage_page;
__u16 usage_id;
__u8 collection_type;
} __packed;
struct hid_rdesc_field {
__u8 field_type; /* enum hid_rdesc_field_type */
__u8 num_collections;
__u16 bits_start;
__u16 bits_end;
__u16 usage_page;
union {
__u16 usage_id; /* For Variable fields */
struct __packed { /* For Array fields */
__u16 usage_minimum;
__u16 usage_maximum;
};
};
__s32 logical_minimum;
__s32 logical_maximum;
struct {
__u8 is_relative:1; /* Data is relative to previous value */
__u8 wraps:1; /* Value wraps around (e.g., rotary encoder) */
__u8 is_nonlinear:1; /* Non-linear relationship between logical/physical */
__u8 has_no_preferred_state:1; /* No rest position (e.g., free-floating joystick) */
__u8 has_null_state:1; /* Can report null/no-data values */
__u8 is_volatile:1; /* Volatile (Output/Feature) - NOT POPULATED, always 0 */
__u8 is_buffered_bytes:1; /* Fixed-size byte stream vs bitfield */
__u8 reserved:1; /* Reserved for future use */
} flags;
struct hid_rdesc_collection collections[HID_MAX_COLLECTIONS];
} __packed;
struct hid_rdesc_report {
__u8 report_id; /* 0 means no report ID */
__u16 size_in_bits;
__u8 num_fields;
struct hid_rdesc_field fields[HID_MAX_FIELDS];
} __packed;
struct hid_rdesc_descriptor {
__u8 num_input_reports;
__u8 num_output_reports;
__u8 num_feature_reports;
struct hid_rdesc_report input_reports[HID_MAX_REPORTS];
struct hid_rdesc_report output_reports[HID_MAX_REPORTS];
struct hid_rdesc_report feature_reports[HID_MAX_REPORTS];
} __packed;
#endif /* __HID_REPORT_DESCRIPTOR_HELPERS_H */
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff