host.controller_config.gamepad_input

Gamepad input via XInput for the preview widget.

Wraps the XInput-Python library with graceful fallback when not installed. Maps XInput axis names to wpilib controller input names.

  1"""Gamepad input via XInput for the preview widget.
  2
  3Wraps the XInput-Python library with graceful fallback when not installed.
  4Maps XInput axis names to wpilib controller input names.
  5"""
  6
  7
  8class GamepadPoller:
  9    """Poll Xbox controllers via XInput with graceful fallback.
 10
 11    If XInput-Python is not installed, ``available`` returns False and
 12    all query methods return empty/zero values.
 13    """
 14
 15    # Map wpilib input names → (source, index/key)
 16    # source: "thumb" or "trigger"
 17    # For thumbs: index 0=left, 1=right; sub 0=X, 1=Y
 18    # For triggers: 0=left, 1=right
 19    # Y-axis negated to match wpilib convention: getLeftY()/getRightY()
 20    # return negative when pushed forward/up.
 21    _AXIS_MAP = {
 22        "left_stick_x":  ("thumb", 0, 0, False),
 23        "left_stick_y":  ("thumb", 0, 1, True),   # negate: wpilib Y-forward = negative
 24        "right_stick_x": ("thumb", 1, 0, False),
 25        "right_stick_y": ("thumb", 1, 1, True),   # negate: wpilib Y-forward = negative
 26        "left_trigger":  ("trigger", 0, None, False),
 27        "right_trigger": ("trigger", 1, None, False),
 28    }
 29
 30    def __init__(self):
 31        self._xinput = None
 32        try:
 33            import XInput
 34            self._xinput = XInput
 35            # Disable XInput's built-in deadzones — our pipeline handles
 36            # deadband, so we want raw values.
 37            XInput.set_deadzone(XInput.DEADZONE_LEFT_THUMB, 0)
 38            XInput.set_deadzone(XInput.DEADZONE_RIGHT_THUMB, 0)
 39            XInput.set_deadzone(XInput.DEADZONE_TRIGGER, 0)
 40        except (ImportError, OSError):
 41            pass  # XInput unavailable; self._xinput stays None, fallback to no gamepad
 42
 43    @property
 44    def available(self) -> bool:
 45        """True if XInput-Python is installed and usable."""
 46        return self._xinput is not None
 47
 48    def get_connected(self) -> list[int]:
 49        """Return indices of connected controllers (0-3)."""
 50        if not self._xinput:
 51            return []
 52        try:
 53            flags = self._xinput.get_connected()
 54            return [i for i, ok in enumerate(flags) if ok]
 55        except Exception:
 56            return []
 57
 58    def get_axis(self, controller_id: int, axis_name: str) -> float:
 59        """Read a single axis by wpilib input name.
 60
 61        Returns 0.0 if controller is disconnected or axis_name unknown.
 62        Stick axes return -1..1, trigger axes return 0..1.
 63        Stick Y axes are negated to match wpilib (forward = negative).
 64        """
 65        mapping = self._AXIS_MAP.get(axis_name)
 66        if not mapping or not self._xinput:
 67            return 0.0
 68        source, idx, sub, negate = mapping
 69        try:
 70            state = self._xinput.get_state(controller_id)
 71        except Exception:
 72            return 0.0
 73        if source == "thumb":
 74            thumbs = self._xinput.get_thumb_values(state)
 75            val = thumbs[idx][sub]
 76            return -val if negate else val
 77        else:
 78            triggers = self._xinput.get_trigger_values(state)
 79            return triggers[idx]
 80
 81    def get_all_axes(self, controller_id: int) -> dict[str, float]:
 82        """Return all 6 axis values for a controller.
 83
 84        Keys are wpilib input names. Returns empty dict on error.
 85        """
 86        if not self._xinput:
 87            return {}
 88        try:
 89            state = self._xinput.get_state(controller_id)
 90        except Exception:
 91            return {}
 92        thumbs = self._xinput.get_thumb_values(state)
 93        triggers = self._xinput.get_trigger_values(state)
 94        return {
 95            "left_stick_x": thumbs[0][0],
 96            "left_stick_y": -thumbs[0][1],
 97            "right_stick_x": thumbs[1][0],
 98            "right_stick_y": -thumbs[1][1],
 99            "left_trigger": triggers[0],
100            "right_trigger": triggers[1],
101        }
class GamepadPoller:
  9class GamepadPoller:
 10    """Poll Xbox controllers via XInput with graceful fallback.
 11
 12    If XInput-Python is not installed, ``available`` returns False and
 13    all query methods return empty/zero values.
 14    """
 15
 16    # Map wpilib input names → (source, index/key)
 17    # source: "thumb" or "trigger"
 18    # For thumbs: index 0=left, 1=right; sub 0=X, 1=Y
 19    # For triggers: 0=left, 1=right
 20    # Y-axis negated to match wpilib convention: getLeftY()/getRightY()
 21    # return negative when pushed forward/up.
 22    _AXIS_MAP = {
 23        "left_stick_x":  ("thumb", 0, 0, False),
 24        "left_stick_y":  ("thumb", 0, 1, True),   # negate: wpilib Y-forward = negative
 25        "right_stick_x": ("thumb", 1, 0, False),
 26        "right_stick_y": ("thumb", 1, 1, True),   # negate: wpilib Y-forward = negative
 27        "left_trigger":  ("trigger", 0, None, False),
 28        "right_trigger": ("trigger", 1, None, False),
 29    }
 30
 31    def __init__(self):
 32        self._xinput = None
 33        try:
 34            import XInput
 35            self._xinput = XInput
 36            # Disable XInput's built-in deadzones — our pipeline handles
 37            # deadband, so we want raw values.
 38            XInput.set_deadzone(XInput.DEADZONE_LEFT_THUMB, 0)
 39            XInput.set_deadzone(XInput.DEADZONE_RIGHT_THUMB, 0)
 40            XInput.set_deadzone(XInput.DEADZONE_TRIGGER, 0)
 41        except (ImportError, OSError):
 42            pass  # XInput unavailable; self._xinput stays None, fallback to no gamepad
 43
 44    @property
 45    def available(self) -> bool:
 46        """True if XInput-Python is installed and usable."""
 47        return self._xinput is not None
 48
 49    def get_connected(self) -> list[int]:
 50        """Return indices of connected controllers (0-3)."""
 51        if not self._xinput:
 52            return []
 53        try:
 54            flags = self._xinput.get_connected()
 55            return [i for i, ok in enumerate(flags) if ok]
 56        except Exception:
 57            return []
 58
 59    def get_axis(self, controller_id: int, axis_name: str) -> float:
 60        """Read a single axis by wpilib input name.
 61
 62        Returns 0.0 if controller is disconnected or axis_name unknown.
 63        Stick axes return -1..1, trigger axes return 0..1.
 64        Stick Y axes are negated to match wpilib (forward = negative).
 65        """
 66        mapping = self._AXIS_MAP.get(axis_name)
 67        if not mapping or not self._xinput:
 68            return 0.0
 69        source, idx, sub, negate = mapping
 70        try:
 71            state = self._xinput.get_state(controller_id)
 72        except Exception:
 73            return 0.0
 74        if source == "thumb":
 75            thumbs = self._xinput.get_thumb_values(state)
 76            val = thumbs[idx][sub]
 77            return -val if negate else val
 78        else:
 79            triggers = self._xinput.get_trigger_values(state)
 80            return triggers[idx]
 81
 82    def get_all_axes(self, controller_id: int) -> dict[str, float]:
 83        """Return all 6 axis values for a controller.
 84
 85        Keys are wpilib input names. Returns empty dict on error.
 86        """
 87        if not self._xinput:
 88            return {}
 89        try:
 90            state = self._xinput.get_state(controller_id)
 91        except Exception:
 92            return {}
 93        thumbs = self._xinput.get_thumb_values(state)
 94        triggers = self._xinput.get_trigger_values(state)
 95        return {
 96            "left_stick_x": thumbs[0][0],
 97            "left_stick_y": -thumbs[0][1],
 98            "right_stick_x": thumbs[1][0],
 99            "right_stick_y": -thumbs[1][1],
100            "left_trigger": triggers[0],
101            "right_trigger": triggers[1],
102        }

