xref: /freebsd/sys/dev/hid/hidwacom.c (revision 4085d1991e2a9e155e3d928955ce8ba53512b8da)
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