From 381cce8ddd07e69bdd68159bd11bd6efef5214a0 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Tue, 27 Jun 2017 08:59:51 +1000 Subject: [PATCH 1/3] touchpad: increase palm edge zones to 8% Most modern touchpads are around 100mm wide, so this provides a ca 8mm edge zone on each side. The extra 3mm should provide for more reliable palm detection, a few touches happen to be just on the edge of the 5mm mark. https://bugs.freedesktop.org/show_bug.cgi?id=101433 Signed-off-by: Peter Hutterer --- src/evdev-mt-touchpad.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/evdev-mt-touchpad.c b/src/evdev-mt-touchpad.c index 2d39e18d..a3455583 100644 --- a/src/evdev-mt-touchpad.c +++ b/src/evdev-mt-touchpad.c @@ -2278,12 +2278,12 @@ tp_init_palmdetect_edge(struct tp_dispatch *tp, if (width < 70.0) return; - /* palm edges are 5% of the width on each side */ - mm.x = width * 0.05; + /* palm edges are 8% of the width on each side */ + mm.x = width * 0.08; edges = evdev_device_mm_to_units(device, &mm); tp->palm.left_edge = edges.x; - mm.x = width * 0.95; + mm.x = width * 0.92; edges = evdev_device_mm_to_units(device, &mm); tp->palm.right_edge = edges.x; } From 25d54b90db6ac056157a7ad5c807c70b43b23c9b Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Wed, 28 Jun 2017 13:09:22 +1000 Subject: [PATCH 2/3] touchpad: add pressure-base palm detection If a touch goes past the fixed pressure threshold it is labelled as a palm and stays a palm. Default value is one that works well here on a T440 and is virtually impossible to trigger by a normal finger or thumb. A udev property is exposed so we can handle this in the udev hwdb and the new tool introduce a few commits ago can help finding the palm detection threshold. Unlike the other palm detection features, once a palm goes past the threshold it remains a palm until the touch is released. This means palm overrides any other palm detection features. For code simplicity, we don't combine the states but merely check for pressure before and after the other palm detection functions. If the pressure triggers, it will trigger before anything else. And if something else is already active (e.g. edge where the pressure doesn't work well) it will trigger as soon as the palm is released. The palm threshold should thus be chosen with some room to spare between the highest finger pressure. https://bugs.freedesktop.org/show_bug.cgi?id=94236 Signed-off-by: Peter Hutterer --- doc/palm-detection.dox | 12 ++ src/evdev-mt-touchpad.c | 71 ++++++++++ src/evdev-mt-touchpad.h | 4 + src/libinput-util.c | 27 ++++ src/libinput-util.h | 1 + test/test-misc.c | 29 +++++ test/test-touchpad.c | 201 ++++++++++++++++++++++++++--- udev/90-libinput-model-quirks.hwdb | 1 + udev/parse_hwdb.py | 6 +- 9 files changed, 330 insertions(+), 22 deletions(-) diff --git a/doc/palm-detection.dox b/doc/palm-detection.dox index 73c81b2c..c8ceb4eb 100644 --- a/doc/palm-detection.dox +++ b/doc/palm-detection.dox @@ -13,6 +13,18 @@ Lenovo T440 happened in the left-most and right-most 5% of the touchpad. The T440 series has one of the largest touchpads, other touchpads are less affected by palm touches. +@section palm_pressure Palm detection based on pressure + +The simplest form of palm detection labels a touch as palm when the pressure +value goes above a certain threshold. This threshold is usually high enough +that it cannot be triggered by a finger movement. One a touch is labelled as +palm based on pressure, it will remain so even if the pressure drops below +the threshold again. This ensures that a palm remains a palm even when the +pressure changes as the user is typing. + +For some information on how to detect pressure on a touch and debug the +pressure ranges, see @ref touchpad_pressure. + @section palm_exclusion_zones Palm exclusion zones libinput enables palm detection on the edge of the touchpad. Two exclusion diff --git a/src/evdev-mt-touchpad.c b/src/evdev-mt-touchpad.c index a3455583..4d0cff97 100644 --- a/src/evdev-mt-touchpad.c +++ b/src/evdev-mt-touchpad.c @@ -746,12 +746,33 @@ tp_palm_detect_edge(struct tp_dispatch *tp, return true; } +static bool +tp_palm_detect_pressure_triggered(struct tp_dispatch *tp, + struct tp_touch *t, + uint64_t time) +{ + if (!tp->palm.use_pressure) + return false; + + if (t->palm.state != PALM_NONE && + t->palm.state != PALM_PRESSURE) + return false; + + if (t->pressure > tp->palm.pressure_threshold) + t->palm.state = PALM_PRESSURE; + + return t->palm.state == PALM_PRESSURE; +} + static void tp_palm_detect(struct tp_dispatch *tp, struct tp_touch *t, uint64_t time) { const char *palm_state; enum touch_palm_state oldstate = t->palm.state; + if (tp_palm_detect_pressure_triggered(tp, t, time)) + goto out; + if (tp_palm_detect_dwt_triggered(tp, t, time)) goto out; @@ -764,8 +785,18 @@ tp_palm_detect(struct tp_dispatch *tp, struct tp_touch *t, uint64_t time) if (tp_palm_detect_edge(tp, t, time)) goto out; + /* Pressure is highest priority because it cannot be released and + * overrides all other checks. So we check once before anything else + * in case pressure triggers on a non-palm touch. And again after + * everything in case one of the others released but we have a + * pressure trigger now. + */ + if (tp_palm_detect_pressure_triggered(tp, t, time)) + goto out; + return; out: + if (oldstate == t->palm.state) return; @@ -782,6 +813,9 @@ out: case PALM_TOOL_PALM: palm_state = "tool-palm"; break; + case PALM_PRESSURE: + palm_state = "pressure"; + break; case PALM_NONE: default: abort(); @@ -2288,6 +2322,42 @@ tp_init_palmdetect_edge(struct tp_dispatch *tp, tp->palm.right_edge = edges.x; } +static int +tp_read_palm_pressure_prop(struct tp_dispatch *tp, + const struct evdev_device *device) +{ + struct udev_device *udev_device = device->udev_device; + const char *prop; + int threshold; + const int default_palm_threshold = 130; + + prop = udev_device_get_property_value(udev_device, + "LIBINPUT_ATTR_PALM_PRESSURE_THRESHOLD"); + if (!prop) + return default_palm_threshold; + + threshold = parse_palm_pressure_property(prop); + + return threshold > 0 ? threshold : default_palm_threshold; +} + +static inline void +tp_init_palmdetect_pressure(struct tp_dispatch *tp, + struct evdev_device *device) +{ + if (!libevdev_has_event_code(device->evdev, EV_ABS, ABS_MT_PRESSURE)) { + tp->palm.use_pressure = false; + return; + } + + tp->palm.pressure_threshold = tp_read_palm_pressure_prop(tp, device); + tp->palm.use_pressure = true; + + evdev_log_debug(device, + "palm: pressure threshold is %d\n", + tp->palm.pressure_threshold); +} + static void tp_init_palmdetect(struct tp_dispatch *tp, struct evdev_device *device) @@ -2308,6 +2378,7 @@ tp_init_palmdetect(struct tp_dispatch *tp, tp->palm.use_mt_tool = true; tp_init_palmdetect_edge(tp, device); + tp_init_palmdetect_pressure(tp, device); } static void diff --git a/src/evdev-mt-touchpad.h b/src/evdev-mt-touchpad.h index 2873c014..6d014607 100644 --- a/src/evdev-mt-touchpad.h +++ b/src/evdev-mt-touchpad.h @@ -58,6 +58,7 @@ enum touch_palm_state { PALM_TYPING, PALM_TRACKPOINT, PALM_TOOL_PALM, + PALM_PRESSURE, }; enum button_event { @@ -343,6 +344,9 @@ struct tp_dispatch { bool monitor_trackpoint; bool use_mt_tool; + + bool use_pressure; + int pressure_threshold; } palm; struct { diff --git a/src/libinput-util.c b/src/libinput-util.c index eeeeca07..9104e9d9 100644 --- a/src/libinput-util.c +++ b/src/libinput-util.c @@ -404,6 +404,33 @@ parse_pressure_range_property(const char *prop, int *hi, int *lo) return true; } +/** + * Helper function to parse the LIBINPUT_ATTR_PALM_PRESSURE_THRESHOLD + * property from udev. Property is of the form: + * LIBINPUT_ATTR_PALM_PRESSURE_THRESHOLD= + * Where the number indicates the minimum threshold to consider a touch to + * be a palm. + * + * @param prop The value of the udev property (without the * + * LIBINPUT_ATTR_PALM_PRESSURE_THRESHOLD=) + * @return The pressure threshold or 0 on error + */ +int +parse_palm_pressure_property(const char *prop) +{ + int threshold = 0; + + if (!prop) + return 0; + + if (!safe_atoi(prop, &threshold) || + threshold < 0 || + threshold > 255) /* No touchpad device has pressure > 255 */ + return 0; + + return threshold; +} + /** * Return the next word in a string pointed to by state before the first * separator character. Call repeatedly to tokenize a whole string. diff --git a/src/libinput-util.h b/src/libinput-util.h index 8d8e3d56..123dbf03 100644 --- a/src/libinput-util.h +++ b/src/libinput-util.h @@ -392,6 +392,7 @@ double parse_trackpoint_accel_property(const char *prop); bool parse_dimension_property(const char *prop, size_t *width, size_t *height); bool parse_calibration_property(const char *prop, float calibration[6]); bool parse_pressure_range_property(const char *prop, int *hi, int *lo); +int parse_palm_pressure_property(const char *prop); enum tpkbcombo_layout { TPKBCOMBO_LAYOUT_UNKNOWN, diff --git a/test/test-misc.c b/test/test-misc.c index e55daed4..2778ed91 100644 --- a/test/test-misc.c +++ b/test/test-misc.c @@ -1044,6 +1044,34 @@ START_TEST(pressure_range_prop_parser) } END_TEST +START_TEST(palm_pressure_parser) +{ + struct parser_test tests[] = { + { "1", 1 }, + { "10", 10 }, + { "255", 255 }, + + { "-12", 0 }, + { "360", 0 }, + { "0", 0 }, + { "-0", 0 }, + { "a", 0 }, + { "10a", 0 }, + { "10-", 0 }, + { "sadfasfd", 0 }, + { "361", 0 }, + { NULL, 0 } + }; + + int i, angle; + + for (i = 0; tests[i].tag != NULL; i++) { + angle = parse_palm_pressure_property(tests[i].tag); + ck_assert_int_eq(angle, tests[i].expected_value); + } +} +END_TEST + START_TEST(time_conversion) { ck_assert_int_eq(us(10), 10); @@ -1308,6 +1336,7 @@ litest_setup_tests_misc(void) litest_add_no_device("misc:parser", reliability_prop_parser); litest_add_no_device("misc:parser", calibration_prop_parser); litest_add_no_device("misc:parser", pressure_range_prop_parser); + litest_add_no_device("misc:parser", palm_pressure_parser); litest_add_no_device("misc:parser", safe_atoi_test); litest_add_no_device("misc:parser", safe_atod_test); litest_add_no_device("misc:parser", strsplit_test); diff --git a/test/test-touchpad.c b/test/test-touchpad.c index a380bcfd..57f0d60a 100644 --- a/test/test-touchpad.c +++ b/test/test-touchpad.c @@ -32,6 +32,27 @@ #include "libinput-util.h" #include "litest.h" +static inline bool +has_disable_while_typing(struct litest_device *device) +{ + return libinput_device_config_dwt_is_available(device->libinput_device); +} + +static inline struct litest_device * +dwt_init_paired_keyboard(struct libinput *li, + struct litest_device *touchpad) +{ + enum litest_device_type which = LITEST_KEYBOARD; + + if (libevdev_get_id_vendor(touchpad->evdev) == VENDOR_ID_APPLE) + which = LITEST_APPLE_KEYBOARD; + + if (libevdev_get_id_vendor(touchpad->evdev) == VENDOR_ID_CHICONY) + which = LITEST_ACER_HAWAII_KEYBOARD; + + return litest_add_device(li, which); +} + START_TEST(touchpad_1fg_motion) { struct litest_device *dev = litest_current_device(); @@ -1373,6 +1394,159 @@ START_TEST(touchpad_palm_detect_tool_palm_tap) } END_TEST +static inline bool +touchpad_has_palm_pressure(struct litest_device *dev) +{ + struct libevdev *evdev = dev->evdev; + + if (libevdev_has_event_code(evdev, EV_ABS, ABS_MT_PRESSURE)) + return true; + + return false; +} + +START_TEST(touchpad_palm_detect_pressure) +{ + struct litest_device *dev = litest_current_device(); + struct libinput *li = dev->libinput; + struct axis_replacement axes[] = { + { ABS_MT_PRESSURE, 75 }, + { -1, 0 } + }; + + if (!touchpad_has_palm_pressure(dev)) + return; + + litest_disable_tap(dev->libinput_device); + litest_drain_events(li); + + litest_touch_down_extended(dev, 0, 50, 99, axes); + litest_touch_move_to(dev, 0, 50, 50, 80, 99, 10, 0); + litest_touch_up(dev, 0); + + litest_assert_empty_queue(li); +} +END_TEST + +START_TEST(touchpad_palm_detect_pressure_late) +{ + struct litest_device *dev = litest_current_device(); + struct libinput *li = dev->libinput; + struct axis_replacement axes[] = { + { ABS_MT_PRESSURE, 75 }, + { -1, 0 } + }; + + if (!touchpad_has_palm_pressure(dev)) + return; + + litest_disable_tap(dev->libinput_device); + litest_drain_events(li); + + litest_touch_down(dev, 0, 50, 50); + litest_touch_move_to(dev, 0, 50, 70, 80, 90, 10, 0); + litest_drain_events(li); + libinput_dispatch(li); + litest_touch_move_to_extended(dev, 0, 80, 90, 50, 20, + axes, 10, 0); + litest_touch_up(dev, 0); + + litest_assert_empty_queue(li); +} +END_TEST + +START_TEST(touchpad_palm_detect_pressure_keep_palm) +{ + struct litest_device *dev = litest_current_device(); + struct libinput *li = dev->libinput; + struct axis_replacement axes[] = { + { ABS_MT_PRESSURE, 75 }, + { -1, 0 } + }; + + if (!touchpad_has_palm_pressure(dev)) + return; + + litest_disable_tap(dev->libinput_device); + litest_drain_events(li); + + litest_touch_down(dev, 0, 80, 90); + litest_touch_move_to_extended(dev, 0, 80, 90, 50, 20, + axes, 10, 0); + litest_touch_move_to(dev, 0, 50, 20, 80, 90, 10, 0); + litest_touch_up(dev, 0); + + litest_assert_empty_queue(li); +} +END_TEST + +START_TEST(touchpad_palm_detect_pressure_after_edge) +{ + struct litest_device *dev = litest_current_device(); + struct libinput *li = dev->libinput; + struct axis_replacement axes[] = { + { ABS_MT_PRESSURE, 75 }, + { -1, 0 } + }; + + if (!touchpad_has_palm_pressure(dev) || + !touchpad_has_palm_detect_size(dev) || + !litest_has_2fg_scroll(dev)) + return; + + litest_enable_2fg_scroll(dev); + litest_disable_tap(dev->libinput_device); + litest_drain_events(li); + + litest_touch_down(dev, 0, 99, 50); + litest_touch_move_to_extended(dev, 0, 99, 50, 20, 50, axes, 20, 0); + litest_touch_up(dev, 0); + libinput_dispatch(li); + + litest_assert_empty_queue(li); +} +END_TEST + +START_TEST(touchpad_palm_detect_pressure_after_dwt) +{ + struct litest_device *touchpad = litest_current_device(); + struct litest_device *keyboard; + struct libinput *li = touchpad->libinput; + struct axis_replacement axes[] = { + { ABS_MT_PRESSURE, 75 }, + { -1, 0 } + }; + + if (!touchpad_has_palm_pressure(touchpad)) + return; + + keyboard = dwt_init_paired_keyboard(li, touchpad); + litest_disable_tap(touchpad->libinput_device); + litest_drain_events(li); + + litest_keyboard_key(keyboard, KEY_A, true); + litest_keyboard_key(keyboard, KEY_A, false); + litest_drain_events(li); + + /* within dwt timeout, dwt blocks events */ + litest_touch_down(touchpad, 0, 50, 50); + litest_touch_move_to_extended(touchpad, 0, 50, 50, 20, 50, axes, 20, 0); + litest_assert_empty_queue(li); + + litest_timeout_dwt_short(); + libinput_dispatch(li); + litest_assert_empty_queue(li); + + /* after dwt timeout, pressure blocks events */ + litest_touch_move_to_extended(touchpad, 0, 20, 50, 50, 50, axes, 20, 0); + litest_touch_up(touchpad, 0); + + litest_assert_empty_queue(li); + + litest_delete_device(keyboard); +} +END_TEST + START_TEST(touchpad_left_handed) { struct litest_device *dev = litest_current_device(); @@ -2619,27 +2793,6 @@ START_TEST(touchpad_initial_state) } END_TEST -static inline bool -has_disable_while_typing(struct litest_device *device) -{ - return libinput_device_config_dwt_is_available(device->libinput_device); -} - -static inline struct litest_device * -dwt_init_paired_keyboard(struct libinput *li, - struct litest_device *touchpad) -{ - enum litest_device_type which = LITEST_KEYBOARD; - - if (libevdev_get_id_vendor(touchpad->evdev) == VENDOR_ID_APPLE) - which = LITEST_APPLE_KEYBOARD; - - if (libevdev_get_id_vendor(touchpad->evdev) == VENDOR_ID_CHICONY) - which = LITEST_ACER_HAWAII_KEYBOARD; - - return litest_add_device(li, which); -} - START_TEST(touchpad_dwt) { struct litest_device *touchpad = litest_current_device(); @@ -5005,6 +5158,12 @@ litest_setup_tests_touchpad(void) litest_add("touchpad:palm", touchpad_palm_detect_tool_palm_on_off, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH); litest_add("touchpad:palm", touchpad_palm_detect_tool_palm_tap, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH); + litest_add("touchpad:palm", touchpad_palm_detect_pressure, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH); + litest_add("touchpad:palm", touchpad_palm_detect_pressure_late, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH); + litest_add("touchpad:palm", touchpad_palm_detect_pressure_keep_palm, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH); + litest_add("touchpad:palm", touchpad_palm_detect_pressure_after_edge, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH); + litest_add("touchpad:palm", touchpad_palm_detect_pressure_after_dwt, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH); + litest_add("touchpad:left-handed", touchpad_left_handed, LITEST_TOUCHPAD|LITEST_BUTTON, LITEST_CLICKPAD); litest_add_for_device("touchpad:left-handed", touchpad_left_handed_appletouch, LITEST_APPLETOUCH); litest_add("touchpad:left-handed", touchpad_left_handed_clickpad, LITEST_CLICKPAD, LITEST_APPLE_CLICKPAD); diff --git a/udev/90-libinput-model-quirks.hwdb b/udev/90-libinput-model-quirks.hwdb index f9e7093a..89e95723 100644 --- a/udev/90-libinput-model-quirks.hwdb +++ b/udev/90-libinput-model-quirks.hwdb @@ -183,6 +183,7 @@ libinput:name:SynPS/2 Synaptics TouchPad:dmi:*svnLENOVO:*:pvrThinkPad??50*: libinput:name:SynPS/2 Synaptics TouchPad:dmi:*svnLENOVO:*:pvrThinkPad??60*: libinput:name:SynPS/2 Synaptics TouchPad:dmi:*svnLENOVO:*:pvrThinkPadX1Carbon3rd:* LIBINPUT_MODEL_LENOVO_T450_TOUCHPAD=1 + LIBINPUT_ATTR_PALM_PRESSURE_THRESHOLD=150 ########################################## # Logitech diff --git a/udev/parse_hwdb.py b/udev/parse_hwdb.py index 8ac64010..5b7b4538 100755 --- a/udev/parse_hwdb.py +++ b/udev/parse_hwdb.py @@ -122,8 +122,12 @@ def property_grammar(): Suppress('=') - kbintegration_tags('VALUE')] + palm_prop = [Literal('LIBINPUT_ATTR_PALM_PRESSURE_THRESHOLD')('NAME') - + Suppress('=') - + INTEGER('X')] + grammar = Or(model_props + size_props + reliability + tpkbcombo + - pressure_prop + kbintegration) + pressure_prop + kbintegration + palm_prop) return grammar From cf0d442ad346aeb89a4fcb308edfe62f2e23a5fd Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Tue, 27 Jun 2017 14:16:57 +1000 Subject: [PATCH 3/3] tools: add a tool to measure touch pressure And update the documentation for how to use the new tool. It's much more interactive than evemu and easier to grasp, so let's advertise that. Signed-off-by: Peter Hutterer --- doc/touchpad-pressure.dox | 78 +++--- meson.build | 9 + tools/libinput-measure-touchpad-pressure | 259 +++++++++++++++++++ tools/libinput-measure-touchpad-pressure.man | 63 +++++ tools/libinput-measure.man | 3 + tools/libinput.man | 3 + 6 files changed, 367 insertions(+), 48 deletions(-) create mode 100755 tools/libinput-measure-touchpad-pressure create mode 100644 tools/libinput-measure-touchpad-pressure.man diff --git a/doc/touchpad-pressure.dox b/doc/touchpad-pressure.dox index 742b19f9..b51e6e5d 100644 --- a/doc/touchpad-pressure.dox +++ b/doc/touchpad-pressure.dox @@ -47,62 +47,44 @@ locally. Note that the hwdb entry is **not public API** and **may change at any time**. Users are advised to @ref reporting_bugs "report a bug" with the updated pressure ranges when testing has completed. -First, install the "evemu" package providing the ```evemu-record``` tool. -Run ```evemu-record``` as root (without arguments) to see a list of devices -and select the touchpad device. Pipe the actual output of the tool into a -file for later analysis. For example: +Use the ```libinput measure touchpad-pressure``` tool provided by libinput. +This tool will search for your touchpad device and print some pressure +statistics, including whether a touch is/was considered logically down. +Example output of the tool is below:
-$ sudo evemu-record > touchpad-pressure.txt
-Available devices:
-/dev/input/event0:	Lid Switch
-/dev/input/event1:	Sleep Button
-/dev/input/event2:	Power Button
-/dev/input/event3:	AT Translated Set 2 keyboard
-/dev/input/event4:	SynPS/2 Synaptics TouchPad
-/dev/input/event5:	ELAN Touchscreen
-[...]
-Select the device event number [0-19]: 4
-#     Ctrl+C to quit, the output will be in touchpad-pressure.txt
+$ sudo libinput measure touchpad-pressure
+Ready for recording data.
+Pressure range used: 8:10
+Palm pressure range used: 65535
+Place a single finger on the touchpad to measure pressure values.
+Ctrl+C to exit
+ 
+Sequence 1190 pressure: min:  39 max:  48 avg:  43 median:  44 tags: down
+Sequence 1191 pressure: min:  49 max:  65 avg:  62 median:  64 tags: down
+Sequence 1192 pressure: min:  40 max:  78 avg:  64 median:  66 tags: down
+Sequence 1193 pressure: min:  36 max:  83 avg:  70 median:  73 tags: down
+Sequence 1194 pressure: min:  43 max:  76 avg:  72 median:  74 tags: down
+Touchpad pressure:  47 min:  47 max:  86 tags: down
 
