xref: /linux/tools/testing/selftests/hid/tests/test_wacom_generic.py (revision 06d07429858317ded2db7986113a9e0129cd599b)
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 collections import namedtuple
31from enum import Enum
32from hidtools.hut import HUT
33from hidtools.hid import HidUnit
34from . import base
35from . import test_multitouch
36import libevdev
37import pytest
38
39import logging
40
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 device
52    OUT = 0
53
54    # Tool is close enough to be sensed, but some data may be invalid
55    # or inaccurate
56    IN_PROXIMITY = 1
57
58    # Tool is close enough to be sensed with high accuracy. All data
59    # valid.
60    IN_RANGE = 2
61
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    pass
77
78
79@attr.s
80class Buttons:
81    """
82    Stylus button state.
83
84    Describes the state of each of the buttons / "side switches" that
85    may be present on a stylus. Buttons set to 'None' indicate the
86    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    @staticmethod
94    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.s
106class ToolID:
107    """
108    Stylus tool identifiers.
109
110    Contains values used to identify a specific stylus, e.g. its serial
111    number and tool-type identifier. Values of ``0`` may sometimes be
112    used for the out-of-range condition.
113    """
114
115    serial = attr.ib()
116    tooltype = attr.ib()
117
118    @staticmethod
119    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 & 0xFFFFFFFF
126        reportdata.serialhi = (self.serial >> 32) & 0xFFFFFFFF
127        reportdata.tooltype = self.tooltype
128
129
130@attr.s
131class 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 field
148        against the range of sizes described by this object. This is
149        an exclusive range comparison (e.g. 0 cm is not within the
150        range 0 cm - 5 cm) and exact unit comparison (e.g. 1 inch is
151        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.value
156            and phys_size > self.min_size
157            and phys_size < self.max_size
158        )
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 None
168        super().__init__(name, "Pen", input_info=info, rdesc=rdesc)
169        self.buttons = Buttons.clear()
170        self.toolid = ToolID.clear()
171        self.proximity = ProximityState.OUT
172        self.offset = 0
173        self.ring = -1
174        self.ek0 = False
175
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 USB
181        interface device. It is crucial that we run tests with the
182        expected device node or things will obviously go off the rails.
183        Use the Wacom driver's usual naming conventions to apply a
184        sensible default filter.
185        """
186        if application in ["Pen", "Pad"]:
187            return evdev.name.endswith(application)
188        else:
189            return True
190
191    def create_report(
192        self, x, y, pressure, buttons=None, toolid=None, proximity=None, reportID=None
193    ):
194        """
195        Return an input report for this device.
196
197        :param x: absolute x
198        :param y: absolute y
199        :param pressure: pressure
200        :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 ability
203             to detect and report attributes of this tool. Use ``None``
204             for unchanged.
205        :param reportID: the numeric report ID for this report, if needed
206        """
207        if buttons is not None:
208            self.buttons = buttons
209        buttons = self.buttons
210
211        if toolid is not None:
212            self.toolid = toolid
213        toolid = self.toolid
214
215        if proximity is not None:
216            self.proximity = proximity
217        proximity = self.proximity
218
219        reportID = reportID or self.default_reportID
220
221        report = ReportData()
222        report.x = x
223        report.y = y
224        report.tippressure = pressure
225        report.tipswitch = pressure > 0
226        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 = 1
241        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 = ring
248        ring = self.ring
249
250        if ek0 is not None:
251            self.ek0 = ek0
252        ek0 = self.ek0
253
254        if ring >= 0:
255            report.wacomtouchring = ring
256            report.wacomtouchringstatus = 1
257        else:
258            report.wacomtouchring = 0x7F
259            report.wacomtouchringstatus = 0
260
261        report.wacomexpresskey00 = ek0
262        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 x
269        :param y: absolute y
270        :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 ability
273             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 = None
301        for v in self.parsed_rdesc.feature_reports.values():
302            if v.report_ID == rnum:
303                rdesc = v
304
305        if rdesc is None:
306            return (1, [])
307
308        result = (1, [])
309        result = self.create_report_offset(rdesc) or result
310        return result
311
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 None
321
322        report = ReportData()
323        report.wacomoffsetleft = self.offset
324        report.wacomoffsettop = self.offset
325        report.wacomoffsetright = self.offset
326        report.wacomoffsetbottom = self.offset
327        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 of
336    reporting X/Y position and if the pen is in contact. No pressure,
337    no barrel switches, no eraser. Notably it *does* report an "In
338    Range" flag, but this is only because the Wacom driver expects
339    one to function properly. The device uses only standard HID usages,
340    not any of Wacom's vendor-defined pages.
341    """
342
343    # fmt: off
344    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: on
377
378    def __init__(self, rdesc=report_descriptor, name=None, info=(0x3, 0x056A, 0x9999)):
379        super().__init__(rdesc, name, info)
380        self.default_reportID = 1
381
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 from
388    an actual device. Position, eraser, pressure, barrel buttons.
389    Uses the Wacom vendor-defined usage page.
390    """
391
392    # fmt: off
393    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 Collection
459    ]
460    # fmt: on
461
462    def __init__(self, rdesc=report_descriptor, name=None, info=(0x3, 0x056A, 0x9999)):
463        super().__init__(rdesc, name, info)
464        self.default_reportID = 16
465
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, though
472    the PTH-460 uses a slightly different descriptor construction (splits
473    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 = 16
479
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=False
487        ):
488            """
489            Assert we see the expected events in response to a report.
490            """
491            uhdev = self.uhdev
492            syn_event = self.syn_event
493            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.application
505                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_rdesc
513            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 usage
521
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 name
527            provided by the hardware. We cannot rely on the definition of
528            this assertion from the base class to work properly.
529            """
530            evdev = uhdev.get_evdev()
531            expected_name = uhdev.name + type
532            if "wacom" not in expected_name.lower():
533                expected_name = "Wacom " + expected_name
534            assert evdev.name == expected_name
535
536        def test_descriptor_physicals(self):
537            """
538            Verify that all HID usages which should have a physical range
539            actually do, and those which shouldn't don't. Also verify that
540            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].usage
546
547            required = {
548                usage_id("Generic Desktop", "X"): PhysRange(
549                    PhysRange.CENTIMETER, 5, 150
550                ),
551                usage_id("Generic Desktop", "Y"): PhysRange(
552                    PhysRange.CENTIMETER, 5, 150
553                ),
554                usage_id("Digitizers", "Width"): PhysRange(
555                    PhysRange.CENTIMETER, 5, 150
556                ),
557                usage_id("Digitizers", "Height"): PhysRange(
558                    PhysRange.CENTIMETER, 5, 150
559                ),
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, 360
570                ),
571                usage_id("Wacom", "Wacom Offset Left"): PhysRange(
572                    PhysRange.CENTIMETER, 0, 0.5
573                ),
574                usage_id("Wacom", "Wacom Offset Top"): PhysRange(
575                    PhysRange.CENTIMETER, 0, 0.5
576                ),
577                usage_id("Wacom", "Wacom Offset Right"): PhysRange(
578                    PhysRange.CENTIMETER, 0, 0.5
579                ),
580                usage_id("Wacom", "Wacom Offset Bottom"): PhysRange(
581                    PhysRange.CENTIMETER, 0, 0.5
582                ),
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 exists
587                    # on Wacom tablets only for backwards compatibility.
588                    continue
589
590                expect_physical = usage in required
591
592                phys_set = field.physical_min != 0 or field.physical_max != 0
593                assert phys_set == expect_physical
594
595                unit_set = field.unit != 0
596                assert unit_set == expect_physical
597
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            pass
606
607        def test_prop_pointer(self):
608            """
609            Todo: Verify that INPUT_PROP_POINTER is set on opaque devices.
610            """
611            pass
612
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 should
633        be sent by the driver.
634        """
635        uhdev = self.uhdev
636
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=True
678        )
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 get
690        the expected events.
691        """
692        uhdev = self.uhdev
693
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_Names
764    )
765    def set_device_params(self, request):
766        request.cls.device_params = request.param
767
768    def create_device(self):
769        return PTHX60_Pen(**self.device_params)
770
771    @pytest.mark.xfail
772    def test_descriptor_physicals(self):
773        # XFAIL: Various documented errata
774        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.uhdev
781
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 SYN
800        self.sync_and_assert_events(
801            uhdev.event_heartbeat(19), [], auto_syn=False, strict=True
802        )
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 the
822            Linux kernel. Tablets historically don't use the same origin
823            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 + 1
828            if reverse:
829                value = info.maximum - value
830            value += num * delta // denom
831            if value > info.maximum:
832                value -= delta
833            elif value < info.minimum:
834                value += delta
835            return value
836
837        uhdev = self.uhdev
838        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 that
880        does not overlap with other contacts. The value of `t` may be
881        incremented over time to move the point along a linear path.
882        """
883        x = 50 + 10 * contact_id + t * 11
884        y = 100 + 100 * contact_id + t * 11
885        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-known
892        locations. The value of `t` may be incremented over time to move the
893        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_id
902        tracking_id = contact_ids.tracking_id
903        slot_num = contact_ids.slot_num
904
905        x = 50 + 10 * contact_id + t * 11
906        y = 100 + 100 * contact_id + t * 11
907
908        # If the data isn't supposed to be stored in any slots, there is
909        # nothing we can check for in the evdev stream.
910        if slot_num is None:
911            assert tracking_id == -1
912            return
913
914        assert evdev.slots[slot_num][libevdev.EV_ABS.ABS_MT_TRACKING_ID] == tracking_id
915        if tracking_id != -1:
916            assert evdev.slots[slot_num][libevdev.EV_ABS.ABS_MT_POSITION_X] == x
917            assert evdev.slots[slot_num][libevdev.EV_ABS.ABS_MT_POSITION_Y] == y
918
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 invalid
931        touch event by most tablets with the exception of a few, that given the
932        confidence bit is set to 1 it should process it as a valid touch to cover
933        the few tablets using contact ID = 0 as a valid touch value.
934        """
935        uhdev = self.uhdev
936        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 events
946        assert evdev.slots[slot][libevdev.EV_ABS.ABS_MT_TRACKING_ID] == 0
947        assert evdev.slots[slot][libevdev.EV_ABS.ABS_MT_POSITION_X] == 50
948        assert evdev.slots[slot][libevdev.EV_ABS.ABS_MT_POSITION_Y] == 100
949
950        t0.tipswitch = False
951        if uhdev.quirks is None or "VALID_IS_INRANGE" not in uhdev.quirks:
952            t0.inrange = False
953        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 events
957        assert evdev.slots[slot][libevdev.EV_ABS.ABS_MT_TRACKING_ID] == -1
958
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.uhdev
966        _evdev = uhdev.get_evdev()
967
968        t0 = test_multitouch.Touch(1, 50, 100)
969        t0.confidence = False
970        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 events
977
978    def test_confidence_multitouch(self):
979        """
980        Bring multiple fingers in contact with the tablet, some with the
981        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.uhdev
987        evdev = uhdev.get_evdev()
988
989        touches = self.make_contacts(5)
990        touches[0].confidence = False
991        touches[2].confidence = False
992        touches[4].confidence = False
993
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 events
999
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, verify
1013        that the contacts move and are reported as up/down as expected
1014        by the state of the tipswitch and confidence bits.
1015        """
1016        t = 0
1017
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 += 1
1033
1034    def test_confidence_loss_a(self):
1035        """
1036        Transition a confident contact to a non-confident contact by
1037        first clearing the tipswitch.
1038
1039        Ensure that the driver reports the transitioned contact as
1040        being removed and that other contacts continue to report
1041        normally. This mode of confidence loss is used by the
1042        DTH-2452.
1043        """
1044        uhdev = self.uhdev
1045        evdev = uhdev.get_evdev()
1046
1047        self.confidence_change_assert_playback(uhdev, evdev, [
1048            # t=0: Contact 0 == Down + confident; Contact 1 == Down + confident
1049            # Both fingers confidently in contact
1050            [(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 + confident
1054            # First finger looses confidence and clears only the tipswitch flag
1055            [(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 + confident
1059            # First finger has lost confidence and has both flags cleared
1060            [(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 + confident
1064            # First finger has lost confidence and has both flags cleared
1065            [(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 by
1072        cleraing both tipswitch and confidence bits simultaneously.
1073
1074        Ensure that the driver reports the transitioned contact as
1075        being removed and that other contacts continue to report
1076        normally. This mode of confidence loss is used by some
1077        AES devices.
1078        """
1079        uhdev = self.uhdev
1080        evdev = uhdev.get_evdev()
1081
1082        self.confidence_change_assert_playback(uhdev, evdev, [
1083            # t=0: Contact 0 == Down + confident; Contact 1 == Down + confident
1084            # Both fingers confidently in contact
1085            [(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 + confident
1089            # First finger looses confidence and has both flags cleared simultaneously
1090            [(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 + confident
1094            # First finger has lost confidence and has both flags cleared
1095            [(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 + confident
1099            # First finger has lost confidence and has both flags cleared
1100            [(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 by
1107        clearing only the confidence bit.
1108
1109        Ensure that the driver reports the transitioned contact as
1110        being removed and that other contacts continue to report
1111        normally.
1112        """
1113        uhdev = self.uhdev
1114        evdev = uhdev.get_evdev()
1115
1116        self.confidence_change_assert_playback(uhdev, evdev, [
1117            # t=0: Contact 0 == Down + confident; Contact 1 == Down + confident
1118            # Both fingers confidently in contact
1119            [(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 + confident
1123            # First finger looses confidence and clears only the confidence flag
1124            [(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 + confident
1128            # First finger has lost confidence and has both flags cleared
1129            [(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 + confident
1133            # First finger has lost confidence and has both flags cleared
1134            [(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.uhdev
1145        evdev = uhdev.get_evdev()
1146
1147        self.confidence_change_assert_playback(uhdev, evdev, [
1148            # t=0: Contact 0 == Down + !confident; Contact 1 == Down + confident
1149            # Only second finger is confidently in contact
1150            [(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 + confident
1154            # First finger gains confidence
1155            [(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 + confident
1159            # First finger remains confident
1160            [(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 + confident
1164            # First finger remains confident
1165            [(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.uhdev
1176        evdev = uhdev.get_evdev()
1177
1178        self.confidence_change_assert_playback(uhdev, evdev, [
1179            # t=0: Contact 0 == Down + confident; Contact 1 == Down + confident
1180            # First and second finger confidently in contact
1181            [(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 + confident
1185            # Firtst finger looses confidence
1186            [(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 + confident
1190            # First finger gains confidence
1191            [(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 + confident
1195            # First finger goes up
1196            [(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