# SPDX-License-Identifier: GPL-2.0 import libevdev from .base_device import BaseDevice from hidtools.util import BusType class InvalidHIDCommunication(Exception): pass class GamepadData(object): pass class AxisMapping(object): """Represents a mapping between a HID type and an evdev event""" def __init__(self, hid, evdev=None): self.hid = hid.lower() if evdev is None: evdev = f"ABS_{hid.upper()}" self.evdev = libevdev.evbit("EV_ABS", evdev) class BaseGamepad(BaseDevice): buttons_map = { 1: "BTN_SOUTH", 2: "BTN_EAST", 3: "BTN_C", 4: "BTN_NORTH", 5: "BTN_WEST", 6: "BTN_Z", 7: "BTN_TL", 8: "BTN_TR", 9: "BTN_TL2", 10: "BTN_TR2", 11: "BTN_SELECT", 12: "BTN_START", 13: "BTN_MODE", 14: "BTN_THUMBL", 15: "BTN_THUMBR", } axes_map = { "left_stick": { "x": AxisMapping("x"), "y": AxisMapping("y"), }, "right_stick": { "x": AxisMapping("z"), "y": AxisMapping("Rz"), }, } def __init__(self, rdesc, application="Game Pad", name=None, input_info=None): assert rdesc is not None super().__init__(name, application, input_info=input_info, rdesc=rdesc) self.buttons = (1, 2, 3) self._buttons = {} self.left = (127, 127) self.right = (127, 127) self.hat_switch = 15 assert self.parsed_rdesc is not None self.fields = [] for r in self.parsed_rdesc.input_reports.values(): if r.application_name == self.application: self.fields.extend([f.usage_name for f in r]) def store_axes(self, which, gamepad, data): amap = self.axes_map[which] x, y = data setattr(gamepad, amap["x"].hid, x) setattr(gamepad, amap["y"].hid, y) def create_report( self, *, left=(None, None), right=(None, None), hat_switch=None, buttons=None, reportID=None, application="Game Pad", ): """ Return an input report for this device. :param left: a tuple of absolute (x, y) value of the left joypad where ``None`` is "leave unchanged" :param right: a tuple of absolute (x, y) value of the right joypad where ``None`` is "leave unchanged" :param hat_switch: an absolute angular value of the hat switch (expressed in 1/8 of circle, 0 being North, 2 East) where ``None`` is "leave unchanged" :param buttons: a dict of index/bool for the button states, where ``None`` is "leave unchanged" :param reportID: the numeric report ID for this report, if needed :param application: the application used to report the values """ if buttons is not None: for i, b in buttons.items(): if i not in self.buttons: raise InvalidHIDCommunication( f"button {i} is not part of this {self.application}" ) if b is not None: self._buttons[i] = b def replace_none_in_tuple(item, default): if item is None: item = (None, None) if None in item: if item[0] is None: item = (default[0], item[1]) if item[1] is None: item = (item[0], default[1]) return item right = replace_none_in_tuple(right, self.right) self.right = right left = replace_none_in_tuple(left, self.left) self.left = left if hat_switch is None: hat_switch = self.hat_switch else: self.hat_switch = hat_switch reportID = reportID or self.default_reportID gamepad = GamepadData() for i, b in self._buttons.items(): gamepad.__setattr__(f"b{i}", int(b) if b is not None else 0) self.store_axes("left_stick", gamepad, left) self.store_axes("right_stick", gamepad, right) gamepad.hatswitch = hat_switch # type: ignore ### gamepad is by default empty return super().create_report( gamepad, reportID=reportID, application=application ) def event( self, *, left=(None, None), right=(None, None), hat_switch=None, buttons=None ): """ Send an input event on the default report ID. :param left: a tuple of absolute (x, y) value of the left joypad where ``None`` is "leave unchanged" :param right: a tuple of absolute (x, y) value of the right joypad where ``None`` is "leave unchanged" :param hat_switch: an absolute angular value of the hat switch where ``None`` is "leave unchanged" :param buttons: a dict of index/bool for the button states, where ``None`` is "leave unchanged" """ r = self.create_report( left=left, right=right, hat_switch=hat_switch, buttons=buttons ) self.call_input_event(r) return [r] class JoystickGamepad(BaseGamepad): buttons_map = { 1: "BTN_TRIGGER", 2: "BTN_THUMB", 3: "BTN_THUMB2", 4: "BTN_TOP", 5: "BTN_TOP2", 6: "BTN_PINKIE", 7: "BTN_BASE", 8: "BTN_BASE2", 9: "BTN_BASE3", 10: "BTN_BASE4", 11: "BTN_BASE5", 12: "BTN_BASE6", 13: "BTN_DEAD", } axes_map = { "left_stick": { "x": AxisMapping("x"), "y": AxisMapping("y"), }, "right_stick": { "x": AxisMapping("rudder"), "y": AxisMapping("throttle"), }, } def __init__(self, rdesc, application="Joystick", name=None, input_info=None): super().__init__(rdesc, application, name, input_info) def create_report( self, *, left=(None, None), right=(None, None), hat_switch=None, buttons=None, reportID=None, application=None, ): """ Return an input report for this device. :param left: a tuple of absolute (x, y) value of the left joypad where ``None`` is "leave unchanged" :param right: a tuple of absolute (x, y) value of the right joypad where ``None`` is "leave unchanged" :param hat_switch: an absolute angular value of the hat switch where ``None`` is "leave unchanged" :param buttons: a dict of index/bool for the button states, where ``None`` is "leave unchanged" :param reportID: the numeric report ID for this report, if needed :param application: the application for this report, if needed """ if application is None: application = "Joystick" return super().create_report( left=left, right=right, hat_switch=hat_switch, buttons=buttons, reportID=reportID, application=application, ) def store_right_joystick(self, gamepad, data): gamepad.rudder, gamepad.throttle = data