xref: /linux/tools/testing/selftests/hid/tests/base_gamepad.py (revision 621cde16e49b3ecf7d59a8106a20aaebfb4a59a9)
1# SPDX-License-Identifier: GPL-2.0
2import libevdev
3
4from .base_device import BaseDevice
5from hidtools.util import BusType
6
7
8class InvalidHIDCommunication(Exception):
9    pass
10
11
12class GamepadData(object):
13    pass
14
15
16class AxisMapping(object):
17    """Represents a mapping between a HID type
18    and an evdev event"""
19
20    def __init__(self, hid, evdev=None):
21        self.hid = hid.lower()
22
23        if evdev is None:
24            evdev = f"ABS_{hid.upper()}"
25
26        self.evdev = libevdev.evbit("EV_ABS", evdev)
27
28
29class BaseGamepad(BaseDevice):
30    buttons_map = {
31        1: "BTN_SOUTH",
32        2: "BTN_EAST",
33        3: "BTN_C",
34        4: "BTN_NORTH",
35        5: "BTN_WEST",
36        6: "BTN_Z",
37        7: "BTN_TL",
38        8: "BTN_TR",
39        9: "BTN_TL2",
40        10: "BTN_TR2",
41        11: "BTN_SELECT",
42        12: "BTN_START",
43        13: "BTN_MODE",
44        14: "BTN_THUMBL",
45        15: "BTN_THUMBR",
46    }
47
48    axes_map = {
49        "left_stick": {
50            "x": AxisMapping("x"),
51            "y": AxisMapping("y"),
52        },
53        "right_stick": {
54            "x": AxisMapping("z"),
55            "y": AxisMapping("Rz"),
56        },
57    }
58
59    def __init__(self, rdesc, application="Game Pad", name=None, input_info=None):
60        assert rdesc is not None
61        super().__init__(name, application, input_info=input_info, rdesc=rdesc)
62        self.buttons = (1, 2, 3)
63        self._buttons = {}
64        self.left = (127, 127)
65        self.right = (127, 127)
66        self.hat_switch = 15
67        assert self.parsed_rdesc is not None
68
69        self.fields = []
70        for r in self.parsed_rdesc.input_reports.values():
71            if r.application_name == self.application:
72                self.fields.extend([f.usage_name for f in r])
73
74    def store_axes(self, which, gamepad, data):
75        amap = self.axes_map[which]
76        x, y = data
77        setattr(gamepad, amap["x"].hid, x)
78        setattr(gamepad, amap["y"].hid, y)
79
80    def create_report(
81        self,
82        *,
83        left=(None, None),
84        right=(None, None),
85        hat_switch=None,
86        buttons=None,
87        reportID=None,
88        application="Game Pad",
89    ):
90        """
91        Return an input report for this device.
92
93        :param left: a tuple of absolute (x, y) value of the left joypad
94            where ``None`` is "leave unchanged"
95        :param right: a tuple of absolute (x, y) value of the right joypad
96            where ``None`` is "leave unchanged"
97        :param hat_switch: an absolute angular value of the hat switch
98            (expressed in 1/8 of circle, 0 being North, 2 East)
99            where ``None`` is "leave unchanged"
100        :param buttons: a dict of index/bool for the button states,
101            where ``None`` is "leave unchanged"
102        :param reportID: the numeric report ID for this report, if needed
103        :param application: the application used to report the values
104        """
105        if buttons is not None:
106            for i, b in buttons.items():
107                if i not in self.buttons:
108                    raise InvalidHIDCommunication(
109                        f"button {i} is not part of this {self.application}"
110                    )
111                if b is not None:
112                    self._buttons[i] = b
113
114        def replace_none_in_tuple(item, default):
115            if item is None:
116                item = (None, None)
117
118            if None in item:
119                if item[0] is None:
120                    item = (default[0], item[1])
121                if item[1] is None:
122                    item = (item[0], default[1])
123
124            return item
125
126        right = replace_none_in_tuple(right, self.right)
127        self.right = right
128        left = replace_none_in_tuple(left, self.left)
129        self.left = left
130
131        if hat_switch is None:
132            hat_switch = self.hat_switch
133        else:
134            self.hat_switch = hat_switch
135
136        reportID = reportID or self.default_reportID
137
138        gamepad = GamepadData()
139        for i, b in self._buttons.items():
140            gamepad.__setattr__(f"b{i}", int(b) if b is not None else 0)
141
142        self.store_axes("left_stick", gamepad, left)
143        self.store_axes("right_stick", gamepad, right)
144        gamepad.hatswitch = hat_switch  # type: ignore  ### gamepad is by default empty
145        return super().create_report(
146            gamepad, reportID=reportID, application=application
147        )
148
149    def event(
150        self, *, left=(None, None), right=(None, None), hat_switch=None, buttons=None
151    ):
152        """
153        Send an input event on the default report ID.
154
155        :param left: a tuple of absolute (x, y) value of the left joypad
156            where ``None`` is "leave unchanged"
157        :param right: a tuple of absolute (x, y) value of the right joypad
158            where ``None`` is "leave unchanged"
159        :param hat_switch: an absolute angular value of the hat switch
160            where ``None`` is "leave unchanged"
161        :param buttons: a dict of index/bool for the button states,
162            where ``None`` is "leave unchanged"
163        """
164        r = self.create_report(
165            left=left, right=right, hat_switch=hat_switch, buttons=buttons
166        )
167        self.call_input_event(r)
168        return [r]
169
170
171class JoystickGamepad(BaseGamepad):
172    buttons_map = {
173        1: "BTN_TRIGGER",
174        2: "BTN_THUMB",
175        3: "BTN_THUMB2",
176        4: "BTN_TOP",
177        5: "BTN_TOP2",
178        6: "BTN_PINKIE",
179        7: "BTN_BASE",
180        8: "BTN_BASE2",
181        9: "BTN_BASE3",
182        10: "BTN_BASE4",
183        11: "BTN_BASE5",
184        12: "BTN_BASE6",
185        13: "BTN_DEAD",
186    }
187
188    axes_map = {
189        "left_stick": {
190            "x": AxisMapping("x"),
191            "y": AxisMapping("y"),
192        },
193        "right_stick": {
194            "x": AxisMapping("rudder"),
195            "y": AxisMapping("throttle"),
196        },
197    }
198
199    def __init__(self, rdesc, application="Joystick", name=None, input_info=None):
200        super().__init__(rdesc, application, name, input_info)
201
202    def create_report(
203        self,
204        *,
205        left=(None, None),
206        right=(None, None),
207        hat_switch=None,
208        buttons=None,
209        reportID=None,
210        application=None,
211    ):
212        """
213        Return an input report for this device.
214
215        :param left: a tuple of absolute (x, y) value of the left joypad
216            where ``None`` is "leave unchanged"
217        :param right: a tuple of absolute (x, y) value of the right joypad
218            where ``None`` is "leave unchanged"
219        :param hat_switch: an absolute angular value of the hat switch
220            where ``None`` is "leave unchanged"
221        :param buttons: a dict of index/bool for the button states,
222            where ``None`` is "leave unchanged"
223        :param reportID: the numeric report ID for this report, if needed
224        :param application: the application for this report, if needed
225        """
226        if application is None:
227            application = "Joystick"
228        return super().create_report(
229            left=left,
230            right=right,
231            hat_switch=hat_switch,
232            buttons=buttons,
233            reportID=reportID,
234            application=application,
235        )
236
237    def store_right_joystick(self, gamepad, data):
238        gamepad.rudder, gamepad.throttle = data
239