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.