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 <[email protected]>
---
 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.
 
+@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

_______________________________________________
wayland-devel mailing list
[email protected]
https://lists.freedesktop.org/mailman/listinfo/wayland-devel

Reply via email to