# 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