Lesson 4.7: Constants, Subsystems & Commands Revisited
🎯 What You’ll Learn
By the end of this lesson you will be able to:
- Explain how Constants.java organizes values using inner classes and why that structure matters
- Describe the full lifecycle of a subsystem — construction, periodic, default commands
- Distinguish between inline commands, standalone command classes, and command compositions
- Trace how constants flow from Constants.java into subsystems and then into commands
- Identify patterns for when to use each command type in your team’s code
The Three Building Blocks — Revisited
In Units 2–3 you learned the basics: Constants hold configuration values, Subsystems control hardware, and Commands define behaviors. That’s the 30,000-foot view. Now let’s zoom in and see how these three pieces actually interact in your team’s code.
Think of it as a supply chain:
Constants.java → Subsystems → Commands → RobotContainer(what values) (what hardware) (what behavior) (when to run)Each layer depends on the one before it. Constants feed into subsystems, subsystems are used by commands, and RobotContainer wires commands to triggers. Let’s explore each layer in depth.
Constants.java — The Configuration Hub
You’ve edited Constants.java in Unit 3 to change a motor speed. Now let’s understand its full structure.
Inner Classes as Namespaces
Open
public final class Constants {
public static final class ShootingConstants { public static final int SHOOTER_LEFT_MOTOR = {robotConfig.canIds.shooterLeft}; public static final int SHOOTER_RIGHT_MOTOR = {robotConfig.canIds.shooterRight}; public static final int TURRET_MOTOR = {robotConfig.canIds.turret}; public static final int FEEDER_MOTOR = {robotConfig.canIds.feeder}; // ... more shooting-related constants }
public static final class IntakeConstants { public static final int INTAKE_LEFT_MOTOR = {robotConfig.canIds.intakeLeft}; public static final int INTAKE_RIGHT_MOTOR = {robotConfig.canIds.intakeRight}; public static final int DEPLOY_LEFT_MOTOR = {robotConfig.canIds.deployLeft}; public static final int DEPLOY_RIGHT_MOTOR = {robotConfig.canIds.deployRight}; // ... more intake-related constants }
public static final class ClimberConstants { public static final int CLIMB_LEFT_MOTOR = {robotConfig.canIds.climbLeft}; public static final int CLIMB_RIGHT_MOTOR = {robotConfig.canIds.climbRight}; public static final int ELEVATOR_MOTOR = {robotConfig.canIds.elevator}; }}Each inner class groups related constants together. When you need the turret motor CAN ID, you write ShootingConstants.TURRET_MOTOR — the inner class name tells you exactly which system it belongs to.
Why Inner Classes Instead of One Big List?
Imagine Constants.java with 50+ constants in a flat list. Finding the right one would be like searching a phone book with no sections. Inner classes act as sections — ShootingConstants, IntakeConstants, ClimberConstants — so you always know where to look.
This also prevents name collisions. Both the shooter and intake might have a LEFT_MOTOR constant, but ShootingConstants.LEFT_MOTOR and IntakeConstants.LEFT_MOTOR are distinct.
Why does Constants.java use inner classes like ShootingConstants and IntakeConstants instead of putting all constants in one flat list?
Subsystems — The Hardware Layer
You know that subsystems control hardware. But there’s more to the lifecycle than just motor methods.
The Subsystem Lifecycle
When your robot boots up, here’s what happens to each subsystem:
-
Construction — RobotContainer creates the subsystem with
new IntakeSubsystem(). The constructor initializes motor controllers, configures settings (brake/coast mode, current limits), and sets initial states. -
Registration — Because the subsystem extends
SubsystemBase, it automatically registers with the Command Scheduler. The scheduler now knows this subsystem exists. -
Periodic — Every 20ms, the scheduler calls the subsystem’s
periodic()method (if it has one). This is where subsystems can update sensor readings, publish telemetry, or run control loops. -
Default Command — If no other command is using the subsystem, the scheduler runs its default command. For the drivetrain, this is usually the teleop drive command.
Construction: Where Constants Meet Hardware
Open
public class IntakeSubsystem extends SubsystemBase { private final TalonFX intakeLeftMotor; private final TalonFX intakeRightMotor; private final TalonFXS intakeLeftActivator; private final TalonFXS intakeRightActivator;
public IntakeSubsystem() { intakeLeftMotor = new TalonFX(IntakeConstants.INTAKE_LEFT_MOTOR); intakeRightMotor = new TalonFX(IntakeConstants.INTAKE_RIGHT_MOTOR); intakeLeftActivator = new TalonFXS(IntakeConstants.DEPLOY_LEFT_MOTOR); intakeRightActivator = new TalonFXS(IntakeConstants.DEPLOY_RIGHT_MOTOR); // ... motor configuration (brake mode, current limits, etc.) }}See the flow? IntakeConstants.INTAKE_LEFT_MOTOR (the CAN ID from Constants.java) gets passed to new TalonFX(...) to create the motor controller object. The constant tells the motor controller which physical device to talk to on the CAN bus.
The periodic() Method
Some subsystems override periodic() to do work every loop cycle:
@Overridepublic void periodic() { // Read sensors, update state, publish telemetry SmartDashboard.putNumber("Intake/Speed", intakeLeftMotor.getVelocity().getValueAsDouble());}This runs every 20ms regardless of what command is active. It’s the subsystem’s heartbeat — always running, always updating.
Default Commands
A default command runs whenever no other command requires the subsystem. The most common example is the drivetrain’s teleop drive:
drivetrain.setDefaultCommand( drivetrain.applyRequest(() -> drive .withVelocityX(-controller.getLeftY() * MaxSpeed) .withVelocityY(-controller.getLeftX() * MaxSpeed) .withRotationalRate(-controller.getRightX() * MaxAngularRate) ));When no autonomous command or button-triggered command is using the drivetrain, this default command reads the joysticks and drives. The moment a button command takes over, the default command pauses. When the button command finishes, the default command resumes automatically.
Commands — The Behavior Layer
In Unit 2 you learned that commands tell the robot what to do. Now let’s look at the three different ways commands are created in your team’s code.
Type 1: Inline Commands (in RobotContainer)
The simplest commands are defined right where they’re used — inline in RobotContainer button bindings:
controller.a().whileTrue( new RunCommand(() -> intakeSubsystem.jostle(), intakeSubsystem));This creates a RunCommand on the spot. No separate file needed. Use inline commands when the behavior is simple — one method call, one subsystem.
Type 2: Standalone Command Classes (in commands/)
Complex behaviors get their own file. Open
public class AutoShootCommand extends Command { private final ShooterSubsystem shooter; private final TurretSubsystem turret;
public AutoShootCommand(ShooterSubsystem shooter, TurretSubsystem turret) { this.shooter = shooter; this.turret = turret; addRequirements(shooter, turret); // Lock both subsystems }
@Override public void execute() { // Aim turret, spin flywheel, feed when ready }
@Override public void end(boolean interrupted) { shooter.stopShooter(); turret.stopTurret(); }}Use standalone commands when the behavior involves multiple subsystems, has complex logic, or needs initialize(), execute(), end(), and isFinished() lifecycle methods.
Type 3: Command Compositions (Sequences and Parallels)
The X button intake binding is a command composition — multiple commands chained together:
new SequentialCommandGroup( new InstantCommand(() -> intakeSubsystem.deployOut(), intakeSubsystem), new WaitCommand(0.3), new InstantCommand(() -> intakeSubsystem.stopDeploy()), new RunCommand(() -> intakeSubsystem.runIntake(0.5), intakeSubsystem))WPILib provides several composition types:
| Composition | Behavior |
|---|---|
SequentialCommandGroup | Run commands one after another |
ParallelCommandGroup | Run all commands at once, finish when ALL are done |
ParallelRaceGroup | Run all commands at once, finish when ANY one is done |
ParallelDeadlineGroup | Run all commands at once, finish when the FIRST (deadline) command is done |
When to Use Which?
| Situation | Command Type |
|---|---|
| Simple one-liner (run a motor, stop a motor) | Inline InstantCommand or RunCommand |
| Multi-step sequence (deploy → wait → run) | SequentialCommandGroup with inline commands |
| Complex multi-subsystem behavior (auto-shoot) | Standalone command class in commands/ |
| Run two things simultaneously (drive + aim) | ParallelCommandGroup or ParallelRaceGroup |
Your team needs a command that aims the turret while simultaneously spinning up the flywheel, then feeds the note once both are ready. Which approach is most appropriate?
How the Three Layers Connect
Let’s trace a complete path from constant to motor output for the intake system:
Constants.java IntakeSubsystem.java RobotContainer.java───────────── ──────────────────── ───────────────────IntakeConstants Constructor reads constants: Creates subsystem: INTAKE_LEFT_MOTOR = 31 → new TalonFX(31) ← new IntakeSubsystem() INTAKE_RIGHT_MOTOR = 32 → new TalonFX(32) DEPLOY_LEFT_MOTOR = 33 → new TalonFXS(33) Binds commands: DEPLOY_RIGHT_MOTOR = 34 → new TalonFXS(34) controller.x().toggleOnTrue( → intakeSubsystem.deployOut() Methods use motors: → intakeSubsystem.runIntake() deployOut() → set(0.15) ) runIntake(speed) → set(speed)The data flows left to right: constants define the hardware addresses, the subsystem uses those addresses to create motor objects and expose methods, and RobotContainer wires those methods to controller buttons via commands.
-
Three command types:
- Inline commands (InstantCommand, RunCommand in RobotContainer) — for simple one-liner behaviors
- Standalone command classes (in commands/ folder) — for complex multi-subsystem behaviors with lifecycle methods
- Command compositions (SequentialCommandGroup, ParallelCommandGroup) — for chaining multiple commands together
-
Default command behavior: When a button-triggered command starts and requires the same subsystem, the default command is interrupted (paused). When the button command finishes or is cancelled, the default command resumes automatically. The command scheduler handles this — you don’t need to restart it manually.
-
Why read from Constants.java: Centralizing CAN IDs in one file means you only change them in one place if hardware is rewired. If CAN IDs were hardcoded in each subsystem, you’d have to find and update every occurrence — easy to miss one and cause a bug.
Advanced Pattern: Subsystem Requirements
One detail that trips up new programmers: the addRequirements() call in commands. When a command declares that it requires a subsystem, the command scheduler enforces exclusive access — only one command can use a subsystem at a time.
// In AutoShootCommand constructor:addRequirements(shooter, turret);
// This means: while AutoShootCommand is running,// NO other command can use shooter or turret.// If another command tries, the scheduler will// cancel one of them.This prevents conflicts. Imagine two commands both trying to set the shooter to different speeds — the robot would jitter between them. Requirements ensure only one command controls each subsystem at any moment.
For inline commands, you pass the subsystem as the second argument:
new InstantCommand(() -> intakeSubsystem.deployOut(), intakeSubsystem)// ^^^^^^^^^^^^^^^^// This is the requirementIf you forget the requirement, the command scheduler won’t know the command uses that subsystem, and two commands could fight over the same motors.
Key Terms
📖 All terms below are also in the full glossary for quick reference.
| Term | Definition |
|---|---|
| Inner Class | A class defined inside another class, used in Constants.java to group related constants by system |
| Default Command | A command that runs on a subsystem whenever no other command requires that subsystem — typically the teleop drive command |
| Command Composition | Combining multiple commands using SequentialCommandGroup, ParallelCommandGroup, or other group types to create complex behaviors |
| Subsystem Requirement | A declaration that a command uses a specific subsystem, enforced by the command scheduler to prevent conflicts |
| Command Scheduler | The WPILib engine that manages which commands run, handles requirements, and calls periodic methods every 20ms |
| Inline Command | A command created directly in RobotContainer (like new RunCommand(...)) rather than in a separate file |
What’s Next?
You now understand how constants, subsystems, and commands work together at a deeper level — from inner class organization to subsystem lifecycles to command composition patterns.
In Activity 4.8: Trace a New Button, you’ll put this knowledge to work by picking a button binding you haven’t traced yet (the A button jostle or B button elevator) and producing a complete CodeTrace from button press to motor output.
In Activity 4.8: Trace a New Button, you’ll put this knowledge to work by picking a button binding you haven’t traced yet (the A button jostle or B button elevator) and producing a complete CodeTrace from button press to motor output.