xref: /linux/tools/testing/selftests/hid/tests/test_wacom_generic.py (revision 48976c0eba2ff3a3b893c35853bdf27369b16655)
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 = base.KernelModule("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
602class PenTabletTest(BaseTest.TestTablet):
603    def assertName(self, uhdev):
604        super().assertName(uhdev, " Pen")
605
606
607class TouchTabletTest(BaseTest.TestTablet):
608    def assertName(self, uhdev):
609        super().assertName(uhdev, " Finger")
610
611
612class TestOpaqueTablet(PenTabletTest):
613    def create_device(self):
614        return OpaqueTablet()
615
616    def test_sanity(self):
617        """
618        Bring a pen into contact with the tablet, then remove it.
619
620        Ensure that we get the basic tool/touch/motion events that should
621        be sent by the driver.
622        """
623        uhdev = self.uhdev
624
625        self.sync_and_assert_events(
626            uhdev.event(
627                100,
628                200,
629                pressure=300,
630                buttons=Buttons.clear(),
631                toolid=ToolID(serial=1, tooltype=1),
632                proximity=ProximityState.IN_RANGE,
633            ),
634            [
635                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN, 1),
636                libevdev.InputEvent(libevdev.EV_ABS.ABS_X, 100),
637                libevdev.InputEvent(libevdev.EV_ABS.ABS_Y, 200),
638                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 1),
639            ],
640        )
641
642        self.sync_and_assert_events(
643            uhdev.event(110, 220, pressure=0),
644            [
645                libevdev.InputEvent(libevdev.EV_ABS.ABS_X, 110),
646                libevdev.InputEvent(libevdev.EV_ABS.ABS_Y, 220),
647                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 0),
648            ],
649        )
650
651        self.sync_and_assert_events(
652            uhdev.event(
653                120,
654                230,
655                pressure=0,
656                toolid=ToolID.clear(),
657                proximity=ProximityState.OUT,
658            ),
659            [
660                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN, 0),
661            ],
662        )
663
664        self.sync_and_assert_events(
665            uhdev.event(130, 240, pressure=0), [], auto_syn=False, strict=True
666        )
667
668    def test_prop_pointer(self):
669        """
670        Verify that INPUT_PROP_POINTER is set and INPUT_PROP_DIRECT
671        is not set on opaque devices.
672        """
673        evdev = self.uhdev.get_evdev()
674        assert libevdev.INPUT_PROP_POINTER in evdev.properties
675        assert libevdev.INPUT_PROP_DIRECT not in evdev.properties
676
677
678class TestOpaqueCTLTablet(TestOpaqueTablet):
679    def create_device(self):
680        return OpaqueCTLTablet()
681
682    def test_buttons(self):
683        """
684        Test that the barrel buttons (side switches) work as expected.
685
686        Press and release each button individually to verify that we get
687        the expected events.
688        """
689        uhdev = self.uhdev
690
691        self.sync_and_assert_events(
692            uhdev.event(
693                100,
694                200,
695                pressure=0,
696                buttons=Buttons.clear(),
697                toolid=ToolID(serial=1, tooltype=1),
698                proximity=ProximityState.IN_RANGE,
699            ),
700            [
701                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN, 1),
702                libevdev.InputEvent(libevdev.EV_ABS.ABS_X, 100),
703                libevdev.InputEvent(libevdev.EV_ABS.ABS_Y, 200),
704                libevdev.InputEvent(libevdev.EV_MSC.MSC_SERIAL, 1),
705            ],
706        )
707
708        self.sync_and_assert_events(
709            uhdev.event(100, 200, pressure=0, buttons=Buttons(primary=True)),
710            [
711                libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS, 1),
712                libevdev.InputEvent(libevdev.EV_MSC.MSC_SERIAL, 1),
713            ],
714        )
715
716        self.sync_and_assert_events(
717            uhdev.event(100, 200, pressure=0, buttons=Buttons(primary=False)),
718            [
719                libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS, 0),
720                libevdev.InputEvent(libevdev.EV_MSC.MSC_SERIAL, 1),
721            ],
722        )
723
724        self.sync_and_assert_events(
725            uhdev.event(100, 200, pressure=0, buttons=Buttons(secondary=True)),
726            [
727                libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS2, 1),
728                libevdev.InputEvent(libevdev.EV_MSC.MSC_SERIAL, 1),
729            ],
730        )
731
732        self.sync_and_assert_events(
733            uhdev.event(100, 200, pressure=0, buttons=Buttons(secondary=False)),
734            [
735                libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS2, 0),
736                libevdev.InputEvent(libevdev.EV_MSC.MSC_SERIAL, 1),
737            ],
738        )
739
740
741PTHX60_Devices = [
742    {"rdesc": wacom_pth660_v145, "info": (0x3, 0x056A, 0x0357)},
743    {"rdesc": wacom_pth660_v150, "info": (0x3, 0x056A, 0x0357)},
744    {"rdesc": wacom_pth860_v145, "info": (0x3, 0x056A, 0x0358)},
745    {"rdesc": wacom_pth860_v150, "info": (0x3, 0x056A, 0x0358)},
746    {"rdesc": wacom_pth460_v105, "info": (0x3, 0x056A, 0x0392)},
747]
748
749PTHX60_Names = [
750    "PTH-660/v145",
751    "PTH-660/v150",
752    "PTH-860/v145",
753    "PTH-860/v150",
754    "PTH-460/v105",
755]
756
757
758class TestPTHX60_Pen(TestOpaqueCTLTablet):
759    @pytest.fixture(
760        autouse=True, scope="class", params=PTHX60_Devices, ids=PTHX60_Names
761    )
762    def set_device_params(self, request):
763        request.cls.device_params = request.param
764
765    def create_device(self):
766        return PTHX60_Pen(**self.device_params)
767
768    @pytest.mark.xfail
769    def test_descriptor_physicals(self):
770        # XFAIL: Various documented errata
771        super().test_descriptor_physicals()
772
773    def test_heartbeat_spurious(self):
774        """
775        Test that the heartbeat report does not send spurious events.
776        """
777        uhdev = self.uhdev
778
779        self.sync_and_assert_events(
780            uhdev.event(
781                100,
782                200,
783                pressure=300,
784                buttons=Buttons.clear(),
785                toolid=ToolID(serial=1, tooltype=0x822),
786                proximity=ProximityState.IN_RANGE,
787            ),
788            [
789                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN, 1),
790                libevdev.InputEvent(libevdev.EV_ABS.ABS_X, 100),
791                libevdev.InputEvent(libevdev.EV_ABS.ABS_Y, 200),
792                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 1),
793            ],
794        )
795
796        # Exactly zero events: not even a SYN
797        self.sync_and_assert_events(
798            uhdev.event_heartbeat(19), [], auto_syn=False, strict=True
799        )
800
801        self.sync_and_assert_events(
802            uhdev.event(110, 200, pressure=300),
803            [
804                libevdev.InputEvent(libevdev.EV_ABS.ABS_X, 110),
805            ],
806        )
807
808    def test_empty_pad_sync(self):
809        self.empty_pad_sync(num=3, denom=16, reverse=True)
810
811    def empty_pad_sync(self, num, denom, reverse):
812        """
813        Test that multiple pad collections do not trigger empty syncs.
814        """
815
816        def offset_rotation(value):
817            """
818            Offset touchring rotation values by the same factor as the
819            Linux kernel. Tablets historically don't use the same origin
820            as HID, and it sometimes changes from tablet to tablet...
821            """
822            evdev = self.uhdev.get_evdev()
823            info = evdev.absinfo[libevdev.EV_ABS.ABS_WHEEL]
824            delta = info.maximum - info.minimum + 1
825            if reverse:
826                value = info.maximum - value
827            value += num * delta // denom
828            if value > info.maximum:
829                value -= delta
830            elif value < info.minimum:
831                value += delta
832            return value
833
834        uhdev = self.uhdev
835        uhdev.application = "Pad"
836        evdev = uhdev.get_evdev()
837
838        print(evdev.name)
839        self.sync_and_assert_events(
840            uhdev.event_pad(reportID=17, ring=0, ek0=1),
841            [
842                libevdev.InputEvent(libevdev.EV_KEY.BTN_0, 1),
843                libevdev.InputEvent(libevdev.EV_ABS.ABS_WHEEL, offset_rotation(0)),
844                libevdev.InputEvent(libevdev.EV_ABS.ABS_MISC, 15),
845            ],
846        )
847
848        self.sync_and_assert_events(
849            uhdev.event_pad(reportID=17, ring=1, ek0=1),
850            [libevdev.InputEvent(libevdev.EV_ABS.ABS_WHEEL, offset_rotation(1))],
851        )
852
853        self.sync_and_assert_events(
854            uhdev.event_pad(reportID=17, ring=2, ek0=0),
855            [
856                libevdev.InputEvent(libevdev.EV_ABS.ABS_WHEEL, offset_rotation(2)),
857                libevdev.InputEvent(libevdev.EV_KEY.BTN_0, 0),
858            ],
859        )
860
861
862class DirectTabletTest():
863    def test_prop_direct(self):
864        """
865        Verify that INPUT_PROP_DIRECT is set and INPUT_PROP_POINTER
866        is not set on display devices.
867        """
868        evdev = self.uhdev.get_evdev()
869        assert libevdev.INPUT_PROP_DIRECT in evdev.properties
870        assert libevdev.INPUT_PROP_POINTER not in evdev.properties
871
872
873class TestDTH2452Tablet(test_multitouch.BaseTest.TestMultitouch, TouchTabletTest, DirectTabletTest):
874    ContactIds = namedtuple("ContactIds", "contact_id, tracking_id, slot_num")
875
876    def create_device(self):
877        return test_multitouch.Digitizer(
878            "DTH 2452",
879            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",
880            input_info=(0x3, 0x056A, 0x0383),
881        )
882
883    def make_contact(self, contact_id=0, t=0):
884        """
885        Make a single touch contact that can move over time.
886
887        Creates a touch object that has a well-known position in space that
888        does not overlap with other contacts. The value of `t` may be
889        incremented over time to move the point along a linear path.
890        """
891        x = 50 + 10 * contact_id + t * 11
892        y = 100 + 100 * contact_id + t * 11
893        return test_multitouch.Touch(contact_id, x, y)
894
895    def make_contacts(self, n, t=0):
896        """
897        Make multiple touch contacts that can move over time.
898
899        Returns a list of `n` touch objects that are positioned at well-known
900        locations. The value of `t` may be incremented over time to move the
901        points along a linear path.
902        """
903        return [self.make_contact(id, t) for id in range(0, n)]
904
905    def assert_contact(self, evdev, contact_ids, t=0):
906        """
907        Assert properties of a contact generated by make_contact.
908        """
909        contact_id = contact_ids.contact_id
910        tracking_id = contact_ids.tracking_id
911        slot_num = contact_ids.slot_num
912
913        x = 50 + 10 * contact_id + t * 11
914        y = 100 + 100 * contact_id + t * 11
915
916        # If the data isn't supposed to be stored in any slots, there is
917        # nothing we can check for in the evdev stream.
918        if slot_num is None:
919            assert tracking_id == -1
920            return
921
922        assert evdev.slots[slot_num][libevdev.EV_ABS.ABS_MT_TRACKING_ID] == tracking_id
923        if tracking_id != -1:
924            assert evdev.slots[slot_num][libevdev.EV_ABS.ABS_MT_POSITION_X] == x
925            assert evdev.slots[slot_num][libevdev.EV_ABS.ABS_MT_POSITION_Y] == y
926
927    def assert_contacts(self, evdev, data, t=0):
928        """
929        Assert properties of a list of contacts generated by make_contacts.
930        """
931        for contact_ids in data:
932            self.assert_contact(evdev, contact_ids, t)
933
934    def test_contact_id_0(self):
935        """
936        Bring a finger in contact with the tablet, then hold it down and remove it.
937
938        Ensure that even with contact ID = 0 which is usually given as an invalid
939        touch event by most tablets with the exception of a few, that given the
940        confidence bit is set to 1 it should process it as a valid touch to cover
941        the few tablets using contact ID = 0 as a valid touch value.
942        """
943        uhdev = self.uhdev
944        evdev = uhdev.get_evdev()
945
946        t0 = test_multitouch.Touch(0, 50, 100)
947        r = uhdev.event([t0])
948        events = uhdev.next_sync_events()
949        self.debug_reports(r, uhdev, events)
950
951        slot = self.get_slot(uhdev, t0, 0)
952
953        assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 1) in events
954        assert evdev.slots[slot][libevdev.EV_ABS.ABS_MT_TRACKING_ID] == 0
955        assert evdev.slots[slot][libevdev.EV_ABS.ABS_MT_POSITION_X] == 50
956        assert evdev.slots[slot][libevdev.EV_ABS.ABS_MT_POSITION_Y] == 100
957
958        t0.tipswitch = False
959        if uhdev.quirks is None or "VALID_IS_INRANGE" not in uhdev.quirks:
960            t0.inrange = False
961        r = uhdev.event([t0])
962        events = uhdev.next_sync_events()
963        self.debug_reports(r, uhdev, events)
964        assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 0) in events
965        assert evdev.slots[slot][libevdev.EV_ABS.ABS_MT_TRACKING_ID] == -1
966
967    def test_confidence_false(self):
968        """
969        Bring a finger in contact with the tablet with confidence set to false.
970
971        Ensure that the confidence bit being set to false should not result in a touch event.
972        """
973        uhdev = self.uhdev
974        _evdev = uhdev.get_evdev()
975
976        t0 = test_multitouch.Touch(1, 50, 100)
977        t0.confidence = False
978        r = uhdev.event([t0])
979        events = uhdev.next_sync_events()
980        self.debug_reports(r, uhdev, events)
981
982        _slot = self.get_slot(uhdev, t0, 0)
983
984        assert not events
985
986    def test_confidence_multitouch(self):
987        """
988        Bring multiple fingers in contact with the tablet, some with the
989        confidence bit set, and some without.
990
991        Ensure that all confident touches are reported and that all non-
992        confident touches are ignored.
993        """
994        uhdev = self.uhdev
995        evdev = uhdev.get_evdev()
996
997        touches = self.make_contacts(5)
998        touches[0].confidence = False
999        touches[2].confidence = False
1000        touches[4].confidence = False
1001
1002        r = uhdev.event(touches)
1003        events = uhdev.next_sync_events()
1004        self.debug_reports(r, uhdev, events)
1005
1006        assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 1) in events
1007
1008        self.assert_contacts(
1009            evdev,
1010            [
1011                self.ContactIds(contact_id=0, tracking_id=-1, slot_num=None),
1012                self.ContactIds(contact_id=1, tracking_id=0, slot_num=0),
1013                self.ContactIds(contact_id=2, tracking_id=-1, slot_num=None),
1014                self.ContactIds(contact_id=3, tracking_id=1, slot_num=1),
1015                self.ContactIds(contact_id=4, tracking_id=-1, slot_num=None),
1016            ],
1017        )
1018
1019    def confidence_change_assert_playback(self, uhdev, evdev, timeline):
1020        """
1021        Assert proper behavior of contacts that move and change tipswitch /
1022        confidence status over time.
1023
1024        Given a `timeline` list of touch states to iterate over, verify
1025        that the contacts move and are reported as up/down as expected
1026        by the state of the tipswitch and confidence bits.
1027        """
1028        t = 0
1029
1030        for state in timeline:
1031            touches = self.make_contacts(len(state), t)
1032
1033            for item in zip(touches, state):
1034                item[0].tipswitch = item[1][1]
1035                item[0].confidence = item[1][2]
1036
1037            r = uhdev.event(touches)
1038            events = uhdev.next_sync_events()
1039            self.debug_reports(r, uhdev, events)
1040
1041            ids = [x[0] for x in state]
1042            self.assert_contacts(evdev, ids, t)
1043
1044            t += 1
1045
1046    def test_confidence_loss_a(self):
1047        """
1048        Transition a confident contact to a non-confident contact by
1049        first clearing the tipswitch.
1050
1051        Ensure that the driver reports the transitioned contact as
1052        being removed and that other contacts continue to report
1053        normally. This mode of confidence loss is used by the
1054        DTH-2452.
1055        """
1056        uhdev = self.uhdev
1057        evdev = uhdev.get_evdev()
1058
1059        self.confidence_change_assert_playback(
1060            uhdev,
1061            evdev,
1062            [
1063                # t=0: Contact 0 == Down + confident; Contact 1 == Down + confident
1064                # Both fingers confidently in contact
1065                [
1066                    (
1067                        self.ContactIds(contact_id=0, tracking_id=0, slot_num=0),
1068                        True,
1069                        True,
1070                    ),
1071                    (
1072                        self.ContactIds(contact_id=1, tracking_id=1, slot_num=1),
1073                        True,
1074                        True,
1075                    ),
1076                ],
1077                # t=1: Contact 0 == !Down + confident; Contact 1 == Down + confident
1078                # First finger looses confidence and clears only the tipswitch flag
1079                [
1080                    (
1081                        self.ContactIds(contact_id=0, tracking_id=-1, slot_num=0),
1082                        False,
1083                        True,
1084                    ),
1085                    (
1086                        self.ContactIds(contact_id=1, tracking_id=1, slot_num=1),
1087                        True,
1088                        True,
1089                    ),
1090                ],
1091                # t=2: Contact 0 == !Down + !confident; Contact 1 == Down + confident
1092                # First finger has lost confidence and has both flags cleared
1093                [
1094                    (
1095                        self.ContactIds(contact_id=0, tracking_id=-1, slot_num=0),
1096                        False,
1097                        False,
1098                    ),
1099                    (
1100                        self.ContactIds(contact_id=1, tracking_id=1, slot_num=1),
1101                        True,
1102                        True,
1103                    ),
1104                ],
1105                # t=3: Contact 0 == !Down + !confident; Contact 1 == Down + confident
1106                # First finger has lost confidence and has both flags cleared
1107                [
1108                    (
1109                        self.ContactIds(contact_id=0, tracking_id=-1, slot_num=0),
1110                        False,
1111                        False,
1112                    ),
1113                    (
1114                        self.ContactIds(contact_id=1, tracking_id=1, slot_num=1),
1115                        True,
1116                        True,
1117                    ),
1118                ],
1119            ],
1120        )
1121
1122    def test_confidence_loss_b(self):
1123        """
1124        Transition a confident contact to a non-confident contact by
1125        cleraing both tipswitch and confidence bits simultaneously.
1126
1127        Ensure that the driver reports the transitioned contact as
1128        being removed and that other contacts continue to report
1129        normally. This mode of confidence loss is used by some
1130        AES devices.
1131        """
1132        uhdev = self.uhdev
1133        evdev = uhdev.get_evdev()
1134
1135        self.confidence_change_assert_playback(
1136            uhdev,
1137            evdev,
1138            [
1139                # t=0: Contact 0 == Down + confident; Contact 1 == Down + confident
1140                # Both fingers confidently in contact
1141                [
1142                    (
1143                        self.ContactIds(contact_id=0, tracking_id=0, slot_num=0),
1144                        True,
1145                        True,
1146                    ),
1147                    (
1148                        self.ContactIds(contact_id=1, tracking_id=1, slot_num=1),
1149                        True,
1150                        True,
1151                    ),
1152                ],
1153                # t=1: Contact 0 == !Down + !confident; Contact 1 == Down + confident
1154                # First finger looses confidence and has both flags cleared simultaneously
1155                [
1156                    (
1157                        self.ContactIds(contact_id=0, tracking_id=-1, slot_num=0),
1158                        False,
1159                        False,
1160                    ),
1161                    (
1162                        self.ContactIds(contact_id=1, tracking_id=1, slot_num=1),
1163                        True,
1164                        True,
1165                    ),
1166                ],
1167                # t=2: Contact 0 == !Down + !confident; Contact 1 == Down + confident
1168                # First finger has lost confidence and has both flags cleared
1169                [
1170                    (
1171                        self.ContactIds(contact_id=0, tracking_id=-1, slot_num=0),
1172                        False,
1173                        False,
1174                    ),
1175                    (
1176                        self.ContactIds(contact_id=1, tracking_id=1, slot_num=1),
1177                        True,
1178                        True,
1179                    ),
1180                ],
1181                # t=3: Contact 0 == !Down + !confident; Contact 1 == Down + confident
1182                # First finger has lost confidence and has both flags cleared
1183                [
1184                    (
1185                        self.ContactIds(contact_id=0, tracking_id=-1, slot_num=0),
1186                        False,
1187                        False,
1188                    ),
1189                    (
1190                        self.ContactIds(contact_id=1, tracking_id=1, slot_num=1),
1191                        True,
1192                        True,
1193                    ),
1194                ],
1195            ],
1196        )
1197
1198    def test_confidence_loss_c(self):
1199        """
1200        Transition a confident contact to a non-confident contact by
1201        clearing only the confidence bit.
1202
1203        Ensure that the driver reports the transitioned contact as
1204        being removed and that other contacts continue to report
1205        normally.
1206        """
1207        uhdev = self.uhdev
1208        evdev = uhdev.get_evdev()
1209
1210        self.confidence_change_assert_playback(
1211            uhdev,
1212            evdev,
1213            [
1214                # t=0: Contact 0 == Down + confident; Contact 1 == Down + confident
1215                # Both fingers confidently in contact
1216                [
1217                    (
1218                        self.ContactIds(contact_id=0, tracking_id=0, slot_num=0),
1219                        True,
1220                        True,
1221                    ),
1222                    (
1223                        self.ContactIds(contact_id=1, tracking_id=1, slot_num=1),
1224                        True,
1225                        True,
1226                    ),
1227                ],
1228                # t=1: Contact 0 == Down + !confident; Contact 1 == Down + confident
1229                # First finger looses confidence and clears only the confidence flag
1230                [
1231                    (
1232                        self.ContactIds(contact_id=0, tracking_id=-1, slot_num=0),
1233                        True,
1234                        False,
1235                    ),
1236                    (
1237                        self.ContactIds(contact_id=1, tracking_id=1, slot_num=1),
1238                        True,
1239                        True,
1240                    ),
1241                ],
1242                # t=2: Contact 0 == !Down + !confident; Contact 1 == Down + confident
1243                # First finger has lost confidence and has both flags cleared
1244                [
1245                    (
1246                        self.ContactIds(contact_id=0, tracking_id=-1, slot_num=0),
1247                        False,
1248                        False,
1249                    ),
1250                    (
1251                        self.ContactIds(contact_id=1, tracking_id=1, slot_num=1),
1252                        True,
1253                        True,
1254                    ),
1255                ],
1256                # t=3: Contact 0 == !Down + !confident; Contact 1 == Down + confident
1257                # First finger has lost confidence and has both flags cleared
1258                [
1259                    (
1260                        self.ContactIds(contact_id=0, tracking_id=-1, slot_num=0),
1261                        False,
1262                        False,
1263                    ),
1264                    (
1265                        self.ContactIds(contact_id=1, tracking_id=1, slot_num=1),
1266                        True,
1267                        True,
1268                    ),
1269                ],
1270            ],
1271        )
1272
1273    def test_confidence_gain_a(self):
1274        """
1275        Transition a contact that was always non-confident to confident.
1276
1277        Ensure that the confident contact is reported normally.
1278        """
1279        uhdev = self.uhdev
1280        evdev = uhdev.get_evdev()
1281
1282        self.confidence_change_assert_playback(
1283            uhdev,
1284            evdev,
1285            [
1286                # t=0: Contact 0 == Down + !confident; Contact 1 == Down + confident
1287                # Only second finger is confidently in contact
1288                [
1289                    (
1290                        self.ContactIds(contact_id=0, tracking_id=-1, slot_num=None),
1291                        True,
1292                        False,
1293                    ),
1294                    (
1295                        self.ContactIds(contact_id=1, tracking_id=0, slot_num=0),
1296                        True,
1297                        True,
1298                    ),
1299                ],
1300                # t=1: Contact 0 == Down + !confident; Contact 1 == Down + confident
1301                # First finger gains confidence
1302                [
1303                    (
1304                        self.ContactIds(contact_id=0, tracking_id=-1, slot_num=None),
1305                        True,
1306                        False,
1307                    ),
1308                    (
1309                        self.ContactIds(contact_id=1, tracking_id=0, slot_num=0),
1310                        True,
1311                        True,
1312                    ),
1313                ],
1314                # t=2: Contact 0 == Down + confident; Contact 1 == Down + confident
1315                # First finger remains confident
1316                [
1317                    (
1318                        self.ContactIds(contact_id=0, tracking_id=1, slot_num=1),
1319                        True,
1320                        True,
1321                    ),
1322                    (
1323                        self.ContactIds(contact_id=1, tracking_id=0, slot_num=0),
1324                        True,
1325                        True,
1326                    ),
1327                ],
1328                # t=3: Contact 0 == Down + confident; Contact 1 == Down + confident
1329                # First finger remains confident
1330                [
1331                    (
1332                        self.ContactIds(contact_id=0, tracking_id=1, slot_num=1),
1333                        True,
1334                        True,
1335                    ),
1336                    (
1337                        self.ContactIds(contact_id=1, tracking_id=0, slot_num=0),
1338                        True,
1339                        True,
1340                    ),
1341                ],
1342            ],
1343        )
1344
1345    def test_confidence_gain_b(self):
1346        """
1347        Transition a contact from non-confident to confident.
1348
1349        Ensure that the confident contact is reported normally.
1350        """
1351        uhdev = self.uhdev
1352        evdev = uhdev.get_evdev()
1353
1354        self.confidence_change_assert_playback(
1355            uhdev,
1356            evdev,
1357            [
1358                # t=0: Contact 0 == Down + confident; Contact 1 == Down + confident
1359                # First and second finger confidently in contact
1360                [
1361                    (
1362                        self.ContactIds(contact_id=0, tracking_id=0, slot_num=0),
1363                        True,
1364                        True,
1365                    ),
1366                    (
1367                        self.ContactIds(contact_id=1, tracking_id=1, slot_num=1),
1368                        True,
1369                        True,
1370                    ),
1371                ],
1372                # t=1: Contact 0 == Down + !confident; Contact 1 == Down + confident
1373                # Firtst finger looses confidence
1374                [
1375                    (
1376                        self.ContactIds(contact_id=0, tracking_id=-1, slot_num=0),
1377                        True,
1378                        False,
1379                    ),
1380                    (
1381                        self.ContactIds(contact_id=1, tracking_id=1, slot_num=1),
1382                        True,
1383                        True,
1384                    ),
1385                ],
1386                # t=2: Contact 0 == Down + confident; Contact 1 == Down + confident
1387                # First finger gains confidence
1388                [
1389                    (
1390                        self.ContactIds(contact_id=0, tracking_id=2, slot_num=0),
1391                        True,
1392                        True,
1393                    ),
1394                    (
1395                        self.ContactIds(contact_id=1, tracking_id=1, slot_num=1),
1396                        True,
1397                        True,
1398                    ),
1399                ],
1400                # t=3: Contact 0 == !Down + confident; Contact 1 == Down + confident
1401                # First finger goes up
1402                [
1403                    (
1404                        self.ContactIds(contact_id=0, tracking_id=-1, slot_num=0),
1405                        False,
1406                        True,
1407                    ),
1408                    (
1409                        self.ContactIds(contact_id=1, tracking_id=1, slot_num=1),
1410                        True,
1411                        True,
1412                    ),
1413                ],
1414            ],
1415        )
1416