Linux kernel mirror (for testing)
git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel
os
linux
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