Creating Subsystems
A subsystem is the hardware layer of your robot in code. It owns the physical objects, exposes a clean API, and keeps everything else at arm's length. Writing a good subsystem is about discipline: knowing what it should contain, and knowing what it absolutely must not.
By the end of this lesson, you will:
- Create a subsystem class that correctly extends
SubsystemBase - Declare hardware objects as private fields and configure them in the constructor
- Write a clean public API — methods commands can call — and explain why those methods must not accept controller inputs
- Use
periodic()for sensor reads and dashboard telemetry - Apply the 2910 naming convention for subsystem fields and methods
- Identify code that does not belong in a subsystem and explain where it should go instead
What a Subsystem Actually Is
A subsystem class has two jobs and two jobs only:
- Encapsulate hardware. The motor controllers, encoders, sensors, and solenoids for one mechanism all live as private fields inside one class. Nothing outside the class can touch them directly. They can only be operated through the methods you expose.
- Expose a clean API. Public methods with clear names:
setSpeed(double speed),stop(),getDistanceInches(). These are the only levers that commands pull. The API is the contract between the subsystem and everything that uses it.
The subsystem does not make decisions. It does not read button state. It does not know whether it's TeleOp or autonomous. It is a hardware abstraction layer — like the steering wheel, pedals, and dashboard in a car. The driver (command) decides what to do. The car (subsystem) provides the interface to do it.
What Belongs in a Subsystem?
Click each item below to find out whether it belongs in a subsystem.
The Anatomy of a Subsystem
Here is a complete IntakeSubsystem built from our Unit 5 intake. Every section has a specific purpose. Click any highlighted token to learn more.
// ── Private hardware fields ──────────────────────
private final WPI_TalonFX m_motor = new WPI_TalonFX(Constants.kIntakeMotorPort);
private final DigitalInput m_beamBreak = new DigitalInput(Constants.kIntakeBeamBreakPort);
// ── Constructor: configure hardware ─────────────
public IntakeSubsystem() {
m_motor.setNeutralMode(NeutralModeValue.Brake);
m_motor.setInverted(true);
m_motor.configStatorCurrentLimit(new StatorCurrentLimitConfiguration(
true, 30, 40, 0.1));
}
// ── Public API — what commands call ─────────────
public void setSpeed(double speed) {
m_motor.set(speed);
}
public void stop() {
m_motor.set(0.0);
}
public boolean hasGamePiece() {
return !m_beamBreak.get(); // NC inversion
}
// ── periodic(): runs every loop, always ─────────
@Override
public void periodic() {
SmartDashboard.putBoolean("Intake/HasGamePiece", hasGamePiece());
SmartDashboard.putNumber("Intake/MotorOutput", m_motor.get());
SmartDashboard.putNumber("Intake/StatorCurrent",
m_motor.getStatorCurrent().getValueAsDouble());
}
}
Publishing getStatorCurrent() in periodic() — as shown above — is a 2910 standard. When a mechanism jams (intake wrapped with string, arm hitting a physical stop, rollers stalled), stator current spikes dramatically. If you have this on the dashboard and in your match log, you will know within seconds after the match exactly what mechanism was overloaded. Watching for unusual current spikes in a match replay is how you prevent a brownout from becoming a habit.
Designing the API: The Right Level of Abstraction
The API is the hardest design decision in a subsystem. Too low-level and the command has to know too much about the hardware. Too high-level and the subsystem makes decisions that belong to the command.
Too low: public WPI_TalonFX getMotor() — exposes the hardware object directly. Commands can now call any method on the motor controller, bypassing all your configuration and current limiting. Encapsulation is gone.
Too high: public void intakeUntilLoaded() — this method blocks until a sensor fires, which is logic that belongs in a command (using isFinished()). A subsystem method should not block or make decisions about when to stop.
Just right: public void setSpeed(double speed), public boolean hasGamePiece() — the subsystem provides immediate, stateless control and sensor read. The command decides when and why to call them.
Before testing a new subsystem with commands, verify these points:
- Hardware constants in Constants.java: Every port number, CAN ID, and DIO port in the subsystem should reference
Constants.kXxx, not a hardcoded number. If you change wiring, you change one file. - Configuration in the constructor, not periodic(): Motor inversion, brake mode, and current limits should be set in the constructor. If they're in
periodic(), they run 50× per second — wasting CPU and fighting any change made between calls. - periodic() is for reading, not commanding: The only motor/actuator calls in
periodic()should be reads (.get(),.getStatorCurrent()). Writing motor speeds inperiodic()removes the command's ability to control that hardware. - All sensor inversions documented: Every
!inversion on a sensor read should have a comment:// NC switch — inverted. This prevents a future programmer from "fixing" the inversion and breaking the robot.
Knowledge Check
1. A teammate writes this method in ShooterSubsystem: public void shootIfBButtonPressed(XboxController controller). What is wrong with this design?
2. You want to set the motor to brake mode and configure current limiting. Where in the subsystem does this code belong?
3. Your DriveSubsystem has a public method public WPI_TalonFX getLeftMotor() that returns the motor controller object. Why is this a design problem?
Build the IntakeSubsystem and DriveSubsystem
- Create
IntakeSubsystem.javain yoursubsystems/folder. Migrate the intake motor and beam break sensor from your Unit 5 robot into it as private fields. Configure the motor in the constructor (brake mode, inversion, current limit). ExposesetSpeed(double),stop(), andhasGamePiece()as public methods. Add SmartDashboard output inperiodic(). - Create
DriveSubsystem.java. Move both drive motors and theDifferentialDriveinto it. In the constructor, configure both motors (current limit, brake mode). ExposearcadeDrive(double speed, double turn)andstop(). Inperiodic(), publish encoder distance, encoder rate, and motor outputs. - Move all port numbers and CAN IDs into
Constants.javaif you haven't already. Ensure every hardcoded number in both subsystem files is replaced with aConstants.kXxxreference. - Instantiate both subsystems in
RobotContainer. Deploy and enable in Test mode. Confirm SmartDashboard values appear and update for both subsystems — this verifies construction andperiodic()run correctly before any commands are attached. - Stretch goal: Add a third subsystem,
GyroSubsystem, that wraps the ADIS16470 or NavX gyro. ExposegetAngleDegrees(),getRotation2d(), andreset(). Inperiodic(), publish the heading. Write a comment in the constructor explaining why you reset the gyro there vs. inautonomousInit().