-Now move a finger at **normal pressure** several times around the touchpad, -as if moving the cursor normally around the screen. Avoid any accidental -palm touches or any excessive or light pressure. +The example output shows five completed touch sequences and one ongoing one. +For each, the respective minimum and maximum pressure values are printed as +well as some statistics. The ```tags``` show that sequence was considered +logically down at some point. This is an interactive tool and its output may +change frequently. Refer to the libinput-measure-touchpad-pressure(1) man +page for more details. -The event recording is then filtered for pressure information, which is -sorted and exported to a new file: +By default, this tool uses the udev hwdb entries for the pressure range. To +narrow down on the best values for your device, specify the 'logically down' +and 'logically up' pressure thresholds with the ```--touch-thresholds`` +argument:
-$ grep --only-matching "ABS_MT_PRESSURE[ ]*[0-9]*" touchpad-pressure.txt | \
-	sed -e "s/ABS_MT_PRESSURE[ ]*//" | \
-	sort -n | uniq -c > touchpad-pressure-statistics.txt
+$ sudo libinput measure touchpad-pressure --touch-thresholds=10:8
 
-The file contains a list of (count, pressure-value) tuples which can be -visualized with gnuplot. Copy the following into a file named -```touchpad-pressure-statistics.gnuplot```: - -
-set style data lines
-plot 'touchpad-pressure-statistics.txt' using 2:1
-pause -1
-
- -Now, you can visualize the touchpad pressure curve with the following -command: -
-$ gnuplot  touchpad-pressure-statistics.gnuplot
-
- -The visualization will show a curve with the various pressure ranges, see -[this bugzilla attachment](https://bugs.freedesktop.org/attachment.cgi?id=130659). -In most cases, the thresholds can be guessed based on this curve. libinput -employes a [Schmitt trigger](https://en.wikipedia.org/wiki/Schmitt_trigger) -with an upper threshold and a lower threshold. A touch is detected when the -pressure goes above the high threshold, a release is detected when the -pressure fallse below the low threshold. Thus, an ideal threshold -combination is with a high threshold slightly above the minimum threshold, a -low threshold on the minimum threshold. +Interact with the touchpad and check if the output of this tool matches your +expectations. Once the thresholds are decided on (e.g. 10 and 8), they can be enabled with the following hwdb file: diff --git a/meson.build b/meson.build index 5ee15204..764e354d 100644 --- a/meson.build +++ b/meson.build @@ -421,6 +421,15 @@ configure_file(input : 'tools/libinput-measure-touchpad-tap.man', install_dir : join_paths(get_option('mandir'), 'man1') ) +install_data('tools/libinput-measure-touchpad-pressure', + install_dir : libinput_tool_path) +configure_file(input : 'tools/libinput-measure-touchpad-pressure.man', + output : 'libinput-measure-touchpad-pressure.1', + configuration : man_config, + install : true, + install_dir : join_paths(get_option('mandir'), 'man1') + ) + if get_option('debug-gui') dep_gtk = dependency('gtk+-3.0') dep_cairo = dependency('cairo') diff --git a/tools/libinput-measure-touchpad-pressure b/tools/libinput-measure-touchpad-pressure new file mode 100755 index 00000000..6e827801 --- /dev/null +++ b/tools/libinput-measure-touchpad-pressure @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +# +# Copyright © 2017 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice (including the next +# paragraph) shall be included in all copies or substantial portions of the +# Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +import sys +import argparse +import evdev +import evdev.ecodes +import pyudev + +class Range(object): + """Class to keep a min/max of a value around""" + def __init__(self): + self.min = float('inf') + self.max = float('-inf') + + def update(self, value): + self.min = min(self.min, value) + self.max = max(self.max, value) + +class Touch(object): + """A single data point of a sequence (i.e. one event frame)""" + + def __init__(self, pressure=None): + self.pressure = pressure + +class TouchSequence(object): + """A touch sequence from beginning to end""" + + def __init__(self, device, tracking_id): + self.device = device + self.tracking_id = tracking_id + self.points = [] + + self.is_active = True + + self.is_down = False + self.was_down = False + self.is_palm = False + self.was_palm = False + + self.prange = Range() + + def append(self, touch): + """Add a Touch to the sequence""" + self.points.append(touch) + self.prange.update(touch.pressure) + + if touch.pressure < self.device.up: + self.is_down = False + elif touch.pressure > self.device.down: + self.is_down = True + self.was_down = True + + self.is_palm = touch.pressure > self.device.palm + if self.is_palm: + self.was_palm = True + + def finalize(self): + """Mark the TouchSequence as complete (finger is up)""" + self.is_active = False + + def avg(self): + """Average pressure value of this sequence""" + return int(sum([p.pressure for p in self.points])/len(self.points)) + + def median(self): + """Median pressure value of this sequence""" + ps = sorted([p.pressure for p in self.points]) + idx = int(len(self.points)/2) + return ps[idx] + + def __str__(self): + return self._str_state() if self.is_active else self._str_summary() + + def _str_summary(self): + s = "Sequence {} pressure: min: {:3d} max: {:3d} avg: {:3d} median: {:3d} tags:".format( + self.tracking_id, + self.prange.min, + self.prange.max, + self.avg(), + self.median()) + if self.was_down: + s += " down" + if self.was_palm: + s += " palm" + + return s + + def _str_state(self): + s = "Touchpad pressure: {:3d} min: {:3d} max: {:3d} tags: {} {}".format( + self.points[-1].pressure, + self.prange.min, + self.prange.max, + "down" if self.is_down else " ", + "palm" if self.is_palm else " " + ) + return s + +class InvalidDeviceError(Exception): + pass + +class Device(object): + def __init__(self, path): + if path is None: + self.path = self.find_touchpad_device() + else: + self.path = path + + self.device = evdev.InputDevice(self.path) + # capabilities rturns a dict with the EV_* codes as key, + # each of which is a list of tuples of (code, AbsInfo) + # + # Get the abs list first (or empty list if missing), + # then extract the pressure absinfo from that + caps = self.device.capabilities(absinfo=True).get(evdev.ecodes.EV_ABS, []) + p = [cap[1] for cap in caps if cap[0] == evdev.ecodes.ABS_MT_PRESSURE] + if not p: + raise InvalidDeviceError("device does not have ABS_MT_PRESSURE") + + p = p[0] + prange = p.max - p.min + + # libinput defaults + self.up = int(p.min + 0.12 * prange) + self.down = int(p.min + 0.10 * prange) + self.palm = 130 # the libinput default + + self._init_thresholds_from_udev() + self.sequences = [] + + def find_touchpad_device(self): + context = pyudev.Context() + for device in context.list_devices(subsystem='input'): + if not device.get('ID_INPUT_TOUCHPAD', 0): + continue + + if not device.device_node or not device.device_node.startswith('/dev/input/event'): + continue + + return device.device_node + print("Unable to find a touchpad device.", file=sys.stderr) + sys.exit(1) + + def _init_thresholds_from_udev(self): + context = pyudev.Context() + ud = pyudev.Devices.from_device_file(context, self.path) + v = ud.get('LIBINPUT_ATTR_PRESSURE_RANGE') + if v: + self.up, self.down = colon_tuple(v) + + v = ud.get('LIBINPUT_ATTR_PALM_PRESSURE_THRESHOLD') + if v: + self.palm = int(v) + + def start_new_sequence(self, tracking_id): + self.sequences.append(TouchSequence(self, tracking_id)) + + def current_sequence(self): + return self.sequences[-1] + +def handle_key(device, event): + tapcodes = [evdev.ecodes.BTN_TOOL_DOUBLETAP, + evdev.ecodes.BTN_TOOL_TRIPLETAP, + evdev.ecodes.BTN_TOOL_QUADTAP, + evdev.ecodes.BTN_TOOL_QUINTTAP] + if event.code in tapcodes and event.value > 0: + print("\rThis tool cannot handle multiple fingers, output will be invalid", file=sys.stderr) + +def handle_abs(device, event): + if event.code == evdev.ecodes.ABS_MT_TRACKING_ID: + if event.value > -1: + device.start_new_sequence(event.value) + else: + s = device.current_sequence() + s.finalize() + print("\r{}".format(s)) + elif event.code == evdev.ecodes.ABS_MT_PRESSURE: + s = device.current_sequence() + s.append(Touch(pressure=event.value)) + print("\r{}".format(s), end="") + +def handle_event(device, event): + if event.type == evdev.ecodes.EV_ABS: + handle_abs(device, event) + elif event.type == evdev.ecodes.EV_KEY: + handle_key(device, event) + +def loop(device): + print("Ready for recording data.") + print("Pressure range used: {}:{}".format(device.down, device.up)) + print("Palm pressure range used: {}".format(device.palm)) + print("Place a single finger on the touchpad to measure pressure values.\n" + "Ctrl+C to exit\n") + + for event in device.device.read_loop(): + handle_event(device, event) + +def colon_tuple(string): + try: + ts = string.split(':') + t = tuple([int(x) for x in ts]) + if len(t) == 2 and t[0] >= t[1]: + return t + except: + pass + + msg = "{} is not in format N:M (N >= M)".format(string) + raise argparse.ArgumentTypeError(msg) + +def main(args): + parser = argparse.ArgumentParser(description="Measure touchpad pressure values") + parser.add_argument('path', metavar='/dev/input/event0', + nargs='?', type=str, help='Path to device (optional)' ) + parser.add_argument('--touch-thresholds', metavar='down:up', + type=colon_tuple, help='Thresholds when a touch is logically down or up') + parser.add_argument('--palm-threshold', metavar='t', + type=int, help='Threshold when a touch is a palm') + args = parser.parse_args() + + try: + device = Device(args.path) + + if args.touch_thresholds is not None: + device.down, device.up = args.touch_thresholds + + if args.palm_threshold is not None: + device.palm = args.palm_threshold + + loop(device) + except KeyboardInterrupt: + pass + except (PermissionError, OSError): + print("Error: failed to open device") + except InvalidDeviceError as e: + print("Error: {}".format(e)) + +if __name__ == "__main__": + main(sys.argv) diff --git a/tools/libinput-measure-touchpad-pressure.man b/tools/libinput-measure-touchpad-pressure.man new file mode 100644 index 00000000..67f0d687 --- /dev/null +++ b/tools/libinput-measure-touchpad-pressure.man @@ -0,0 +1,63 @@ +.TH libinput-measure-touchpad-pressure "1" +.SH NAME +libinput\-measure\-touchpad\-pressure \- measure pressure properties of devices +.SH SYNOPSIS +.B libinput measure touchpad\-pressure [\-\-help] [options] +[\fI/dev/input/event0\fI] +.SH DESCRIPTION +.PP +The +.B "libinput measure touchpad\-pressure" +tool measures the pressure of touches on a touchpad. This is +an interactive tool. When executed, the tool will prompt the user to +interact with the touchpad. On termination, the tool prints a summary of the +pressure values seen. This data should be attached to any +pressure\-related bug report. +.PP +For a full description on how libinput's pressure-to-click behavior works, see +the online documentation here: +.I https://wayland.freedesktop.org/libinput/doc/latest/touchpad_pressure.html +and +.I https://wayland.freedesktop.org/libinput/doc/latest/palm_detection.html +.PP +This is a debugging tool only, its output may change at any time. Do not +rely on the output. +.PP +This tool usually needs to be run as root to have access to the +/dev/input/eventX nodes. +.SH OPTIONS +If a device node is given, this tool opens that device node. Otherwise, this +tool searches for the first node that looks like a touchpad and uses that +node. +.TP 8 +.B \-\-help +Print help +.TP 8 +.B \-\-touch\-thresholds=\fI"down:up"\fR +Set the logical touch pressure thresholds to +.I down +and +.I up, +respectively. When a touch exceeds the pressure in +.I down +it is considered logically down. If a touch is logically down and goes below +the pressure in +.I up, +it is considered logically up. The thresholds have to be in +device-specific pressure values and it is required that +.I down +>= +.I up. +.TP 8 +.B \-\-palm\-threshold=\fIN\fR +Assume a palm threshold of +.I N. +The threshold has to be in device-specific pressure values. +.PP +If the touch-thresholds or the palm-threshold are not provided, +this tool uses the thresholds provided by the udev hwdb (if any) or the +built-in defaults. +.SH LIBINPUT +Part of the +.B libinput(1) +suite diff --git a/tools/libinput-measure.man b/tools/libinput-measure.man index 843f001e..d91afdd0 100644 --- a/tools/libinput-measure.man +++ b/tools/libinput-measure.man @@ -24,6 +24,9 @@ Features that can be measured include .TP 8 .B libinput\-measure\-touchpad\-tap\-time(1) Measure tap-to-click time +.TP 8 +.B libinput\-measure\-touchpad\-pressure(1) +Measure touch pressure .SH LIBINPUT Part of the .B libinput(1) diff --git a/tools/libinput.man b/tools/libinput.man index 3098f5e3..a093465f 100644 --- a/tools/libinput.man +++ b/tools/libinput.man @@ -47,6 +47,9 @@ Measure various properties of devices .TP 8 .B libinput\-measure\-touchpad\-tap(1) Measure tap-to-click time +.TP 8 +.B libinput\-measure\-touchpad\-pressure(1) +Measure touch pressure. .SH LIBINPUT Part of the .B libinput(1)