Writing Custom Commands¶
The built-in commands, factories, and decorators cover most needs, but real robots always need a few bespoke commands — a PID drive-to-distance, a lift profile, a vision-aligned turn. This guide shows the recommended patterns.
1. Implement Command¶
At minimum, implement execute(). Most real commands should also implement:
initialize()— reset timers and internal state (commands must be re-runnable)isFinished()— define completionend(boolean interrupted)— stop motors safelygetRequirements()— declare subsystem ownership
public class DriveDistanceCommand implements Command {
private final DriveSubsystem drive;
private final double targetInches;
public DriveDistanceCommand(DriveSubsystem drive, double targetInches) {
this.drive = drive;
this.targetInches = targetInches;
}
@Override
public void initialize() {
drive.resetEncoders();
}
@Override
public void execute() {
drive.driveForward(0.4);
}
@Override
public boolean isFinished() {
return drive.getAverageDistanceInches() >= targetInches;
}
@Override
public void end(boolean interrupted) {
drive.stop();
}
@Override
public Set<Subsystem> getRequirements() {
return Set.of(drive);
}
}
2. Or extend a base class¶
Three convenience bases save boilerplate for common shapes:
Managed lifecycle with protected hooks and extra state queries (justFinished(), wasInterrupted()):
public class HomeLiftCommand extends BaseCommand {
@Override protected void onInitialize() { lift.startHoming(); }
@Override protected void onExecute() { lift.stepDown(); }
@Override protected boolean onIsFinished() { return lift.atLimitSwitch(); }
@Override protected void onEnd(boolean interrupted) { lift.zeroAndStop(); }
}
Fixed-duration commands with a live elapsed-time callback:
Custom decorators around an existing command:
public class TimedLogCommand extends CommandWrapper {
private double start;
public TimedLogCommand(Command inner) { super(inner); }
@Override protected void onInitialize() {
start = RobotClock.seconds();
super.onInitialize();
}
@Override protected void onEnd(boolean interrupted) {
super.onEnd(interrupted);
System.out.println(getName() + " took "
+ (RobotClock.seconds() - start) + " s");
}
}
3. Prefer composition when possible¶
Ask "can this be a factory + decorators?" before writing a class. The subsystem factories even carry requirements for you:
// Class-free drive-for-time (requires the drivetrain):
Command driveOut = drive.run(() -> drive.tankDrive(0.5, 0.5))
.withTimeout(2.0)
.finallyDo(drive::stop);
// Class-free "intake until loaded, max 3 s" (requires the intake):
Command autoIntake = intake.startEnd(intake::in, intake::stop)
.until(sensor::hasGamePiece)
.withTimeout(3.0);
Write a class when the command has real internal state — controllers, filters, motion profiles — or coordinates multiple subsystems with custom logic.
4. Use groups for composition¶
Command auto = new SequentialCommandGroup(
new InstantCommand(claw::close),
new WaitCommand(0.2),
new ParallelCommandGroup(
new LiftToHeightCommand(lift, LiftHeight.HIGH),
new DriveDistanceCommand(drive, 24)
)
);
Safety and correctness checklist¶
- ✅ Always stop actuators in
end(...)— it runs on interruption too. - ✅ Keep
execute()non-blocking and fast; neversleep()or loop-wait. - ✅ Declare all required subsystems in
getRequirements(). - ✅ Reset state in
initialize(), not the constructor — commands get reused. - ✅ Route hardware access through subsystem methods, not raw motors in commands.
Common mistakes¶
| Mistake | Symptom |
|---|---|
| Forgetting requirements | Two commands fight over one mechanism; default command never yields |
Blocking inside execute() |
The whole scheduler (and OpMode loop) stalls |
| State only set in constructor | Command works once, misbehaves when rescheduled |
No interrupted handling in end |
Motors keep running after a cancel or preemption |
Scheduling in periodic() unconditionally |
Command spam — schedule from triggers or sequences instead |