mirror of
https://gitlab.freedesktop.org/libinput/libinput.git
synced 2025-12-20 13:50:15 +01:00
Let's enforce a consistent (and verifiable) style everywhere. Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
396 lines
14 KiB
Python
Executable file
396 lines
14 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# vim: set expandtab shiftwidth=4:
|
|
# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil -*- */
|
|
#
|
|
# Copyright © 2020 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
|
|
|
|
try:
|
|
import libevdev
|
|
import pyudev
|
|
except ModuleNotFoundError as e:
|
|
print("Error: {}".format(str(e)), file=sys.stderr)
|
|
print(
|
|
"One or more python modules are missing. Please install those "
|
|
"modules and re-run this tool."
|
|
)
|
|
sys.exit(1)
|
|
|
|
|
|
class DeviceError(Exception):
|
|
pass
|
|
|
|
|
|
class Point:
|
|
def __init__(self, x=None, y=None):
|
|
self.x = x
|
|
self.y = y
|
|
|
|
|
|
class Touchpad(object):
|
|
def __init__(self, evdev):
|
|
x = evdev.absinfo[libevdev.EV_ABS.ABS_X]
|
|
y = evdev.absinfo[libevdev.EV_ABS.ABS_Y]
|
|
if not x or not y:
|
|
raise DeviceError("Device does not have an x or axis")
|
|
|
|
if not x.resolution or not y.resolution:
|
|
print("Device does not have resolutions.", file=sys.stderr)
|
|
x.resolution = 1
|
|
y.resolution = 1
|
|
|
|
self.xrange = x.maximum - x.minimum
|
|
self.yrange = y.maximum - y.minimum
|
|
self.width = self.xrange / x.resolution
|
|
self.height = self.yrange / y.resolution
|
|
|
|
self._x = x
|
|
self._y = y
|
|
|
|
# We try to make the touchpad at least look proportional. The
|
|
# terminal character space is (guesswork) ca 2.3 times as high as
|
|
# wide.
|
|
self.columns = 30
|
|
self.rows = int(
|
|
self.columns
|
|
* (self.yrange // y.resolution)
|
|
// (self.xrange // x.resolution)
|
|
/ 2.3
|
|
)
|
|
self.pos = Point(0, 0)
|
|
self.min = Point()
|
|
self.max = Point()
|
|
|
|
@property
|
|
def x(self):
|
|
return self._x
|
|
|
|
@property
|
|
def y(self):
|
|
return self._y
|
|
|
|
@x.setter
|
|
def x(self, x):
|
|
self._x.minimum = min(self.x.minimum, x)
|
|
self._x.maximum = max(self.x.maximum, x)
|
|
self.min.x = min(x, self.min.x or 0xFFFFFFFF)
|
|
self.max.x = max(x, self.max.x or -0xFFFFFFFF)
|
|
# we calculate the position based on the original range.
|
|
# this means on devices with a narrower range than advertised, not
|
|
# all corners may be reachable in the touchpad drawing.
|
|
self.pos.x = min(0.99, (x - self._x.minimum) / self.xrange)
|
|
|
|
@y.setter
|
|
def y(self, y):
|
|
self._y.minimum = min(self.y.minimum, y)
|
|
self._y.maximum = max(self.y.maximum, y)
|
|
self.min.y = min(y, self.min.y or 0xFFFFFFFF)
|
|
self.max.y = max(y, self.max.y or -0xFFFFFFFF)
|
|
# we calculate the position based on the original range.
|
|
# this means on devices with a narrower range than advertised, not
|
|
# all corners may be reachable in the touchpad drawing.
|
|
self.pos.y = min(0.99, (y - self._y.minimum) / self.yrange)
|
|
|
|
def update_from_data(self):
|
|
if None in [self.min.x, self.min.y, self.max.x, self.max.y]:
|
|
raise DeviceError("Insufficient data to continue")
|
|
self._x.minimum = self.min.x
|
|
self._x.maximum = self.max.x
|
|
self._y.minimum = self.min.y
|
|
self._y.maximum = self.max.y
|
|
|
|
def draw(self):
|
|
print(
|
|
"Detected axis range: x [{:4d}..{:4d}], y [{:4d}..{:4d}]".format(
|
|
self.min.x if self.min.x is not None else 0,
|
|
self.max.x if self.max.x is not None else 0,
|
|
self.min.y if self.min.y is not None else 0,
|
|
self.max.y if self.max.y is not None else 0,
|
|
)
|
|
)
|
|
|
|
print()
|
|
print("Move one finger along all edges of the touchpad".center(self.columns))
|
|
print("until the detected axis range stops changing.".center(self.columns))
|
|
|
|
top = int(self.pos.y * self.rows)
|
|
|
|
print("+{}+".format("".ljust(self.columns, "-")))
|
|
for row in range(0, top):
|
|
print("|{}|".format("".ljust(self.columns)))
|
|
|
|
left = int(self.pos.x * self.columns)
|
|
right = max(0, self.columns - 1 - left)
|
|
print("|{}{}{}|".format("".ljust(left), "O", "".ljust(right)))
|
|
|
|
for row in range(top + 1, self.rows):
|
|
print("|{}|".format("".ljust(self.columns)))
|
|
|
|
print("+{}+".format("".ljust(self.columns, "-")))
|
|
|
|
print("Press Ctrl+C to stop".center(self.columns))
|
|
|
|
print("\033[{}A".format(self.rows + 8), flush=True)
|
|
|
|
self.rows_printed = self.rows + 8
|
|
|
|
def erase(self):
|
|
# Erase all previous lines so we're not left with rubbish
|
|
for row in range(self.rows_printed):
|
|
print("\033[K")
|
|
print("\033[{}A".format(self.rows_printed))
|
|
|
|
|
|
def dimension(string):
|
|
try:
|
|
ts = string.split("x")
|
|
t = tuple([int(x) for x in ts])
|
|
if len(t) == 2:
|
|
return t
|
|
except: # noqa
|
|
pass
|
|
|
|
msg = "{} is not in format WxH".format(string)
|
|
raise argparse.ArgumentTypeError(msg)
|
|
|
|
|
|
def between(v1, v2, deviation):
|
|
return v1 - deviation < v2 < v1 + deviation
|
|
|
|
|
|
def dmi_modalias_match(modalias):
|
|
modalias = modalias.split(":")
|
|
dmi = {"svn": None, "pvr": None, "pn": None}
|
|
for m in modalias:
|
|
for key in dmi:
|
|
if m.startswith(key):
|
|
dmi[key] = m[len(key) :]
|
|
|
|
# Based on the current 60-evdev.hwdb, Lenovo uses pvr and everyone else
|
|
# uses pn to provide a human-identifiable match
|
|
if dmi["svn"] == "LENOVO":
|
|
return "dmi:*svn{}:*pvr{}*".format(dmi["svn"], dmi["pvr"])
|
|
else:
|
|
return "dmi:*svn{}:*pn{}*".format(dmi["svn"], dmi["pn"])
|
|
|
|
|
|
def main(args):
|
|
parser = argparse.ArgumentParser(description="Measure the touchpad size")
|
|
parser.add_argument(
|
|
"size",
|
|
metavar="WxH",
|
|
type=dimension,
|
|
help="Touchpad size (width by height) in mm",
|
|
)
|
|
parser.add_argument(
|
|
"path",
|
|
metavar="/dev/input/event0",
|
|
nargs="?",
|
|
type=str,
|
|
help="Path to device (optional)",
|
|
)
|
|
context = pyudev.Context()
|
|
|
|
args = parser.parse_args()
|
|
if not args.path:
|
|
for device in context.list_devices(subsystem="input"):
|
|
if device.get("ID_INPUT_TOUCHPAD", 0) and (
|
|
device.device_node or ""
|
|
).startswith("/dev/input/event"):
|
|
args.path = device.device_node
|
|
name = "unknown"
|
|
parent = device
|
|
while parent is not None:
|
|
n = parent.get("NAME", None)
|
|
if n:
|
|
name = n
|
|
break
|
|
parent = parent.parent
|
|
|
|
print("Using {}: {}".format(name, device.device_node))
|
|
break
|
|
else:
|
|
print("Unable to find a touchpad device.", file=sys.stderr)
|
|
return 1
|
|
|
|
dev = pyudev.Devices.from_device_file(context, args.path)
|
|
overrides = [p for p in dev.properties if p.startswith("EVDEV_ABS")]
|
|
if overrides:
|
|
print()
|
|
print("********************************************************************")
|
|
print("WARNING: axis overrides already in place for this device:")
|
|
for prop in overrides:
|
|
print(" {}={}".format(prop, dev.properties[prop]))
|
|
print("The systemd hwdb already overrides the axis ranges and/or resolution.")
|
|
print("This tool is not needed unless you want to verify the axis overrides.")
|
|
print("********************************************************************")
|
|
print()
|
|
|
|
try:
|
|
fd = open(args.path, "rb")
|
|
evdev = libevdev.Device(fd)
|
|
touchpad = Touchpad(evdev)
|
|
print(
|
|
"Kernel specified touchpad size: {:.1f}x{:.1f}mm".format(
|
|
touchpad.width, touchpad.height
|
|
)
|
|
)
|
|
print("User specified touchpad size: {:.1f}x{:.1f}mm".format(*args.size))
|
|
|
|
print()
|
|
print(
|
|
"Kernel axis range: x [{:4d}..{:4d}], y [{:4d}..{:4d}]".format(
|
|
touchpad.x.minimum,
|
|
touchpad.x.maximum,
|
|
touchpad.y.minimum,
|
|
touchpad.y.maximum,
|
|
)
|
|
)
|
|
|
|
print("Put your finger on the touchpad to start\033[1A")
|
|
|
|
try:
|
|
touchpad.draw()
|
|
while True:
|
|
for event in evdev.events():
|
|
if event.matches(libevdev.EV_ABS.ABS_X):
|
|
touchpad.x = event.value
|
|
elif event.matches(libevdev.EV_ABS.ABS_Y):
|
|
touchpad.y = event.value
|
|
elif event.matches(libevdev.EV_SYN.SYN_REPORT):
|
|
touchpad.draw()
|
|
except KeyboardInterrupt:
|
|
touchpad.erase()
|
|
touchpad.update_from_data()
|
|
|
|
print(
|
|
"Detected axis range: x [{:4d}..{:4d}], y [{:4d}..{:4d}]".format(
|
|
touchpad.x.minimum,
|
|
touchpad.x.maximum,
|
|
touchpad.y.minimum,
|
|
touchpad.y.maximum,
|
|
)
|
|
)
|
|
|
|
touchpad.x.resolution = round(
|
|
(touchpad.x.maximum - touchpad.x.minimum) / args.size[0]
|
|
)
|
|
touchpad.y.resolution = round(
|
|
(touchpad.y.maximum - touchpad.y.minimum) / args.size[1]
|
|
)
|
|
|
|
print(
|
|
"Resolutions calculated based on user-specified size: x {}, y {} units/mm".format(
|
|
touchpad.x.resolution, touchpad.y.resolution
|
|
)
|
|
)
|
|
|
|
# If both x/y are within some acceptable deviation, we skip the axis
|
|
# overrides and only override the resolution
|
|
xorig = evdev.absinfo[libevdev.EV_ABS.ABS_X]
|
|
yorig = evdev.absinfo[libevdev.EV_ABS.ABS_Y]
|
|
deviation = 1.5 * touchpad.x.resolution # 1.5 mm rounding on each side
|
|
skip = between(xorig.minimum, touchpad.x.minimum, deviation)
|
|
skip = skip and between(xorig.maximum, touchpad.x.maximum, deviation)
|
|
deviation = 1.5 * touchpad.y.resolution # 1.5 mm rounding on each side
|
|
skip = skip and between(yorig.minimum, touchpad.y.minimum, deviation)
|
|
skip = skip and between(yorig.maximum, touchpad.y.maximum, deviation)
|
|
|
|
if skip:
|
|
print()
|
|
print(
|
|
"Note: Axis ranges within acceptable deviation, skipping min/max override"
|
|
)
|
|
print()
|
|
|
|
print()
|
|
print("Suggested hwdb entry:")
|
|
|
|
use_dmi = evdev.id["bustype"] not in [0x03, 0x05] # USB, Bluetooth
|
|
if use_dmi:
|
|
modalias = open("/sys/class/dmi/id/modalias").read().strip()
|
|
print(
|
|
"Note: the dmi modalias match is a guess based on your machine's modalias:"
|
|
)
|
|
print(" ", modalias)
|
|
print(
|
|
"Please verify that this is the most sensible match and adjust if necessary."
|
|
)
|
|
|
|
print("-8<--------------------------")
|
|
print("# Laptop model description (e.g. Lenovo X1 Carbon 5th)")
|
|
if use_dmi:
|
|
print("evdev:name:{}:{}*".format(evdev.name, dmi_modalias_match(modalias)))
|
|
else:
|
|
print(
|
|
"evdev:input:b{:04X}v{:04X}p{:04X}*".format(
|
|
evdev.id["bustype"], evdev.id["vendor"], evdev.id["product"]
|
|
)
|
|
)
|
|
print(
|
|
" EVDEV_ABS_00={}:{}:{}".format(
|
|
touchpad.x.minimum if not skip else "",
|
|
touchpad.x.maximum if not skip else "",
|
|
touchpad.x.resolution,
|
|
)
|
|
)
|
|
print(
|
|
" EVDEV_ABS_01={}:{}:{}".format(
|
|
touchpad.y.minimum if not skip else "",
|
|
touchpad.y.maximum if not skip else "",
|
|
touchpad.y.resolution,
|
|
)
|
|
)
|
|
if evdev.absinfo[libevdev.EV_ABS.ABS_MT_POSITION_X]:
|
|
print(
|
|
" EVDEV_ABS_35={}:{}:{}".format(
|
|
touchpad.x.minimum if not skip else "",
|
|
touchpad.x.maximum if not skip else "",
|
|
touchpad.x.resolution,
|
|
)
|
|
)
|
|
print(
|
|
" EVDEV_ABS_36={}:{}:{}".format(
|
|
touchpad.y.minimum if not skip else "",
|
|
touchpad.y.maximum if not skip else "",
|
|
touchpad.y.resolution,
|
|
)
|
|
)
|
|
print("-8<--------------------------")
|
|
print(
|
|
"Instructions on what to do with this snippet are in /usr/lib/udev/hwdb.d/60-evdev.hwdb"
|
|
)
|
|
except DeviceError as e:
|
|
print("Error: {}".format(e), file=sys.stderr)
|
|
return 1
|
|
except PermissionError:
|
|
print("Unable to open device. Please run me as root", file=sys.stderr)
|
|
return 1
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv))
|