Poll Xbox controllers via XInput with graceful fallback.

If XInput-Python is not installed, available returns False and all query methods return empty/zero values.

available: bool
44    @property
45    def available(self) -> bool:
46        """True if XInput-Python is installed and usable."""
47        return self._xinput is not None

True if XInput-Python is installed and usable.

def get_connected(self) -> list[int]:
49    def get_connected(self) -> list[int]:
50        """Return indices of connected controllers (0-3)."""
51        if not self._xinput:
52            return []
53        try:
54            flags = self._xinput.get_connected()
55            return [i for i, ok in enumerate(flags) if ok]
56        except Exception:
57            return []

Return indices of connected controllers (0-3).

def get_axis(self, controller_id: int, axis_name: str) -> float:
59    def get_axis(self, controller_id: int, axis_name: str) -> float:
60        """Read a single axis by wpilib input name.
61
62        Returns 0.0 if controller is disconnected or axis_name unknown.
63        Stick axes return -1..1, trigger axes return 0..1.
64        Stick Y axes are negated to match wpilib (forward = negative).
65        """
66        mapping = self._AXIS_MAP.get(axis_name)
67        if not mapping or not self._xinput:
68            return 0.0
69        source, idx, sub, negate = mapping
70        try:
71            state = self._xinput.get_state(controller_id)
72        except Exception:
73            return 0.0
74        if source == "thumb":
75            thumbs = self._xinput.get_thumb_values(state)
76            val = thumbs[idx][sub]
77            return -val if negate else val
78        else:
79            triggers = self._xinput.get_trigger_values(state)
80            return triggers[idx]

Read a single axis by wpilib input name.

Returns 0.0 if controller is disconnected or axis_name unknown. Stick axes return -1..1, trigger axes return 0..1. Stick Y axes are negated to match wpilib (forward = negative).

def get_all_axes(self, controller_id: int) -> dict[str, float]:
 82    def get_all_axes(self, controller_id: int) -> dict[str, float]:
 83        """Return all 6 axis values for a controller.
 84
 85        Keys are wpilib input names. Returns empty dict on error.
 86        """
 87        if not self._xinput:
 88            return {}
 89        try:
 90            state = self._xinput.get_state(controller_id)
 91        except Exception:
 92            return {}
 93        thumbs = self._xinput.get_thumb_values(state)
 94        triggers = self._xinput.get_trigger_values(state)
 95        return {
 96            "left_stick_x": thumbs[0][0],
 97            "left_stick_y": -thumbs[0][1],
 98            "right_stick_x": thumbs[1][0],
 99            "right_stick_y": -thumbs[1][1],
100            "left_trigger": triggers[0],
101            "right_trigger": triggers[1],
102        }

Return all 6 axis values for a controller.

Keys are wpilib input names. Returns empty dict on error.