xref: /openbmc/linux/tools/testing/selftests/hid/tests/test_wacom_generic.py (revision 1ac731c529cd4d6adbce134754b51ff7d822b145)
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 """
13 Tests for the Wacom driver generic codepath.
14 
15 This module tests the function of the Wacom driver's generic codepath.
16 The generic codepath is used by devices which are not explicitly listed
17 in the driver's device table. It uses the device's HID descriptor to
18 decode reports sent by the device.
19 """
20 
21 from .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 
29 import attr
30 from enum import Enum
31 from hidtools.hut import HUT
32 from hidtools.hid import HidUnit
33 from . import base
34 from . import test_multitouch
35 import libevdev
36 import pytest
37 
38 import logging
39 
40 logger = logging.getLogger("hidtools.test.wacom")
41 
42 KERNEL_MODULE = ("wacom", "wacom")
43 
44 
45 class 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 
70 class ReportData:
71     """
72     Placeholder for HID report values.
73     """
74 
75     pass
76 
77 
78 @attr.s
79 class 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
105 class 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
130 class 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 
160 class 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 
330 class 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 
382 class 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 
466 class 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 
480 class 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 
613 class PenTabletTest(BaseTest.TestTablet):
614     def assertName(self, uhdev):
615         super().assertName(uhdev, " Pen")
616 
617 
618 class TouchTabletTest(BaseTest.TestTablet):
619     def assertName(self, uhdev):
620         super().assertName(uhdev, " Finger")
621 
622 
623 class 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 
680 class 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 
743 PTHX60_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 
751 PTHX60_Names = [
752     "PTH-660/v145",
753     "PTH-660/v150",
754     "PTH-860/v145",
755     "PTH-860/v150",
756     "PTH-460/v105",
757 ]
758 
759 
760 class 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 
864 class 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