1#!/bin/env python3
2# SPDX-License-Identifier: GPL-2.0
3# -*- coding: utf-8 -*-
4#
5# Copyright (c) 2020 Benjamin Tissoires <benjamin.tissoires@gmail.com>
6# Copyright (c) 2020 Red Hat, Inc.
7#
8
9from .base import application_matches
10from .test_gamepad import BaseTest
11from hidtools.device.sony_gamepad import (
12    PS3Controller,
13    PS4ControllerBluetooth,
14    PS4ControllerUSB,
15    PS5ControllerBluetooth,
16    PS5ControllerUSB,
17    PSTouchPoint,
18)
19from hidtools.util import BusType
20
21import libevdev
22import logging
23import pytest
24
25logger = logging.getLogger("hidtools.test.sony")
26
27PS3_MODULE = ("sony", "hid_sony")
28PS4_MODULE = ("playstation", "hid_playstation")
29PS5_MODULE = ("playstation", "hid_playstation")
30
31
32class SonyBaseTest:
33    class SonyTest(BaseTest.TestGamepad):
34        pass
35
36    class SonyPS4ControllerTest(SonyTest):
37        kernel_modules = [PS4_MODULE]
38
39        def test_accelerometer(self):
40            uhdev = self.uhdev
41            evdev = uhdev.get_evdev("Accelerometer")
42
43            for x in range(-32000, 32000, 4000):
44                r = uhdev.event(accel=(x, None, None))
45                events = uhdev.next_sync_events("Accelerometer")
46                self.debug_reports(r, uhdev, events)
47
48                assert libevdev.InputEvent(libevdev.EV_ABS.ABS_X) in events
49                value = evdev.value[libevdev.EV_ABS.ABS_X]
50                # Check against range due to small loss in precision due
51                # to inverse calibration, followed by calibration by hid-sony.
52                assert x - 1 <= value <= x + 1
53
54            for y in range(-32000, 32000, 4000):
55                r = uhdev.event(accel=(None, y, None))
56                events = uhdev.next_sync_events("Accelerometer")
57                self.debug_reports(r, uhdev, events)
58
59                assert libevdev.InputEvent(libevdev.EV_ABS.ABS_Y) in events
60                value = evdev.value[libevdev.EV_ABS.ABS_Y]
61                assert y - 1 <= value <= y + 1
62
63            for z in range(-32000, 32000, 4000):
64                r = uhdev.event(accel=(None, None, z))
65                events = uhdev.next_sync_events("Accelerometer")
66                self.debug_reports(r, uhdev, events)
67
68                assert libevdev.InputEvent(libevdev.EV_ABS.ABS_Z) in events
69                value = evdev.value[libevdev.EV_ABS.ABS_Z]
70                assert z - 1 <= value <= z + 1
71
72        def test_gyroscope(self):
73            uhdev = self.uhdev
74            evdev = uhdev.get_evdev("Accelerometer")
75
76            for rx in range(-2000000, 2000000, 200000):
77                r = uhdev.event(gyro=(rx, None, None))
78                events = uhdev.next_sync_events("Accelerometer")
79                self.debug_reports(r, uhdev, events)
80
81                assert libevdev.InputEvent(libevdev.EV_ABS.ABS_RX) in events
82                value = evdev.value[libevdev.EV_ABS.ABS_RX]
83                # Sensor internal value is 16-bit, but calibrated is 22-bit, so
84                # 6-bit (64) difference, so allow a range of +/- 64.
85                assert rx - 64 <= value <= rx + 64
86
87            for ry in range(-2000000, 2000000, 200000):
88                r = uhdev.event(gyro=(None, ry, None))
89                events = uhdev.next_sync_events("Accelerometer")
90                self.debug_reports(r, uhdev, events)
91
92                assert libevdev.InputEvent(libevdev.EV_ABS.ABS_RY) in events
93                value = evdev.value[libevdev.EV_ABS.ABS_RY]
94                assert ry - 64 <= value <= ry + 64
95
96            for rz in range(-2000000, 2000000, 200000):
97                r = uhdev.event(gyro=(None, None, rz))
98                events = uhdev.next_sync_events("Accelerometer")
99                self.debug_reports(r, uhdev, events)
100
101                assert libevdev.InputEvent(libevdev.EV_ABS.ABS_RZ) in events
102                value = evdev.value[libevdev.EV_ABS.ABS_RZ]
103                assert rz - 64 <= value <= rz + 64
104
105        def test_battery(self):
106            uhdev = self.uhdev
107
108            assert uhdev.power_supply_class is not None
109
110            # DS4 capacity levels are in increments of 10.
111            # Battery is never below 5%.
112            for i in range(5, 105, 10):
113                uhdev.battery.capacity = i
114                uhdev.event()
115                assert uhdev.power_supply_class.capacity == i
116
117            # Discharging tests only make sense for BlueTooth.
118            if uhdev.bus == BusType.BLUETOOTH:
119                uhdev.battery.cable_connected = False
120                uhdev.battery.capacity = 45
121                uhdev.event()
122                assert uhdev.power_supply_class.status == "Discharging"
123
124            uhdev.battery.cable_connected = True
125            uhdev.battery.capacity = 5
126            uhdev.event()
127            assert uhdev.power_supply_class.status == "Charging"
128
129            uhdev.battery.capacity = 100
130            uhdev.event()
131            assert uhdev.power_supply_class.status == "Charging"
132
133            uhdev.battery.full = True
134            uhdev.event()
135            assert uhdev.power_supply_class.status == "Full"
136
137        def test_mt_single_touch(self):
138            """send a single touch in the first slot of the device,
139            and release it."""
140            uhdev = self.uhdev
141            evdev = uhdev.get_evdev("Touch Pad")
142
143            t0 = PSTouchPoint(1, 50, 100)
144            r = uhdev.event(touch=[t0])
145            events = uhdev.next_sync_events("Touch Pad")
146            self.debug_reports(r, uhdev, events)
147
148            assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 1) in events
149            assert evdev.slots[0][libevdev.EV_ABS.ABS_MT_TRACKING_ID] == 0
150            assert evdev.slots[0][libevdev.EV_ABS.ABS_MT_POSITION_X] == 50
151            assert evdev.slots[0][libevdev.EV_ABS.ABS_MT_POSITION_Y] == 100
152
153            t0.tipswitch = False
154            r = uhdev.event(touch=[t0])
155            events = uhdev.next_sync_events("Touch Pad")
156            self.debug_reports(r, uhdev, events)
157            assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 0) in events
158            assert evdev.slots[0][libevdev.EV_ABS.ABS_MT_TRACKING_ID] == -1
159
160        def test_mt_dual_touch(self):
161            """Send 2 touches in the first 2 slots.
162            Make sure the kernel sees this as a dual touch.
163            Release and check
164
165            Note: PTP will send here BTN_DOUBLETAP emulation"""
166            uhdev = self.uhdev
167            evdev = uhdev.get_evdev("Touch Pad")
168
169            t0 = PSTouchPoint(1, 50, 100)
170            t1 = PSTouchPoint(2, 150, 200)
171
172            r = uhdev.event(touch=[t0])
173            events = uhdev.next_sync_events("Touch Pad")
174            self.debug_reports(r, uhdev, events)
175
176            assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 1) in events
177            assert evdev.value[libevdev.EV_KEY.BTN_TOUCH] == 1
178            assert evdev.slots[0][libevdev.EV_ABS.ABS_MT_TRACKING_ID] == 0
179            assert evdev.slots[0][libevdev.EV_ABS.ABS_MT_POSITION_X] == 50
180            assert evdev.slots[0][libevdev.EV_ABS.ABS_MT_POSITION_Y] == 100
181            assert evdev.slots[1][libevdev.EV_ABS.ABS_MT_TRACKING_ID] == -1
182
183            r = uhdev.event(touch=[t0, t1])
184            events = uhdev.next_sync_events("Touch Pad")
185            self.debug_reports(r, uhdev, events)
186            assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH) not in events
187            assert evdev.value[libevdev.EV_KEY.BTN_TOUCH] == 1
188            assert (
189                libevdev.InputEvent(libevdev.EV_ABS.ABS_MT_POSITION_X, 5) not in events
190            )
191            assert (
192                libevdev.InputEvent(libevdev.EV_ABS.ABS_MT_POSITION_Y, 10) not in events
193            )
194            assert evdev.slots[0][libevdev.EV_ABS.ABS_MT_TRACKING_ID] == 0
195            assert evdev.slots[0][libevdev.EV_ABS.ABS_MT_POSITION_X] == 50
196            assert evdev.slots[0][libevdev.EV_ABS.ABS_MT_POSITION_Y] == 100
197            assert evdev.slots[1][libevdev.EV_ABS.ABS_MT_TRACKING_ID] == 1
198            assert evdev.slots[1][libevdev.EV_ABS.ABS_MT_POSITION_X] == 150
199            assert evdev.slots[1][libevdev.EV_ABS.ABS_MT_POSITION_Y] == 200
200
201            t0.tipswitch = False
202            r = uhdev.event(touch=[t0, t1])
203            events = uhdev.next_sync_events("Touch Pad")
204            self.debug_reports(r, uhdev, events)
205            assert evdev.slots[0][libevdev.EV_ABS.ABS_MT_TRACKING_ID] == -1
206            assert evdev.slots[1][libevdev.EV_ABS.ABS_MT_TRACKING_ID] == 1
207            assert libevdev.InputEvent(libevdev.EV_ABS.ABS_MT_POSITION_X) not in events
208            assert libevdev.InputEvent(libevdev.EV_ABS.ABS_MT_POSITION_Y) not in events
209
210            t1.tipswitch = False
211            r = uhdev.event(touch=[t1])
212
213            events = uhdev.next_sync_events("Touch Pad")
214            self.debug_reports(r, uhdev, events)
215            assert evdev.slots[0][libevdev.EV_ABS.ABS_MT_TRACKING_ID] == -1
216            assert evdev.slots[1][libevdev.EV_ABS.ABS_MT_TRACKING_ID] == -1
217
218
219class TestPS3Controller(SonyBaseTest.SonyTest):
220    kernel_modules = [PS3_MODULE]
221
222    def create_device(self):
223        controller = PS3Controller()
224        controller.application_matches = application_matches
225        return controller
226
227    @pytest.fixture(autouse=True)
228    def start_controller(self):
229        # emulate a 'PS' button press to tell the kernel we are ready to accept events
230        self.assert_button(17)
231
232        # drain any remaining udev events
233        while self.uhdev.dispatch(10):
234            pass
235
236        def test_led(self):
237            for k, v in self.uhdev.led_classes.items():
238                # the kernel might have set a LED for us
239                logger.info(f"{k}: {v.brightness}")
240
241                idx = int(k[-1]) - 1
242                assert self.uhdev.hw_leds.get_led(idx)[0] == bool(v.brightness)
243
244                v.brightness = 0
245                self.uhdev.dispatch(10)
246                assert self.uhdev.hw_leds.get_led(idx)[0] is False
247
248                v.brightness = v.max_brightness
249                self.uhdev.dispatch(10)
250                assert self.uhdev.hw_leds.get_led(idx)[0]
251
252
253class CalibratedPS4Controller(object):
254    # DS4 reports uncalibrated sensor data. Calibration coefficients
255    # can be retrieved using a feature report (0x2 USB / 0x5 BT).
256    # The values below are the processed calibration values for the
257    # DS4s matching the feature reports of PS4ControllerBluetooth/USB
258    # as dumped from hid-sony 'ds4_get_calibration_data'.
259    #
260    # Note we duplicate those values here in case the kernel changes them
261    # so we can have tests passing even if hid-tools doesn't have the
262    # correct values.
263    accelerometer_calibration_data = {
264        "x": {"bias": -73, "numer": 16384, "denom": 16472},
265        "y": {"bias": -352, "numer": 16384, "denom": 16344},
266        "z": {"bias": 81, "numer": 16384, "denom": 16319},
267    }
268    gyroscope_calibration_data = {
269        "x": {"bias": 0, "numer": 1105920, "denom": 17827},
270        "y": {"bias": 0, "numer": 1105920, "denom": 17777},
271        "z": {"bias": 0, "numer": 1105920, "denom": 17748},
272    }
273
274
275class CalibratedPS4ControllerBluetooth(CalibratedPS4Controller, PS4ControllerBluetooth):
276    pass
277
278
279class TestPS4ControllerBluetooth(SonyBaseTest.SonyPS4ControllerTest):
280    def create_device(self):
281        controller = CalibratedPS4ControllerBluetooth()
282        controller.application_matches = application_matches
283        return controller
284
285
286class CalibratedPS4ControllerUSB(CalibratedPS4Controller, PS4ControllerUSB):
287    pass
288
289
290class TestPS4ControllerUSB(SonyBaseTest.SonyPS4ControllerTest):
291    def create_device(self):
292        controller = CalibratedPS4ControllerUSB()
293        controller.application_matches = application_matches
294        return controller
295
296
297class CalibratedPS5Controller(object):
298    # DualSense reports uncalibrated sensor data. Calibration coefficients
299    # can be retrieved using feature report 0x09.
300    # The values below are the processed calibration values for the
301    # DualSene matching the feature reports of PS5ControllerBluetooth/USB
302    # as dumped from hid-playstation 'dualsense_get_calibration_data'.
303    #
304    # Note we duplicate those values here in case the kernel changes them
305    # so we can have tests passing even if hid-tools doesn't have the
306    # correct values.
307    accelerometer_calibration_data = {
308        "x": {"bias": 0, "numer": 16384, "denom": 16374},
309        "y": {"bias": -114, "numer": 16384, "denom": 16362},
310        "z": {"bias": 2, "numer": 16384, "denom": 16395},
311    }
312    gyroscope_calibration_data = {
313        "x": {"bias": 0, "numer": 1105920, "denom": 17727},
314        "y": {"bias": 0, "numer": 1105920, "denom": 17728},
315        "z": {"bias": 0, "numer": 1105920, "denom": 17769},
316    }
317
318
319class CalibratedPS5ControllerBluetooth(CalibratedPS5Controller, PS5ControllerBluetooth):
320    pass
321
322
323class TestPS5ControllerBluetooth(SonyBaseTest.SonyPS4ControllerTest):
324    kernel_modules = [PS5_MODULE]
325
326    def create_device(self):
327        controller = CalibratedPS5ControllerBluetooth()
328        controller.application_matches = application_matches
329        return controller
330
331
332class CalibratedPS5ControllerUSB(CalibratedPS5Controller, PS5ControllerUSB):
333    pass
334
335
336class TestPS5ControllerUSB(SonyBaseTest.SonyPS4ControllerTest):
337    kernel_modules = [PS5_MODULE]
338
339    def create_device(self):
340        controller = CalibratedPS5ControllerUSB()
341        controller.application_matches = application_matches
342        return controller
343