| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367 |
- #!/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 argparse
- import sys
- try:
- import libevdev
- import pyudev
- except ModuleNotFoundError as e:
- print(f"Error: {e!s}", 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:
- 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):
- min_x = self.min.x if self.min.x is not None else 0
- max_x = self.max.x if self.max.x is not None else 0
- min_y = self.min.y if self.min.y is not None else 0
- max_y = self.max.y if self.max.y is not None else 0
- print(
- f"Detected axis range: x [{min_x:4d}..{max_x:4d}], y [{min_y:4d}..{max_y:4d}]"
- )
- 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(f"+{''.ljust(self.columns, '-')}+")
- for row in range(top):
- print(f"|{''.ljust(self.columns)}|")
- left = int(self.pos.x * self.columns)
- right = max(0, self.columns - 1 - left)
- print(f"|{''.ljust(left)}O{''.ljust(right)}|")
- for row in range(top + 1, self.rows):
- print(f"|{''.ljust(self.columns)}|")
- print(f"+{''.ljust(self.columns, '-')}+")
- print("Press Ctrl+C to stop".center(self.columns))
- print(f"\033[{self.rows + 8}A", 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(f"\033[{self.rows_printed}A")
- 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 = f"{string} is not in format WxH"
- 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 f"dmi:*svn{dmi['svn']}:*pvr{dmi['pvr']}*"
- else:
- return f"dmi:*svn{dmi['svn']}:*pn{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(f"Using {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(f" {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") # noqa: SIM115
- evdev = libevdev.Device(fd)
- touchpad = Touchpad(evdev)
- print(
- f"Kernel specified touchpad size: {touchpad.width:.1f}x{touchpad.height:.1f}mm"
- )
- print(
- f"User specified touchpad size: {args.size[0]:.1f}x{args.size[1]:.1f}mm"
- )
- print()
- print(
- f"Kernel axis range: x [{touchpad.x.minimum:4d}..{touchpad.x.maximum:4d}],"
- f" y [{touchpad.y.minimum:4d}..{touchpad.y.maximum:4d}]"
- )
- 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(
- f"Detected axis range: x [{touchpad.x.minimum:4d}..{touchpad.x.maximum:4d}],"
- f" y [{touchpad.y.minimum:4d}..{touchpad.y.maximum:4d}]"
- )
- 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(
- f"Resolutions calculated based on user-specified size:"
- f" x {touchpad.x.resolution}, y {touchpad.y.resolution} units/mm"
- )
- # 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:
- with open("/sys/class/dmi/id/modalias") as f:
- modalias = f.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(f"evdev:name:{evdev.name}:{dmi_modalias_match(modalias)}*")
- else:
- print(
- f"evdev:input:b{evdev.id['bustype']:04X}"
- f"v{evdev.id['vendor']:04X}"
- f"p{evdev.id['product']:04X}*"
- )
- xmin = touchpad.x.minimum if not skip else ""
- xmax = touchpad.x.maximum if not skip else ""
- ymin = touchpad.y.minimum if not skip else ""
- ymax = touchpad.y.maximum if not skip else ""
- print(f" EVDEV_ABS_00={xmin}:{xmax}:{touchpad.x.resolution}")
- print(f" EVDEV_ABS_01={ymin}:{ymax}:{touchpad.y.resolution}")
- if evdev.absinfo[libevdev.EV_ABS.ABS_MT_POSITION_X]:
- print(f" EVDEV_ABS_35={xmin}:{xmax}:{touchpad.x.resolution}")
- print(f" EVDEV_ABS_36={ymin}:{ymax}:{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(f"Error: {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))
|