Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds Swerve2/3/4/6ControllerCommand to Commands2 #38

Closed
wants to merge 15 commits into from
Closed
2 changes: 2 additions & 0 deletions commands2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from .sequentialcommandgroup import SequentialCommandGroup
from .startendcommand import StartEndCommand
from .subsystem import Subsystem
from .swervecontrollercommand import SwerveControllerCommand
from .timedcommandrobot import TimedCommandRobot
from .trapezoidprofilesubsystem import TrapezoidProfileSubsystem
from .waitcommand import WaitCommand
Expand Down Expand Up @@ -75,6 +76,7 @@
"SequentialCommandGroup",
"StartEndCommand",
"Subsystem",
"SwerveControllerCommand",
"TimedCommandRobot",
"TrapezoidProfileSubsystem",
"WaitCommand",
Expand Down
292 changes: 292 additions & 0 deletions commands2/swervecontrollercommand.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
# Copyright (c) FIRST and other WPILib contributors.
# Open Source Software; you can modify and/or share it under the terms of
# the WPILib BSD license file in the root directory of this project.
from __future__ import annotations
from typing import Callable, Optional, Union, Tuple
from typing_extensions import overload
from overtake import overtake
from wpimath.controller import (
HolonomicDriveController,
PIDController,
ProfiledPIDControllerRadians,
)
from wpimath.geometry import Pose2d, Rotation2d
from wpimath.kinematics import (
SwerveDrive2Kinematics,
SwerveDrive3Kinematics,
SwerveDrive4Kinematics,
SwerveDrive6Kinematics,
SwerveModuleState,
)
from wpimath.trajectory import Trajectory
from wpilib import Timer

from .command import Command
from .subsystem import Subsystem


class SwerveControllerCommand(Command):
"""
A command that uses two PID controllers (PIDController) and a ProfiledPIDController
(ProfiledPIDController) to follow a trajectory (Trajectory) with a swerve drive.

This command outputs the raw desired Swerve Module States (SwerveModuleState) in an
array. The desired wheel and module rotation velocities should be taken from those and used in
velocity PIDs.

The robot angle controller does not follow the angle given by the trajectory but rather goes
to the angle given in the final state of the trajectory.
"""

