sign in
Start typing to search across all 16 weeks…
home / offseason / week o3
Offseason · Week 3 of 8

Command-Based Architecture

Subsystems, Commands, Triggers, and the Command Scheduler — the way 2974 actually structures robot code.

Subsystems

A subsystem is a class that owns hardware and exposes what that hardware can do. The key rule: nothing outside the subsystem directly touches its motors or sensors. Everything goes through methods. This is how 2974's Rebuilt codebase is structured — Intake, Indexer, Shooter, Drivetrain are all subsystems.

Minimal Subsystem Structure

java — IntakeSubsystem.java (WRT style)
import edu.wpi.first.wpilibj2.command.SubsystemBase;
import com.ctre.phoenix6.hardware.TalonFX;
import com.ctre.phoenix6.configs.TalonFXConfiguration;
import com.ctre.phoenix6.signals.NeutralModeValue;

public class IntakeSubsystem extends SubsystemBase {

    // -- Hardware --
    private final TalonFX m_rollerMotor;

    // -- Constants --
    private static final int    kMotorID        = 9;
    private static final double kIntakeSpeed    = 0.8;
    private static final int    kCurrentLimit_A = 40;

    public IntakeSubsystem() {
        m_rollerMotor = new TalonFX(kMotorID);
        var config = new TalonFXConfiguration();
        config.MotorOutput.NeutralMode = NeutralModeValue.Coast;
        config.CurrentLimits.StatorCurrentLimit = kCurrentLimit_A;
        config.CurrentLimits.StatorCurrentLimitEnable = true;
        m_rollerMotor.getConfigurator().apply(config);
    }

    // ── Public methods (Commands call these) ───────────────────
    public void intake()  { m_rollerMotor.set( kIntakeSpeed); }
    public void eject()   { m_rollerMotor.set(-kIntakeSpeed); }
    public void stop()    { m_rollerMotor.set(0); }

    // ── Sensor reads ───────────────────────────────────────────
    public double getCurrent_A() {
        return m_rollerMotor.getStatorCurrent().getValueAsDouble();
    }

    @Override
    public void periodic() {
        // Log data every 20ms for debugging
        SmartDashboard.putNumber("Intake/Current_A", getCurrent_A());
    }
}

getConfigurator().apply(): Phoenix 6 uses a configuration object pattern — build a TalonFXConfiguration, set all your options, then call getConfigurator().apply(config) once in the constructor. no burnFlash() needed.

Superstructure Pattern

In Rebuilt, there's a Superstructure class that coordinates multiple subsystems. It's not a WPILib thing — it's a design pattern the team uses. Instead of commands reaching into multiple subsystems, the Superstructure has state-machine methods like intake(), score(), stow() that internally orchestrate the Intake, Indexer, and Shooter together.

java — Superstructure concept
public class Superstructure {
    private final IntakeSubsystem  m_intake;
    private final IndexerSubsystem m_indexer;
    private final ShooterSubsystem m_shooter;

    // High-level action: coordinates all three subsystems
    public void runIntake() {
        m_intake.intake();
        m_indexer.index();
    }

    public void stopAll() {
        m_intake.stop();
        m_indexer.stop();
        m_shooter.stop();
    }
}

Topic 1 — Quick Check


Command Lifecycle

Commands are units of robot behavior. Every command goes through the same 4-method lifecycle — understanding this cold will save you hours of debugging.

initialize()
One-time setup
Runs once when the command is scheduled. Good for resetting timers, setting initial state, or seeding a target value.
execute()
The loop
Runs every 20ms while the command is active. This is where you control hardware — set motor speeds, update PID, etc.
isFinished()
Done condition
Returns true to tell the Scheduler to end the command. Return false to keep running forever (teleop drive). Return true once a sensor condition is met.
end(boolean)
Cleanup
Runs once when done. The boolean arg is true if interrupted. Always stop motors here so they don't run away when interrupted.
java — Command pattern (WRT naming)
public class IntakeUntilSensorCmd extends Command {
    private final IntakeSubsystem  m_intake;
    private final IndexerSubsystem m_indexer;

    public IntakeUntilSensorCmd(IntakeSubsystem intake, IndexerSubsystem indexer) {
        m_intake  = intake;
        m_indexer = indexer;
        addRequirements(intake, indexer); // claim both subsystems
    }

    @Override
    public void initialize() {
        // Start spinning as soon as command begins
        m_intake.intake();
        m_indexer.index();
    }

    @Override
    public void execute() {
        // Nothing to do here — motors already running from initialize()
        // If we needed to adjust speed based on current, it would go here
    }

    @Override
    public boolean isFinished() {
        // Stop when current spike detects a game piece
        return m_indexer.hasPiece();
    }

    @Override
    public void end(boolean interrupted) {
        // Always stop motors — this runs even if interrupted
        m_intake.stop();
        m_indexer.stop();
    }
}

Naming convention: In Rebuilt, command methods use a Cmd() suffix on the subsystem — e.g. intake.intakeCmd() returns a Command that runs intake. This keeps command logic close to the subsystem that owns the hardware instead of in a separate file for every small action.

java — Command factory on the subsystem (Cmd() pattern)
// Inside IntakeSubsystem.java
public Command intakeCmd() {
    return startEnd(this::intake, this::stop)
        .withName("IntakeCmd");
}

