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