@overload
def __init__(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would benefit from the @overload approach too.

An easy way to see what they should look like is download the 2023 commands whl file, unzip it, and peek at commands2/_impl/__init__.pyi which has all of the definitions that the prior version defined.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually had no idea that @overload existed. If you haven't figured it out yet, not a Python programmer :)

Hopefully tomorrow I can work up some updates for the other PRs as well that will hopefully make this much cleaner than me just creating overloads by arbitrarily forcing optional arguments to be required in certain permutations.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, overload brings its own share of troubles -- it's just for the type checker, you still have to figure out which set of parameters are incoming.

See

def __init__(self, *args, **kwargs):
for how it was used elsewhere here. It would be nice if there was a generic way to validate/parse them, but there isn't yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just pushed a change to this one implementing overload.

I don't know if this is too much of a breaking change, but I forced kwargs for all the non-positional arguments and then added the overload for the type checker. I added more tests to validate, and it seems to "work".

I don't know enough about Python that by explicitly making the subsystem a Tuple and forcing kwargs, what that does on the other end of things.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out the type checker was not happy about it in Mypy, though when stepping through with a debugger, both in my force Tuple, and in the status quo (before the Tuple change) at runtime addRequirements receives a tuple[Subsystem] as it's type, not a Subsystem (which is what the type checker in Mypy says is required.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's important to remember that type checking is not enforced at runtime, so running code can sometimes work perfectly fine even if mypy is very angry about it. The primary utility of type hints is twofold: often mypy/pyright is right and type errors indicate that you should fix something (and you can find out before running the code), and it also serves as a useful API documentation to indicate what the function is expecting to receive.

I think we should investigate https://github.com/gabrieldemarmiesse/overtake + beartype to see if we can make this multiple dispatch stuff easier to write.

self,
trajectory: Trajectory,
pose: Callable[[], Pose2d],
kinematics: Union[
SwerveDrive2Kinematics,
SwerveDrive3Kinematics,
SwerveDrive4Kinematics,
SwerveDrive6Kinematics,
],
xController: PIDController,
yController: PIDController,
thetaController: ProfiledPIDControllerRadians,
desiredRotation: Callable[[], Rotation2d],
outputModuleStates: Callable[[SwerveModuleState], None],
requirements: Tuple[Subsystem],
) -> None:
"""
Constructs a new SwerveControllerCommand that when executed will follow the
provided trajectory. This command will not return output voltages but
rather raw module states from the position controllers which need to be put
into a velocity PID.

Note: The controllers will *not* set the outputVolts to zero upon
completion of the path- this is left to the user, since it is not
appropriate for paths with nonstationary endstates.
:param trajectory: The trajectory to follow.
:param pose: A function that supplies the robot pose - use one of the odometry classes to
provide this.
:param kinematics: The kinematics for the robot drivetrain. Can be kinematics for 2/3/4/6
SwerveKinematics.
:param outputModuleStates: The raw output module states from the position controllers.
:param requirements: The subsystems to require.
:param xController: The Trajectory Tracker PID controller
for the robot's x position.
:param yController: The Trajectory Tracker PID controller
for the robot's y position.
:param thetaController: The Trajectory Tracker PID controller
for angle for the robot.
:param desiredRotation: The angle that the drivetrain should be
facing. This is sampled at each time step.
"""
super().__init__()
self._trajectory = trajectory
self._pose = pose
self._kinematics = kinematics
self._outputModuleStates = outputModuleStates
self._controller = HolonomicDriveController(
xController, yController, thetaController
)
self._desiredRotation = desiredRotation
self._timer = Timer()
self.addRequirements(requirements) # type: ignore

@overload
def __init__(
self,
trajectory: Trajectory,
pose: Callable[[], Pose2d],
kinematics: Union[
SwerveDrive2Kinematics,
SwerveDrive3Kinematics,
SwerveDrive4Kinematics,
SwerveDrive6Kinematics,
],
xController: PIDController,
yController: PIDController,
thetaController: ProfiledPIDControllerRadians,
outputModuleStates: Callable[[SwerveModuleState], None],
requirements: Tuple[Subsystem],
) -> None:
"""
Constructs a new SwerveControllerCommand that when executed will follow the
provided trajectory. This command will not return output voltages but
rather raw module states from the position controllers which need to be put
into a velocity PID.

Note: The controllers will *not* set the outputVolts to zero upon
completion of the path- this is left to the user, since it is not
appropriate for paths with nonstationary endstates.

Note 2: The final rotation of the robot will be set to the rotation of
the final pose in the trajectory. The robot will not follow the rotations
from the poses at each timestep. If alternate rotation behavior is desired,
the other constructor with a supplier for rotation should be used.

:param trajectory: The trajectory to follow.
:param pose: A function that supplies the robot pose - use one of the odometry classes to
provide this.
:param kinematics: The kinematics for the robot drivetrain. Can be kinematics for 2/3/4/6
SwerveKinematics.
:param xController: The Trajectory Tracker PID controller
for the robot's x position.
:param yController: The Trajectory Tracker PID controller
for the robot's y position.
:param thetaController: The Trajectory Tracker PID controller
for angle for the robot.
:param outputModuleStates: The raw output module states from the position controllers.
:param requirements: The subsystems to require.
"""
super().__init__()
self._trajectory = trajectory
self._pose = pose
self._kinematics = kinematics
self._outputModuleStates = outputModuleStates
self._controller = HolonomicDriveController(
xController, yController, thetaController
)
self._desiredRotation = self._trajectory.states()[-1].pose.rotation
self._timer = Timer()
self.addRequirements(requirements) # type:ignore

@overload
def __init__(
self,
trajectory: Trajectory,
pose: Callable[[], Pose2d],
kinematics: Union[
SwerveDrive2Kinematics,
SwerveDrive3Kinematics,
SwerveDrive4Kinematics,
SwerveDrive6Kinematics,
],
controller: HolonomicDriveController,
desiredRotation: Callable[[], Rotation2d],
outputModuleStates: Callable[[SwerveModuleState], None],
requirements: Tuple[Subsystem],
) -> None:
"""
Constructs a new SwerveControllerCommand that when executed will follow the
provided trajectory. This command will not return output voltages but
rather raw module states from the position controllers which need to be put
into a velocity PID.

Note: The controllers will *not* set the outputVolts to zero upon
completion of the path- this is left to the user, since it is not
appropriate for paths with nonstationary endstates.

:param trajectory: The trajectory to follow.
:param pose: A function that supplies the robot pose - use one of the odometry classes to
provide this.
:param kinematics: The kinematics for the robot drivetrain. Can be kinematics for 2/3/4/6
SwerveKinematics.
:param controller: The HolonomicDriveController for the drivetrain.
:param desiredRotation: The angle that the drivetrain should be
facing. This is sampled at each time step.
:param outputModuleStates: The raw output module states from the position controllers.
:param requirements: The subsystems to require.

"""
super().__init__()
self._trajectory = trajectory
self._pose = pose
self._kinematics = kinematics
self._outputModuleStates = outputModuleStates
self._controller = controller
self._desiredRotation = desiredRotation
self._timer = Timer()
self.addRequirements(requirements) # type:ignore

@overload
def __init__(
self,
trajectory: Trajectory,
pose: Callable[[], Pose2d],
kinematics: Union[
SwerveDrive2Kinematics,
SwerveDrive3Kinematics,
SwerveDrive4Kinematics,
SwerveDrive6Kinematics,
],
controller: HolonomicDriveController,
outputModuleStates: Callable[[SwerveModuleState], None],
requirements: Tuple[Subsystem],
) -> None:
"""
Constructs a new SwerveControllerCommand that when executed will follow the
provided trajectory. This command will not return output voltages but
rather raw module states from the position controllers which need to be put
into a velocity PID.

Note: The controllers will *not* set the outputVolts to zero upon
completion of the path- this is left to the user, since it is not
appropriate for paths with nonstationary endstates.

Note 2: The final rotation of the robot will be set to the rotation of
the final pose in the trajectory. The robot will not follow the rotations
from the poses at each timestep. If alternate rotation behavior is desired,
the other constructor with a supplier for rotation should be used.

:param trajectory: The trajectory to follow.
:param pose: A function that supplies the robot pose - use one of the odometry classes to
provide this.
:param kinematics: The kinematics for the robot drivetrain. Can be kinematics for 2/3/4/6
SwerveKinematics.
:param controller: The HolonomicDriveController for the drivetrain.
:param outputModuleStates: The raw output module states from the position controllers.
:param requirements: The subsystems to require.

"""
super().__init__()
self._trajectory = trajectory
self._pose = pose
self._kinematics = kinematics
self._outputModuleStates = outputModuleStates
self._controller = controller
self._desiredRotation = self._trajectory.states()[-1].pose.rotation
self._timer = Timer()
self.addRequirements(requirements) # type: ignore

@overtake(runtime_type_checker="beartype")
def __init__(
self,
trajectory: Trajectory,
pose: Callable[[], Pose2d],
kinematics: Union[
SwerveDrive2Kinematics,
SwerveDrive3Kinematics,
SwerveDrive4Kinematics,
SwerveDrive6Kinematics,
],
outputModuleStates: Callable[[SwerveModuleState], None],
requirements: Tuple[Subsystem],
*,
controller: Optional[HolonomicDriveController] = None,
xController: Optional[PIDController] = None,
yController: Optional[PIDController] = None,
thetaController: Optional[ProfiledPIDControllerRadians] = None,
desiredRotation: Optional[Callable[[], Rotation2d]] = None,
):
...

def initialize(self):
self._timer.restart()

def execute(self):
curTime = self._timer.get()
desiredState = self._trajectory.sample(curTime)

targetChassisSpeeds = self._controller.calculate(
self._pose(), desiredState, self._desiredRotation()
)
targetModuleStates = self._kinematics.toSwerveModuleStates(targetChassisSpeeds)

self._outputModuleStates(targetModuleStates)

def end(self, interrupted):
self._timer.stop()

def isFinished(self):
return self._timer.hasElapsed(self._trajectory.totalTime())
7 changes: 6 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@
description="WPILib command framework v2",
url="https://github.com/robotpy/robotpy-commands-v2",
packages=["commands2"],
install_requires=["wpilib<2025,>=2024.0.0b2", "typing_extensions>=4.1.0,<5"],
install_requires=[
"wpilib<2025,>=2024.0.0b2",
"typing_extensions>=4.1.0,<5",
"overtake~=0.4.0",
"beartype~=0.16.4",
],
license="BSD-3-Clause",
python_requires=">=3.8",
include_package_data=True,
Expand Down
Loading