[PATCH libinput 1/2] tablet: add a quirk for the HUION PenTablet that doesn't send proximity out events

Peter Hutterer peter.hutterer at who-t.net
Thu Sep 14 01:04:47 UTC 2017


Could be fixed in the kernel, but these tablets are effectively abandoned and
fixing them is a one-by-one issue. Let's put the infrastructure in place to
have this fixed once for this type of device and move on.

Signed-off-by: Peter Hutterer <peter.hutterer at who-t.net>
---
 doc/device-configuration-via-udev.dox |  19 +++++
 src/evdev-tablet.c                    | 113 +++++++++++++++++++++++++++++
 src/evdev-tablet.h                    |   7 ++
 src/evdev.c                           |   1 +
 src/evdev.h                           |   1 +
 test/litest-device-huion-pentablet.c  |  15 ++--
 test/litest.c                         |   6 ++
 test/litest.h                         |   3 +
 test/test-tablet.c                    | 132 ++++++++++++++++++++++++++++++++++
 udev/90-libinput-model-quirks.hwdb    |  19 +++++
 10 files changed, 312 insertions(+), 4 deletions(-)

diff --git a/doc/device-configuration-via-udev.dox b/doc/device-configuration-via-udev.dox
index 2ebfa321..3050cd80 100644
--- a/doc/device-configuration-via-udev.dox
+++ b/doc/device-configuration-via-udev.dox
@@ -162,4 +162,23 @@ model quirks hwdb for instructions.
 This property must not be used for any other purpose, no specific behavior
 is guaranteed.
 
+ at subsection model_specific_configuration_huion_tablets Graphics tablets without BTN_TOOL_PEN proximity events
+
+On graphics tablets, the <b>BTN_TOOL_PEN</b> bit signals that the pen is in
+detectable range and will send events. When the pen leaves the sensor range,
+the bit must be unset to signal that the tablet is out of proximity again.
+Some HUION PenTablet devices are buggy and do not send this event. To a
+caller, it thus looks like the pen is constantly in proximity. This causes
+unexpected behavior in applications that rely on tablet device proximity.
+
+The property <b>LIBINPUT_MODEL_TABLET_NO_PROXIMITY_OUT</b> may be set
+by a user in a local hwdb file. This property designates the tablet
+to be buggy and that libinput should work around this bug.
+
+Many of the affected tablets cannot be detected automatically by libinput
+because HUION tablets reuse USB IDs.  Local configuration is required to set
+this property. Refer to the libinput model quirks hwdb for instructions.
+
+This property must not be used for any other purpose, no specific behavior
+is guaranteed.
 */
diff --git a/src/evdev-tablet.c b/src/evdev-tablet.c
index 77540496..057b498c 100644
--- a/src/evdev-tablet.c
+++ b/src/evdev-tablet.c
@@ -32,6 +32,10 @@
 #include <libwacom/libwacom.h>
 #endif
 
+/* The tablet sends events every ~2ms , 50ms should be plenty enough to
+   detect out-of-range */
+#define FORCED_PROXOUT_TIMEOUT ms2us(50)
+
 #define tablet_set_status(tablet_,s_) (tablet_)->status |= (s_)
 #define tablet_unset_status(tablet_,s_) (tablet_)->status &= ~(s_)
 #define tablet_has_status(tablet_,s_) (!!((tablet_)->status & (s_)))
@@ -1633,6 +1637,97 @@ tablet_reset_state(struct tablet_dispatch *tablet)
 	       sizeof(tablet->button_state));
 }
 
