HID: bpf: Add support for the Waltop Batteryless Tablet

Several bugs as outlined in udev-hid-bpf#66 and udev-hid-bpf!198:
- pressure curve is far from linear
- tilt range is ±60, not ±127
- pressing the second button sets both tip down and
  barrel switch

Fix the second button by adding a Secondary Barrel Switch in the
existing padding and check for the tip down/barrel switch down combo.
When  both values become true at the same time, set the Secondary Barrel
Switch instead.

Implement a custom pressure curve that maps the hardware range 0-102
linearly to the logical range 0-1224, and maps the hardware range
103-2047 logarithmically to the logical range 1232-2047. This mapping
isn’t perfect, but it’s way more natural than the stock configuration.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
Signed-off-by: Jan Felix Langenbach <JanFelix.Langenbach@protonmail.com>
Link: https://gitlab.freedesktop.org/libevdev/udev-hid-bpf/-/merge_requests/200
Signed-off-by: Benjamin Tissoires <bentiss@kernel.org>
Signed-off-by: Jiri Kosina <jkosina@suse.com>
This commit is contained in:
Benjamin Tissoires 2025-11-18 18:16:26 +01:00 committed by Jiri Kosina
parent 029dff1c31
commit 040adbe801
1 changed files with 321 additions and 0 deletions

View File

@ -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";