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
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.
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.
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.
// 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.
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
| Method | When command runs | Common use |
|---|---|---|
onTrue(cmd) | Once, when button pressed | One-shot actions (fire, reset) |
onFalse(cmd) | Once, when button released | Cleanup on release |
whileTrue(cmd) | While button held (restarts if it ends) | Intake, eject, any held action |
toggleOnTrue(cmd) | Toggles on/off with each press | Shooter 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.
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.
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.
// In RobotContainer constructor (or subsystem constructor) m_drive.setDefaultCommand( m_drive.teleopDriveCmd( () -> -m_driver.getLeftY(), () -> -m_driver.getRightX() ) );
Topic 4 — Coding Prompt
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!!