[wpilib,cmd] Cache HID wrappers (#8970)

Store DriverStation-owned GenericHID and Gamepad instances in Java and
C++, and expose the cached objects to Python bindings.

Move hand-written command gamepad and joystick wrappers to compose
cached CommandGenericHID instances plus typed HID wrappers, including a
Python CommandGamepad.

This will let us remove UserControls, while helping ensure that we don't
have state smashing between GenericHID objects.

Another bonus is without inheritance, intellisense will no longer show a
bunch of annoying methods, and instead just what actually exists.

---------

Co-authored-by: Peter Johnson <johnson.peter@gmail.com>
This commit is contained in:
Thad House
2026-06-11 09:42:39 -07:00
committed by GitHub
parent fe499ede4c
commit c647e67de0
105 changed files with 4210 additions and 1336 deletions

View File

@@ -1,4 +1,5 @@
from .commandgenerichid import CommandGenericHID
from .commandgamepad import CommandGamepad
from .commandjoystick import CommandJoystick
from .commandnidsps4controller import CommandNiDsPS4Controller
from .commandnidsxboxcontroller import CommandNiDsXboxController
@@ -10,6 +11,7 @@ from .trigger import Trigger
__all__ = [
"Trigger",
"CommandGenericHID",
"CommandGamepad",
"CommandJoystick",
"CommandNiDsPS4Controller",
"CommandNiDsXboxController",

View File

@@ -0,0 +1,190 @@
# validated: 2024-01-20 DS 92149efa11fa button/CommandGamepad.java
from typing import Optional
from wpilib import DriverStation, EventLoop, Gamepad
from .commandgenerichid import CommandGenericHID
from .trigger import Trigger
def _enum_value(value) -> int:
try:
return int(value)
except TypeError:
return value.value
class CommandGamepad:
"""
A version of :class:`wpilib.Gamepad` with :class:`.Trigger` factories for command-based.
"""
_hid: CommandGenericHID
_gamepad: Gamepad
def __init__(self, port: int):
"""
Construct an instance of a controller.
:param port: The port index on the Driver Station that the controller is plugged into.
"""
self._hid = CommandGenericHID.getCommandGenericHID(port)
self._gamepad = DriverStation.getGamepad(port)
def __getattr__(self, name: str):
return getattr(self._hid, name)
def getHID(self) -> CommandGenericHID:
"""
Get the underlying CommandGenericHID object.
:returns: the wrapped CommandGenericHID object
"""
return self._hid
def getGamepad(self) -> Gamepad:
"""
Get the underlying Gamepad object.
:returns: the wrapped Gamepad object
"""
return self._gamepad
def button(self, button, loop: Optional[EventLoop] = None) -> Trigger:
"""
Constructs an event instance around this button's digital signal.
:param button: the button index or :class:`wpilib.Gamepad.Button`
:param loop: the event loop instance to attach the event to
"""
return self._hid.button(_enum_value(button), loop)
def faceDown(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.FACE_DOWN, loop)
def faceRight(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.FACE_RIGHT, loop)
def faceLeft(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.FACE_LEFT, loop)
def faceUp(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.FACE_UP, loop)
def back(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.BACK, loop)
def guide(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.GUIDE, loop)
def start(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.START, loop)
def leftStick(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.LEFT_STICK, loop)
def rightStick(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.RIGHT_STICK, loop)
def leftBumper(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.LEFT_BUMPER, loop)
def rightBumper(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.RIGHT_BUMPER, loop)
def dpadUp(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.DPAD_UP, loop)
def dpadDown(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.DPAD_DOWN, loop)
def dpadLeft(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.DPAD_LEFT, loop)
def dpadRight(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.DPAD_RIGHT, loop)
def misc1(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.MISC_1, loop)
def rightPaddle1(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.RIGHT_PADDLE_1, loop)
def leftPaddle1(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.LEFT_PADDLE_1, loop)
def rightPaddle2(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.RIGHT_PADDLE_2, loop)
def leftPaddle2(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.LEFT_PADDLE_2, loop)
def touchpad(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.TOUCHPAD, loop)
def misc2(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.MISC_2, loop)
def misc3(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.MISC_3, loop)
def misc4(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.MISC_4, loop)
def misc5(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.MISC_5, loop)
def misc6(self, loop: Optional[EventLoop] = None) -> Trigger:
return self.button(Gamepad.Button.MISC_6, loop)
def leftTrigger(
self, threshold: float = 0.5, loop: Optional[EventLoop] = None
) -> Trigger:
return self.axisGreaterThan(Gamepad.Axis.LEFT_TRIGGER, threshold, loop)
def rightTrigger(
self, threshold: float = 0.5, loop: Optional[EventLoop] = None
) -> Trigger:
return self.axisGreaterThan(Gamepad.Axis.RIGHT_TRIGGER, threshold, loop)
def axisLessThan(
self, axis, threshold: float, loop: Optional[EventLoop] = None
) -> Trigger:
return self._hid.axisLessThan(_enum_value(axis), threshold, loop)
def axisGreaterThan(
self, axis, threshold: float, loop: Optional[EventLoop] = None
) -> Trigger:
return self._hid.axisGreaterThan(_enum_value(axis), threshold, loop)
def axisMagnitudeGreaterThan(
self, axis, threshold: float, loop: Optional[EventLoop] = None
) -> Trigger:
return self._hid.axisMagnitudeGreaterThan(_enum_value(axis), threshold, loop)
def getAxis(self, axis) -> float:
"""
Get the value of the axis.
:param axis: the axis index or :class:`wpilib.Gamepad.Axis`
"""
return self._hid.getRawAxis(_enum_value(axis))
def getLeftX(self) -> float:
return self._gamepad.getLeftX()
def getLeftY(self) -> float:
return self._gamepad.getLeftY()
def getRightX(self) -> float:
return self._gamepad.getRightX()
def getRightY(self) -> float:
return self._gamepad.getRightY()
def getLeftTriggerAxis(self) -> float:
return self._gamepad.getLeftTriggerAxis()
def getRightTriggerAxis(self) -> float:
return self._gamepad.getRightTriggerAxis()

View File

@@ -1,24 +1,41 @@
# validated: 2024-01-20 DS 92149efa11fa button/CommandGenericHID.java
from typing import Optional
import threading
from typing import ClassVar, Optional, final
from wpilib import EventLoop, GenericHID
from wpilib import DriverStation, EventLoop, GenericHID
from ..commandscheduler import CommandScheduler
from .trigger import Trigger
@final
class CommandGenericHID:
"""
A version of :class:`wpilib.GenericHID` with :class:`.Trigger` factories for command-based.
"""
_hids: ClassVar[dict[int, "CommandGenericHID"]] = {}
_hids_lock = threading.Lock()
def __init__(self, port: int):
"""
Construct an instance of a device.
:param port: The port on the Driver Station that the device is plugged into.
"""
self._hid = GenericHID(port)
self._hid = DriverStation.getGenericHID(port)
@classmethod
def getCommandGenericHID(cls, port: int) -> "CommandGenericHID":
"""
Gets the CommandGenericHID object for the given port.
"""
with cls._hids_lock:
hid = cls._hids.get(port)
if hid is None:
hid = cls(port)
cls._hids[port] = hid
return hid
def getHID(self) -> GenericHID:
"""
@@ -221,3 +238,8 @@ class CommandGenericHID:
:returns: True if the HID is connected.
"""
return self._hid.isConnected()
def _resetCommandGenericHIDData() -> None:
with CommandGenericHID._hids_lock:
CommandGenericHID._hids.clear()

View File

@@ -8,12 +8,13 @@ from .commandgenerichid import CommandGenericHID
from .trigger import Trigger
class CommandJoystick(CommandGenericHID):
class CommandJoystick:
"""
A version of :class:`wpilib.Joystick` with :class:`.Trigger` factories for command-based.
"""
_hid: Joystick
_hid: CommandGenericHID
_joystick: Joystick
def __init__(self, port: int):
"""
@@ -22,17 +23,28 @@ class CommandJoystick(CommandGenericHID):
:param port: The port index on the Driver Station that the controller is plugged into.
"""
super().__init__(port)
self._hid = Joystick(port)
self._hid = CommandGenericHID.getCommandGenericHID(port)
self._joystick = Joystick(self._hid.getHID())
def getHID(self) -> Joystick:
def __getattr__(self, name: str):
return getattr(self._hid, name)
def getHID(self) -> CommandGenericHID:
"""
Get the underlying GenericHID object.
Get the underlying CommandGenericHID object.
:returns: the wrapped GenericHID object
:returns: the wrapped CommandGenericHID object
"""
return self._hid
def getJoystick(self) -> Joystick:
"""
Get the underlying Joystick object.
:returns: the wrapped Joystick object
"""
return self._joystick
def trigger(self, loop: Optional[EventLoop] = None) -> Trigger:
"""
Constructs an event instance around the trigger button's digital signal.
@@ -45,7 +57,7 @@ class CommandJoystick(CommandGenericHID):
"""
if loop is None:
loop = CommandScheduler.getInstance().getDefaultButtonLoop()
return Trigger(loop, lambda: self._hid.getTrigger())
return Trigger(loop, lambda: self._joystick.getTrigger())
def top(self, loop: Optional[EventLoop] = None) -> Trigger:
"""
@@ -59,7 +71,7 @@ class CommandJoystick(CommandGenericHID):
"""
if loop is None:
loop = CommandScheduler.getInstance().getDefaultButtonLoop()
return Trigger(loop, lambda: self._hid.getTop())
return Trigger(loop, lambda: self._joystick.getTop())
def setXChannel(self, channel: int):
"""
@@ -67,7 +79,7 @@ class CommandJoystick(CommandGenericHID):
:param channel: The channel to set the axis to.
"""
self._hid.setXChannel(channel)
self._joystick.setXChannel(channel)
def setYChannel(self, channel: int):
"""
@@ -75,7 +87,7 @@ class CommandJoystick(CommandGenericHID):
:param channel: The channel to set the axis to.
"""
self._hid.setYChannel(channel)
self._joystick.setYChannel(channel)
def setZChannel(self, channel: int):
"""
@@ -83,7 +95,7 @@ class CommandJoystick(CommandGenericHID):
:param channel: The channel to set the axis to.
"""
self._hid.setZChannel(channel)
self._joystick.setZChannel(channel)
def setThrottleChannel(self, channel: int):
"""
@@ -91,7 +103,7 @@ class CommandJoystick(CommandGenericHID):
:param channel: The channel to set the axis to.
"""
self._hid.setThrottleChannel(channel)
self._joystick.setThrottleChannel(channel)
def setTwistChannel(self, channel: int):
"""
@@ -99,7 +111,7 @@ class CommandJoystick(CommandGenericHID):
:param channel: The channel to set the axis to.
"""
self._hid.setTwistChannel(channel)
self._joystick.setTwistChannel(channel)
def getXChannel(self) -> int:
"""
@@ -107,7 +119,7 @@ class CommandJoystick(CommandGenericHID):
:returns: The channel for the axis.
"""
return self._hid.getXChannel()
return self._joystick.getXChannel()
def getYChannel(self) -> int:
"""
@@ -115,7 +127,7 @@ class CommandJoystick(CommandGenericHID):
:returns: The channel for the axis.
"""
return self._hid.getYChannel()
return self._joystick.getYChannel()
def getZChannel(self) -> int:
"""
@@ -123,7 +135,7 @@ class CommandJoystick(CommandGenericHID):
:returns: The channel for the axis.
"""
return self._hid.getZChannel()
return self._joystick.getZChannel()
def getTwistChannel(self) -> int:
"""
@@ -131,7 +143,7 @@ class CommandJoystick(CommandGenericHID):
:returns: The channel for the axis.
"""
return self._hid.getTwistChannel()
return self._joystick.getTwistChannel()
def getThrottleChannel(self) -> int:
"""
@@ -139,7 +151,7 @@ class CommandJoystick(CommandGenericHID):
:returns: The channel for the axis.
"""
return self._hid.getThrottleChannel()
return self._joystick.getThrottleChannel()
def getX(self) -> float:
"""
@@ -150,7 +162,7 @@ class CommandJoystick(CommandGenericHID):
:returns: the x position
"""
return self._hid.getX()
return self._joystick.getX()
def getY(self) -> float:
"""
@@ -161,7 +173,7 @@ class CommandJoystick(CommandGenericHID):
:returns: the y position
"""
return self._hid.getY()
return self._joystick.getY()
def getZ(self) -> float:
"""
@@ -169,7 +181,7 @@ class CommandJoystick(CommandGenericHID):
:returns: the z position
"""
return self._hid.getZ()
return self._joystick.getZ()
def getTwist(self) -> float:
"""
@@ -178,7 +190,7 @@ class CommandJoystick(CommandGenericHID):
:returns: The Twist value of the joystick.
"""
return self._hid.getTwist()
return self._joystick.getTwist()
def getThrottle(self) -> float:
"""
@@ -187,7 +199,7 @@ class CommandJoystick(CommandGenericHID):
:returns: The Throttle value of the joystick.
"""
return self._hid.getThrottle()
return self._joystick.getThrottle()
def getMagnitude(self) -> float:
"""
@@ -196,7 +208,7 @@ class CommandJoystick(CommandGenericHID):
:returns: The magnitude of the direction vector
"""
return self._hid.getMagnitude()
return self._joystick.getMagnitude()
def getDirectionRadians(self) -> float:
"""
@@ -213,7 +225,7 @@ class CommandJoystick(CommandGenericHID):
#
# It's rotated 90 degrees CCW (y is negated and the arguments are reversed)
# so that 0 radians is forward.
return self._hid.getDirectionRadians()
return self._joystick.getDirectionRadians()
def getDirectionDegrees(self) -> float:
"""
@@ -222,4 +234,4 @@ class CommandJoystick(CommandGenericHID):
:returns: The direction of the vector in degrees
"""
return self._hid.getDirectionDegrees()
return self._joystick.getDirectionDegrees()

View File

@@ -8,11 +8,12 @@ from .commandgenerichid import CommandGenericHID
from .trigger import Trigger
class CommandNiDsPS4Controller(CommandGenericHID):
class CommandNiDsPS4Controller:
"""
A version of NI DS PS4Controller with Trigger factories for command-based.
"""
_command_hid: CommandGenericHID
_hid: NiDsPS4Controller
def __init__(self, port: int):
@@ -21,14 +22,25 @@ class CommandNiDsPS4Controller(CommandGenericHID):
:param port: The port index on the Driver Station that the device is plugged into.
"""
super().__init__(port)
self._hid = NiDsPS4Controller(port)
self._command_hid = CommandGenericHID.getCommandGenericHID(port)
self._hid = NiDsPS4Controller(self._command_hid.getHID())
def getHID(self) -> NiDsPS4Controller:
def __getattr__(self, name: str):
return getattr(self._command_hid, name)
def getHID(self) -> CommandGenericHID:
"""
Get the underlying GenericHID object.
Get the underlying CommandGenericHID object.
:returns: the wrapped GenericHID object
:returns: the wrapped CommandGenericHID object
"""
return self._command_hid
def getNiDsPS4Controller(self) -> NiDsPS4Controller:
"""
Get the underlying NiDsPS4Controller object.
:returns: the wrapped NiDsPS4Controller object
"""
return self._hid

View File

@@ -8,11 +8,12 @@ from .commandgenerichid import CommandGenericHID
from .trigger import Trigger
class CommandNiDsXboxController(CommandGenericHID):
class CommandNiDsXboxController:
"""
A version of NI DS XboxController with Trigger factories for command-based.
"""
_command_hid: CommandGenericHID
_hid: NiDsXboxController
def __init__(self, port: int):
@@ -21,14 +22,25 @@ class CommandNiDsXboxController(CommandGenericHID):
:param port: The port index on the Driver Station that the controller is plugged into.
"""
super().__init__(port)
self._hid = NiDsXboxController(port)
self._command_hid = CommandGenericHID.getCommandGenericHID(port)
self._hid = NiDsXboxController(self._command_hid.getHID())
def getHID(self) -> NiDsXboxController:
def __getattr__(self, name: str):
return getattr(self._command_hid, name)
def getHID(self) -> CommandGenericHID:
"""
Get the underlying GenericHID object.
Get the underlying CommandGenericHID object.
:returns: the wrapped GenericHID object
:returns: the wrapped CommandGenericHID object
"""
return self._command_hid
def getNiDsXboxController(self) -> NiDsXboxController:
"""
Get the underlying NiDsXboxController object.
:returns: the wrapped NiDsXboxController object
"""
return self._hid