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/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/src/evdev-mt-touchpad.c b/src/evdev-mt-touchpad.c index 2d39e18d..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(); @@ -2278,16 +2312,52 @@ 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; } +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/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) diff --git a/udev/90-libinput-model-quirks.hwdb b/udev/90-libinput-model-quirks.hwdb index 955c3acf..3ec1ac1f 100644 --- a/udev/90-libinput-model-quirks.hwdb +++ b/udev/90-libinput-model-quirks.hwdb @@ -186,6 +186,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