1*4085d199SAbdelkader Boudih /*
2*4085d199SAbdelkader Boudih * Copyright (c) 2026 Abdelkader Boudih <freebsd@seuros.com>
3*4085d199SAbdelkader Boudih *
4*4085d199SAbdelkader Boudih * SPDX-License-Identifier: BSD-2-Clause
5*4085d199SAbdelkader Boudih */
6*4085d199SAbdelkader Boudih
7*4085d199SAbdelkader Boudih /*
8*4085d199SAbdelkader Boudih * Wacom ExpressKey Remote Driver
9*4085d199SAbdelkader Boudih *
10*4085d199SAbdelkader Boudih * HID driver for Wacom ExpressKey Remote (USB HID, vendor 0x056a).
11*4085d199SAbdelkader Boudih * The device uses a vendor-specific HID usage page (0xFF0C) with
12*4085d199SAbdelkader Boudih * proprietary report formats. Only a single remote is supported;
13*4085d199SAbdelkader Boudih * multi-remote pairing (up to 5) is not implemented.
14*4085d199SAbdelkader Boudih *
15*4085d199SAbdelkader Boudih * Protocol decoded from USB traffic capture (HydraUSB3 analyser).
16*4085d199SAbdelkader Boudih * Report structure cross-referenced with the HID report descriptor
17*4085d199SAbdelkader Boudih * and publicly available Wacom protocol documentation.
18*4085d199SAbdelkader Boudih *
19*4085d199SAbdelkader Boudih * Report ID 0x10 (DEVICE_LIST): pairing status (ignored)
20*4085d199SAbdelkader Boudih * Report ID 0x11 (REMOTE): button and touch ring events
21*4085d199SAbdelkader Boudih * buf[0]: report ID (0x11)
22*4085d199SAbdelkader Boudih * buf[3..5]: serial number (LE 24-bit)
23*4085d199SAbdelkader Boudih * buf[7]: battery (bit 7 = charging, bits 0-6 = percent 0-100)
24*4085d199SAbdelkader Boudih * buf[9]: buttons 0-7
25*4085d199SAbdelkader Boudih * buf[10]: buttons 8-15
26*4085d199SAbdelkader Boudih * buf[11]: bits 0-1 = buttons 16-17, bits 6-7 = touch ring mode
27*4085d199SAbdelkader Boudih * buf[12]: touch ring (bit 7 = active, bits 0-6 = position 1-72,
28*4085d199SAbdelkader Boudih * where raw value 1 maps to position 0 and 72 maps to 71)
29*4085d199SAbdelkader Boudih */
30*4085d199SAbdelkader Boudih
31*4085d199SAbdelkader Boudih #include <sys/cdefs.h>
32*4085d199SAbdelkader Boudih
33*4085d199SAbdelkader Boudih #include "opt_hid.h"
34*4085d199SAbdelkader Boudih
35*4085d199SAbdelkader Boudih #include <sys/param.h>
36*4085d199SAbdelkader Boudih #include <sys/bus.h>
37*4085d199SAbdelkader Boudih #include <sys/kernel.h>
38*4085d199SAbdelkader Boudih #include <sys/lock.h>
39*4085d199SAbdelkader Boudih #include <sys/malloc.h>
40*4085d199SAbdelkader Boudih #include <sys/module.h>
41*4085d199SAbdelkader Boudih #include <sys/mutex.h>
42*4085d199SAbdelkader Boudih #include <sys/sysctl.h>
43*4085d199SAbdelkader Boudih
44*4085d199SAbdelkader Boudih #include <dev/evdev/input.h>
45*4085d199SAbdelkader Boudih #include <dev/evdev/evdev.h>
46*4085d199SAbdelkader Boudih
47*4085d199SAbdelkader Boudih #define HID_DEBUG_VAR hidwacom_debug
48*4085d199SAbdelkader Boudih #include <dev/hid/hid.h>
49*4085d199SAbdelkader Boudih #include <dev/hid/hidbus.h>
50*4085d199SAbdelkader Boudih #include "usbdevs.h"
51*4085d199SAbdelkader Boudih
52*4085d199SAbdelkader Boudih #ifdef HID_DEBUG
53*4085d199SAbdelkader Boudih static int hidwacom_debug = 0;
54*4085d199SAbdelkader Boudih
55*4085d199SAbdelkader Boudih static SYSCTL_NODE(_hw_hid, OID_AUTO, hidwacom, CTLFLAG_RW, 0,
56*4085d199SAbdelkader Boudih "Wacom ExpressKey Remote");
57*4085d199SAbdelkader Boudih SYSCTL_INT(_hw_hid_hidwacom, OID_AUTO, debug, CTLFLAG_RWTUN,
58*4085d199SAbdelkader Boudih &hidwacom_debug, 0, "Debug level");
59*4085d199SAbdelkader Boudih #endif
60*4085d199SAbdelkader Boudih
61*4085d199SAbdelkader Boudih /* Report IDs */
62*4085d199SAbdelkader Boudih #define WACOM_REPORT_DEVICE_LIST 0x10
63*4085d199SAbdelkader Boudih #define WACOM_REPORT_REMOTE 0x11
64*4085d199SAbdelkader Boudih
65*4085d199SAbdelkader Boudih /* Minimum report length (report ID + 12 data bytes for ring at buf[12]) */
66*4085d199SAbdelkader Boudih #define WACOM_REMOTE_MIN_LEN 13
67*4085d199SAbdelkader Boudih
68*4085d199SAbdelkader Boudih /* Touch ring modes (buf[11] bits 6-7) */
69*4085d199SAbdelkader Boudih #define WACOM_RING_MODE_MASK 0xc0
70*4085d199SAbdelkader Boudih #define WACOM_RING_MODE_SHIFT 6
71*4085d199SAbdelkader Boudih
72*4085d199SAbdelkader Boudih /* Touch ring */
73*4085d199SAbdelkader Boudih #define WACOM_RING_ACTIVE 0x80
74*4085d199SAbdelkader Boudih #define WACOM_RING_POSITION_MASK 0x7f
75*4085d199SAbdelkader Boudih #define WACOM_RING_MAX 71
76*4085d199SAbdelkader Boudih
77*4085d199SAbdelkader Boudih /* Battery: bit 7 = charging flag, bits 0-6 = percent */
78*4085d199SAbdelkader Boudih #define WACOM_BATTERY_CHARGING 0x80
79*4085d199SAbdelkader Boudih #define WACOM_BATTERY_PERCENT_MASK 0x7f
80*4085d199SAbdelkader Boudih
81*4085d199SAbdelkader Boudih /* Pad device ID for ABS_MISC activity marker (matches Linux PAD_DEVICE_ID) */
82*4085d199SAbdelkader Boudih #define WACOM_PAD_DEVICE_ID 0x0f
83*4085d199SAbdelkader Boudih
84*4085d199SAbdelkader Boudih #define HIDWACOM_DEV_NAME "Wacom ExpressKey Remote"
85*4085d199SAbdelkader Boudih
86*4085d199SAbdelkader Boudih /* Button evdev keycodes */
87*4085d199SAbdelkader Boudih static const uint16_t hidwacom_buttons[] = {
88*4085d199SAbdelkader Boudih BTN_0, BTN_1, BTN_2, BTN_3, BTN_4, BTN_5, BTN_6, BTN_7, /* buf[9] */
89*4085d199SAbdelkader Boudih BTN_8, BTN_9, BTN_A, BTN_B, BTN_C, BTN_X, BTN_Y, BTN_Z, /* buf[10] */
90*4085d199SAbdelkader Boudih BTN_BASE, BTN_BASE2, /* buf[11] */
91*4085d199SAbdelkader Boudih };
92*4085d199SAbdelkader Boudih
93*4085d199SAbdelkader Boudih struct hidwacom_softc {
94*4085d199SAbdelkader Boudih struct mtx sc_mtx;
95*4085d199SAbdelkader Boudih struct evdev_dev *sc_evdev;
96*4085d199SAbdelkader Boudih };
97*4085d199SAbdelkader Boudih
98*4085d199SAbdelkader Boudih static void
hidwacom_intr(void * context,void * data,hid_size_t len)99*4085d199SAbdelkader Boudih hidwacom_intr(void *context, void *data, hid_size_t len)
100*4085d199SAbdelkader Boudih {
101*4085d199SAbdelkader Boudih struct hidwacom_softc *sc = context;
102*4085d199SAbdelkader Boudih uint8_t *buf = data;
103*4085d199SAbdelkader Boudih uint8_t report_id;
104*4085d199SAbdelkader Boudih uint8_t b9, b10, b11, ring, battery;
105*4085d199SAbdelkader Boudih uint32_t serial;
106*4085d199SAbdelkader Boudih int i, pos;
107*4085d199SAbdelkader Boudih
108*4085d199SAbdelkader Boudih if (len < 1)
109*4085d199SAbdelkader Boudih return;
110*4085d199SAbdelkader Boudih
111*4085d199SAbdelkader Boudih report_id = buf[0];
112*4085d199SAbdelkader Boudih
113*4085d199SAbdelkader Boudih if (report_id == WACOM_REPORT_DEVICE_LIST) {
114*4085d199SAbdelkader Boudih DPRINTFN(2, "device list report (pairing)\n");
115*4085d199SAbdelkader Boudih return;
116*4085d199SAbdelkader Boudih }
117*4085d199SAbdelkader Boudih
118*4085d199SAbdelkader Boudih if (report_id != WACOM_REPORT_REMOTE) {
119*4085d199SAbdelkader Boudih DPRINTFN(1, "unknown report id: 0x%02x\n", report_id);
120*4085d199SAbdelkader Boudih return;
121*4085d199SAbdelkader Boudih }
122*4085d199SAbdelkader Boudih
123*4085d199SAbdelkader Boudih if (len < WACOM_REMOTE_MIN_LEN) {
124*4085d199SAbdelkader Boudih DPRINTFN(1, "short remote report: %zu\n", (size_t)len);
125*4085d199SAbdelkader Boudih return;
126*4085d199SAbdelkader Boudih }
127*4085d199SAbdelkader Boudih
128*4085d199SAbdelkader Boudih mtx_lock(&sc->sc_mtx);
129*4085d199SAbdelkader Boudih
130*4085d199SAbdelkader Boudih /* Serial number (LE 24-bit) */
131*4085d199SAbdelkader Boudih serial = buf[3] + ((uint32_t)buf[4] << 8) + ((uint32_t)buf[5] << 16);
132*4085d199SAbdelkader Boudih
133*4085d199SAbdelkader Boudih /* Battery: percent in bits 0-6, charging flag in bit 7 */
134*4085d199SAbdelkader Boudih battery = buf[7] & WACOM_BATTERY_PERCENT_MASK;
135*4085d199SAbdelkader Boudih
136*4085d199SAbdelkader Boudih /* Extract button bytes */
137*4085d199SAbdelkader Boudih b9 = buf[9];
138*4085d199SAbdelkader Boudih b10 = buf[10];
139*4085d199SAbdelkader Boudih b11 = buf[11];
140*4085d199SAbdelkader Boudih
141*4085d199SAbdelkader Boudih /* Buttons 0-7 from buf[9] */
142*4085d199SAbdelkader Boudih for (i = 0; i < 8; i++)
143*4085d199SAbdelkader Boudih evdev_push_key(sc->sc_evdev, hidwacom_buttons[i],
144*4085d199SAbdelkader Boudih (b9 >> i) & 1);
145*4085d199SAbdelkader Boudih
146*4085d199SAbdelkader Boudih /* Buttons 8-15 from buf[10] */
147*4085d199SAbdelkader Boudih for (i = 0; i < 8; i++)
148*4085d199SAbdelkader Boudih evdev_push_key(sc->sc_evdev, hidwacom_buttons[8 + i],
149*4085d199SAbdelkader Boudih (b10 >> i) & 1);
150*4085d199SAbdelkader Boudih
151*4085d199SAbdelkader Boudih /* Buttons 16-17 from buf[11] bits 0-1 */
152*4085d199SAbdelkader Boudih evdev_push_key(sc->sc_evdev, BTN_BASE, b11 & 0x01);
153*4085d199SAbdelkader Boudih evdev_push_key(sc->sc_evdev, BTN_BASE2, b11 & 0x02);
154*4085d199SAbdelkader Boudih
155*4085d199SAbdelkader Boudih /* Touch ring: raw position is 1-based (1..72); subtract 1 for 0..71 */
156*4085d199SAbdelkader Boudih ring = buf[12];
157*4085d199SAbdelkader Boudih if (ring & WACOM_RING_ACTIVE) {
158*4085d199SAbdelkader Boudih pos = (ring & WACOM_RING_POSITION_MASK) - 1;
159*4085d199SAbdelkader Boudih if (pos < 0)
160*4085d199SAbdelkader Boudih pos = 0;
161*4085d199SAbdelkader Boudih if (pos > WACOM_RING_MAX)
162*4085d199SAbdelkader Boudih pos = WACOM_RING_MAX;
163*4085d199SAbdelkader Boudih evdev_push_abs(sc->sc_evdev, ABS_WHEEL, pos);
164*4085d199SAbdelkader Boudih evdev_push_abs(sc->sc_evdev, ABS_MISC,
165*4085d199SAbdelkader Boudih (b11 >> WACOM_RING_MODE_SHIFT) & 0x03);
166*4085d199SAbdelkader Boudih } else {
167*4085d199SAbdelkader Boudih evdev_push_abs(sc->sc_evdev, ABS_WHEEL, 0);
168*4085d199SAbdelkader Boudih evdev_push_abs(sc->sc_evdev, ABS_MISC, 0);
169*4085d199SAbdelkader Boudih }
170*4085d199SAbdelkader Boudih
171*4085d199SAbdelkader Boudih /* Battery percent */
172*4085d199SAbdelkader Boudih evdev_push_event(sc->sc_evdev, EV_MSC, MSC_PULSELED, battery);
173*4085d199SAbdelkader Boudih
174*4085d199SAbdelkader Boudih /* Remote serial number */
175*4085d199SAbdelkader Boudih evdev_push_event(sc->sc_evdev, EV_MSC, MSC_SERIAL, serial);
176*4085d199SAbdelkader Boudih
177*4085d199SAbdelkader Boudih evdev_sync(sc->sc_evdev);
178*4085d199SAbdelkader Boudih
179*4085d199SAbdelkader Boudih mtx_unlock(&sc->sc_mtx);
180*4085d199SAbdelkader Boudih }
181*4085d199SAbdelkader Boudih
182*4085d199SAbdelkader Boudih static const struct hid_device_id hidwacom_devs[] = {
183*4085d199SAbdelkader Boudih { HID_BVP(BUS_USB, USB_VENDOR_WACOM,
184*4085d199SAbdelkader Boudih USB_PRODUCT_WACOM_EXPRESSKEY_REMOTE) },
185*4085d199SAbdelkader Boudih };
186*4085d199SAbdelkader Boudih
187*4085d199SAbdelkader Boudih static int
hidwacom_probe(device_t dev)188*4085d199SAbdelkader Boudih hidwacom_probe(device_t dev)
189*4085d199SAbdelkader Boudih {
190*4085d199SAbdelkader Boudih int error;
191*4085d199SAbdelkader Boudih
192*4085d199SAbdelkader Boudih error = HIDBUS_LOOKUP_DRIVER_INFO(dev, hidwacom_devs);
193*4085d199SAbdelkader Boudih if (error != 0)
194*4085d199SAbdelkader Boudih return (error);
195*4085d199SAbdelkader Boudih
196*4085d199SAbdelkader Boudih if (hidbus_get_index(dev) != 0)
197*4085d199SAbdelkader Boudih return (ENXIO);
198*4085d199SAbdelkader Boudih
199*4085d199SAbdelkader Boudih hidbus_set_desc(dev, HIDWACOM_DEV_NAME);
200*4085d199SAbdelkader Boudih return (BUS_PROBE_DEFAULT);
201*4085d199SAbdelkader Boudih }
202*4085d199SAbdelkader Boudih
203*4085d199SAbdelkader Boudih static int
hidwacom_attach(device_t dev)204*4085d199SAbdelkader Boudih hidwacom_attach(device_t dev)
205*4085d199SAbdelkader Boudih {
206*4085d199SAbdelkader Boudih struct hidwacom_softc *sc = device_get_softc(dev);
207*4085d199SAbdelkader Boudih const struct hid_device_info *hw;
208*4085d199SAbdelkader Boudih int i, error;
209*4085d199SAbdelkader Boudih
210*4085d199SAbdelkader Boudih hw = hid_get_device_info(dev);
211*4085d199SAbdelkader Boudih mtx_init(&sc->sc_mtx, "hidwacom", NULL, MTX_DEF);
212*4085d199SAbdelkader Boudih
213*4085d199SAbdelkader Boudih sc->sc_evdev = evdev_alloc();
214*4085d199SAbdelkader Boudih evdev_set_name(sc->sc_evdev, HIDWACOM_DEV_NAME);
215*4085d199SAbdelkader Boudih evdev_set_phys(sc->sc_evdev, device_get_nameunit(dev));
216*4085d199SAbdelkader Boudih evdev_set_id(sc->sc_evdev, hw->idBus, hw->idVendor, hw->idProduct,
217*4085d199SAbdelkader Boudih hw->idVersion);
218*4085d199SAbdelkader Boudih evdev_set_serial(sc->sc_evdev, hw->serial);
219*4085d199SAbdelkader Boudih
220*4085d199SAbdelkader Boudih evdev_support_event(sc->sc_evdev, EV_SYN);
221*4085d199SAbdelkader Boudih evdev_support_event(sc->sc_evdev, EV_KEY);
222*4085d199SAbdelkader Boudih evdev_support_event(sc->sc_evdev, EV_ABS);
223*4085d199SAbdelkader Boudih evdev_support_event(sc->sc_evdev, EV_MSC);
224*4085d199SAbdelkader Boudih
225*4085d199SAbdelkader Boudih for (i = 0; i < (int)nitems(hidwacom_buttons); i++)
226*4085d199SAbdelkader Boudih evdev_support_key(sc->sc_evdev, hidwacom_buttons[i]);
227*4085d199SAbdelkader Boudih
228*4085d199SAbdelkader Boudih evdev_support_abs(sc->sc_evdev, ABS_WHEEL, 0, WACOM_RING_MAX,
229*4085d199SAbdelkader Boudih 0, 0, 0);
230*4085d199SAbdelkader Boudih evdev_support_abs(sc->sc_evdev, ABS_MISC, 0, 3, 0, 0, 0);
231*4085d199SAbdelkader Boudih evdev_support_msc(sc->sc_evdev, MSC_SERIAL);
232*4085d199SAbdelkader Boudih evdev_support_msc(sc->sc_evdev, MSC_PULSELED);
233*4085d199SAbdelkader Boudih
234*4085d199SAbdelkader Boudih error = evdev_register_mtx(sc->sc_evdev, &sc->sc_mtx);
235*4085d199SAbdelkader Boudih if (error != 0) {
236*4085d199SAbdelkader Boudih device_printf(dev, "evdev_register_mtx failed: %d\n", error);
237*4085d199SAbdelkader Boudih goto fail;
238*4085d199SAbdelkader Boudih }
239*4085d199SAbdelkader Boudih
240*4085d199SAbdelkader Boudih hidbus_set_intr(dev, hidwacom_intr, sc);
241*4085d199SAbdelkader Boudih
242*4085d199SAbdelkader Boudih error = hid_intr_start(dev);
243*4085d199SAbdelkader Boudih if (error != 0) {
244*4085d199SAbdelkader Boudih device_printf(dev, "hid_intr_start failed: %d\n", error);
245*4085d199SAbdelkader Boudih hid_intr_stop(dev);
246*4085d199SAbdelkader Boudih goto fail;
247*4085d199SAbdelkader Boudih }
248*4085d199SAbdelkader Boudih
249*4085d199SAbdelkader Boudih return (0);
250*4085d199SAbdelkader Boudih
251*4085d199SAbdelkader Boudih fail:
252*4085d199SAbdelkader Boudih if (sc->sc_evdev != NULL)
253*4085d199SAbdelkader Boudih evdev_free(sc->sc_evdev);
254*4085d199SAbdelkader Boudih mtx_destroy(&sc->sc_mtx);
255*4085d199SAbdelkader Boudih return (error);
256*4085d199SAbdelkader Boudih }
257*4085d199SAbdelkader Boudih
258*4085d199SAbdelkader Boudih static int
hidwacom_detach(device_t dev)259*4085d199SAbdelkader Boudih hidwacom_detach(device_t dev)
260*4085d199SAbdelkader Boudih {
261*4085d199SAbdelkader Boudih struct hidwacom_softc *sc = device_get_softc(dev);
262*4085d199SAbdelkader Boudih int error;
263*4085d199SAbdelkader Boudih
264*4085d199SAbdelkader Boudih error = hid_intr_stop(dev);
265*4085d199SAbdelkader Boudih if (error != 0) {
266*4085d199SAbdelkader Boudih device_printf(dev, "hid_intr_stop failed: %d\n", error);
267*4085d199SAbdelkader Boudih return (error);
268*4085d199SAbdelkader Boudih }
269*4085d199SAbdelkader Boudih evdev_free(sc->sc_evdev);
270*4085d199SAbdelkader Boudih mtx_destroy(&sc->sc_mtx);
271*4085d199SAbdelkader Boudih
272*4085d199SAbdelkader Boudih return (0);
273*4085d199SAbdelkader Boudih }
274*4085d199SAbdelkader Boudih
275*4085d199SAbdelkader Boudih static device_method_t hidwacom_methods[] = {
276*4085d199SAbdelkader Boudih DEVMETHOD(device_probe, hidwacom_probe),
277*4085d199SAbdelkader Boudih DEVMETHOD(device_attach, hidwacom_attach),
278*4085d199SAbdelkader Boudih DEVMETHOD(device_detach, hidwacom_detach),
279*4085d199SAbdelkader Boudih DEVMETHOD_END
280*4085d199SAbdelkader Boudih };
281*4085d199SAbdelkader Boudih
282*4085d199SAbdelkader Boudih static driver_t hidwacom_driver = {
283*4085d199SAbdelkader Boudih "hidwacom",
284*4085d199SAbdelkader Boudih hidwacom_methods,
285*4085d199SAbdelkader Boudih sizeof(struct hidwacom_softc)
286*4085d199SAbdelkader Boudih };
287*4085d199SAbdelkader Boudih
288*4085d199SAbdelkader Boudih DRIVER_MODULE(hidwacom, hidbus, hidwacom_driver, NULL, NULL);
289*4085d199SAbdelkader Boudih MODULE_DEPEND(hidwacom, hid, 1, 1, 1);
290*4085d199SAbdelkader Boudih MODULE_DEPEND(hidwacom, hidbus, 1, 1, 1);
291*4085d199SAbdelkader Boudih MODULE_DEPEND(hidwacom, evdev, 1, 1, 1);
292*4085d199SAbdelkader Boudih MODULE_VERSION(hidwacom, 1);
293*4085d199SAbdelkader Boudih HID_PNP_INFO(hidwacom_devs);
294