public Command ejectCmd() {
    return startEnd(this::eject, this::stop)
        .withName("EjectCmd");
}

Topic 2 — Quick Check


Triggers & Bindings

Triggers connect driver inputs (and sensor states) to commands. In 2974's codebase, you'll find trigger variable names with the trg_ prefix. All bindings live in RobotContainer.

CommandXboxController

Use CommandXboxController (not the old XboxController). It exposes each button directly as a Trigger object you can chain commands onto.

java — RobotContainer.java
import edu.wpi.first.wpilibj2.command.button.CommandXboxController;

public class RobotContainer {

    // ── Subsystems ─────────────────────────────────────────────
    private final IntakeSubsystem  m_intake  = new IntakeSubsystem();
    private final ShooterSubsystem m_shooter = new ShooterSubsystem();

    // ── Controllers ───────────────────────────────────────────
    private final CommandXboxController m_driver   = new CommandXboxController(0);
    private final CommandXboxController m_operator = new CommandXboxController(1);

    public RobotContainer() {
        configureBindings();
    }

    private void configureBindings() {
        // trg_ prefix for trigger variables (WRT convention)
        var trg_intakeHeld  = m_operator.rightTrigger(0.1);
        var trg_ejectHeld   = m_operator.leftTrigger(0.1);
        var trg_shootButton = m_operator.rightBumper();

        // whileTrue: runs while button held, ends when released
        trg_intakeHeld.whileTrue(m_intake.intakeCmd());
        trg_ejectHeld.whileTrue(m_intake.ejectCmd());

        // onTrue: runs once when button is pressed, command runs to completion
        trg_shootButton.onTrue(m_shooter.shootCmd());
    }
}

Trigger Methods

MethodWhen command runsCommon use
onTrue(cmd)Once, when button pressedOne-shot actions (fire, reset)
onFalse(cmd)Once, when button releasedCleanup on release
whileTrue(cmd)While button held (restarts if it ends)Intake, eject, any held action
toggleOnTrue(cmd)Toggles on/off with each pressShooter spin-up

Sensor Triggers

Triggers don't have to come from buttons. You can make a trigger from any boolean condition — sensor readings, game state, etc.

java — trigger from sensor
import edu.wpi.first.wpilibj2.command.button.Trigger;

// Trigger fires when indexer current exceeds 30A (piece detected)
var trg_hasPiece = new Trigger(() -> m_indexer.getCurrent_A() > 30.0);

// Automatically rumble controller when piece is picked up
trg_hasPiece.onTrue(
    Commands.runOnce(() -> m_driver.getHID().setRumble(RumbleType.kBothRumble, 0.5))
    .andThen(Commands.waitSeconds(0.3))
    .andThen(Commands.runOnce(() -> m_driver.getHID().setRumble(RumbleType.kBothRumble, 0)))
);

Topic 3 — Quick Check


Command Compositions

You can chain and combine commands without writing new Command classes. These compositional methods are static helpers in Commands.* — import them with a static import.

java — command compositions
import static edu.wpi.first.wpilibj2.command.Commands.*;

// sequence: A → B → C, one at a time
Command auto = sequence(
    m_drive.driveDistanceCmd(1.5),    // drive 1.5m
    m_shooter.spinUpCmd(),             // then spin up shooter
    waitUntil(m_shooter::atSpeed),     // wait for speed
    m_indexer.feedCmd()                // then fire
);

// parallel: A and B run simultaneously, wait for BOTH
Command spinAndDrive = parallel(
    m_shooter.spinUpCmd(),
    m_drive.driveDistanceCmd(2.0)
);

// race: run A and B, end when EITHER finishes
Command intakeOrTimeout = race(
    m_intake.intakeUntilPieceCmd(),
    waitSeconds(3.0)  // safety timeout
);

// runOnce: one-time lambda, no subsystem requirement
Command resetOdometry = runOnce(() -> m_drive.resetPose(startPose));

Always add timeouts to autos. race(actualCommand, waitSeconds(5.0)) prevents a broken sensor from freezing your entire autonomous. In competition, autos that hang are worse than autos that do nothing.

Default Commands

A default command is what runs on a subsystem when no other command is using it. The drivetrain's default command is teleop drive — when no auto command is claiming the drive, the driver has control.

java — registering defaults in constructor
// In RobotContainer constructor (or subsystem constructor)
m_drive.setDefaultCommand(
    m_drive.teleopDriveCmd(
        () -> -m_driver.getLeftY(),
        () -> -m_driver.getRightX()
    )
);

Topic 4 — Coding Prompt

Write RobotContainer Bindings
Triggers, commands, Cmd() pattern

Write a configureBindings() method for a robot with an IntakeSubsystem and ShooterSubsystem. The operator's right trigger (threshold 0.1) should run intake while held. The right bumper should trigger a shoot sequence: spin up the shooter, wait until at speed, then feed for 0.5 seconds. Use WRT naming conventions (trg_ prefix, Cmd() methods).

Topic 4 — Quick Check


Weekly Test

covers command-based architecture. your score goes to the leads :) try it from memory first!!

📋
O3 weekly test
subsystems, commands, triggers, compositions · 8 questions