+static inline void
+tablet_proximity_out_quirk_set_timer(struct tablet_dispatch *tablet,
+				     uint64_t time)
+{
+	libinput_timer_set(&tablet->quirks.prox_out_timer,
+			   time + FORCED_PROXOUT_TIMEOUT);
+}
+
+static void
+tablet_proximity_out_quirk_timer_func(uint64_t now, void *data)
+{
+	struct tablet_dispatch *tablet = data;
+	struct input_event events[2] = {
+		{ .time = us2tv(now),
+		  .type = EV_KEY,
+		  .code = BTN_TOOL_PEN,
+		  .value = 0 },
+		{ .time = us2tv(now),
+		  .type = EV_SYN,
+		  .code = SYN_REPORT,
+		  .value = 0 },
+	};
+	struct input_event *e;
+
+	if (tablet->quirks.last_event_time > now - FORCED_PROXOUT_TIMEOUT) {
+		tablet_proximity_out_quirk_set_timer(tablet,
+						     tablet->quirks.last_event_time);
+		return;
+	}
+
+	ARRAY_FOR_EACH(events, e) {
+		tablet->base.interface->process(&tablet->base,
+						 tablet->device,
+						 e,
+						 now);
+	}
+
+	tablet->quirks.proximity_out_forced = true;
+}
+
+/**
+ * Handling for the proximity out workaround. Some tablets only send
+ * BTN_TOOL_PEN on the very first event, then leave it set even when the pen
+ * leaves the detectable range. To libinput this looks like we always have
+ * the pen in proximity.
+ *
+ * To avoid this, we set a timer on BTN_TOOL_PEN in. We expect the tablet to
+ * continuously send events, and while it's doing so we keep updating the
+ * timer. Once we go Xms without an event we assume proximity out and inject
+ * a BTN_TOOL_PEN event into the sequence through the timer func.
+ *
+ * We need to remember that we did that, on the first event after the
+ * timeout we need to inject a BTN_TOOL_PEN event again to force proximity
+ * in.
+ */
+static inline void
+tablet_proximity_out_quirk_update(struct tablet_dispatch *tablet,
+				  struct evdev_device *device,
+				  struct input_event *e,
+				  uint64_t time)
+{
+	if (!tablet->quirks.need_to_force_prox_out)
+		return;
+
+	if (e->type == EV_SYN) {
+		/* If the timer function forced prox out before,
+		   fake a BTN_TOOL_PEN event */
+		if (tablet->quirks.proximity_out_forced) {
+
+			struct input_event fake_event = {
+				.time = us2tv(time),
+				.type = EV_KEY,
+				.code = BTN_TOOL_PEN,
+				.value = 1,
+			};
+
+			tablet->base.interface->process(&tablet->base,
+							device,
+							&fake_event,
+							time);
+			tablet->quirks.proximity_out_forced = false;
+		}
+		tablet->quirks.last_event_time = time;
+	} else if (e->type == EV_KEY && e->code == BTN_TOOL_PEN) {
+		if (e->value)
+			tablet_proximity_out_quirk_set_timer(tablet, time);
+		else
+			libinput_timer_cancel(&tablet->quirks.prox_out_timer);
+	}
+}
+
 static void
 tablet_process(struct evdev_dispatch *dispatch,
 	       struct evdev_device *device,
@@ -1641,6 +1736,9 @@ tablet_process(struct evdev_dispatch *dispatch,
 {
 	struct tablet_dispatch *tablet = tablet_dispatch(dispatch);
 
+	/* Warning: this may inject events */
+	tablet_proximity_out_quirk_update(tablet, device, e, time);
+
 	switch (e->type) {
 	case EV_ABS:
 		tablet_process_absolute(tablet, device, e, time);
@@ -1683,6 +1781,9 @@ tablet_destroy(struct evdev_dispatch *dispatch)
 	struct tablet_dispatch *tablet = tablet_dispatch(dispatch);
 	struct libinput_tablet_tool *tool, *tmp;
 
+	libinput_timer_cancel(&tablet->quirks.prox_out_timer);
+	libinput_timer_destroy(&tablet->quirks.prox_out_timer);
+
 	list_for_each_safe(tool, tmp, &tablet->tool_list, link) {
 		libinput_tablet_tool_unref(tool);
 	}
@@ -1722,6 +1823,7 @@ tablet_check_initial_proximity(struct evdev_device *device,
 			       struct evdev_dispatch *dispatch)
 {
 	struct tablet_dispatch *tablet = tablet_dispatch(dispatch);
+	struct libinput *li = tablet_libinput_context(tablet);
 	bool tool_in_prox = false;
 	int code, state;
 	enum libinput_tablet_tool_type tool;
@@ -1743,6 +1845,8 @@ tablet_check_initial_proximity(struct evdev_device *device,
 		return;
 
 	tablet_update_tool(tablet, device, tool, state);
+	if (tablet->quirks.need_to_force_prox_out)
+		tablet_proximity_out_quirk_set_timer(tablet, libinput_now(li));
 
 	tablet->current_tool_id =
 		libevdev_get_event_value(device->evdev,
@@ -1922,6 +2026,15 @@ tablet_init(struct tablet_dispatch *tablet,
 
 	tablet_set_status(tablet, TABLET_TOOL_OUT_OF_PROXIMITY);
 
+	if (device->model_flags & EVDEV_MODEL_TABLET_NO_PROXIMITY_OUT) {
+		tablet->quirks.need_to_force_prox_out = true;
+		libinput_timer_init(&tablet->quirks.prox_out_timer,
+				    tablet_libinput_context(tablet),
+				    "proxout",
+				    tablet_proximity_out_quirk_timer_func,
+				    tablet);
+	}
+
 	return 0;
 }
 
diff --git a/src/evdev-tablet.h b/src/evdev-tablet.h
index 7d17e366..ba05e88c 100644
--- a/src/evdev-tablet.h
+++ b/src/evdev-tablet.h
@@ -83,6 +83,13 @@ struct tablet_dispatch {
 
 	/* The paired touch device on devices with both pen & touch */
 	struct evdev_device *touch_device;
+
+	struct {
+		bool need_to_force_prox_out;
+		struct libinput_timer prox_out_timer;
+		bool proximity_out_forced;
+		uint64_t last_event_time;
+	} quirks;
 };
 
 static inline struct tablet_dispatch*
diff --git a/src/evdev.c b/src/evdev.c
index 46f8ad57..a9ffd9de 100644
--- a/src/evdev.c
+++ b/src/evdev.c
@@ -2742,6 +2742,7 @@ evdev_read_model_flags(struct evdev_device *device)
 		MODEL(HP_PAVILION_DM4_TOUCHPAD),
 		MODEL(APPLE_TOUCHPAD_ONEBUTTON),
 		MODEL(LOGITECH_MARBLE_MOUSE),
+		MODEL(TABLET_NO_PROXIMITY_OUT),
 #undef MODEL
 		{ "ID_INPUT_TRACKBALL", EVDEV_MODEL_TRACKBALL },
 		{ NULL, EVDEV_MODEL_DEFAULT },
diff --git a/src/evdev.h b/src/evdev.h
index 5192927c..bb4c3a03 100644
--- a/src/evdev.h
+++ b/src/evdev.h
@@ -127,6 +127,7 @@ enum evdev_device_model {
 	EVDEV_MODEL_HP_PAVILION_DM4_TOUCHPAD = (1 << 24),
 	EVDEV_MODEL_APPLE_TOUCHPAD_ONEBUTTON = (1 << 25),
 	EVDEV_MODEL_LOGITECH_MARBLE_MOUSE = (1 << 26),
+	EVDEV_MODEL_TABLET_NO_PROXIMITY_OUT = (1 << 27),
 };
 
 enum evdev_button_scroll_state {
diff --git a/test/litest-device-huion-pentablet.c b/test/litest-device-huion-pentablet.c
index e37d5c09..dbbdcb01 100644
--- a/test/litest-device-huion-pentablet.c
+++ b/test/litest-device-huion-pentablet.c
@@ -42,10 +42,6 @@ static struct input_event proximity_in[] = {
 };
 
 static struct input_event proximity_out[] = {
-	{ .type = EV_ABS, .code = ABS_X, .value = 0 },
-	{ .type = EV_ABS, .code = ABS_Y, .value = 0 },
-	{ .type = EV_KEY, .code = BTN_TOOL_PEN, .value = 0 },
-	{ .type = EV_SYN, .code = SYN_REPORT, .value = 0 },
 	{ .type = -1, .code = -1 },
 };
 
@@ -98,6 +94,16 @@ static int events[] = {
 	-1, -1,
 };
 
+static const char udev_rule[] =
+"ACTION==\"remove\", GOTO=\"huion_end\"\n"
+"KERNEL!=\"event*\", GOTO=\"huion_end\"\n"
+"ENV{ID_INPUT_TABLET}==\"\", GOTO=\"huion_end\"\n"
+"\n"
+"ATTRS{name}==\"litest HUION PenTablet Pen\","
+"    ENV{LIBINPUT_MODEL_TABLET_NO_PROXIMITY_OUT}=\"1\"\n"
+"\n"
+"LABEL=\"huion_end\"";
+
 struct litest_test_device litest_huion_tablet_device = {
 	.type = LITEST_HUION_TABLET,
 	.features = LITEST_TABLET,
@@ -109,4 +115,5 @@ struct litest_test_device litest_huion_tablet_device = {
 	.id = &input_id,
 	.events = events,
 	.absinfo = absinfo,
+	.udev_rule = udev_rule,
 };
diff --git a/test/litest.c b/test/litest.c
index 1f238431..868dfb69 100644
--- a/test/litest.c
+++ b/test/litest.c
@@ -3340,6 +3340,12 @@ litest_timeout_trackpoint(void)
 }
 
 void
+litest_timeout_tablet_proxout(void)
+{
+	msleep(70);
+}
+
+void
 litest_push_event_frame(struct litest_device *dev)
 {
 	litest_assert(dev->skip_ev_syn >= 0);
diff --git a/test/litest.h b/test/litest.h
index 679406dc..8f3c3bc9 100644
--- a/test/litest.h
+++ b/test/litest.h
@@ -730,6 +730,9 @@ void
 litest_timeout_trackpoint(void);
 
 void
+litest_timeout_tablet_proxout(void);
+
+void
 litest_push_event_frame(struct litest_device *dev);
 
 void
diff --git a/test/test-tablet.c b/test/test-tablet.c
index a9ad9592..4a36c79a 100644
--- a/test/test-tablet.c
+++ b/test/test-tablet.c
@@ -250,6 +250,22 @@ START_TEST(tip_down_prox_in)
 }
 END_TEST
 
+static inline bool
+tablet_has_proxout_quirk(struct litest_device *dev)
+{
+	struct udev_device *udev_device;
+	bool has_quirk;
+
+	udev_device = libinput_device_get_udev_device(dev->libinput_device);
+
+	has_quirk = !!udev_device_get_property_value(udev_device,
+			   "LIBINPUT_MODEL_TABLET_NO_PROXIMITY_OUT");
+
+	udev_device_unref(udev_device);
+
+	return has_quirk;
+}
+
 START_TEST(tip_up_prox_out)
 {
 	struct litest_device *dev = litest_current_device();
@@ -262,6 +278,9 @@ START_TEST(tip_up_prox_out)
 		{ -1, -1 }
 	};
 
+	if (tablet_has_proxout_quirk(dev))
+		return;
+
 	litest_tablet_proximity_in(dev, 10, 10, axes);
 	litest_event(dev, EV_KEY, BTN_TOUCH, 1);
 	litest_event(dev, EV_SYN, SYN_REPORT, 0);
@@ -616,6 +635,9 @@ START_TEST(tip_state_proximity)
 	litest_tablet_proximity_out(dev);
 	libinput_dispatch(li);
 
+	litest_timeout_tablet_proxout();
+	libinput_dispatch(li);
+
 	event = libinput_get_event(li);
 	tablet_event = litest_is_tablet_event(event,
 					      LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY);
@@ -808,6 +830,9 @@ START_TEST(proximity_in_out)
 	litest_tablet_proximity_out(dev);
 	libinput_dispatch(li);
 
+	litest_timeout_tablet_proxout();
+	libinput_dispatch(li);
+
 	while ((event = libinput_get_event(li))) {
 		if (libinput_event_get_type(event) ==
 		    LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY) {
@@ -917,7 +942,9 @@ START_TEST(proximity_out_clear_buttons)
 		litest_event(dev, EV_KEY, button, 1);
 		litest_event(dev, EV_SYN, SYN_REPORT, 0);
 		litest_tablet_proximity_out(dev);
+		libinput_dispatch(li);
 
+		litest_timeout_tablet_proxout();
 		libinput_dispatch(li);
 
 		while ((event = libinput_get_event(li))) {
@@ -1042,6 +1069,9 @@ START_TEST(proximity_has_axes)
 	litest_tablet_proximity_out(dev);
 	libinput_dispatch(li);
 
+	litest_timeout_tablet_proxout();
+	libinput_dispatch(li);
+
 	event = libinput_get_event(li);
 	tablet_event = litest_is_tablet_event(event,
 					      LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY);
@@ -2506,6 +2536,10 @@ START_TEST(tool_in_prox_before_start)
 	litest_event(dev, EV_SYN, SYN_REPORT, 0);
 	litest_assert_only_typed_events(li, LIBINPUT_EVENT_TABLET_TOOL_BUTTON);
 	litest_tablet_proximity_out(dev);
+	libinput_dispatch(li);
+
+	litest_timeout_tablet_proxout();
+	libinput_dispatch(li);
 
 	litest_wait_for_event_of_type(li,
 				      LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY,
@@ -4314,6 +4348,101 @@ START_TEST(cintiq_touch_arbitration_remove_tablet)
 }
 END_TEST
 
+START_TEST(huion_static_btn_tool_pen)
+{
+	struct litest_device *dev = litest_current_device();
+	struct libinput *li = dev->libinput;
+	int i;
+
+	litest_drain_events(li);
+
+	litest_event(dev, EV_ABS, ABS_X, 20000);
+	litest_event(dev, EV_ABS, ABS_Y, 20000);
+	litest_event(dev, EV_ABS, ABS_PRESSURE, 100);
+	litest_event(dev, EV_KEY, BTN_TOOL_PEN, 1);
+	litest_event(dev, EV_SYN, SYN_REPORT, 0);
+	litest_drain_events(li);
+
+	for (i = 0; i < 10; i++) {
+		litest_event(dev, EV_ABS, ABS_X, 20000 + 10 * i);
+		litest_event(dev, EV_ABS, ABS_Y, 20000 - 10 * i);
+		litest_event(dev, EV_SYN, SYN_REPORT, 0);
+		libinput_dispatch(li);
+	}
+	litest_assert_only_typed_events(li,
+					LIBINPUT_EVENT_TABLET_TOOL_AXIS);
+
+	/* Wait past the timeout to expect a proximity out */
+	litest_timeout_tablet_proxout();
+	libinput_dispatch(li);
+	litest_assert_tablet_proximity_event(li,
+			     LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_OUT);
+	libinput_dispatch(li);
+
+	/* New events should fake a proximity in again */
+	litest_event(dev, EV_ABS, ABS_X, 20000);
+	litest_event(dev, EV_ABS, ABS_Y, 20000);
+	litest_event(dev, EV_SYN, SYN_REPORT, 0);
+	libinput_dispatch(li);
+	litest_assert_tablet_proximity_event(li,
+			     LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_IN);
+	libinput_dispatch(li);
+
+	for (i = 0; i < 10; i++) {
+		litest_event(dev, EV_ABS, ABS_X, 20000 + 10 * i);
+		litest_event(dev, EV_ABS, ABS_Y, 20000 - 10 * i);
+		litest_event(dev, EV_SYN, SYN_REPORT, 0);
+		libinput_dispatch(li);
+	}
+	litest_assert_only_typed_events(li,
+					LIBINPUT_EVENT_TABLET_TOOL_AXIS);
+	litest_timeout_tablet_proxout();
+	libinput_dispatch(li);
+	litest_assert_tablet_proximity_event(li,
+			     LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_OUT);
+	libinput_dispatch(li);
+
+	/* New events, just to ensure cleanup paths are correct */
+	litest_event(dev, EV_ABS, ABS_X, 20000);
+	litest_event(dev, EV_ABS, ABS_Y, 20000);
+	litest_event(dev, EV_SYN, SYN_REPORT, 0);
+	libinput_dispatch(li);
+}
+END_TEST
+
+START_TEST(huion_static_btn_tool_pen_no_timeout_during_usage)
+{
+	struct litest_device *dev = litest_current_device();
+	struct libinput *li = dev->libinput;
+	int i;
+
+	litest_drain_events(li);
+
+	litest_event(dev, EV_ABS, ABS_X, 20000);
+	litest_event(dev, EV_ABS, ABS_Y, 20000);
+	litest_event(dev, EV_ABS, ABS_PRESSURE, 100);
+	litest_event(dev, EV_KEY, BTN_TOOL_PEN, 1);
+	litest_event(dev, EV_SYN, SYN_REPORT, 0);
+	litest_drain_events(li);
+
+	/* take longer than the no-activity timeout */
+	for (i = 0; i < 50; i++) {
+		litest_event(dev, EV_ABS, ABS_X, 20000 + 10 * i);
+		litest_event(dev, EV_ABS, ABS_Y, 20000 - 10 * i);
+		litest_event(dev, EV_SYN, SYN_REPORT, 0);
+		libinput_dispatch(li);
+		msleep(5);
+	}
+	litest_assert_only_typed_events(li,
+					LIBINPUT_EVENT_TABLET_TOOL_AXIS);
+	litest_timeout_tablet_proxout();
+	libinput_dispatch(li);
+	litest_assert_tablet_proximity_event(li,
+			     LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_OUT);
+	libinput_dispatch(li);
+}
+END_TEST
+
 void
 litest_setup_tests_tablet(void)
 {
@@ -4409,4 +4538,7 @@ litest_setup_tests_tablet(void)
 	litest_add_for_device("tablet:touch-arbitration", cintiq_touch_arbitration_suspend_touch_device, LITEST_WACOM_CINTIQ_13HDT_FINGER);
 	litest_add_for_device("tablet:touch-arbitration", cintiq_touch_arbitration_remove_touch, LITEST_WACOM_CINTIQ_13HDT_PEN);
 	litest_add_for_device("tablet:touch-arbitration", cintiq_touch_arbitration_remove_tablet, LITEST_WACOM_CINTIQ_13HDT_FINGER);
+
+	litest_add_for_device("tablet:quirks", huion_static_btn_tool_pen, LITEST_HUION_TABLET);
+	litest_add_for_device("tablet:quirks", huion_static_btn_tool_pen_no_timeout_during_usage, LITEST_HUION_TABLET);
 }
diff --git a/udev/90-libinput-model-quirks.hwdb b/udev/90-libinput-model-quirks.hwdb
index 72fcdca1..867ffa53 100644
--- a/udev/90-libinput-model-quirks.hwdb
+++ b/udev/90-libinput-model-quirks.hwdb
@@ -163,6 +163,25 @@ libinput:name:AlpsPS/2 ALPS GlidePoint:dmi:*svnHP:pnHPZBookStudioG3:*
  LIBINPUT_MODEL_HP_ZBOOK_STUDIO_G3=1
 
 ##########################################
+# HUION
+##########################################
+#
+# HUION PenTablet device. Some of these devices send a BTN_TOOL_PEN event
+# with value 1 on the first event received by the device but never send the
+# matching BTN_TOOL_PEN value 0 event. The device appears as if it was
+# permanently in proximity.
+#
+# If the tablet is affected by this bug, copy the two lines below into a new
+# file
+# /etc/udev/hwdb.d/90-libinput-huion-pentablet-proximity-quirk.hwdb, then run
+# sudo udevadm hwdb --update and reboot.
+#
+# Note that HUION re-uses USB IDs for its devices, not ever HUION tablet is
+# affected by this bug.
+#libinput:name:PenTablet Pen:dmi:*
+# LIBINPUT_MODEL_TABLET_NO_PROXIMITY_OUT=1
+
+##########################################
 # LENOVO
 ##########################################
 
-- 
2.13.5



More information about the wayland-devel mailing list