239 lines · python
1# SPDX-License-Identifier: GPL-2.02import libevdev3 4from .base_device import BaseDevice5from hidtools.util import BusType6 7 8class InvalidHIDCommunication(Exception):9 pass10 11 12class GamepadData(object):13 pass14 15 16class AxisMapping(object):17 """Represents a mapping between a HID type18 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 None61 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 = 1567 assert self.parsed_rdesc is not None68 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 = data77 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 joypad94 where ``None`` is "leave unchanged"95 :param right: a tuple of absolute (x, y) value of the right joypad96 where ``None`` is "leave unchanged"97 :param hat_switch: an absolute angular value of the hat switch98 (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 needed103 :param application: the application used to report the values104 """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] = b113 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 item125 126 right = replace_none_in_tuple(right, self.right)127 self.right = right128 left = replace_none_in_tuple(left, self.left)129 self.left = left130 131 if hat_switch is None:132 hat_switch = self.hat_switch133 else:134 self.hat_switch = hat_switch135 136 reportID = reportID or self.default_reportID137 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 empty145 return super().create_report(146 gamepad, reportID=reportID, application=application147 )148 149 def event(150 self, *, left=(None, None), right=(None, None), hat_switch=None, buttons=None151 ):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 joypad156 where ``None`` is "leave unchanged"157 :param right: a tuple of absolute (x, y) value of the right joypad158 where ``None`` is "leave unchanged"159 :param hat_switch: an absolute angular value of the hat switch160 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=buttons166 )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 joypad216 where ``None`` is "leave unchanged"217 :param right: a tuple of absolute (x, y) value of the right joypad218 where ``None`` is "leave unchanged"219 :param hat_switch: an absolute angular value of the hat switch220 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 needed224 :param application: the application for this report, if needed225 """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 = data239