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