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( 1001 uhdev, 1002 evdev, 1003 [ 1004 self.ContactIds(contact_id=0, tracking_id=-1, slot_num=None), 1005 self.ContactIds(contact_id=1, tracking_id=0, slot_num=0), 1006 self.ContactIds(contact_id=2, tracking_id=-1, slot_num=None), 1007 self.ContactIds(contact_id=3, tracking_id=1, slot_num=1), 1008 self.ContactIds(contact_id=4, tracking_id=-1, slot_num=None), 1009 ], 1010 ) 1011 1012 def confidence_change_assert_playback(self, uhdev, evdev, timeline): 1013 """ 1014 Assert proper behavior of contacts that move and change tipswitch / 1015 confidence status over time. 1016 1017 Given a `timeline` list of touch states to iterate over, verify 1018 that the contacts move and are reported as up/down as expected 1019 by the state of the tipswitch and confidence bits. 1020 """ 1021 t = 0 1022 1023 for state in timeline: 1024 touches = self.make_contacts(len(state), t) 1025 1026 for item in zip(touches, state): 1027 item[0].tipswitch = item[1][1] 1028 item[0].confidence = item[1][2] 1029 1030 r = uhdev.event(touches) 1031 events = uhdev.next_sync_events() 1032 self.debug_reports(r, uhdev, events) 1033 1034 ids = [x[0] for x in state] 1035 self.assert_contacts(uhdev, evdev, ids, t) 1036 1037 t += 1 1038 1039 def test_confidence_loss_a(self): 1040 """ 1041 Transition a confident contact to a non-confident contact by 1042 first clearing the tipswitch. 1043 1044 Ensure that the driver reports the transitioned contact as 1045 being removed and that other contacts continue to report 1046 normally. This mode of confidence loss is used by the 1047 DTH-2452. 1048 """ 1049 uhdev = self.uhdev 1050 evdev = uhdev.get_evdev() 1051 1052 self.confidence_change_assert_playback( 1053 uhdev, 1054 evdev, 1055 [ 1056 # t=0: Contact 0 == Down + confident; Contact 1 == Down + confident 1057 # Both fingers confidently in contact 1058 [ 1059 ( 1060 self.ContactIds(contact_id=0, tracking_id=0, slot_num=0), 1061 True, 1062 True, 1063 ), 1064 ( 1065 self.ContactIds(contact_id=1, tracking_id=1, slot_num=1), 1066 True, 1067 True, 1068 ), 1069 ], 1070 # t=1: Contact 0 == !Down + confident; Contact 1 == Down + confident 1071 # First finger looses confidence and clears only the tipswitch flag 1072 [ 1073 ( 1074 self.ContactIds(contact_id=0, tracking_id=-1, slot_num=0), 1075 False, 1076 True, 1077 ), 1078 ( 1079 self.ContactIds(contact_id=1, tracking_id=1, slot_num=1), 1080 True, 1081 True, 1082 ), 1083 ], 1084 # t=2: Contact 0 == !Down + !confident; Contact 1 == Down + confident 1085 # First finger has lost confidence and has both flags cleared 1086 [ 1087 ( 1088 self.ContactIds(contact_id=0, tracking_id=-1, slot_num=0), 1089 False, 1090 False, 1091 ), 1092 ( 1093 self.ContactIds(contact_id=1, tracking_id=1, slot_num=1), 1094 True, 1095 True, 1096 ), 1097 ], 1098 # t=3: Contact 0 == !Down + !confident; Contact 1 == Down + confident 1099 # First finger has lost confidence and has both flags cleared 1100 [ 1101 ( 1102 self.ContactIds(contact_id=0, tracking_id=-1, slot_num=0), 1103 False, 1104 False, 1105 ), 1106 ( 1107 self.ContactIds(contact_id=1, tracking_id=1, slot_num=1), 1108 True, 1109 True, 1110 ), 1111 ], 1112 ], 1113 ) 1114 1115 def test_confidence_loss_b(self): 1116 """ 1117 Transition a confident contact to a non-confident contact by 1118 cleraing both tipswitch and confidence bits simultaneously. 1119 1120 Ensure that the driver reports the transitioned contact as 1121 being removed and that other contacts continue to report 1122 normally. This mode of confidence loss is used by some 1123 AES devices. 1124 """ 1125 uhdev = self.uhdev 1126 evdev = uhdev.get_evdev() 1127 1128 self.confidence_change_assert_playback( 1129 uhdev, 1130 evdev, 1131 [ 1132 # t=0: Contact 0 == Down + confident; Contact 1 == Down + confident 1133 # Both fingers confidently in contact 1134 [ 1135 ( 1136 self.ContactIds(contact_id=0, tracking_id=0, slot_num=0), 1137 True, 1138 True, 1139 ), 1140 ( 1141 self.ContactIds(contact_id=1, tracking_id=1, slot_num=1), 1142 True, 1143 True, 1144 ), 1145 ], 1146 # t=1: Contact 0 == !Down + !confident; Contact 1 == Down + confident 1147 # First finger looses confidence and has both flags cleared simultaneously 1148 [ 1149 ( 1150 self.ContactIds(contact_id=0, tracking_id=-1, slot_num=0), 1151 False, 1152 False, 1153 ), 1154 ( 1155 self.ContactIds(contact_id=1, tracking_id=1, slot_num=1), 1156 True, 1157 True, 1158 ), 1159 ], 1160 # t=2: Contact 0 == !Down + !confident; Contact 1 == Down + confident 1161 # First finger has lost confidence and has both flags cleared 1162 [ 1163 ( 1164 self.ContactIds(contact_id=0, tracking_id=-1, slot_num=0), 1165 False, 1166 False, 1167 ), 1168 ( 1169 self.ContactIds(contact_id=1, tracking_id=1, slot_num=1), 1170 True, 1171 True, 1172 ), 1173 ], 1174 # t=3: Contact 0 == !Down + !confident; Contact 1 == Down + confident 1175 # First finger has lost confidence and has both flags cleared 1176 [ 1177 ( 1178 self.ContactIds(contact_id=0, tracking_id=-1, slot_num=0), 1179 False, 1180 False, 1181 ), 1182 ( 1183 self.ContactIds(contact_id=1, tracking_id=1, slot_num=1), 1184 True, 1185 True, 1186 ), 1187 ], 1188 ], 1189 ) 1190 1191 def test_confidence_loss_c(self): 1192 """ 1193 Transition a confident contact to a non-confident contact by 1194 clearing only the confidence bit. 1195 1196 Ensure that the driver reports the transitioned contact as 1197 being removed and that other contacts continue to report 1198 normally. 1199 """ 1200 uhdev = self.uhdev 1201 evdev = uhdev.get_evdev() 1202 1203 self.confidence_change_assert_playback( 1204 uhdev, 1205 evdev, 1206 [ 1207 # t=0: Contact 0 == Down + confident; Contact 1 == Down + confident 1208 # Both fingers confidently in contact 1209 [ 1210 ( 1211 self.ContactIds(contact_id=0, tracking_id=0, slot_num=0), 1212 True, 1213 True, 1214 ), 1215 ( 1216 self.ContactIds(contact_id=1, tracking_id=1, slot_num=1), 1217 True, 1218 True, 1219 ), 1220 ], 1221 # t=1: Contact 0 == Down + !confident; Contact 1 == Down + confident 1222 # First finger looses confidence and clears only the confidence flag 1223 [ 1224 ( 1225 self.ContactIds(contact_id=0, tracking_id=-1, slot_num=0), 1226 True, 1227 False, 1228 ), 1229 ( 1230 self.ContactIds(contact_id=1, tracking_id=1, slot_num=1), 1231 True, 1232 True, 1233 ), 1234 ], 1235 # t=2: Contact 0 == !Down + !confident; Contact 1 == Down + confident 1236 # First finger has lost confidence and has both flags cleared 1237 [ 1238 ( 1239 self.ContactIds(contact_id=0, tracking_id=-1, slot_num=0), 1240 False, 1241 False, 1242 ), 1243 ( 1244 self.ContactIds(contact_id=1, tracking_id=1, slot_num=1), 1245 True, 1246 True, 1247 ), 1248 ], 1249 # t=3: Contact 0 == !Down + !confident; Contact 1 == Down + confident 1250 # First finger has lost confidence and has both flags cleared 1251 [ 1252 ( 1253 self.ContactIds(contact_id=0, tracking_id=-1, slot_num=0), 1254 False, 1255 False, 1256 ), 1257 ( 1258 self.ContactIds(contact_id=1, tracking_id=1, slot_num=1), 1259 True, 1260 True, 1261 ), 1262 ], 1263 ], 1264 ) 1265 1266 def test_confidence_gain_a(self): 1267 """ 1268 Transition a contact that was always non-confident to confident. 1269 1270 Ensure that the confident contact is reported normally. 1271 """ 1272 uhdev = self.uhdev 1273 evdev = uhdev.get_evdev() 1274 1275 self.confidence_change_assert_playback( 1276 uhdev, 1277 evdev, 1278 [ 1279 # t=0: Contact 0 == Down + !confident; Contact 1 == Down + confident 1280 # Only second finger is confidently in contact 1281 [ 1282 ( 1283 self.ContactIds(contact_id=0, tracking_id=-1, slot_num=None), 1284 True, 1285 False, 1286 ), 1287 ( 1288 self.ContactIds(contact_id=1, tracking_id=0, slot_num=0), 1289 True, 1290 True, 1291 ), 1292 ], 1293 # t=1: Contact 0 == Down + !confident; Contact 1 == Down + confident 1294 # First finger gains confidence 1295 [ 1296 ( 1297 self.ContactIds(contact_id=0, tracking_id=-1, slot_num=None), 1298 True, 1299 False, 1300 ), 1301 ( 1302 self.ContactIds(contact_id=1, tracking_id=0, slot_num=0), 1303 True, 1304 True, 1305 ), 1306 ], 1307 # t=2: Contact 0 == Down + confident; Contact 1 == Down + confident 1308 # First finger remains confident 1309 [ 1310 ( 1311 self.ContactIds(contact_id=0, tracking_id=1, slot_num=1), 1312 True, 1313 True, 1314 ), 1315 ( 1316 self.ContactIds(contact_id=1, tracking_id=0, slot_num=0), 1317 True, 1318 True, 1319 ), 1320 ], 1321 # t=3: Contact 0 == Down + confident; Contact 1 == Down + confident 1322 # First finger remains confident 1323 [ 1324 ( 1325 self.ContactIds(contact_id=0, tracking_id=1, slot_num=1), 1326 True, 1327 True, 1328 ), 1329 ( 1330 self.ContactIds(contact_id=1, tracking_id=0, slot_num=0), 1331 True, 1332 True, 1333 ), 1334 ], 1335 ], 1336 ) 1337 1338 def test_confidence_gain_b(self): 1339 """ 1340 Transition a contact from non-confident to confident. 1341 1342 Ensure that the confident contact is reported normally. 1343 """ 1344 uhdev = self.uhdev 1345 evdev = uhdev.get_evdev() 1346 1347 self.confidence_change_assert_playback( 1348 uhdev, 1349 evdev, 1350 [ 1351 # t=0: Contact 0 == Down + confident; Contact 1 == Down + confident 1352 # First and second finger confidently in contact 1353 [ 1354 ( 1355 self.ContactIds(contact_id=0, tracking_id=0, slot_num=0), 1356 True, 1357 True, 1358 ), 1359 ( 1360 self.ContactIds(contact_id=1, tracking_id=1, slot_num=1), 1361 True, 1362 True, 1363 ), 1364 ], 1365 # t=1: Contact 0 == Down + !confident; Contact 1 == Down + confident 1366 # Firtst finger looses confidence 1367 [ 1368 ( 1369 self.ContactIds(contact_id=0, tracking_id=-1, slot_num=0), 1370 True, 1371 False, 1372 ), 1373 ( 1374 self.ContactIds(contact_id=1, tracking_id=1, slot_num=1), 1375 True, 1376 True, 1377 ), 1378 ], 1379 # t=2: Contact 0 == Down + confident; Contact 1 == Down + confident 1380 # First finger gains confidence 1381 [ 1382 ( 1383 self.ContactIds(contact_id=0, tracking_id=2, slot_num=0), 1384 True, 1385 True, 1386 ), 1387 ( 1388 self.ContactIds(contact_id=1, tracking_id=1, slot_num=1), 1389 True, 1390 True, 1391 ), 1392 ], 1393 # t=3: Contact 0 == !Down + confident; Contact 1 == Down + confident 1394 # First finger goes up 1395 [ 1396 ( 1397 self.ContactIds(contact_id=0, tracking_id=-1, slot_num=0), 1398 False, 1399 True, 1400 ), 1401 ( 1402 self.ContactIds(contact_id=1, tracking_id=1, slot_num=1), 1403 True, 1404 True, 1405 ), 1406 ], 1407 ], 1408 ) 1409