| name | ftclib |
| description | Expert guidance for FTCLib command-based programming. Use when creating subsystems and commands, binding buttons, using PID controllers, composing command groups, working with hardware wrappers, or implementing FTC robot logic with FTCLib. |
FTCLib Command-Based Programming Skill
This skill provides comprehensive knowledge about FTCLib v2.1.1, a broad FTC library implementing command-based programming, hardware abstractions, and control systems.
Core Philosophy
FTCLib uses a declarative programming paradigm inspired by WPILib. Instead of writing iterative loop logic, you declare what the robot should do through subsystems and commands, then bind actions to triggers.
Traditional approach (imperative):
override fun loop() {
if (gamepad1.a) {
intake.setPower(1.0)
} else {
intake.setPower(0.0)
}
}
FTCLib approach (declarative):
fun init() {
gamepad1.getButton(ButtonType.A).whileHeld(RunIntake(intake))
}
Command-Based Architecture
The Three Core Components
1. Subsystems - Hardware organization
- Represent robot mechanisms (drivetrain, intake, shooter, etc.)
- Encapsulate hardware access (motors, sensors, servos)
- Extend
SubsystemBase
- Define
periodic() for continuous updates
- Protect hardware from conflicting access
2. Commands - Robot actions
- Implement robot behaviors (drive, shoot, pick up, etc.)
- Extend
CommandBase or use convenience commands
- Declare subsystem requirements
- Lifecycle:
initialize() → execute() → isFinished() → end()
- Can be composed into complex sequences
3. CommandScheduler - Execution manager
- Singleton that manages all active commands
- Polls buttons for triggers
- Prevents resource conflicts
- Handles interruption and completion
- Must call
run() in OpMode loop
Subsystem Pattern
class Intake(val robot: Robot) : SubsystemBase() {
@Configurable
object IntakeConstants {
var intakePower: Double = 0.8
}
fun setPower(power: Double) {
robot.hardware.setIntakePower(power)
}
fun setReverse(reverse: Boolean) {
robot.hardware.setIntakeReverse(reverse)
}
override fun periodic() {
robot.telemetry.addData("Intake Power", robot.hardware.intakePower)
}
}
Key concepts:
- One subsystem per mechanism
- Access hardware through abstraction layer (robot.hardware)
- No business logic in periodic() - use commands instead
- periodic() for telemetry, sensor monitoring, safety checks
Command Pattern
class RunIntake(
private val intake: Intake,
private val power: Double = IntakeConstants.intakePower
) : CommandBase() {
init {
addRequirements(intake)
}
override fun initialize() {
intake.setPower(power)
}
override fun execute() {
}
override fun isFinished(): Boolean {
return false
}
override fun end(interrupted: Boolean) {
intake.setPower(0.0)
}
}
Command lifecycle:
- Schedule - Added to scheduler
- Initialize -
initialize() called once
- Execute -
execute() called repeatedly
- Check finish -
isFinished() polled each cycle
- End -
end(interrupted) called for cleanup
CommandScheduler Usage
In Robot.kt (central coordinator):
class Robot() : CommandRobot() {
val intake = Intake(this)
val drivetrain = Drivetrain(this)
init {
drivetrain.defaultCommand = Drive(drivetrain, gamepad1)
gamepad1.getButton(ButtonType.A).whileHeld(RunIntake(intake))
}
override fun run() {
gamepad1.tick()
gamepad2.tick()
super.run()
hardware.tick()
}
}
Critical: Must call CommandScheduler.getInstance().run() every loop cycle (or super.run() in CommandRobot).
Button Bindings and Triggers
GamepadEx Usage
Wrap standard gamepads with GamepadEx (or use custom GamepadIO):
val gamepad1 = GamepadEx(hardwareMap.get("gamepad1"))
val button = gamepad1.getGamepadButton(GamepadKeys.Button.A)
In this codebase:
gamepad1.getButton(ButtonType.A)
Binding Methods
whenPressed / whenActive:
- Schedules command once when button pressed
- Single execution per press
gamepad1.getButton(ButtonType.A).whenPressed(ShootBall(shooter))
whileHeld / whileActiveContinuous:
- Schedules command when pressed, cancels when released
- If command finishes while held, reschedules automatically
gamepad2.getButton(ButtonType.X).whileHeld(RunIntake(intake))
whenHeld / whileActiveOnce:
- Schedules when pressed, cancels when released
- Does NOT reschedule if command finishes while held
gamepad1.getButton(ButtonType.B).whenHeld(DriveToTarget(drivetrain))
whenReleased / whenInactive:
- Schedules command only on release (button up)
gamepad2.getButton(ButtonType.B).whenReleased(TransferArtifact(transfer))
toggleWhenPressed / toggleWhenActive:
- First press: schedule command
- Second press: cancel command
- Can toggle between two different commands (v1.2.0+)
gamepad1.getButton(ButtonType.LeftBumper).toggleWhenPressed(EngageBrake(brake))
button.toggleWhenPressed(CommandA(), CommandB())
cancelWhenPressed / cancelWhenActive:
- Immediately cancels specified command
gamepad1.getButton(ButtonType.Back).cancelWhenPressed(autonomousCommand)
Custom Triggers
Create triggers from any boolean condition:
val trigger = Trigger { intake.hasArtifact() }
trigger.whenActive(IndexArtifact(spiceRack))
val bothButtons = Trigger { gamepad1.a && gamepad1.b }
bothButtons.whenActive(SpecialMove())
Trigger { sensor.isPressed() }
.and(Trigger { !intake.isFull() })
.whenActive(RunIntake(intake))
Command Groups
Combine simple commands into complex sequences.
SequentialCommandGroup
Execute commands one after another:
SequentialCommandGroup(
FollowPedroPath(pathing, paths.toPickup),
RunIntake(intake).withTimeout(2000),
FollowPedroPath(pathing, paths.toGoal),
ShootArtifact(shooter, spiceRack)
)
Each command must finish before next starts.
ParallelCommandGroup
Run commands simultaneously, finish when ALL complete:
ParallelCommandGroup(
RunShooter(shooter),
IndexArtifact(spiceRack)
)
Constraint: Commands cannot require same subsystem.
ParallelRaceGroup
Run commands in parallel, finish when ANY completes:
ParallelRaceGroup(
FollowPedroPath(pathing, paths.toSample),
RunIntake(intake),
WaitUntilCommand { intake.hasArtifact() }
)
First command to finish cancels all others.
ParallelDeadlineGroup
Run commands in parallel, finish when deadline command completes:
ParallelDeadlineGroup(
FollowPedroPath(pathing, paths.toGoal),
RunShooter(shooter),
AutoAim(turntable)
)
When deadline finishes, all others are cancelled.
Composition Patterns
Method chaining (decorator methods):
RunIntake(intake)
.withTimeout(2000)
.andThen(IndexArtifact(spiceRack))
.andThen(ShootArtifact(shooter))
FollowPedroPath(pathing, path)
.alongWith(RunIntake(intake))
.raceWith(WaitUntilCommand { intake.hasArtifact() })
Nested groups:
SequentialCommandGroup(
ParallelRaceGroup(
FollowPedroPath(pathing, paths.toSample),
RunIntake(intake)
),
SequentialCommandGroup(
FollowPedroPath(pathing, paths.toGoal),
ShootArtifact(shooter)
)
)
Convenience Commands
Pre-built commands for common patterns.
InstantCommand
Execute action immediately, finish instantly:
gamepad1.getButton(ButtonType.DPadUp).whenPressed(
InstantCommand({ drivetrain.fieldCentric = !drivetrain.fieldCentric })
)
Use for: toggles, one-shot actions, state changes.
RunCommand
Run action repeatedly in execute():
val drive = RunCommand(
{ drivetrain.drive(gamepad1.leftY, gamepad1.leftX, gamepad1.rightX) },
drivetrain
)
drivetrain.defaultCommand = drive
Use for: default commands, continuous operations.
WaitCommand
Wait for specified time:
SequentialCommandGroup(
ShootArtifact(shooter),
WaitCommand(500),
IndexNext(spiceRack)
)
WaitUntilCommand
Block until condition true:
WaitUntilCommand { intake.hasArtifact() }
ConditionalCommand
Choose command based on boolean:
ConditionalCommand(
ScoreInHighGoal(shooter),
ScoreInLowGoal(intake),
{ spiceRack.artifactCount > 5 }
)
SelectCommand
Choose from multiple commands (state machine):
SelectCommand(
mapOf(
GameState.INTAKE to RunIntake(intake),
GameState.SCORE to ShootArtifact(shooter),
GameState.PARK to DriveToBase(drivetrain)
),
{ currentState }
)
FunctionalCommand
Define command inline without subclassing:
FunctionalCommand(
init = { intake.deploy() },
execute = { intake.run() },
end = { interrupted -> intake.stop() },
isFinished = { intake.hasArtifact() },
requirements = setOf(intake)
)
PerpetualCommand
Wrap command to run forever (ignore isFinished):
RunShooter(shooter).perpetually()
Command Decorators
Modify commands without creating new classes.
withTimeout(millis) - Add timeout:
RunIntake(intake).withTimeout(3000)
beforeStarting(runnable) - Execute before initialize:
ShootArtifact(shooter).beforeStarting { shooter.spinUp() }
andThen(commands) - Sequential composition:
FollowPath(path1).andThen(FollowPath(path2))
alongWith(commands) - Parallel composition:
FollowPath(path).alongWith(RunIntake(intake))
raceWith(commands) - Parallel race:
FollowPath(path).raceWith(WaitCommand(5000))
deadlineWith(commands) - Parallel deadline:
FollowPath(path).deadlineWith(RunShooter(shooter))
interruptOn(condition) - Conditional cancel:
FollowPath(path).interruptOn { sensor.isTriggered() }
whenFinished(runnable) - Execute after end:
ShootArtifact(shooter).whenFinished { spiceRack.index() }
perpetually() - Run forever:
RunShooter(shooter).perpetually()
asProxy() - Schedule as proxy:
autonomousRoutine.asProxy()
Default Commands
Subsystems can have default commands that run when no other command requires them.
class Drivetrain : SubsystemBase() {
}
drivetrain.defaultCommand = Drive(drivetrain, gamepad1)
Use cases:
- Driver control for drivetrain
- Idle states (hold position, brake)
- Background monitoring
Behavior:
- Runs when subsystem not required by other commands
- Automatically cancelled when other command needs subsystem
- Automatically rescheduled when subsystem freed
Resource Management
Subsystem requirements prevent conflicts:
class RunIntake(val intake: Intake) : CommandBase() {
init {
addRequirements(intake)
}
}
Scheduler behavior:
- Only one command per subsystem at a time
- New command interrupts current if:
- Current is interruptible (default: true)
- New command requires same subsystem
- Command groups inherit requirements from children
Making commands uninterruptible:
override fun runsWhenDisabled(): Boolean = true
Integration with This Codebase
Robot Coordinator Pattern
class Robot(
var hardware: RobotIO,
var gamepad1: GamepadIO,
var gamepad2: GamepadIO,
var telemetry: TelemetryIO,
type: OpModeType
) : CommandRobot() {
val drivetrain = Drivetrain(this)
val intake = Intake(this)
val shooter = Shooter(this)
init {
if (type == OpModeType.TELEOP) {
initTeleop()
} else {
initAuto()
}
}
fun initTeleop() {
drivetrain.defaultCommand = Drive(drivetrain, gamepad1, gamepad2)
gamepad1.getButton(ButtonType.A).toggleWhenPressed(RunIntake(intake))
gamepad2.getButton(ButtonType.X).whenPressed(ShootArtifact(shooter))
}
override fun run() {
gamepad1.tick()
gamepad2.tick()
super.run()
hardware.tick()
}
}
Hardware Abstraction
All hardware access through RobotIO interface:
class Intake(val robot: Robot) : SubsystemBase() {
fun setPower(power: Double) {
robot.hardware.setIntakePower(power)
}
fun hasArtifact(): Boolean {
return robot.hardware.intakeColorSensor() > threshold
}
}
Benefits:
- Testable (can use mock RobotIO)
- Centralizes hardware caching
- Clean subsystem code
Configurable Constants
Use @Configurable for runtime tuning:
@Configurable
object ShooterConstants {
var targetRPM: Double = 5500.0
var kP: Double = 0.001
var kI: Double = 0.0
var kD: Double = 0.00005
var kF: Double = 0.0001
}
Access via FTC Dashboard for live tuning.
Command File Organization
common/subsystems/
├── Drivetrain.kt # Subsystem + Drive command
├── Intake.kt # Subsystem + RunIntake command
├── Shooter.kt # Subsystem + RunShooter, ShootArtifact commands
├── SpiceRack.kt # Subsystem + indexing commands
└── ...
Pattern: Define related commands in same file as subsystem.
Best Practices
1. Single Responsibility
- One subsystem per mechanism
- Commands do one thing well
- Compose complex behaviors from simple commands
2. Declare Requirements
- Always call
addRequirements(subsystem) in command init
- Prevents conflicts and ensures proper scheduling
3. Cleanup in end()
- Stop motors, release resources
- Reset state to safe default
- Handle both normal and interrupted endings
4. Use Default Commands
- Driver control for teleop
- Idle behaviors (hold position, brake)
- Always active when subsystem free
5. Prefer Composition
- Build complex sequences from command groups
- Reuse simple commands
- Leverage decorators (.andThen, .alongWith, etc.)
6. Immutable Commands
- Don't modify command state from outside
- Let scheduler manage lifecycle
- Create new instances if parameters change
7. Hardware Abstraction
- Access hardware through RobotIO/HardwareIO
- Enables testing and simulation
- Centralize caching and optimization
Common Patterns
Timeout with Fallback
ParallelRaceGroup(
RunIntake(intake).withTimeout(3000),
WaitUntilCommand { intake.hasArtifact() }
).andThen(
ConditionalCommand(
IndexArtifact(spiceRack),
FlashLED(LED.RED),
{ intake.hasArtifact() }
)
)
State Machine
enum class IntakeState { IDLE, RUNNING, INDEXING, EJECTING }
var currentState = IntakeState.IDLE
SelectCommand(
mapOf(
IntakeState.RUNNING to RunIntake(intake),
IntakeState.INDEXING to IndexArtifact(spiceRack),
IntakeState.EJECTING to ReverseIntake(intake)
),
{ currentState }
)
Sensor-Triggered Sequence
SequentialCommandGroup(
RunIntake(intake),
WaitUntilCommand { intake.hasArtifact() },
InstantCommand({ intake.stop() }),
IndexArtifact(spiceRack)
)
Parallel with Shared Resource (Illegal!)
ParallelCommandGroup(
FollowPath(path, drivetrain),
AutoAim(drivetrain)
)
class FollowPathWithAim(drivetrain: Drivetrain, path: PathChain) : CommandBase() {
init { addRequirements(drivetrain) }
override fun execute() {
follower.update()
aimAtTarget()
}
}
Troubleshooting
Command not running:
- Check
addRequirements() called in init
- Verify CommandScheduler.run() called in loop
- Check for conflicting commands on same subsystem
- Ensure button binding uses correct method
Command interrupted unexpectedly:
- Another command requiring same subsystem scheduled
- Default command taking over when command finishes
- Check button bindings for conflicts
Buttons not responding:
- Verify gamepad.tick() called before scheduler.run()
- Check button type matches gamepad
- Ensure binding method correct (whenPressed vs whileHeld)
Command never finishes:
- isFinished() always returns false
- Use .withTimeout() as safety
- Check finish condition logic
Motors don't stop:
- Cleanup not in end() method
- Command interrupted before end() called
- Hardware abstraction not updating
Quick Reference
| Task | Code |
|---|
| Create subsystem | class MySubsystem : SubsystemBase() |
| Create command | class MyCommand(subsystem: SubsystemBase) : CommandBase() |
| Declare requirement | addRequirements(subsystem) in command init |
| Bind button | gamepad.getButton(type).whenPressed(command) |
| Sequential group | SequentialCommandGroup(cmd1, cmd2, cmd3) |
| Parallel group | ParallelCommandGroup(cmd1, cmd2) |
| Race group | ParallelRaceGroup(cmd1, cmd2) |
| Add timeout | command.withTimeout(millis) |
| Default command | subsystem.defaultCommand = command |
| Run scheduler | CommandScheduler.getInstance().run() |
| Instant command | InstantCommand({ /* action */ }) |
| Wait command | WaitCommand(millis) |
| Conditional | ConditionalCommand(ifTrue, ifFalse, condition) |
Resources