This part of the commands tutorial covers the code that needs to be written to represent the commands and systems presented in the previous tutorial.
The actual code is too big to contain here, and can be seen in the code repository.
Changes for 2020+
The 2020+ versions of WPILib have a slightly changed structure from the descriptions given throughout this tutorial, but are generally similar. The main important changes relevant to this guide are as listed below:
Command
,Subsystem
, andCommandGroup
are now interfaces, meaning that you'll need to useCommandBase
,SubsystemBase
, andCommandGroupBase
to implement them effectively.Scheduler
is nowCommandScheduler
and has an access ofCommandScheduler.getInstance().run()
instead ofScheduler.run()
- Using
require(Subsystem... subsytems)
has been renamed toaddRequirements(Subsystem... subsytems)
TimedCommand
no longer exists, meaning the user will have to implement/override theboolean isFinished()
method from theCommand
interface in order to stop after a certain time. Alternatively, you could use aWaitCommand
and aDeadlineGroup
. Refer to the WPILib command group documentation for more information.
Subsystems
The foundation for the rest of the code comes from the Subsystem
classes,
so we'll write them first.
DriveTrain
DriveTrain
is very similar to the past tutorials.
As with the other subsystem we will add, Arm
, there is an empty method --
initDefaultCommand
-- which would be used to add a command that will run
if the subsystem has nothing else using it. We frequently use this for
the joystick command, as it's the "default" state of operation.
Arm
Arm
is a simple class that controls one motor, as if moving a very light
robot arm. It's not too complicated, basically just a single-motor form of
DriveSide
.
Commands
The actual logic for the autonomous routine comes from the commands.
DriveForward
shows one of the simpler commands, since it doesn't need to
do anything complicated. It extends TimedCommand
, which is a special variant
of Command
that will finish when the time runs out. This is exactly what
we want DriveForward
to do. The execute()
method is called every time
Scheduler.run()
is called and the command is running. In it, we simply
set the DriveTrain
power to whatever was passed in the constructor. This
allows us to use this command for any power we want to set, not just the
one we're using for this auto.
To represent the requirement of the DriveTrain
, we first get the DriveTrain
subsystem from Robot
(Robot.inst.driveTrain
). We need to use the same
DriveTrain
everywhere, or the command system won't work properly. The actual
requirement is setup by the line requires(dt);
. The Scheduler
will use this
information to ensure that no other Command
requiring the DriveTrain
will
run at the same time.
Rotate
is a little bit more complicated, but is still similar to
DriveForward
. We don't have a timer, so we don't extend TimedCommand
.
We still need the DriveTrain
, so we get it from Robot
and require
it. We use a new method, initialize()
, to figure out which way we need to
turn. We can't do this in the constructor, because at that point the robot
is just turning on. We don't want to measure the position until we're going
to start turning, which is when initialize()
is called.
Rotate
's execute()
is similar to DriveForward
, it simply applies the
power to the motors. isFinished()
is a new method. It is called after
execute()
to find out if the command is done running. In a TimedCommand
,
it returns true
after a certain amount of time has passed. In Rotate
,
we finish once we've reached the target amount of degrees.
SetArmPosition
is very similar to Rotate
, except with the Arm
. We still
get the Arm
from Robot
, call requires(arm)
, and setup the power direction
in initialize()
. moveTowardPosition
is an example of a "control loop",
which we'll discuss later. For now, you can ignore it. We again setup
isFinished()
to stop when we reach the target position.
All of these commands follow the general design principle of "do one thing
well". Each one doesn't do much on its own, but we can combine them using
CommandGroup
to create a full autonomous routine. I have done such in the
FirstAutoCommandGroup
class. Creating a CommandGroup
is very simple:
just extend CommandGroup
, make a constructor, and call addSequential
with each command you want to add. Here, I setup the auto discussed in the
last part, Commands I.
This covers the basics of writing commands and combining them to form complex routines. The next part will cover running commands at the same time, or in parallel.