Lesson 2.0 - Angelbots1339/ProgramingLessons GitHub Wiki
In this lesson you will learn the basics of Command-Based Programming by creating and running simple autonomous routines. All the code will be done in the Auto
and AutoFactory
classes located at `src\main\java\frc\robot\commands.
Command-Based Programming is the design pattern we use to structure our robot code. It is based around two core abstractions: Subsystems, which you learned about in Lesson-1.0, and Commands. Read the WPI Docs to learn more.
Commands represent actions the robot can take. Commands run when scheduled, until they are interrupted or their end condition is met. See Commands for more info.
Subsystems represent independently-controlled collections of robot hardware (such as motor controllers, sensors, pneumatic actuators, etc.) that operate together. They allow for more organized and modular code. Subsystems back the resource-management system of command-based: only one command can use a given subsystem at the same time.
!
The Command Scheduler handles everything to do with running commands, such as starting, ending, and interrupting them. It ensures there are no conflicts, so that every subsystem is only running a single command at a time.
There are lots of convenience features and different ways to create and run commands. If you are curious, you can read the WPI command based docs.
The first way of creating commands is having a separate file for each one. Take a look in Auto.java
for an example. There are 4 default methods:
// Called when the command is initially scheduled.
@Override
public void initialize() {}
// Called every time the scheduler runs while the command is scheduled.
@Override
public void execute() {}
// Called once the command ends or is interrupted.
@Override
public void end(boolean interrupted) {}
// Returns true when the command should end.
@Override
public boolean isFinished() {
return false;
}
which are pretty self-explanatory. Initialize
is called once when the command starts, Execute
is called repeatedly, End
is called when the command is ended, and isFinished
can be used to end the command yourself when it's done with its goal.
In the constructor:
private Drive drive; // Make local variable
/** Creates a new Test. */
public Auto(Drive drive) { // Pass in the subsystem from RobotContainer
// Use addRequirements() here to declare subsystem dependencies.
this.drive = drive; // Set the local variable
addRequirements(drive); // Tell command scheduler the requirements
}
you notice that we are doing something with the Drive
subsystem.
Every subsystem gets instanced in RobotContainer
, and those instances must be passed around where they are needed. When a new Auto
command is created, you must pass the instances of the subsystems that it requires, and then use addRequirements
. This tells the Command Scheduler what subsystems a given command requires so it can resource manage correctly. In this case, the only subsystem that needs to be passed in is Drive
.
One apparent issue is that you can't access the subsystem anywhere outside of the constructor, like in the execute()
method, where you need to. To get around this, you make a local variable, and then assign the instance to it. That's where you get to the confusing this.drive = drive
.
Now, that you can access the Drive
subsystem inside all the command's methods, you can make the command do something.
Make a simple auto that drives forward at 1m/s, and then stops the robot once it's moved 5 meters.
Put your code in Auto.java
.
Tip
Use the Drive.drive(double forwardVel)
method to move, and Drive.getPos().getX()
to get the current X value of the robot's position. Keep in mind you are feeding it velocity values for a tank drive.
How to run tests Expected result
!
Solution
// Called when the command is initially scheduled.
@Override
public void initialize() {
drive.drive(1);
}
// Called every time the scheduler runs while the command is scheduled.
@Override
public void execute() {}
// Called once the command ends or is interrupted.
@Override
public void end(boolean interrupted) {
drive.drive(0);
}
// Returns true when the command should end.
@Override
public boolean isFinished() {
return drive.getPos().getX() >= 5;
}
Making an entire file for a command can be necessary when there's a lot of code, but for many smaller commands like the one you just wrote that do very little, there are better ways.
Inline Commands can make code easier to read and much shorter to write. There are a few types, and you should read more.
As an example, the code from Lesson 2.1 can be shortened to a StartEndCommand
. This type of inline command takes 2 methods and the required subsystems as parameters. It runs the first method when the command is scheduled, and the second one when the command is ended. Essentially the initialize()
and end()
methods inside your Auto
command.
StartEndCommand myCommand = new StartEndCommand(() -> {/*Code*/}, mySubsystem1.stopMotors(), mySubsystem1);
If you notice, we are using Lambdas in place of methods () -> {}
. These are essentially inline methods, and are common in inline commands to keep them, well, inline. Any code you want to run goes in the curly braces {}
, and if you only have one line of code to run, you can forgo the braces and semicolon ;
. You can also use Method References instead of Lambdas in certain places, so if you see class::method
anywhere in the code, it's basically just a lambda.
The issue with the StartEndCommand, if you notice, is that it will never end on its own, and relies on being stopped by the command scheduler eventually. In this case, you want to stop the robot once it has moved 5 meters, so you need to add on some extra code.
This can be achieved through Command Compositions. In this example, we can use .until()
to end the command once a condition is met (once again, we use Lambdas).
myCommand.until(() -> /* Condition */)
Use inline commands to make an auto that drives forward at a velocity of 2m/s until it reaches 10 meters, where it should stop.
Put your code in AutoFactory.java
inside the getSimpleInlineAuto
method. Create a command, and then make sure the method returns that command.
How to run tests Expected result
!
Solution
public static Command getAuto(Drive drive) {
Command command = new StartEndCommand(
() -> drive.drive(2),
() -> drive.drive(0),
drive).until(() -> drive.getPos().getX() >= 10);
return command;
}
As you should know, autonomous routines can get a lot more complex than simply driving in a straight line. The easiest way to do this is break the auto into several commands, and run those commands one after another.
Sequential Command Groups do exactly this. They allow you to string together several commands that run one after another. When the first command ends, the next will start, and so on to the end. As an example, you can use this to deploy an intake and then start running it, or for placing an object that requires multiple steps. In this case, you might drive forward, turn, and then drive forward again.
// Commands will run in sequence
Command command = new SequentialCommandGroup(
new StartEndCommand(
() -> {},
() -> {},
mySubsystem1),
new StartEndCommand(
() -> {},
() -> {},
mySubsystem1)
);
Parallel Command Groups allow you to run two commands at the same time (in parallel, as the name suggests). There are 3 different types, and the difference between them is when the entire group ends.
- Parallel, the command group finishes only when all commands end.
- Race, the command group finishes when any command ends. All other commands in the group are then interrupted.
- Deadline, similar to race, but finishes when a specific command ends, and all others are interrupted.
Create an auto that drives forward until it hits 3 meters on X, turns 90º, and then drives and runs the intake at the same time until it hits 3 meters on the Y.
Put your code in AutoFactory.java
inside the getSequentialAuto
method. Create a command, and then make sure the method returns that command.
Tip
- Use
Intake.runIntake(double speed)
to run the intake. - Use
Drive.drive(double forwardVel, double turnVel)
to turn. Remember you are feeding the drive base a velocity.forwardVel
is in meters per second, andturnVel
is in degrees per second. - Use
Drive.getPos().getRotation().getDegrees()
to get the current rotation of the robot. - Turn with a positive velocity to get to 90 deg fastest.
- You are supposed to use both Sequential and Parallel groups.
How to run tests Expected result
!
Solution
public static Command getSequentialAuto(Drive drive, Intake intake) {
Command command = new SequentialCommandGroup(
new StartEndCommand(
() -> drive.drive(0.5),
() -> drive.drive(0),
drive).until(() -> drive.getPos().getX() == 3),
new StartEndCommand(
() -> drive.drive(0, 45),
() -> drive.drive(0, 0),
drive).until(() -> drive.getPos().getRotation().getDegrees() == 90),
new ParallelRaceGroup(
new StartEndCommand(
() -> drive.drive(0.5),
() -> drive.drive(0),
drive).until(() -> drive.getPos().getY() == 3),
new StartEndCommand(
() -> intake.runIntake(1),
() -> intake.runIntake(0),
intake)));
return command;
}
Go on to Lesson-3.0.