Command-Based Programming: Intake Example
Command-Based Programming: Intake Example
In previous sections, we explored time-based programming, where robot actions were controlled using delays and sequential logic.
While that approach works for simple behaviors, it becomes difficult to scale as robots grow more complex.
This tutorial introduces command-based programming, a structured and modular approach used in FRC to manage robot behavior more effectively.
—
Why Command-Based?
Instead of writing one long sequence of actions, command-based programming separates your robot into:
Subsystems → represent hardware and capabilities
Commands → represent actions over time
RobotContainer → connects controls to commands
Think of it like this:
Subsystem = what the robot has
Command = what the robot does
—
The Intake Example
We will model an intake with two mechanisms:
Arm (Up/Down) - Controlled by position (rotations) - Uses closed-loop position control
Rollers - Controlled by velocity (RPM) - Uses closed-loop velocity control
Even though these are different control loops, they belong to the same subsystem.
—
Subsystem Design
The subsystem represents the hardware and exposes capabilities, not full behaviors.
public class IntakeSubsystem extends SubsystemBase {
// Motors
private final CANSparkMax armMotor;
private final CANSparkMax rollerMotor;
// Encoders
private final RelativeEncoder armEncoder;
private final RelativeEncoder rollerEncoder;
// PID Controllers
private final SparkMaxPIDController armPID;
private final SparkMaxPIDController rollerPID;
public IntakeSubsystem() {
armMotor = new CANSparkMax(1, MotorType.kBrushless);
rollerMotor = new CANSparkMax(2, MotorType.kBrushless);
armEncoder = armMotor.getEncoder();
rollerEncoder = rollerMotor.getEncoder();
armPID = armMotor.getPIDController();
rollerPID = rollerMotor.getPIDController();
// Configure PID (example values)
armPID.setP(0.1);
rollerPID.setP(0.0002);
armMotor.setIdleMode(IdleMode.kBrake);
}
### Arm (Position Control)
public void setArmPosition(double rotations) {
armPID.setReference(rotations, ControlType.kPosition);
}
public double getArmPosition() {
return armEncoder.getPosition();
}
public boolean isArmAtPosition(double target) {
return Math.abs(getArmPosition() - target) < 0.2;
}
### Rollers (Velocity Control)
public void setRollerVelocity(double rpm) {
rollerPID.setReference(rpm, ControlType.kVelocity);
}
public void stopRollers() {
rollerMotor.set(0);
}
public void stopAll() {
armMotor.set(0);
rollerMotor.set(0);
}
}
Key Idea: The subsystem does not decide when to act. It only provides the ability to act.
—
Command Design
Commands define behavior over time.
They answer: - When does something start? - When does it stop? - What happens in between?
—
Deploy Intake Command (Arm Only)
public class DeployIntakeCommand extends CommandBase {
private final IntakeSubsystem intake;
private final double targetPosition;
public DeployIntakeCommand(IntakeSubsystem intake, double targetPosition) {
this.intake = intake;
this.targetPosition = targetPosition;
addRequirements(intake);
}
@Override
public void initialize() {
intake.setArmPosition(targetPosition);
}
@Override
public boolean isFinished() {
return intake.isArmAtPosition(targetPosition);
}
@Override
public void end(boolean interrupted) {
intake.stopAll();
}
}
Concepts:
initialize()runs onceisFinished()determines when to stopPID handles the motion automatically
—
Run Rollers Command
public class RunRollersCommand extends CommandBase {
private final IntakeSubsystem intake;
private final double speedRPM;
public RunRollersCommand(IntakeSubsystem intake, double speedRPM) {
this.intake = intake;
this.speedRPM = speedRPM;
addRequirements(intake);
}
@Override
public void initialize() {
intake.setRollerVelocity(speedRPM);
}
@Override
public boolean isFinished() {
return false;
}
@Override
public void end(boolean interrupted) {
intake.stopRollers();
}
}
Concepts:
Runs continuously
Ends when interrupted (e.g., button release)
—
Combining Commands
One of the biggest advantages of command-based programming is composition.
### Sequential (one after another)
new DeployIntakeCommand(intake, 10.0)
.andThen(new RunRollersCommand(intake, 3000));
### Parallel (at the same time)
new DeployIntakeCommand(intake, 10.0)
.alongWith(new RunRollersCommand(intake, 3000));
—
Controller Binding
Commands are triggered by user input in RobotContainer.
controller.a().whileTrue(
new RunRollersCommand(intake, 3000)
);
controller.b().onTrue(
new DeployIntakeCommand(intake, 10.0)
);
controller.x().whileTrue(
new DeployIntakeCommand(intake, 10.0)
.alongWith(new RunRollersCommand(intake, 3000))
);
—
Subsystem Requirements
Each command declares:
addRequirements(intake);
This ensures:
Only one command controls the subsystem at a time
Prevents conflicting motor commands
—
Key Takeaways
Subsystems define capabilities
Commands define behavior over time
Closed-loop control lets you set goals instead of power
Commands can be combined and reused
—
Comparison to Time-Based Programming
Feature |
Time-Based |
Command-Based |
|---|---|---|
Structure |
Linear |
Modular |
Scalability |
Limited |
High |
Reusability |
Low |
High |
Parallel Actions |
Difficult |
Built-in |
—
Next Steps
To continue learning, consider:
Adding soft limits to the arm
Tuning PID values for better control
Introducing feedforward for gravity compensation
Exploring default commands for idle behavior
Command-based programming may feel more complex at first, but it enables cleaner, safer, and more scalable robot code.