selftests/hid: import base_gamepad.py from hid-tools
[linux-block.git] / tools / testing / selftests / hid / tests / base_gamepad.py
1 # SPDX-License-Identifier: GPL-2.0
2 import libevdev
3
4 from .base_device import BaseDevice
5 from hidtools.util import BusType
6
7
8 class InvalidHIDCommunication(Exception):
9     pass
10
11
12 class GamepadData(object):
13     pass
14
15
16 class 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
29 class 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
171 class 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