1#!/bin/env python3
2# SPDX-License-Identifier: GPL-2.0
3# -*- 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 listed
17in the driver's device table. It uses the device's HID descriptor to
18decode 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 attr
30from enum import Enum
31from hidtools.hut import HUT
32from hidtools.hid import HidUnit
33from . import base
34from . import test_multitouch
35import libevdev
36import pytest
37
38import logging
39
40logger = logging.getLogger("hidtools.test.wacom")
41
42KERNEL_MODULE = ("wacom", "wacom")
43
44
45class ProximityState(Enum):
46    """
47    Enumeration of allowed proximity states.
48    """
49
50    # Tool is not able to be sensed by the device
51    OUT = 0
52
53    # Tool is close enough to be sensed, but some data may be invalid
54    # or inaccurate
55    IN_PROXIMITY = 1
56
57    # Tool is close enough to be sensed with high accuracy. All data
58    # valid.
59    IN_RANGE = 2
60
61    def fill(self, reportdata):
62        """Fill a report with approrpiate HID properties/values."""
63        reportdata.inrange = self in [ProximityState.IN_RANGE]
64        reportdata.wacomsense = self in [
65            ProximityState.IN_PROXIMITY,
66            ProximityState.IN_RANGE,
67        ]
68
69
70class ReportData:
71    """
72    Placeholder for HID report values.
73    """
74
75    pass
76
77
78@attr.s
79class Buttons:
80    """
81    Stylus button state.
82
83    Describes the state of each of the buttons / "side switches" that
84    may be present on a stylus. Buttons set to 'None' indicate the
85    state is "unchanged" since the previous event.
86    """
87
88    primary = attr.ib(default=None)
89    secondary = attr.ib(default=None)
90    tertiary = attr.ib(default=None)
91
92    @staticmethod
93    def clear():
94        """Button object with all states cleared."""
95        return Buttons(False, False, False)
96
97    def fill(self, reportdata):
98        """Fill a report with approrpiate HID properties/values."""
99        reportdata.barrelswitch = int(self.primary or 0)
100        reportdata.secondarybarrelswitch = int(self.secondary or 0)
101        reportdata.b3 = int(self.tertiary or 0)
102
103
104@attr.s
105class ToolID:
106    """
107    Stylus tool identifiers.
108
109    Contains values used to identify a specific stylus, e.g. its serial
110    number and tool-type identifier. Values of ``0`` may sometimes be
111    used for the out-of-range condition.
112    """
113
114    serial = attr.ib()
115    tooltype = attr.ib()
116
117    @staticmethod
118    def clear():
119        """ToolID object with all fields cleared."""
120        return ToolID(0, 0)
121
122    def fill(self, reportdata):
123        """Fill a report with approrpiate HID properties/values."""
124        reportdata.transducerserialnumber = self.serial & 0xFFFFFFFF
125        reportdata.serialhi = (self.serial >> 32) & 0xFFFFFFFF
126        reportdata.tooltype = self.tooltype
127
128
129@attr.s
130class PhysRange:
131    """
132    Range of HID physical values, with units.
133    """
134
135    unit = attr.ib()
136    min_size = attr.ib()
137    max_size = attr.ib()
138
139    CENTIMETER = HidUnit.from_string("SILinear: cm")
140    DEGREE = HidUnit.from_string("EnglishRotation: deg")
141
142    def contains(self, field):
143        """
144        Check if the physical size of the provided field is in range.
145
146        Compare the physical size described by the provided HID field
147        against the range of sizes described by this object. This is
148        an exclusive range comparison (e.g. 0 cm is not within the
149        range 0 cm - 5 cm) and exact unit comparison (e.g. 1 inch is
150        not within the range 0 cm - 5 cm).
151        """
152        phys_size = (field.physical_max - field.physical_min) * 10 ** (field.unit_exp)
153        return (
154            field.unit == self.unit.value
155            and phys_size > self.min_size
156            and phys_size < self.max_size
157        )
158
159
160class BaseTablet(base.UHIDTestDevice):
161    """
162    Skeleton object for all kinds of tablet devices.
163    """
164
165    def __init__(self, rdesc, name=None, info=None):
166        assert rdesc is not None
167        super().__init__(name, "Pen", input_info=info, rdesc=rdesc)
168        self.buttons = Buttons.clear()
169        self.toolid = ToolID.clear()
170        self.proximity = ProximityState.OUT
171        self.offset = 0
172        self.ring = -1
173        self.ek0 = False
174
175    def match_evdev_rule(self, application, evdev):
176        """
177        Filter out evdev nodes based on the requested application.
178
179        The Wacom driver may create several device nodes for each USB
180        interface device. It is crucial that we run tests with the
181        expected device node or things will obviously go off the rails.
182        Use the Wacom driver's usual naming conventions to apply a
183        sensible default filter.
184        """
185        if application in ["Pen", "Pad"]:
186            return evdev.name.endswith(application)
187        else:
188            return True
189
190    def create_report(
191        self, x, y, pressure, buttons=None, toolid=None, proximity=None, reportID=None
192    ):
193        """
194        Return an input report for this device.
195
196        :param x: absolute x
197        :param y: absolute y
198        :param pressure: pressure
199        :param buttons: stylus button state. Use ``None`` for unchanged.
200        :param toolid: tool identifiers. Use ``None`` for unchanged.
201        :param proximity: a ProximityState indicating the sensor's ability
202             to detect and report attributes of this tool. Use ``None``
203             for unchanged.
204        :param reportID: the numeric report ID for this report, if needed
205        """
206        if buttons is not None:
207            self.buttons = buttons
208        buttons = self.buttons
209
210        if toolid is not None:
211            self.toolid = toolid
212        toolid = self.toolid
213
214        if proximity is not None:
215            self.proximity = proximity
216        proximity = self.proximity
217
218        reportID = reportID or self.default_reportID
219
220        report = ReportData()
221        report.x = x
222        report.y = y
223        report.tippressure = pressure
224        report.tipswitch = pressure > 0
225        buttons.fill(report)
226        proximity.fill(report)
227        toolid.fill(report)
228
229        return super().create_report(report, reportID=reportID)
230
231    def create_report_heartbeat(self, reportID):
232        """
233        Return a heartbeat input report for this device.
234
235        Heartbeat reports generally contain battery status information,
236        among other things.
237        """
238        report = ReportData()
239        report.wacombatterycharging = 1
240        return super().create_report(report, reportID=reportID)
241
242    def create_report_pad(self, reportID, ring, ek0):
243        report = ReportData()
244
245        if ring is not None:
246            self.ring = ring
247        ring = self.ring
248
249        if ek0 is not None:
250            self.ek0 = ek0
251        ek0 = self.ek0
252
253        if ring >= 0:
254            report.wacomtouchring = ring
255            report.wacomtouchringstatus = 1
256        else:
257            report.wacomtouchring = 0x7F
258            report.wacomtouchringstatus = 0
259
260        report.wacomexpresskey00 = ek0
261        return super().create_report(report, reportID=reportID)
262
263    def event(self, x, y, pressure, buttons=None, toolid=None, proximity=None):
264        """
265        Send an input event on the default report ID.
266
267        :param x: absolute x
268        :param y: absolute y
269        :param buttons: stylus button state. Use ``None`` for unchanged.
270        :param toolid: tool identifiers. Use ``None`` for unchanged.
271        :param proximity: a ProximityState indicating the sensor's ability
272             to detect and report attributes of this tool. Use ``None``
273             for unchanged.
274        """
275        r = self.create_report(x, y, pressure, buttons, toolid, proximity)
276        self.call_input_event(r)
277        return [r]
278
279    def event_heartbeat(self, reportID):
280        """
281        Send a heartbeat event on the requested report ID.
282        """
283        r = self.create_report_heartbeat(reportID)
284        self.call_input_event(r)
285        return [r]
286
287    def event_pad(self, reportID, ring=None, ek0=None):
288        """
289        Send a pad event on the requested report ID.
290        """
291        r = self.create_report_pad(reportID, ring, ek0)
292        self.call_input_event(r)
293        return [r]
294
295    def get_report(self, req, rnum, rtype):
296        if rtype != self.UHID_FEATURE_REPORT:
297            return (1, [])
298
299        rdesc = None
300        for v in self.parsed_rdesc.feature_reports.values():
301            if v.report_ID == rnum:
302                rdesc = v
303
304        if rdesc is None:
305            return (1, [])
306
307        result = (1, [])
308        result = self.create_report_offset(rdesc) or result
309        return result
310
311    def create_report_offset(self, rdesc):
312        require = [
313            "Wacom Offset Left",
314            "Wacom Offset Top",
315            "Wacom Offset Right",
316            "Wacom Offset Bottom",
317        ]
318        if not set(require).issubset(set([f.usage_name for f in rdesc])):
319            return None
320
321        report = ReportData()
322        report.wacomoffsetleft = self.offset
323        report.wacomoffsettop = self.offset
324        report.wacomoffsetright = self.offset
325        report.wacomoffsetbottom = self.offset
326        r = rdesc.create_report([report], None)
327        return (0, r)
328
329
330class OpaqueTablet(BaseTablet):
331    """
332    Bare-bones opaque tablet with a minimum of features.
333
334    A tablet stripped down to its absolute core. It is capable of
335    reporting X/Y position and if the pen is in contact. No pressure,
336    no barrel switches, no eraser. Notably it *does* report an "In
337    Range" flag, but this is only because the Wacom driver expects
338    one to function properly. The device uses only standard HID usages,
339    not any of Wacom's vendor-defined pages.
340    """
341
342    # fmt: off
343    report_descriptor = [
344        0x05, 0x0D,                     # . Usage Page (Digitizer),
345        0x09, 0x01,                     # . Usage (Digitizer),
346        0xA1, 0x01,                     # . Collection (Application),
347        0x85, 0x01,                     # .     Report ID (1),
348        0x09, 0x20,                     # .     Usage (Stylus),
349        0xA1, 0x00,                     # .     Collection (Physical),
350        0x09, 0x42,                     # .         Usage (Tip Switch),
351        0x09, 0x32,                     # .         Usage (In Range),
352        0x15, 0x00,                     # .         Logical Minimum (0),
353        0x25, 0x01,                     # .         Logical Maximum (1),
354        0x75, 0x01,                     # .         Report Size (1),
355        0x95, 0x02,                     # .         Report Count (2),
356        0x81, 0x02,                     # .         Input (Variable),
357        0x95, 0x06,                     # .         Report Count (6),
358        0x81, 0x03,                     # .         Input (Constant, Variable),
359        0x05, 0x01,                     # .         Usage Page (Desktop),
360        0x09, 0x30,                     # .         Usage (X),
361        0x27, 0x80, 0x3E, 0x00, 0x00,   # .         Logical Maximum (16000),
362        0x47, 0x80, 0x3E, 0x00, 0x00,   # .         Physical Maximum (16000),
363        0x65, 0x11,                     # .         Unit (Centimeter),
364        0x55, 0x0D,                     # .         Unit Exponent (13),
365        0x75, 0x10,                     # .         Report Size (16),
366        0x95, 0x01,                     # .         Report Count (1),
367        0x81, 0x02,                     # .         Input (Variable),
368        0x09, 0x31,                     # .         Usage (Y),
369        0x27, 0x28, 0x23, 0x00, 0x00,   # .         Logical Maximum (9000),
370        0x47, 0x28, 0x23, 0x00, 0x00,   # .         Physical Maximum (9000),
371        0x81, 0x02,                     # .         Input (Variable),
372        0xC0,                           # .     End Collection,
373        0xC0,                           # . End Collection,
374    ]
375    # fmt: on
376
377    def __init__(self, rdesc=report_descriptor, name=None, info=(0x3, 0x056A, 0x9999)):
378        super().__init__(rdesc, name, info)
379        self.default_reportID = 1
380
381
382class OpaqueCTLTablet(BaseTablet):
383    """
384    Opaque tablet similar to something in the CTL product line.
385
386    A pen-only tablet with most basic features you would expect from
387    an actual device. Position, eraser, pressure, barrel buttons.
388    Uses the Wacom vendor-defined usage page.
389    """
390
391    # fmt: off
392    report_descriptor = [
393        0x06, 0x0D, 0xFF,               # . Usage Page (Vnd Wacom Emr),
394        0x09, 0x01,                     # . Usage (Digitizer),
395        0xA1, 0x01,                     # . Collection (Application),
396        0x85, 0x10,                     # .     Report ID (16),
397        0x09, 0x20,                     # .     Usage (Stylus),
398        0x35, 0x00,                     # .     Physical Minimum (0),
399        0x45, 0x00,                     # .     Physical Maximum (0),
400        0x15, 0x00,                     # .     Logical Minimum (0),
401        0x25, 0x01,                     # .     Logical Maximum (1),
402        0xA1, 0x00,                     # .     Collection (Physical),
403        0x09, 0x42,                     # .         Usage (Tip Switch),
404        0x09, 0x44,                     # .         Usage (Barrel Switch),
405        0x09, 0x5A,                     # .         Usage (Secondary Barrel Switch),
406        0x09, 0x45,                     # .         Usage (Eraser),
407        0x09, 0x3C,                     # .         Usage (Invert),
408        0x09, 0x32,                     # .         Usage (In Range),
409        0x09, 0x36,                     # .         Usage (In Proximity),
410        0x25, 0x01,                     # .         Logical Maximum (1),
411        0x75, 0x01,                     # .         Report Size (1),
412        0x95, 0x07,                     # .         Report Count (7),
413        0x81, 0x02,                     # .         Input (Variable),
414        0x95, 0x01,                     # .         Report Count (1),
415        0x81, 0x03,                     # .         Input (Constant, Variable),
416        0x0A, 0x30, 0x01,               # .         Usage (X),
417        0x65, 0x11,                     # .         Unit (Centimeter),
418        0x55, 0x0D,                     # .         Unit Exponent (13),
419        0x47, 0x80, 0x3E, 0x00, 0x00,   # .         Physical Maximum (16000),
420        0x27, 0x80, 0x3E, 0x00, 0x00,   # .         Logical Maximum (16000),
421        0x75, 0x18,                     # .         Report Size (24),
422        0x95, 0x01,                     # .         Report Count (1),
423        0x81, 0x02,                     # .         Input (Variable),
424        0x0A, 0x31, 0x01,               # .         Usage (Y),
425        0x47, 0x28, 0x23, 0x00, 0x00,   # .         Physical Maximum (9000),
426        0x27, 0x28, 0x23, 0x00, 0x00,   # .         Logical Maximum (9000),
427        0x81, 0x02,                     # .         Input (Variable),
428        0x09, 0x30,                     # .         Usage (Tip Pressure),
429        0x55, 0x00,                     # .         Unit Exponent (0),
430        0x65, 0x00,                     # .         Unit,
431        0x47, 0x00, 0x00, 0x00, 0x00,   # .         Physical Maximum (0),
432        0x26, 0xFF, 0x0F,               # .         Logical Maximum (4095),
433        0x75, 0x10,                     # .         Report Size (16),
434        0x81, 0x02,                     # .         Input (Variable),
435        0x75, 0x08,                     # .         Report Size (8),
436        0x95, 0x06,                     # .         Report Count (6),
437        0x81, 0x03,                     # .         Input (Constant, Variable),
438        0x0A, 0x32, 0x01,               # .         Usage (Z),
439        0x25, 0x3F,                     # .         Logical Maximum (63),
440        0x75, 0x08,                     # .         Report Size (8),
441        0x95, 0x01,                     # .         Report Count (1),
442        0x81, 0x02,                     # .         Input (Variable),
443        0x09, 0x5B,                     # .         Usage (Transducer Serial Number),
444        0x09, 0x5C,                     # .         Usage (Transducer Serial Number Hi),
445        0x17, 0x00, 0x00, 0x00, 0x80,   # .         Logical Minimum (-2147483648),
446        0x27, 0xFF, 0xFF, 0xFF, 0x7F,   # .         Logical Maximum (2147483647),
447        0x75, 0x20,                     # .         Report Size (32),
448        0x95, 0x02,                     # .         Report Count (2),
449        0x81, 0x02,                     # .         Input (Variable),
450        0x09, 0x77,                     # .         Usage (Tool Type),
451        0x15, 0x00,                     # .         Logical Minimum (0),
452        0x26, 0xFF, 0x0F,               # .         Logical Maximum (4095),
453        0x75, 0x10,                     # .         Report Size (16),
454        0x95, 0x01,                     # .         Report Count (1),
455        0x81, 0x02,                     # .         Input (Variable),
456        0xC0,                           # .     End Collection,
457        0xC0                            # . End Collection
458    ]
459    # fmt: on
460
461    def __init__(self, rdesc=report_descriptor, name=None, info=(0x3, 0x056A, 0x9999)):
462        super().__init__(rdesc, name, info)
463        self.default_reportID = 16
464
465
466class PTHX60_Pen(BaseTablet):
467    """
468    Pen interface of a PTH-660 / PTH-860 / PTH-460 tablet.
469
470    This generation of devices are nearly identical to each other, though
471    the PTH-460 uses a slightly different descriptor construction (splits
472    the pad among several physical collections)
473    """
474
475    def __init__(self, rdesc=None, name=None, info=None):
476        super().__init__(rdesc, name, info)
477        self.default_reportID = 16
478
479
480class BaseTest:
481    class TestTablet(base.BaseTestCase.TestUhid):
482        kernel_modules = [KERNEL_MODULE]
483
484        def sync_and_assert_events(
485            self, report, expected_events, auto_syn=True, strict=False
486        ):
487            """
488            Assert we see the expected events in response to a report.
489            """
490            uhdev = self.uhdev
491            syn_event = self.syn_event
492            if auto_syn:
493                expected_events.append(syn_event)
494            actual_events = uhdev.next_sync_events()
495            self.debug_reports(report, uhdev, actual_events)
496            if strict:
497                self.assertInputEvents(expected_events, actual_events)
498            else:
499                self.assertInputEventsIn(expected_events, actual_events)
500
501        def get_usages(self, uhdev):
502            def get_report_usages(report):
503                application = report.application
504                for field in report.fields:
505                    if field.usages is not None:
506                        for usage in field.usages:
507                            yield (field, usage, application)
508                    else:
509                        yield (field, field.usage, application)
510
511            desc = uhdev.parsed_rdesc
512            reports = [
513                *desc.input_reports.values(),
514                *desc.feature_reports.values(),
515                *desc.output_reports.values(),
516            ]
517            for report in reports:
518                for usage in get_report_usages(report):
519                    yield usage
520
521        def assertName(self, uhdev, type):
522            """
523            Assert that the name is as we expect.
524
525            The Wacom driver applies a number of decorations to the name
526            provided by the hardware. We cannot rely on the definition of
527            this assertion from the base class to work properly.
528            """
529            evdev = uhdev.get_evdev()
530            expected_name = uhdev.name + type
531            if "wacom" not in expected_name.lower():
532                expected_name = "Wacom " + expected_name
533            assert evdev.name == expected_name
534
535        def test_descriptor_physicals(self):
536            """
537            Verify that all HID usages which should have a physical range
538            actually do, and those which shouldn't don't. Also verify that
539            the associated unit is correct and within a sensible range.
540            """
541
542            def usage_id(page_name, usage_name):
543                page = HUT.usage_page_from_name(page_name)
544                return (page.page_id << 16) | page[usage_name].usage
545
546            required = {
547                usage_id("Generic Desktop", "X"): PhysRange(
548                    PhysRange.CENTIMETER, 5, 150
549                ),
550                usage_id("Generic Desktop", "Y"): PhysRange(
551                    PhysRange.CENTIMETER, 5, 150
552                ),
553                usage_id("Digitizers", "Width"): PhysRange(
554                    PhysRange.CENTIMETER, 5, 150
555                ),
556                usage_id("Digitizers", "Height"): PhysRange(
557                    PhysRange.CENTIMETER, 5, 150
558                ),
559                usage_id("Digitizers", "X Tilt"): PhysRange(PhysRange.DEGREE, 90, 180),
560                usage_id("Digitizers", "Y Tilt"): PhysRange(PhysRange.DEGREE, 90, 180),
561                usage_id("Digitizers", "Twist"): PhysRange(PhysRange.DEGREE, 358, 360),
562                usage_id("Wacom", "X Tilt"): PhysRange(PhysRange.DEGREE, 90, 180),
563                usage_id("Wacom", "Y Tilt"): PhysRange(PhysRange.DEGREE, 90, 180),
564                usage_id("Wacom", "Twist"): PhysRange(PhysRange.DEGREE, 358, 360),
565                usage_id("Wacom", "X"): PhysRange(PhysRange.CENTIMETER, 5, 150),
566                usage_id("Wacom", "Y"): PhysRange(PhysRange.CENTIMETER, 5, 150),
567                usage_id("Wacom", "Wacom TouchRing"): PhysRange(
568                    PhysRange.DEGREE, 358, 360
569                ),
570                usage_id("Wacom", "Wacom Offset Left"): PhysRange(
571                    PhysRange.CENTIMETER, 0, 0.5
572                ),
573                usage_id("Wacom", "Wacom Offset Top"): PhysRange(
574                    PhysRange.CENTIMETER, 0, 0.5
575                ),
576                usage_id("Wacom", "Wacom Offset Right"): PhysRange(
577                    PhysRange.CENTIMETER, 0, 0.5
578                ),
579                usage_id("Wacom", "Wacom Offset Bottom"): PhysRange(
580                    PhysRange.CENTIMETER, 0, 0.5
581                ),
582            }
583            for field, usage, application in self.get_usages(self.uhdev):
584                if application == usage_id("Generic Desktop", "Mouse"):
585                    # Ignore the vestigial Mouse collection which exists
586                    # on Wacom tablets only for backwards compatibility.
587                    continue
588
589                expect_physical = usage in required
590
591                phys_set = field.physical_min != 0 or field.physical_max != 0
592                assert phys_set == expect_physical
593
594                unit_set = field.unit != 0
595                assert unit_set == expect_physical
596
597                if unit_set:
598                    assert required[usage].contains(field)
599
600        def test_prop_direct(self):
601            """
602            Todo: Verify that INPUT_PROP_DIRECT is set on display devices.
603            """
604            pass
605
606        def test_prop_pointer(self):
607            """
608            Todo: Verify that INPUT_PROP_POINTER is set on opaque devices.
609            """
610            pass
611
612
613class PenTabletTest(BaseTest.TestTablet):
614    def assertName(self, uhdev):
615        super().assertName(uhdev, " Pen")
616
617
618class TouchTabletTest(BaseTest.TestTablet):
619    def assertName(self, uhdev):
620        super().assertName(uhdev, " Finger")
621
622
623class TestOpaqueTablet(PenTabletTest):
624    def create_device(self):
625        return OpaqueTablet()
626
627    def test_sanity(self):
628        """
629        Bring a pen into contact with the tablet, then remove it.
630
631        Ensure that we get the basic tool/touch/motion events that should
632        be sent by the driver.
633        """
634        uhdev = self.uhdev
635
636        self.sync_and_assert_events(
637            uhdev.event(
638                100,
639                200,
640                pressure=300,
641                buttons=Buttons.clear(),
642                toolid=ToolID(serial=1, tooltype=1),
643                proximity=ProximityState.IN_RANGE,
644            ),
645            [
646                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN, 1),
647                libevdev.InputEvent(libevdev.EV_ABS.ABS_X, 100),
648                libevdev.InputEvent(libevdev.EV_ABS.ABS_Y, 200),
649                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 1),
650            ],
651        )
652
653        self.sync_and_assert_events(
654            uhdev.event(110, 220, pressure=0),
655            [
656                libevdev.InputEvent(libevdev.EV_ABS.ABS_X, 110),
657                libevdev.InputEvent(libevdev.EV_ABS.ABS_Y, 220),
658                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 0),
659            ],
660        )
661
662        self.sync_and_assert_events(
663            uhdev.event(
664                120,
665                230,
666                pressure=0,
667                toolid=ToolID.clear(),
668                proximity=ProximityState.OUT,
669            ),
670            [
671                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN, 0),
672            ],
673        )
674
675        self.sync_and_assert_events(
676            uhdev.event(130, 240, pressure=0), [], auto_syn=False, strict=True
677        )
678
679
680class TestOpaqueCTLTablet(TestOpaqueTablet):
681    def create_device(self):
682        return OpaqueCTLTablet()
683
684    def test_buttons(self):
685        """
686        Test that the barrel buttons (side switches) work as expected.
687
688        Press and release each button individually to verify that we get
689        the expected events.
690        """
691        uhdev = self.uhdev
692
693        self.sync_and_assert_events(
694            uhdev.event(
695                100,
696                200,
697                pressure=0,
698                buttons=Buttons.clear(),
699                toolid=ToolID(serial=1, tooltype=1),
700                proximity=ProximityState.IN_RANGE,
701            ),
702            [
703                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN, 1),
704                libevdev.InputEvent(libevdev.EV_ABS.ABS_X, 100),
705                libevdev.InputEvent(libevdev.EV_ABS.ABS_Y, 200),
706                libevdev.InputEvent(libevdev.EV_MSC.MSC_SERIAL, 1),
707            ],
708        )
709
710        self.sync_and_assert_events(
711            uhdev.event(100, 200, pressure=0, buttons=Buttons(primary=True)),
712            [
713                libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS, 1),
714                libevdev.InputEvent(libevdev.EV_MSC.MSC_SERIAL, 1),
715            ],
716        )
717
718        self.sync_and_assert_events(
719            uhdev.event(100, 200, pressure=0, buttons=Buttons(primary=False)),
720            [
721                libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS, 0),
722                libevdev.InputEvent(libevdev.EV_MSC.MSC_SERIAL, 1),
723            ],
724        )
725
726        self.sync_and_assert_events(
727            uhdev.event(100, 200, pressure=0, buttons=Buttons(secondary=True)),
728            [
729                libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS2, 1),
730                libevdev.InputEvent(libevdev.EV_MSC.MSC_SERIAL, 1),
731            ],
732        )
733
734        self.sync_and_assert_events(
735            uhdev.event(100, 200, pressure=0, buttons=Buttons(secondary=False)),
736            [
737                libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS2, 0),
738                libevdev.InputEvent(libevdev.EV_MSC.MSC_SERIAL, 1),
739            ],
740        )
741
742
743PTHX60_Devices = [
744    {"rdesc": wacom_pth660_v145, "info": (0x3, 0x056A, 0x0357)},
745    {"rdesc": wacom_pth660_v150, "info": (0x3, 0x056A, 0x0357)},
746    {"rdesc": wacom_pth860_v145, "info": (0x3, 0x056A, 0x0358)},
747    {"rdesc": wacom_pth860_v150, "info": (0x3, 0x056A, 0x0358)},
748    {"rdesc": wacom_pth460_v105, "info": (0x3, 0x056A, 0x0392)},
749]
750
751PTHX60_Names = [
752    "PTH-660/v145",
753    "PTH-660/v150",
754    "PTH-860/v145",
755    "PTH-860/v150",
756    "PTH-460/v105",
757]
758
759
760class TestPTHX60_Pen(TestOpaqueCTLTablet):
761    @pytest.fixture(
762        autouse=True, scope="class", params=PTHX60_Devices, ids=PTHX60_Names
763    )
764    def set_device_params(self, request):
765        request.cls.device_params = request.param
766
767    def create_device(self):
768        return PTHX60_Pen(**self.device_params)
769
770    @pytest.mark.xfail
771    def test_descriptor_physicals(self):
772        # XFAIL: Various documented errata
773        super().test_descriptor_physicals()
774
775    def test_heartbeat_spurious(self):
776        """
777        Test that the heartbeat report does not send spurious events.
778        """
779        uhdev = self.uhdev
780
781        self.sync_and_assert_events(
782            uhdev.event(
783                100,
784                200,
785                pressure=300,
786                buttons=Buttons.clear(),
787                toolid=ToolID(serial=1, tooltype=0x822),
788                proximity=ProximityState.IN_RANGE,
789            ),
790            [
791                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN, 1),
792                libevdev.InputEvent(libevdev.EV_ABS.ABS_X, 100),
793                libevdev.InputEvent(libevdev.EV_ABS.ABS_Y, 200),
794                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 1),
795            ],
796        )
797
798        # Exactly zero events: not even a SYN
799        self.sync_and_assert_events(
800            uhdev.event_heartbeat(19), [], auto_syn=False, strict=True
801        )
802
803        self.sync_and_assert_events(
804            uhdev.event(110, 200, pressure=300),
805            [
806                libevdev.InputEvent(libevdev.EV_ABS.ABS_X, 110),
807            ],
808        )
809
810    def test_empty_pad_sync(self):
811        self.empty_pad_sync(num=3, denom=16, reverse=True)
812
813    def empty_pad_sync(self, num, denom, reverse):
814        """
815        Test that multiple pad collections do not trigger empty syncs.
816        """
817
818        def offset_rotation(value):
819            """
820            Offset touchring rotation values by the same factor as the
821            Linux kernel. Tablets historically don't use the same origin
822            as HID, and it sometimes changes from tablet to tablet...
823            """
824            evdev = self.uhdev.get_evdev()
825            info = evdev.absinfo[libevdev.EV_ABS.ABS_WHEEL]
826            delta = info.maximum - info.minimum + 1
827            if reverse:
828                value = info.maximum - value
829            value += num * delta // denom
830            if value > info.maximum:
831                value -= delta
832            elif value < info.minimum:
833                value += delta
834            return value
835
836        uhdev = self.uhdev
837        uhdev.application = "Pad"
838        evdev = uhdev.get_evdev()
839
840        print(evdev.name)
841        self.sync_and_assert_events(
842            uhdev.event_pad(reportID=17, ring=0, ek0=1),
843            [
844                libevdev.InputEvent(libevdev.EV_KEY.BTN_0, 1),
845                libevdev.InputEvent(libevdev.EV_ABS.ABS_WHEEL, offset_rotation(0)),
846                libevdev.InputEvent(libevdev.EV_ABS.ABS_MISC, 15),
847            ],
848        )
849
850        self.sync_and_assert_events(
851            uhdev.event_pad(reportID=17, ring=1, ek0=1),
852            [libevdev.InputEvent(libevdev.EV_ABS.ABS_WHEEL, offset_rotation(1))],
853        )
854
855        self.sync_and_assert_events(
856            uhdev.event_pad(reportID=17, ring=2, ek0=0),
857            [
858                libevdev.InputEvent(libevdev.EV_ABS.ABS_WHEEL, offset_rotation(2)),
859                libevdev.InputEvent(libevdev.EV_KEY.BTN_0, 0),
860            ],
861        )
862
863
864class TestDTH2452Tablet(test_multitouch.BaseTest.TestMultitouch, TouchTabletTest):
865    def create_device(self):
866        return test_multitouch.Digitizer(
867            "DTH 2452",
868            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",
869            input_info=(0x3, 0x056A, 0x0383),
870        )
871
872    def test_contact_id_0(self):
873        """
874        Bring a finger in contact with the tablet, then hold it down and remove it.
875
876        Ensure that even with contact ID = 0 which is usually given as an invalid
877        touch event by most tablets with the exception of a few, that given the
878        confidence bit is set to 1 it should process it as a valid touch to cover
879        the few tablets using contact ID = 0 as a valid touch value.
880        """
881        uhdev = self.uhdev
882        evdev = uhdev.get_evdev()
883
884        t0 = test_multitouch.Touch(0, 50, 100)
885        r = uhdev.event([t0])
886        events = uhdev.next_sync_events()
887        self.debug_reports(r, uhdev, events)
888
889        slot = self.get_slot(uhdev, t0, 0)
890
891        assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 1) in events
892        assert evdev.slots[slot][libevdev.EV_ABS.ABS_MT_TRACKING_ID] == 0
893        assert evdev.slots[slot][libevdev.EV_ABS.ABS_MT_POSITION_X] == 50
894        assert evdev.slots[slot][libevdev.EV_ABS.ABS_MT_POSITION_Y] == 100
895
896        t0.tipswitch = False
897        if uhdev.quirks is None or "VALID_IS_INRANGE" not in uhdev.quirks:
898            t0.inrange = False
899        r = uhdev.event([t0])
900        events = uhdev.next_sync_events()
901        self.debug_reports(r, uhdev, events)
902        assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 0) in events
903        assert evdev.slots[slot][libevdev.EV_ABS.ABS_MT_TRACKING_ID] == -1
904
905    def test_confidence_false(self):
906        """
907        Bring a finger in contact with the tablet with confidence set to false.
908
909        Ensure that the confidence bit being set to false should not result in a touch event.
910        """
911        uhdev = self.uhdev
912        evdev = uhdev.get_evdev()
913
914        t0 = test_multitouch.Touch(1, 50, 100)
915        t0.confidence = False
916        r = uhdev.event([t0])
917        events = uhdev.next_sync_events()
918        self.debug_reports(r, uhdev, events)
919
920        slot = self.get_slot(uhdev, t0, 0)
921
922        assert not events