Subsystems¶
A subsystem represents a cohesive piece of robot hardware: drivetrain, intake, lift, shooter, arm. Subsystems are the resource the scheduler arbitrates — a command that requires a subsystem gets exclusive use of it while running.
Two base classes are provided:
Subsystem(valleyLib-core) — the platform-agnostic base.FtcSubsystem(valleyLib-ftc) — adds aprotected HardwareMap hardwareMapfield for FTC hardware access.
Writing a subsystem¶
public class LiftSubsystem extends FtcSubsystem {
private final DcMotor liftMotor;
public LiftSubsystem(HardwareMap hardwareMap) {
super(hardwareMap);
liftMotor = hardwareMap.get(DcMotor.class, "lift");
}
// High-level actions — commands call these, never the motor directly
public void raise() { liftMotor.setPower(0.8); }
public void lower() { liftMotor.setPower(-0.5); }
public void stop() { liftMotor.setPower(0); }
public boolean atTop() { return liftMotor.getCurrentPosition() > 2200; }
@Override
public void periodic() {
// Runs every scheduler cycle regardless of active commands:
// sensor caching, state estimation, safety checks.
}
}
Automatic registration
The Subsystem constructor registers the instance with CommandScheduler.getInstance() automatically. Just construct your subsystems in initialize() — no extra wiring needed.
periodic()¶
Called once per scheduler cycle, before commands execute, whether or not any command is using the subsystem. Use it for:
- reading/caching sensors once per loop
- state estimation (odometry, filters)
- safety limits (e.g., cut power at end stops)
The Pedro integration uses exactly this hook: PedroSubsystem.periodic() calls follower.update() so path following advances automatically.
simulationPeriodic()¶
Called only when the scheduler's simulation mode is enabled (or via runSimulationStep()). Override it to advance a desktop physics model — see Desktop Simulation & Testing.
Default commands¶
A default command runs whenever no other command requires the subsystem. The classic example is joystick driving:
How it works each cycle:
- If the subsystem is idle (no command holds its requirement), the scheduler schedules the default command.
- When another command that requires the subsystem is scheduled (say, an auto-align bound to a button), the default command is interrupted.
- When that command ends, the subsystem is idle again, so the default command resumes on the next cycle.
drive.setDefaultCommand(command); // set (or replace)
drive.getDefaultCommand(); // returns the command, or null
Tip
Default commands typically never finish on their own (subsystem.run(...) is ideal). A default command that finishes will simply be rescheduled next cycle.
Requirements vs. periodic¶
Two different mechanisms, often confused:
| Mechanism | Runs when | Use for |
|---|---|---|
periodic() |
Always, every cycle | Sensors, estimation, safety |
| Commands requiring the subsystem | Only while scheduled | Actuation / behavior |
Keep actuation in commands and observation in periodic(), and the scheduler's exclusivity guarantees stay meaningful.
Command factories¶
Every subsystem carries WPILib-style factory methods that build commands requiring that subsystem — so they participate fully in scheduler conflict resolution (preempting and being preempted), unlike the requirement-free Commands factories:
| Factory | Behavior |
|---|---|
subsystem.runOnce(action) |
Runs the action once, then finishes |
subsystem.run(action) |
Runs the action every cycle, never finishes on its own — ideal for default commands |
subsystem.startEnd(onStart, onEnd) |
onStart when scheduled, onEnd when it ends (finished or interrupted) — ideal for whileTrue |
subsystem.runEnd(action, onEnd) |
Runs every cycle, plus an end action |
subsystem.idle() |
Holds the subsystem and does nothing — an explicit "stay still" in parallel groups, or a placeholder default |
Factory commands are named after the subsystem (Intake.startEnd, Drive.run) so the command logger output reads well. Override getName() on the subsystem to change the prefix.
// Default drive command that owns the drivetrain:
drive.setDefaultCommand(drive.run(() ->
drive.tankDrive(driver.leftY(), driver.rightY())));
// Hold-to-run intake with proper requirements:
driver.a().whileTrue(intake.startEnd(intake::in, intake::stop));
The factory pattern shines as named methods on the subsystem, keeping hardware detail inside it:
Call sites then read like English: driver.y().onTrue(lift.raiseCommand()).
Which factory class?
Use subsystem factories for anything that actuates the subsystem (they carry the requirement). Use the static Commands factories for glue that touches no mechanism — waits, telemetry markers, combining groups.
Design guidelines¶
- One mechanism, one subsystem. If two mechanisms can move independently, make them separate subsystems so commands can run in parallel.
- Expose actions, not hardware.
lift.raise()beatslift.getMotor().setPower(0.8)scattered across ten commands. - Prefer subsystem command factories over hand-rolled classes for simple behaviors; write a command class when there's real internal state (controllers, profiles) — see Writing Custom Commands.