1// SPDX-License-Identifier: GPL-2.0-only
2/* Copyright (c) 2023 Benjamin Tissoires
3 */
4
5#include "vmlinux.h"
6#include "hid_bpf.h"
7#include "hid_bpf_helpers.h"
8#include <bpf/bpf_tracing.h>
9
10#define VID_UGEE 0x28BD /* VID is shared with SinoWealth and Glorious and prob others */
11#define PID_ARTIST_PRO14_GEN2 0x095A
12#define PID_ARTIST_PRO16_GEN2 0x095B
13
14HID_BPF_CONFIG(
15	HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_UGEE, PID_ARTIST_PRO14_GEN2),
16	HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_UGEE, PID_ARTIST_PRO16_GEN2)
17);
18
19/*
20 * We need to amend the report descriptor for the following:
21 * - the device reports Eraser instead of using Secondary Barrel Switch
22 * - when the eraser button is pressed and the stylus is touching the tablet,
23 *   the device sends Tip Switch instead of sending Eraser
24 *
25 * This descriptor uses physical dimensions of the 16" device.
26 */
27static const __u8 fixed_rdesc[] = {
28	0x05, 0x0d,                    // Usage Page (Digitizers)             0
29	0x09, 0x02,                    // Usage (Pen)                         2
30	0xa1, 0x01,                    // Collection (Application)            4
31	0x85, 0x07,                    //  Report ID (7)                      6
32	0x09, 0x20,                    //  Usage (Stylus)                     8
33	0xa1, 0x00,                    //  Collection (Physical)              10
34	0x09, 0x42,                    //   Usage (Tip Switch)                12
35	0x09, 0x44,                    //   Usage (Barrel Switch)             14
36	0x09, 0x5a,                    //   Usage (Secondary Barrel Switch)   16  /* changed from 0x45 (Eraser) to 0x5a (Secondary Barrel Switch) */
37	0x09, 0x3c,                    //   Usage (Invert)                    18
38	0x09, 0x45,                    //   Usage (Eraser)                    16  /* created over a padding bit at offset 29-33 */
39	0x15, 0x00,                    //   Logical Minimum (0)               20
40	0x25, 0x01,                    //   Logical Maximum (1)               22
41	0x75, 0x01,                    //   Report Size (1)                   24
42	0x95, 0x05,                    //   Report Count (5)                  26  /* changed from 4 to 5 */
43	0x81, 0x02,                    //   Input (Data,Var,Abs)              28
44	0x09, 0x32,                    //   Usage (In Range)                  34
45	0x15, 0x00,                    //   Logical Minimum (0)               36
46	0x25, 0x01,                    //   Logical Maximum (1)               38
47	0x95, 0x01,                    //   Report Count (1)                  40
48	0x81, 0x02,                    //   Input (Data,Var,Abs)              42
49	0x95, 0x02,                    //   Report Count (2)                  44
50	0x81, 0x03,                    //   Input (Cnst,Var,Abs)              46
51	0x75, 0x10,                    //   Report Size (16)                  48
52	0x95, 0x01,                    //   Report Count (1)                  50
53	0x35, 0x00,                    //   Physical Minimum (0)              52
54	0xa4,                          //   Push                              54
55	0x05, 0x01,                    //   Usage Page (Generic Desktop)      55
56	0x09, 0x30,                    //   Usage (X)                         57
57	0x65, 0x13,                    //   Unit (EnglishLinear: in)          59
58	0x55, 0x0d,                    //   Unit Exponent (-3)                61
59	0x46, 0xff, 0x34,              //   Physical Maximum (13567)          63
60	0x26, 0xff, 0x7f,              //   Logical Maximum (32767)           66
61	0x81, 0x02,                    //   Input (Data,Var,Abs)              69
62	0x09, 0x31,                    //   Usage (Y)                         71
63	0x46, 0x20, 0x21,              //   Physical Maximum (8480)           73
64	0x26, 0xff, 0x7f,              //   Logical Maximum (32767)           76
65	0x81, 0x02,                    //   Input (Data,Var,Abs)              79
66	0xb4,                          //   Pop                               81
67	0x09, 0x30,                    //   Usage (Tip Pressure)              82
68	0x45, 0x00,                    //   Physical Maximum (0)              84
69	0x26, 0xff, 0x3f,              //   Logical Maximum (16383)           86
70	0x81, 0x42,                    //   Input (Data,Var,Abs,Null)         89
71	0x09, 0x3d,                    //   Usage (X Tilt)                    91
72	0x15, 0x81,                    //   Logical Minimum (-127)            93
73	0x25, 0x7f,                    //   Logical Maximum (127)             95
74	0x75, 0x08,                    //   Report Size (8)                   97
75	0x95, 0x01,                    //   Report Count (1)                  99
76	0x81, 0x02,                    //   Input (Data,Var,Abs)              101
77	0x09, 0x3e,                    //   Usage (Y Tilt)                    103
78	0x15, 0x81,                    //   Logical Minimum (-127)            105
79	0x25, 0x7f,                    //   Logical Maximum (127)             107
80	0x81, 0x02,                    //   Input (Data,Var,Abs)              109
81	0xc0,                          //  End Collection                     111
82	0xc0,                          // End Collection                      112
83};
84
85SEC("fmod_ret/hid_bpf_rdesc_fixup")
86int BPF_PROG(hid_fix_rdesc_xppen_artistpro16gen2, struct hid_bpf_ctx *hctx)
87{
88	__u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, 4096 /* size */);
89
90	if (!data)
91		return 0; /* EPERM check */
92
93	__builtin_memcpy(data, fixed_rdesc, sizeof(fixed_rdesc));
94
95	/* Fix the Physical maximum values for different sizes of the device
96	 * The 14" screen device descriptor size is 11.874" x 7.421"
97	 */
98	if (hctx->hid->product == PID_ARTIST_PRO14_GEN2) {
99		data[63] = 0x2e;
100		data[62] = 0x62;
101		data[73] = 0x1c;
102		data[72] = 0xfd;
103	}
104
105	return sizeof(fixed_rdesc);
106}
107
108SEC("fmod_ret/hid_bpf_device_event")
109int BPF_PROG(xppen_16_fix_eraser, struct hid_bpf_ctx *hctx)
110{
111	__u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, 10 /* size */);
112
113	if (!data)
114		return 0; /* EPERM check */
115
116	if ((data[1] & 0x29) != 0x29) /* tip switch=1 invert=1 inrange=1 */
117		return 0;
118
119	/* xor bits 0,3 and 4: convert Tip Switch + Invert into Eraser only */
120	data[1] ^= 0x19;
121
122	return 0;
123}
124
125/*
126 * Static coordinate offset table based on positive only angles
127 * Two tables are needed, because the logical coordinates are scaled
128 *
129 * The table can be generated by Python like this:
130 * >>> full_scale = 11.874 # the display width/height in inches
131 * >>> tip_height = 0.055677699 # the center of the pen coil distance from screen in inch (empirical)
132 * >>> h = tip_height * (32767 / full_scale) # height of the coil in logical coordinates
133 * >>> [round(h*math.sin(math.radians(d))) for d in range(0, 128)]
134 * [0, 13, 26, ....]
135 */
136
137/* 14" inch screen 11.874" x 7.421" */
138static const __u16 angle_offsets_horizontal_14[128] = {
139	0, 3, 5, 8, 11, 13, 16, 19, 21, 24, 27, 29, 32, 35, 37, 40, 42, 45, 47, 50, 53,
140	55, 58, 60, 62, 65, 67, 70, 72, 74, 77, 79, 81, 84, 86, 88, 90, 92, 95, 97, 99,
141	101, 103, 105, 107, 109, 111, 112, 114, 116, 118, 119, 121, 123, 124, 126, 127,
142	129, 130, 132, 133, 134, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146,
143	147, 148, 148, 149, 150, 150, 151, 151, 152, 152, 153, 153, 153, 153, 153, 154,
144	154, 154, 154, 154, 153, 153, 153, 153, 153, 152, 152, 151, 151, 150, 150, 149,
145	148, 148, 147, 146, 145, 144, 143, 142, 141, 140, 139, 138, 137, 136, 134, 133,
146	132, 130, 129, 127, 126, 124, 123
147};
148static const __u16 angle_offsets_vertical_14[128] = {
149	0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 59, 64, 68, 72, 76, 80, 84,
150	88, 92, 96, 100, 104, 108, 112, 115, 119, 123, 127, 130, 134, 137, 141, 145, 148,
151	151, 155, 158, 161, 165, 168, 171, 174, 177, 180, 183, 186, 188, 191, 194, 196,
152	199, 201, 204, 206, 208, 211, 213, 215, 217, 219, 221, 223, 225, 226, 228, 230,
153	231, 232, 234, 235, 236, 237, 239, 240, 240, 241, 242, 243, 243, 244, 244, 245,
154	245, 246, 246, 246, 246, 246, 246, 246, 245, 245, 244, 244, 243, 243, 242, 241,
155	240, 240, 239, 237, 236, 235, 234, 232, 231, 230, 228, 226, 225, 223, 221, 219,
156	217, 215, 213, 211, 208, 206, 204, 201, 199, 196
157};
158
159/* 16" inch screen 13.567" x 8.480" */
160static const __u16 angle_offsets_horizontal_16[128] = {
161	0, 2, 5, 7, 9, 12, 14, 16, 19, 21, 23, 26, 28, 30, 33, 35, 37, 39, 42, 44, 46, 48,
162	50, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 86, 88, 90,
163	92, 93, 95, 97, 98, 100, 101, 103, 105, 106, 107, 109, 110, 111, 113, 114, 115,
164	116, 118, 119, 120, 121, 122, 123, 124, 125, 126, 126, 127, 128, 129, 129, 130,
165	130, 131, 132, 132, 132, 133, 133, 133, 134, 134, 134, 134, 134, 134, 134, 134,
166	134, 134, 134, 134, 134, 133, 133, 133, 132, 132, 132, 131, 130, 130, 129, 129,
167	128, 127, 126, 126, 125, 124, 123, 122, 121, 120, 119, 118, 116, 115, 114, 113,
168	111, 110, 109, 107
169};
170static const __u16 angle_offsets_vertical_16[128] = {
171	0, 4, 8, 11, 15, 19, 22, 26, 30, 34, 37, 41, 45, 48, 52, 56, 59, 63, 66, 70, 74,
172	77, 81, 84, 88, 91, 94, 98, 101, 104, 108, 111, 114, 117, 120, 123, 126, 129, 132,
173	135, 138, 141, 144, 147, 149, 152, 155, 157, 160, 162, 165, 167, 170, 172, 174,
174	176, 178, 180, 182, 184, 186, 188, 190, 192, 193, 195, 197, 198, 199, 201, 202,
175	203, 205, 206, 207, 208, 209, 210, 210, 211, 212, 212, 213, 214, 214, 214, 215,
176	215, 215, 215, 215, 215, 215, 215, 215, 214, 214, 214, 213, 212, 212, 211, 210,
177	210, 209, 208, 207, 206, 205, 203, 202, 201, 199, 198, 197, 195, 193, 192, 190,
178	188, 186, 184, 182, 180, 178, 176, 174, 172
179};
180
181static void compensate_coordinates_by_tilt(__u8 *data, const __u8 idx,
182		const __s8 tilt, const __u16 (*compensation_table)[128])
183{
184	__u16 coords = data[idx+1];
185
186	coords <<= 8;
187	coords += data[idx];
188
189	__u8 direction = tilt > 0 ? 0 : 1; /* Positive tilt means we need to subtract the compensation (vs. negative angle where we need to add) */
190	__u8 angle = tilt > 0 ? tilt : -tilt;
191
192	if (angle > 127)
193		return;
194
195	__u16 compensation = (*compensation_table)[angle];
196
197	if (direction == 0) {
198		coords = (coords > compensation) ? coords - compensation : 0;
199	} else {
200		const __u16 logical_maximum = 32767;
201		__u16 max = logical_maximum - compensation;
202
203		coords = (coords < max) ? coords + compensation : logical_maximum;
204	}
205
206	data[idx] = coords & 0xff;
207	data[idx+1] = coords >> 8;
208}
209
210SEC("fmod_ret/hid_bpf_device_event")
211int BPF_PROG(xppen_16_fix_angle_offset, struct hid_bpf_ctx *hctx)
212{
213	__u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, 10 /* size */);
214
215	if (!data)
216		return 0; /* EPERM check */
217
218	/*
219	 * Compensate X and Y offset caused by tilt.
220	 *
221	 * The magnetic center moves when the pen is tilted, because the coil
222	 * is not touching the screen.
223	 *
224	 * a (tilt angle)
225	 * |  /... h (coil distance from tip)
226	 * | /
227	 * |/______
228	 *         |x (position offset)
229	 *
230	 * x = sin a * h
231	 *
232	 * Subtract the offset from the coordinates. Use the precomputed table!
233	 *
234	 * bytes 0   - report id
235	 *       1   - buttons
236	 *       2-3 - X coords (logical)
237	 *       4-5 - Y coords
238	 *       6-7 - pressure (ignore)
239	 *       8   - tilt X
240	 *       9   - tilt Y
241	 */
242
243	__s8 tilt_x = (__s8) data[8];
244	__s8 tilt_y = (__s8) data[9];
245
246	if (hctx->hid->product == PID_ARTIST_PRO14_GEN2) {
247		compensate_coordinates_by_tilt(data, 2, tilt_x, &angle_offsets_horizontal_14);
248		compensate_coordinates_by_tilt(data, 4, tilt_y, &angle_offsets_vertical_14);
249	} else if (hctx->hid->product == PID_ARTIST_PRO16_GEN2) {
250		compensate_coordinates_by_tilt(data, 2, tilt_x, &angle_offsets_horizontal_16);
251		compensate_coordinates_by_tilt(data, 4, tilt_y, &angle_offsets_vertical_16);
252	}
253
254	return 0;
255}
256
257SEC("syscall")
258int probe(struct hid_bpf_probe_args *ctx)
259{
260	/*
261	 * The device exports 3 interfaces.
262	 */
263	ctx->retval = ctx->rdesc_size != 113;
264	if (ctx->retval)
265		ctx->retval = -EINVAL;
266
267	/* ensure the kernel isn't fixed already */
268	if (ctx->rdesc[17] != 0x45) /* Eraser */
269		ctx->retval = -EINVAL;
270
271	return 0;
272}
273
274char _license[] SEC("license") = "GPL";
275