brintos

brintos / linux-shallow public Read only

0
0
Text · 47.5 KiB · b62c7db Raw
1199 lines · python
1#!/bin/env python32# SPDX-License-Identifier: GPL-2.03# -*- coding: utf-8 -*-4#5# Copyright (c) 2017 Benjamin Tissoires <benjamin.tissoires@gmail.com>6# Copyright (c) 2017 Red Hat, Inc.7# Copyright (c) 2020 Wacom Technology Corp.8#9# Authors:10#     Jason Gerecke <jason.gerecke@wacom.com>11 12"""13Tests for the Wacom driver generic codepath.14 15This module tests the function of the Wacom driver's generic codepath.16The generic codepath is used by devices which are not explicitly listed17in the driver's device table. It uses the device's HID descriptor to18decode reports sent by the device.19"""20 21from .descriptors_wacom import (22    wacom_pth660_v145,23    wacom_pth660_v150,24    wacom_pth860_v145,25    wacom_pth860_v150,26    wacom_pth460_v105,27)28 29import attr30from collections import namedtuple31from enum import Enum32from hidtools.hut import HUT33from hidtools.hid import HidUnit34from . import base35from . import test_multitouch36import libevdev37import pytest38 39import logging40 41logger = logging.getLogger("hidtools.test.wacom")42 43KERNEL_MODULE = ("wacom", "wacom")44 45 46class ProximityState(Enum):47    """48    Enumeration of allowed proximity states.49    """50 51    # Tool is not able to be sensed by the device52    OUT = 053 54    # Tool is close enough to be sensed, but some data may be invalid55    # or inaccurate56    IN_PROXIMITY = 157 58    # Tool is close enough to be sensed with high accuracy. All data59    # valid.60    IN_RANGE = 261 62    def fill(self, reportdata):63        """Fill a report with approrpiate HID properties/values."""64        reportdata.inrange = self in [ProximityState.IN_RANGE]65        reportdata.wacomsense = self in [66            ProximityState.IN_PROXIMITY,67            ProximityState.IN_RANGE,68        ]69 70 71class ReportData:72    """73    Placeholder for HID report values.74    """75 76    pass77 78 79@attr.s80class Buttons:81    """82    Stylus button state.83 84    Describes the state of each of the buttons / "side switches" that85    may be present on a stylus. Buttons set to 'None' indicate the86    state is "unchanged" since the previous event.87    """88 89    primary = attr.ib(default=None)90    secondary = attr.ib(default=None)91    tertiary = attr.ib(default=None)92 93    @staticmethod94    def clear():95        """Button object with all states cleared."""96        return Buttons(False, False, False)97 98    def fill(self, reportdata):99        """Fill a report with approrpiate HID properties/values."""100        reportdata.barrelswitch = int(self.primary or 0)101        reportdata.secondarybarrelswitch = int(self.secondary or 0)102        reportdata.b3 = int(self.tertiary or 0)103 104 105@attr.s106class ToolID:107    """108    Stylus tool identifiers.109 110    Contains values used to identify a specific stylus, e.g. its serial111    number and tool-type identifier. Values of ``0`` may sometimes be112    used for the out-of-range condition.113    """114 115    serial = attr.ib()116    tooltype = attr.ib()117 118    @staticmethod119    def clear():120        """ToolID object with all fields cleared."""121        return ToolID(0, 0)122 123    def fill(self, reportdata):124        """Fill a report with approrpiate HID properties/values."""125        reportdata.transducerserialnumber = self.serial & 0xFFFFFFFF126        reportdata.serialhi = (self.serial >> 32) & 0xFFFFFFFF127        reportdata.tooltype = self.tooltype128 129 130@attr.s131class PhysRange:132    """133    Range of HID physical values, with units.134    """135 136    unit = attr.ib()137    min_size = attr.ib()138    max_size = attr.ib()139 140    CENTIMETER = HidUnit.from_string("SILinear: cm")141    DEGREE = HidUnit.from_string("EnglishRotation: deg")142 143    def contains(self, field):144        """145        Check if the physical size of the provided field is in range.146 147        Compare the physical size described by the provided HID field148        against the range of sizes described by this object. This is149        an exclusive range comparison (e.g. 0 cm is not within the150        range 0 cm - 5 cm) and exact unit comparison (e.g. 1 inch is151        not within the range 0 cm - 5 cm).152        """153        phys_size = (field.physical_max - field.physical_min) * 10 ** (field.unit_exp)154        return (155            field.unit == self.unit.value156            and phys_size > self.min_size157            and phys_size < self.max_size158        )159 160 161class BaseTablet(base.UHIDTestDevice):162    """163    Skeleton object for all kinds of tablet devices.164    """165 166    def __init__(self, rdesc, name=None, info=None):167        assert rdesc is not None168        super().__init__(name, "Pen", input_info=info, rdesc=rdesc)169        self.buttons = Buttons.clear()170        self.toolid = ToolID.clear()171        self.proximity = ProximityState.OUT172        self.offset = 0173        self.ring = -1174        self.ek0 = False175 176    def match_evdev_rule(self, application, evdev):177        """178        Filter out evdev nodes based on the requested application.179 180        The Wacom driver may create several device nodes for each USB181        interface device. It is crucial that we run tests with the182        expected device node or things will obviously go off the rails.183        Use the Wacom driver's usual naming conventions to apply a184        sensible default filter.185        """186        if application in ["Pen", "Pad"]:187            return evdev.name.endswith(application)188        else:189            return True190 191    def create_report(192        self, x, y, pressure, buttons=None, toolid=None, proximity=None, reportID=None193    ):194        """195        Return an input report for this device.196 197        :param x: absolute x198        :param y: absolute y199        :param pressure: pressure200        :param buttons: stylus button state. Use ``None`` for unchanged.201        :param toolid: tool identifiers. Use ``None`` for unchanged.202        :param proximity: a ProximityState indicating the sensor's ability203             to detect and report attributes of this tool. Use ``None``204             for unchanged.205        :param reportID: the numeric report ID for this report, if needed206        """207        if buttons is not None:208            self.buttons = buttons209        buttons = self.buttons210 211        if toolid is not None:212            self.toolid = toolid213        toolid = self.toolid214 215        if proximity is not None:216            self.proximity = proximity217        proximity = self.proximity218 219        reportID = reportID or self.default_reportID220 221        report = ReportData()222        report.x = x223        report.y = y224        report.tippressure = pressure225        report.tipswitch = pressure > 0226        buttons.fill(report)227        proximity.fill(report)228        toolid.fill(report)229 230        return super().create_report(report, reportID=reportID)231 232    def create_report_heartbeat(self, reportID):233        """234        Return a heartbeat input report for this device.235 236        Heartbeat reports generally contain battery status information,237        among other things.238        """239        report = ReportData()240        report.wacombatterycharging = 1241        return super().create_report(report, reportID=reportID)242 243    def create_report_pad(self, reportID, ring, ek0):244        report = ReportData()245 246        if ring is not None:247            self.ring = ring248        ring = self.ring249 250        if ek0 is not None:251            self.ek0 = ek0252        ek0 = self.ek0253 254        if ring >= 0:255            report.wacomtouchring = ring256            report.wacomtouchringstatus = 1257        else:258            report.wacomtouchring = 0x7F259            report.wacomtouchringstatus = 0260 261        report.wacomexpresskey00 = ek0262        return super().create_report(report, reportID=reportID)263 264    def event(self, x, y, pressure, buttons=None, toolid=None, proximity=None):265        """266        Send an input event on the default report ID.267 268        :param x: absolute x269        :param y: absolute y270        :param buttons: stylus button state. Use ``None`` for unchanged.271        :param toolid: tool identifiers. Use ``None`` for unchanged.272        :param proximity: a ProximityState indicating the sensor's ability273             to detect and report attributes of this tool. Use ``None``274             for unchanged.275        """276        r = self.create_report(x, y, pressure, buttons, toolid, proximity)277        self.call_input_event(r)278        return [r]279 280    def event_heartbeat(self, reportID):281        """282        Send a heartbeat event on the requested report ID.283        """284        r = self.create_report_heartbeat(reportID)285        self.call_input_event(r)286        return [r]287 288    def event_pad(self, reportID, ring=None, ek0=None):289        """290        Send a pad event on the requested report ID.291        """292        r = self.create_report_pad(reportID, ring, ek0)293        self.call_input_event(r)294        return [r]295 296    def get_report(self, req, rnum, rtype):297        if rtype != self.UHID_FEATURE_REPORT:298            return (1, [])299 300        rdesc = None301        for v in self.parsed_rdesc.feature_reports.values():302            if v.report_ID == rnum:303                rdesc = v304 305        if rdesc is None:306            return (1, [])307 308        result = (1, [])309        result = self.create_report_offset(rdesc) or result310        return result311 312    def create_report_offset(self, rdesc):313        require = [314            "Wacom Offset Left",315            "Wacom Offset Top",316            "Wacom Offset Right",317            "Wacom Offset Bottom",318        ]319        if not set(require).issubset(set([f.usage_name for f in rdesc])):320            return None321 322        report = ReportData()323        report.wacomoffsetleft = self.offset324        report.wacomoffsettop = self.offset325        report.wacomoffsetright = self.offset326        report.wacomoffsetbottom = self.offset327        r = rdesc.create_report([report], None)328        return (0, r)329 330 331class OpaqueTablet(BaseTablet):332    """333    Bare-bones opaque tablet with a minimum of features.334 335    A tablet stripped down to its absolute core. It is capable of336    reporting X/Y position and if the pen is in contact. No pressure,337    no barrel switches, no eraser. Notably it *does* report an "In338    Range" flag, but this is only because the Wacom driver expects339    one to function properly. The device uses only standard HID usages,340    not any of Wacom's vendor-defined pages.341    """342 343    # fmt: off344    report_descriptor = [345        0x05, 0x0D,                     # . Usage Page (Digitizer),346        0x09, 0x01,                     # . Usage (Digitizer),347        0xA1, 0x01,                     # . Collection (Application),348        0x85, 0x01,                     # .     Report ID (1),349        0x09, 0x20,                     # .     Usage (Stylus),350        0xA1, 0x00,                     # .     Collection (Physical),351        0x09, 0x42,                     # .         Usage (Tip Switch),352        0x09, 0x32,                     # .         Usage (In Range),353        0x15, 0x00,                     # .         Logical Minimum (0),354        0x25, 0x01,                     # .         Logical Maximum (1),355        0x75, 0x01,                     # .         Report Size (1),356        0x95, 0x02,                     # .         Report Count (2),357        0x81, 0x02,                     # .         Input (Variable),358        0x95, 0x06,                     # .         Report Count (6),359        0x81, 0x03,                     # .         Input (Constant, Variable),360        0x05, 0x01,                     # .         Usage Page (Desktop),361        0x09, 0x30,                     # .         Usage (X),362        0x27, 0x80, 0x3E, 0x00, 0x00,   # .         Logical Maximum (16000),363        0x47, 0x80, 0x3E, 0x00, 0x00,   # .         Physical Maximum (16000),364        0x65, 0x11,                     # .         Unit (Centimeter),365        0x55, 0x0D,                     # .         Unit Exponent (13),366        0x75, 0x10,                     # .         Report Size (16),367        0x95, 0x01,                     # .         Report Count (1),368        0x81, 0x02,                     # .         Input (Variable),369        0x09, 0x31,                     # .         Usage (Y),370        0x27, 0x28, 0x23, 0x00, 0x00,   # .         Logical Maximum (9000),371        0x47, 0x28, 0x23, 0x00, 0x00,   # .         Physical Maximum (9000),372        0x81, 0x02,                     # .         Input (Variable),373        0xC0,                           # .     End Collection,374        0xC0,                           # . End Collection,375    ]376    # fmt: on377 378    def __init__(self, rdesc=report_descriptor, name=None, info=(0x3, 0x056A, 0x9999)):379        super().__init__(rdesc, name, info)380        self.default_reportID = 1381 382 383class OpaqueCTLTablet(BaseTablet):384    """385    Opaque tablet similar to something in the CTL product line.386 387    A pen-only tablet with most basic features you would expect from388    an actual device. Position, eraser, pressure, barrel buttons.389    Uses the Wacom vendor-defined usage page.390    """391 392    # fmt: off393    report_descriptor = [394        0x06, 0x0D, 0xFF,               # . Usage Page (Vnd Wacom Emr),395        0x09, 0x01,                     # . Usage (Digitizer),396        0xA1, 0x01,                     # . Collection (Application),397        0x85, 0x10,                     # .     Report ID (16),398        0x09, 0x20,                     # .     Usage (Stylus),399        0x35, 0x00,                     # .     Physical Minimum (0),400        0x45, 0x00,                     # .     Physical Maximum (0),401        0x15, 0x00,                     # .     Logical Minimum (0),402        0x25, 0x01,                     # .     Logical Maximum (1),403        0xA1, 0x00,                     # .     Collection (Physical),404        0x09, 0x42,                     # .         Usage (Tip Switch),405        0x09, 0x44,                     # .         Usage (Barrel Switch),406        0x09, 0x5A,                     # .         Usage (Secondary Barrel Switch),407        0x09, 0x45,                     # .         Usage (Eraser),408        0x09, 0x3C,                     # .         Usage (Invert),409        0x09, 0x32,                     # .         Usage (In Range),410        0x09, 0x36,                     # .         Usage (In Proximity),411        0x25, 0x01,                     # .         Logical Maximum (1),412        0x75, 0x01,                     # .         Report Size (1),413        0x95, 0x07,                     # .         Report Count (7),414        0x81, 0x02,                     # .         Input (Variable),415        0x95, 0x01,                     # .         Report Count (1),416        0x81, 0x03,                     # .         Input (Constant, Variable),417        0x0A, 0x30, 0x01,               # .         Usage (X),418        0x65, 0x11,                     # .         Unit (Centimeter),419        0x55, 0x0D,                     # .         Unit Exponent (13),420        0x47, 0x80, 0x3E, 0x00, 0x00,   # .         Physical Maximum (16000),421        0x27, 0x80, 0x3E, 0x00, 0x00,   # .         Logical Maximum (16000),422        0x75, 0x18,                     # .         Report Size (24),423        0x95, 0x01,                     # .         Report Count (1),424        0x81, 0x02,                     # .         Input (Variable),425        0x0A, 0x31, 0x01,               # .         Usage (Y),426        0x47, 0x28, 0x23, 0x00, 0x00,   # .         Physical Maximum (9000),427        0x27, 0x28, 0x23, 0x00, 0x00,   # .         Logical Maximum (9000),428        0x81, 0x02,                     # .         Input (Variable),429        0x09, 0x30,                     # .         Usage (Tip Pressure),430        0x55, 0x00,                     # .         Unit Exponent (0),431        0x65, 0x00,                     # .         Unit,432        0x47, 0x00, 0x00, 0x00, 0x00,   # .         Physical Maximum (0),433        0x26, 0xFF, 0x0F,               # .         Logical Maximum (4095),434        0x75, 0x10,                     # .         Report Size (16),435        0x81, 0x02,                     # .         Input (Variable),436        0x75, 0x08,                     # .         Report Size (8),437        0x95, 0x06,                     # .         Report Count (6),438        0x81, 0x03,                     # .         Input (Constant, Variable),439        0x0A, 0x32, 0x01,               # .         Usage (Z),440        0x25, 0x3F,                     # .         Logical Maximum (63),441        0x75, 0x08,                     # .         Report Size (8),442        0x95, 0x01,                     # .         Report Count (1),443        0x81, 0x02,                     # .         Input (Variable),444        0x09, 0x5B,                     # .         Usage (Transducer Serial Number),445        0x09, 0x5C,                     # .         Usage (Transducer Serial Number Hi),446        0x17, 0x00, 0x00, 0x00, 0x80,   # .         Logical Minimum (-2147483648),447        0x27, 0xFF, 0xFF, 0xFF, 0x7F,   # .         Logical Maximum (2147483647),448        0x75, 0x20,                     # .         Report Size (32),449        0x95, 0x02,                     # .         Report Count (2),450        0x81, 0x02,                     # .         Input (Variable),451        0x09, 0x77,                     # .         Usage (Tool Type),452        0x15, 0x00,                     # .         Logical Minimum (0),453        0x26, 0xFF, 0x0F,               # .         Logical Maximum (4095),454        0x75, 0x10,                     # .         Report Size (16),455        0x95, 0x01,                     # .         Report Count (1),456        0x81, 0x02,                     # .         Input (Variable),457        0xC0,                           # .     End Collection,458        0xC0                            # . End Collection459    ]460    # fmt: on461 462    def __init__(self, rdesc=report_descriptor, name=None, info=(0x3, 0x056A, 0x9999)):463        super().__init__(rdesc, name, info)464        self.default_reportID = 16465 466 467class PTHX60_Pen(BaseTablet):468    """469    Pen interface of a PTH-660 / PTH-860 / PTH-460 tablet.470 471    This generation of devices are nearly identical to each other, though472    the PTH-460 uses a slightly different descriptor construction (splits473    the pad among several physical collections)474    """475 476    def __init__(self, rdesc=None, name=None, info=None):477        super().__init__(rdesc, name, info)478        self.default_reportID = 16479 480 481class BaseTest:482    class TestTablet(base.BaseTestCase.TestUhid):483        kernel_modules = [KERNEL_MODULE]484 485        def sync_and_assert_events(486            self, report, expected_events, auto_syn=True, strict=False487        ):488            """489            Assert we see the expected events in response to a report.490            """491            uhdev = self.uhdev492            syn_event = self.syn_event493            if auto_syn:494                expected_events.append(syn_event)495            actual_events = uhdev.next_sync_events()496            self.debug_reports(report, uhdev, actual_events)497            if strict:498                self.assertInputEvents(expected_events, actual_events)499            else:500                self.assertInputEventsIn(expected_events, actual_events)501 502        def get_usages(self, uhdev):503            def get_report_usages(report):504                application = report.application505                for field in report.fields:506                    if field.usages is not None:507                        for usage in field.usages:508                            yield (field, usage, application)509                    else:510                        yield (field, field.usage, application)511 512            desc = uhdev.parsed_rdesc513            reports = [514                *desc.input_reports.values(),515                *desc.feature_reports.values(),516                *desc.output_reports.values(),517            ]518            for report in reports:519                for usage in get_report_usages(report):520                    yield usage521 522        def assertName(self, uhdev, type):523            """524            Assert that the name is as we expect.525 526            The Wacom driver applies a number of decorations to the name527            provided by the hardware. We cannot rely on the definition of528            this assertion from the base class to work properly.529            """530            evdev = uhdev.get_evdev()531            expected_name = uhdev.name + type532            if "wacom" not in expected_name.lower():533                expected_name = "Wacom " + expected_name534            assert evdev.name == expected_name535 536        def test_descriptor_physicals(self):537            """538            Verify that all HID usages which should have a physical range539            actually do, and those which shouldn't don't. Also verify that540            the associated unit is correct and within a sensible range.541            """542 543            def usage_id(page_name, usage_name):544                page = HUT.usage_page_from_name(page_name)545                return (page.page_id << 16) | page[usage_name].usage546 547            required = {548                usage_id("Generic Desktop", "X"): PhysRange(549                    PhysRange.CENTIMETER, 5, 150550                ),551                usage_id("Generic Desktop", "Y"): PhysRange(552                    PhysRange.CENTIMETER, 5, 150553                ),554                usage_id("Digitizers", "Width"): PhysRange(555                    PhysRange.CENTIMETER, 5, 150556                ),557                usage_id("Digitizers", "Height"): PhysRange(558                    PhysRange.CENTIMETER, 5, 150559                ),560                usage_id("Digitizers", "X Tilt"): PhysRange(PhysRange.DEGREE, 90, 180),561                usage_id("Digitizers", "Y Tilt"): PhysRange(PhysRange.DEGREE, 90, 180),562                usage_id("Digitizers", "Twist"): PhysRange(PhysRange.DEGREE, 358, 360),563                usage_id("Wacom", "X Tilt"): PhysRange(PhysRange.DEGREE, 90, 180),564                usage_id("Wacom", "Y Tilt"): PhysRange(PhysRange.DEGREE, 90, 180),565                usage_id("Wacom", "Twist"): PhysRange(PhysRange.DEGREE, 358, 360),566                usage_id("Wacom", "X"): PhysRange(PhysRange.CENTIMETER, 5, 150),567                usage_id("Wacom", "Y"): PhysRange(PhysRange.CENTIMETER, 5, 150),568                usage_id("Wacom", "Wacom TouchRing"): PhysRange(569                    PhysRange.DEGREE, 358, 360570                ),571                usage_id("Wacom", "Wacom Offset Left"): PhysRange(572                    PhysRange.CENTIMETER, 0, 0.5573                ),574                usage_id("Wacom", "Wacom Offset Top"): PhysRange(575                    PhysRange.CENTIMETER, 0, 0.5576                ),577                usage_id("Wacom", "Wacom Offset Right"): PhysRange(578                    PhysRange.CENTIMETER, 0, 0.5579                ),580                usage_id("Wacom", "Wacom Offset Bottom"): PhysRange(581                    PhysRange.CENTIMETER, 0, 0.5582                ),583            }584            for field, usage, application in self.get_usages(self.uhdev):585                if application == usage_id("Generic Desktop", "Mouse"):586                    # Ignore the vestigial Mouse collection which exists587                    # on Wacom tablets only for backwards compatibility.588                    continue589 590                expect_physical = usage in required591 592                phys_set = field.physical_min != 0 or field.physical_max != 0593                assert phys_set == expect_physical594 595                unit_set = field.unit != 0596                assert unit_set == expect_physical597 598                if unit_set:599                    assert required[usage].contains(field)600 601        def test_prop_direct(self):602            """603            Todo: Verify that INPUT_PROP_DIRECT is set on display devices.604            """605            pass606 607        def test_prop_pointer(self):608            """609            Todo: Verify that INPUT_PROP_POINTER is set on opaque devices.610            """611            pass612 613 614class PenTabletTest(BaseTest.TestTablet):615    def assertName(self, uhdev):616        super().assertName(uhdev, " Pen")617 618 619class TouchTabletTest(BaseTest.TestTablet):620    def assertName(self, uhdev):621        super().assertName(uhdev, " Finger")622 623 624class TestOpaqueTablet(PenTabletTest):625    def create_device(self):626        return OpaqueTablet()627 628    def test_sanity(self):629        """630        Bring a pen into contact with the tablet, then remove it.631 632        Ensure that we get the basic tool/touch/motion events that should633        be sent by the driver.634        """635        uhdev = self.uhdev636 637        self.sync_and_assert_events(638            uhdev.event(639                100,640                200,641                pressure=300,642                buttons=Buttons.clear(),643                toolid=ToolID(serial=1, tooltype=1),644                proximity=ProximityState.IN_RANGE,645            ),646            [647                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN, 1),648                libevdev.InputEvent(libevdev.EV_ABS.ABS_X, 100),649                libevdev.InputEvent(libevdev.EV_ABS.ABS_Y, 200),650                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 1),651            ],652        )653 654        self.sync_and_assert_events(655            uhdev.event(110, 220, pressure=0),656            [657                libevdev.InputEvent(libevdev.EV_ABS.ABS_X, 110),658                libevdev.InputEvent(libevdev.EV_ABS.ABS_Y, 220),659                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 0),660            ],661        )662 663        self.sync_and_assert_events(664            uhdev.event(665                120,666                230,667                pressure=0,668                toolid=ToolID.clear(),669                proximity=ProximityState.OUT,670            ),671            [672                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN, 0),673            ],674        )675 676        self.sync_and_assert_events(677            uhdev.event(130, 240, pressure=0), [], auto_syn=False, strict=True678        )679 680 681class TestOpaqueCTLTablet(TestOpaqueTablet):682    def create_device(self):683        return OpaqueCTLTablet()684 685    def test_buttons(self):686        """687        Test that the barrel buttons (side switches) work as expected.688 689        Press and release each button individually to verify that we get690        the expected events.691        """692        uhdev = self.uhdev693 694        self.sync_and_assert_events(695            uhdev.event(696                100,697                200,698                pressure=0,699                buttons=Buttons.clear(),700                toolid=ToolID(serial=1, tooltype=1),701                proximity=ProximityState.IN_RANGE,702            ),703            [704                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN, 1),705                libevdev.InputEvent(libevdev.EV_ABS.ABS_X, 100),706                libevdev.InputEvent(libevdev.EV_ABS.ABS_Y, 200),707                libevdev.InputEvent(libevdev.EV_MSC.MSC_SERIAL, 1),708            ],709        )710 711        self.sync_and_assert_events(712            uhdev.event(100, 200, pressure=0, buttons=Buttons(primary=True)),713            [714                libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS, 1),715                libevdev.InputEvent(libevdev.EV_MSC.MSC_SERIAL, 1),716            ],717        )718 719        self.sync_and_assert_events(720            uhdev.event(100, 200, pressure=0, buttons=Buttons(primary=False)),721            [722                libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS, 0),723                libevdev.InputEvent(libevdev.EV_MSC.MSC_SERIAL, 1),724            ],725        )726 727        self.sync_and_assert_events(728            uhdev.event(100, 200, pressure=0, buttons=Buttons(secondary=True)),729            [730                libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS2, 1),731                libevdev.InputEvent(libevdev.EV_MSC.MSC_SERIAL, 1),732            ],733        )734 735        self.sync_and_assert_events(736            uhdev.event(100, 200, pressure=0, buttons=Buttons(secondary=False)),737            [738                libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS2, 0),739                libevdev.InputEvent(libevdev.EV_MSC.MSC_SERIAL, 1),740            ],741        )742 743 744PTHX60_Devices = [745    {"rdesc": wacom_pth660_v145, "info": (0x3, 0x056A, 0x0357)},746    {"rdesc": wacom_pth660_v150, "info": (0x3, 0x056A, 0x0357)},747    {"rdesc": wacom_pth860_v145, "info": (0x3, 0x056A, 0x0358)},748    {"rdesc": wacom_pth860_v150, "info": (0x3, 0x056A, 0x0358)},749    {"rdesc": wacom_pth460_v105, "info": (0x3, 0x056A, 0x0392)},750]751 752PTHX60_Names = [753    "PTH-660/v145",754    "PTH-660/v150",755    "PTH-860/v145",756    "PTH-860/v150",757    "PTH-460/v105",758]759 760 761class TestPTHX60_Pen(TestOpaqueCTLTablet):762    @pytest.fixture(763        autouse=True, scope="class", params=PTHX60_Devices, ids=PTHX60_Names764    )765    def set_device_params(self, request):766        request.cls.device_params = request.param767 768    def create_device(self):769        return PTHX60_Pen(**self.device_params)770 771    @pytest.mark.xfail772    def test_descriptor_physicals(self):773        # XFAIL: Various documented errata774        super().test_descriptor_physicals()775 776    def test_heartbeat_spurious(self):777        """778        Test that the heartbeat report does not send spurious events.779        """780        uhdev = self.uhdev781 782        self.sync_and_assert_events(783            uhdev.event(784                100,785                200,786                pressure=300,787                buttons=Buttons.clear(),788                toolid=ToolID(serial=1, tooltype=0x822),789                proximity=ProximityState.IN_RANGE,790            ),791            [792                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN, 1),793                libevdev.InputEvent(libevdev.EV_ABS.ABS_X, 100),794                libevdev.InputEvent(libevdev.EV_ABS.ABS_Y, 200),795                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 1),796            ],797        )798 799        # Exactly zero events: not even a SYN800        self.sync_and_assert_events(801            uhdev.event_heartbeat(19), [], auto_syn=False, strict=True802        )803 804        self.sync_and_assert_events(805            uhdev.event(110, 200, pressure=300),806            [807                libevdev.InputEvent(libevdev.EV_ABS.ABS_X, 110),808            ],809        )810 811    def test_empty_pad_sync(self):812        self.empty_pad_sync(num=3, denom=16, reverse=True)813 814    def empty_pad_sync(self, num, denom, reverse):815        """816        Test that multiple pad collections do not trigger empty syncs.817        """818 819        def offset_rotation(value):820            """821            Offset touchring rotation values by the same factor as the822            Linux kernel. Tablets historically don't use the same origin823            as HID, and it sometimes changes from tablet to tablet...824            """825            evdev = self.uhdev.get_evdev()826            info = evdev.absinfo[libevdev.EV_ABS.ABS_WHEEL]827            delta = info.maximum - info.minimum + 1828            if reverse:829                value = info.maximum - value830            value += num * delta // denom831            if value > info.maximum:832                value -= delta833            elif value < info.minimum:834                value += delta835            return value836 837        uhdev = self.uhdev838        uhdev.application = "Pad"839        evdev = uhdev.get_evdev()840 841        print(evdev.name)842        self.sync_and_assert_events(843            uhdev.event_pad(reportID=17, ring=0, ek0=1),844            [845                libevdev.InputEvent(libevdev.EV_KEY.BTN_0, 1),846                libevdev.InputEvent(libevdev.EV_ABS.ABS_WHEEL, offset_rotation(0)),847                libevdev.InputEvent(libevdev.EV_ABS.ABS_MISC, 15),848            ],849        )850 851        self.sync_and_assert_events(852            uhdev.event_pad(reportID=17, ring=1, ek0=1),853            [libevdev.InputEvent(libevdev.EV_ABS.ABS_WHEEL, offset_rotation(1))],854        )855 856        self.sync_and_assert_events(857            uhdev.event_pad(reportID=17, ring=2, ek0=0),858            [859                libevdev.InputEvent(libevdev.EV_ABS.ABS_WHEEL, offset_rotation(2)),860                libevdev.InputEvent(libevdev.EV_KEY.BTN_0, 0),861            ],862        )863 864 865class TestDTH2452Tablet(test_multitouch.BaseTest.TestMultitouch, TouchTabletTest):866    ContactIds = namedtuple("ContactIds", "contact_id, tracking_id, slot_num")867 868    def create_device(self):869        return test_multitouch.Digitizer(870            "DTH 2452",871            rdesc="05 0d 09 04 a1 01 85 0c 95 01 75 08 15 00 26 ff 00 81 03 09 54 81 02 09 22 a1 02 05 0d 95 01 75 01 25 01 09 42 81 02 81 03 09 47 81 02 95 05 81 03 09 51 26 ff 00 75 10 95 01 81 02 35 00 65 11 55 0e 05 01 09 30 26 a0 44 46 96 14 81 42 09 31 26 9a 26 46 95 0b 81 42 05 0d 75 08 95 01 15 00 09 48 26 5f 00 46 7c 14 81 02 09 49 25 35 46 7d 0b 81 02 45 00 65 00 55 00 c0 05 0d 09 22 a1 02 05 0d 95 01 75 01 25 01 09 42 81 02 81 03 09 47 81 02 95 05 81 03 09 51 26 ff 00 75 10 95 01 81 02 35 00 65 11 55 0e 05 01 09 30 26 a0 44 46 96 14 81 42 09 31 26 9a 26 46 95 0b 81 42 05 0d 75 08 95 01 15 00 09 48 26 5f 00 46 7c 14 81 02 09 49 25 35 46 7d 0b 81 02 45 00 65 00 55 00 c0 05 0d 09 22 a1 02 05 0d 95 01 75 01 25 01 09 42 81 02 81 03 09 47 81 02 95 05 81 03 09 51 26 ff 00 75 10 95 01 81 02 35 00 65 11 55 0e 05 01 09 30 26 a0 44 46 96 14 81 42 09 31 26 9a 26 46 95 0b 81 42 05 0d 75 08 95 01 15 00 09 48 26 5f 00 46 7c 14 81 02 09 49 25 35 46 7d 0b 81 02 45 00 65 00 55 00 c0 05 0d 09 22 a1 02 05 0d 95 01 75 01 25 01 09 42 81 02 81 03 09 47 81 02 95 05 81 03 09 51 26 ff 00 75 10 95 01 81 02 35 00 65 11 55 0e 05 01 09 30 26 a0 44 46 96 14 81 42 09 31 26 9a 26 46 95 0b 81 42 05 0d 75 08 95 01 15 00 09 48 26 5f 00 46 7c 14 81 02 09 49 25 35 46 7d 0b 81 02 45 00 65 00 55 00 c0 05 0d 09 22 a1 02 05 0d 95 01 75 01 25 01 09 42 81 02 81 03 09 47 81 02 95 05 81 03 09 51 26 ff 00 75 10 95 01 81 02 35 00 65 11 55 0e 05 01 09 30 26 a0 44 46 96 14 81 42 09 31 26 9a 26 46 95 0b 81 42 05 0d 75 08 95 01 15 00 09 48 26 5f 00 46 7c 14 81 02 09 49 25 35 46 7d 0b 81 02 45 00 65 00 55 00 c0 05 0d 27 ff ff 00 00 75 10 95 01 09 56 81 02 75 08 95 0e 81 03 09 55 26 ff 00 75 08 b1 02 85 0a 06 00 ff 09 c5 96 00 01 b1 02 c0 06 00 ff 09 01 a1 01 09 01 85 13 15 00 26 ff 00 75 08 95 3f 81 02 06 00 ff 09 01 15 00 26 ff 00 75 08 95 3f 91 02 c0",872            input_info=(0x3, 0x056A, 0x0383),873        )874 875    def make_contact(self, contact_id=0, t=0):876        """877        Make a single touch contact that can move over time.878 879        Creates a touch object that has a well-known position in space that880        does not overlap with other contacts. The value of `t` may be881        incremented over time to move the point along a linear path.882        """883        x = 50 + 10 * contact_id + t * 11884        y = 100 + 100 * contact_id + t * 11885        return test_multitouch.Touch(contact_id, x, y)886 887    def make_contacts(self, n, t=0):888        """889        Make multiple touch contacts that can move over time.890 891        Returns a list of `n` touch objects that are positioned at well-known892        locations. The value of `t` may be incremented over time to move the893        points along a linear path.894        """895        return [ self.make_contact(id, t) for id in range(0, n) ]896 897    def assert_contact(self, uhdev, evdev, contact_ids, t=0):898        """899        Assert properties of a contact generated by make_contact.900        """901        contact_id = contact_ids.contact_id902        tracking_id = contact_ids.tracking_id903        slot_num = contact_ids.slot_num904 905        x = 50 + 10 * contact_id + t * 11906        y = 100 + 100 * contact_id + t * 11907 908        # If the data isn't supposed to be stored in any slots, there is909        # nothing we can check for in the evdev stream.910        if slot_num is None:911            assert tracking_id == -1912            return913 914        assert evdev.slots[slot_num][libevdev.EV_ABS.ABS_MT_TRACKING_ID] == tracking_id915        if tracking_id != -1:916            assert evdev.slots[slot_num][libevdev.EV_ABS.ABS_MT_POSITION_X] == x917            assert evdev.slots[slot_num][libevdev.EV_ABS.ABS_MT_POSITION_Y] == y918 919    def assert_contacts(self, uhdev, evdev, data, t=0):920        """921        Assert properties of a list of contacts generated by make_contacts.922        """923        for contact_ids in data:924            self.assert_contact(uhdev, evdev, contact_ids, t)925 926    def test_contact_id_0(self):927        """928        Bring a finger in contact with the tablet, then hold it down and remove it.929 930        Ensure that even with contact ID = 0 which is usually given as an invalid931        touch event by most tablets with the exception of a few, that given the932        confidence bit is set to 1 it should process it as a valid touch to cover933        the few tablets using contact ID = 0 as a valid touch value.934        """935        uhdev = self.uhdev936        evdev = uhdev.get_evdev()937 938        t0 = test_multitouch.Touch(0, 50, 100)939        r = uhdev.event([t0])940        events = uhdev.next_sync_events()941        self.debug_reports(r, uhdev, events)942 943        slot = self.get_slot(uhdev, t0, 0)944 945        assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 1) in events946        assert evdev.slots[slot][libevdev.EV_ABS.ABS_MT_TRACKING_ID] == 0947        assert evdev.slots[slot][libevdev.EV_ABS.ABS_MT_POSITION_X] == 50948        assert evdev.slots[slot][libevdev.EV_ABS.ABS_MT_POSITION_Y] == 100949 950        t0.tipswitch = False951        if uhdev.quirks is None or "VALID_IS_INRANGE" not in uhdev.quirks:952            t0.inrange = False953        r = uhdev.event([t0])954        events = uhdev.next_sync_events()955        self.debug_reports(r, uhdev, events)956        assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 0) in events957        assert evdev.slots[slot][libevdev.EV_ABS.ABS_MT_TRACKING_ID] == -1958 959    def test_confidence_false(self):960        """961        Bring a finger in contact with the tablet with confidence set to false.962 963        Ensure that the confidence bit being set to false should not result in a touch event.964        """965        uhdev = self.uhdev966        _evdev = uhdev.get_evdev()967 968        t0 = test_multitouch.Touch(1, 50, 100)969        t0.confidence = False970        r = uhdev.event([t0])971        events = uhdev.next_sync_events()972        self.debug_reports(r, uhdev, events)973 974        _slot = self.get_slot(uhdev, t0, 0)975 976        assert not events977 978    def test_confidence_multitouch(self):979        """980        Bring multiple fingers in contact with the tablet, some with the981        confidence bit set, and some without.982 983        Ensure that all confident touches are reported and that all non-984        confident touches are ignored.985        """986        uhdev = self.uhdev987        evdev = uhdev.get_evdev()988 989        touches = self.make_contacts(5)990        touches[0].confidence = False991        touches[2].confidence = False992        touches[4].confidence = False993 994        r = uhdev.event(touches)995        events = uhdev.next_sync_events()996        self.debug_reports(r, uhdev, events)997 998        assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 1) in events999 1000        self.assert_contacts(uhdev, evdev,1001            [ self.ContactIds(contact_id = 0, tracking_id = -1, slot_num = None),1002              self.ContactIds(contact_id = 1, tracking_id = 0, slot_num = 0),1003              self.ContactIds(contact_id = 2, tracking_id = -1, slot_num = None),1004              self.ContactIds(contact_id = 3, tracking_id = 1, slot_num = 1),1005              self.ContactIds(contact_id = 4, tracking_id = -1, slot_num = None) ])1006 1007    def confidence_change_assert_playback(self, uhdev, evdev, timeline):1008        """1009        Assert proper behavior of contacts that move and change tipswitch /1010        confidence status over time.1011 1012        Given a `timeline` list of touch states to iterate over, verify1013        that the contacts move and are reported as up/down as expected1014        by the state of the tipswitch and confidence bits.1015        """1016        t = 01017 1018        for state in timeline:1019            touches = self.make_contacts(len(state), t)1020 1021            for item in zip(touches, state):1022                item[0].tipswitch = item[1][1]1023                item[0].confidence = item[1][2]1024 1025            r = uhdev.event(touches)1026            events = uhdev.next_sync_events()1027            self.debug_reports(r, uhdev, events)1028 1029            ids = [ x[0] for x in state ]1030            self.assert_contacts(uhdev, evdev, ids, t)1031 1032            t += 11033 1034    def test_confidence_loss_a(self):1035        """1036        Transition a confident contact to a non-confident contact by1037        first clearing the tipswitch.1038 1039        Ensure that the driver reports the transitioned contact as1040        being removed and that other contacts continue to report1041        normally. This mode of confidence loss is used by the1042        DTH-2452.1043        """1044        uhdev = self.uhdev1045        evdev = uhdev.get_evdev()1046 1047        self.confidence_change_assert_playback(uhdev, evdev, [1048            # t=0: Contact 0 == Down + confident; Contact 1 == Down + confident1049            # Both fingers confidently in contact1050            [(self.ContactIds(contact_id = 0, tracking_id = 0, slot_num = 0), True, True),1051             (self.ContactIds(contact_id = 1, tracking_id = 1, slot_num = 1), True, True)],1052 1053            # t=1: Contact 0 == !Down + confident; Contact 1 == Down + confident1054            # First finger looses confidence and clears only the tipswitch flag1055            [(self.ContactIds(contact_id = 0, tracking_id = -1, slot_num = 0), False, True),1056             (self.ContactIds(contact_id = 1, tracking_id = 1, slot_num = 1), True, True)],1057 1058            # t=2: Contact 0 == !Down + !confident; Contact 1 == Down + confident1059            # First finger has lost confidence and has both flags cleared1060            [(self.ContactIds(contact_id = 0, tracking_id = -1, slot_num = 0), False, False),1061             (self.ContactIds(contact_id = 1, tracking_id = 1, slot_num = 1), True, True)],1062 1063            # t=3: Contact 0 == !Down + !confident; Contact 1 == Down + confident1064            # First finger has lost confidence and has both flags cleared1065            [(self.ContactIds(contact_id = 0, tracking_id = -1, slot_num = 0), False, False),1066             (self.ContactIds(contact_id = 1, tracking_id = 1, slot_num = 1), True, True)]1067        ])1068 1069    def test_confidence_loss_b(self):1070        """1071        Transition a confident contact to a non-confident contact by1072        cleraing both tipswitch and confidence bits simultaneously.1073 1074        Ensure that the driver reports the transitioned contact as1075        being removed and that other contacts continue to report1076        normally. This mode of confidence loss is used by some1077        AES devices.1078        """1079        uhdev = self.uhdev1080        evdev = uhdev.get_evdev()1081 1082        self.confidence_change_assert_playback(uhdev, evdev, [1083            # t=0: Contact 0 == Down + confident; Contact 1 == Down + confident1084            # Both fingers confidently in contact1085            [(self.ContactIds(contact_id = 0, tracking_id = 0, slot_num = 0), True, True),1086             (self.ContactIds(contact_id = 1, tracking_id = 1, slot_num = 1), True, True)],1087 1088            # t=1: Contact 0 == !Down + !confident; Contact 1 == Down + confident1089            # First finger looses confidence and has both flags cleared simultaneously1090            [(self.ContactIds(contact_id = 0, tracking_id = -1, slot_num = 0), False, False),1091             (self.ContactIds(contact_id = 1, tracking_id = 1, slot_num = 1), True, True)],1092 1093            # t=2: Contact 0 == !Down + !confident; Contact 1 == Down + confident1094            # First finger has lost confidence and has both flags cleared1095            [(self.ContactIds(contact_id = 0, tracking_id = -1, slot_num = 0), False, False),1096             (self.ContactIds(contact_id = 1, tracking_id = 1, slot_num = 1), True, True)],1097 1098            # t=3: Contact 0 == !Down + !confident; Contact 1 == Down + confident1099            # First finger has lost confidence and has both flags cleared1100            [(self.ContactIds(contact_id = 0, tracking_id = -1, slot_num = 0), False, False),1101             (self.ContactIds(contact_id = 1, tracking_id = 1, slot_num = 1), True, True)]1102        ])1103 1104    def test_confidence_loss_c(self):1105        """1106        Transition a confident contact to a non-confident contact by1107        clearing only the confidence bit.1108 1109        Ensure that the driver reports the transitioned contact as1110        being removed and that other contacts continue to report1111        normally.1112        """1113        uhdev = self.uhdev1114        evdev = uhdev.get_evdev()1115 1116        self.confidence_change_assert_playback(uhdev, evdev, [1117            # t=0: Contact 0 == Down + confident; Contact 1 == Down + confident1118            # Both fingers confidently in contact1119            [(self.ContactIds(contact_id = 0, tracking_id = 0, slot_num = 0), True, True),1120             (self.ContactIds(contact_id = 1, tracking_id = 1, slot_num = 1), True, True)],1121 1122            # t=1: Contact 0 == Down + !confident; Contact 1 == Down + confident1123            # First finger looses confidence and clears only the confidence flag1124            [(self.ContactIds(contact_id = 0, tracking_id = -1, slot_num = 0), True, False),1125             (self.ContactIds(contact_id = 1, tracking_id = 1, slot_num = 1), True, True)],1126 1127            # t=2: Contact 0 == !Down + !confident; Contact 1 == Down + confident1128            # First finger has lost confidence and has both flags cleared1129            [(self.ContactIds(contact_id = 0, tracking_id = -1, slot_num = 0), False, False),1130             (self.ContactIds(contact_id = 1, tracking_id = 1, slot_num = 1), True, True)],1131 1132            # t=3: Contact 0 == !Down + !confident; Contact 1 == Down + confident1133            # First finger has lost confidence and has both flags cleared1134            [(self.ContactIds(contact_id = 0, tracking_id = -1, slot_num = 0), False, False),1135             (self.ContactIds(contact_id = 1, tracking_id = 1, slot_num = 1), True, True)]1136        ])1137 1138    def test_confidence_gain_a(self):1139        """1140        Transition a contact that was always non-confident to confident.1141 1142        Ensure that the confident contact is reported normally.1143        """1144        uhdev = self.uhdev1145        evdev = uhdev.get_evdev()1146 1147        self.confidence_change_assert_playback(uhdev, evdev, [1148            # t=0: Contact 0 == Down + !confident; Contact 1 == Down + confident1149            # Only second finger is confidently in contact1150            [(self.ContactIds(contact_id = 0, tracking_id = -1, slot_num = None), True, False),1151             (self.ContactIds(contact_id = 1, tracking_id = 0, slot_num = 0), True, True)],1152 1153            # t=1: Contact 0 == Down + !confident; Contact 1 == Down + confident1154            # First finger gains confidence1155            [(self.ContactIds(contact_id = 0, tracking_id = -1, slot_num = None), True, False),1156             (self.ContactIds(contact_id = 1, tracking_id = 0, slot_num = 0), True, True)],1157 1158            # t=2: Contact 0 == Down + confident; Contact 1 == Down + confident1159            # First finger remains confident1160            [(self.ContactIds(contact_id = 0, tracking_id = 1, slot_num = 1), True, True),1161             (self.ContactIds(contact_id = 1, tracking_id = 0, slot_num = 0), True, True)],1162 1163            # t=3: Contact 0 == Down + confident; Contact 1 == Down + confident1164            # First finger remains confident1165            [(self.ContactIds(contact_id = 0, tracking_id = 1, slot_num = 1), True, True),1166             (self.ContactIds(contact_id = 1, tracking_id = 0, slot_num = 0), True, True)]1167        ])1168 1169    def test_confidence_gain_b(self):1170        """1171        Transition a contact from non-confident to confident.1172 1173        Ensure that the confident contact is reported normally.1174        """1175        uhdev = self.uhdev1176        evdev = uhdev.get_evdev()1177 1178        self.confidence_change_assert_playback(uhdev, evdev, [1179            # t=0: Contact 0 == Down + confident; Contact 1 == Down + confident1180            # First and second finger confidently in contact1181            [(self.ContactIds(contact_id = 0, tracking_id = 0, slot_num = 0), True, True),1182             (self.ContactIds(contact_id = 1, tracking_id = 1, slot_num = 1), True, True)],1183 1184            # t=1: Contact 0 == Down + !confident; Contact 1 == Down + confident1185            # Firtst finger looses confidence1186            [(self.ContactIds(contact_id = 0, tracking_id = -1, slot_num = 0), True, False),1187             (self.ContactIds(contact_id = 1, tracking_id = 1, slot_num = 1), True, True)],1188 1189            # t=2: Contact 0 == Down + confident; Contact 1 == Down + confident1190            # First finger gains confidence1191            [(self.ContactIds(contact_id = 0, tracking_id = 2, slot_num = 0), True, True),1192             (self.ContactIds(contact_id = 1, tracking_id = 1, slot_num = 1), True, True)],1193 1194            # t=3: Contact 0 == !Down + confident; Contact 1 == Down + confident1195            # First finger goes up1196            [(self.ContactIds(contact_id = 0, tracking_id = -1, slot_num = 0), False, True),1197             (self.ContactIds(contact_id = 1, tracking_id = 1, slot_num = 1), True, True)]1198        ])1199