Unit 6 · Lesson 3

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.

Subsystem Boundary Checker — click each item
WPI_TalonFX motor field
Encoder configuration
XboxController declaration
getDistanceInches() method
if (controller.getAButton())
SmartDashboard.putNumber()
setSpeed(double speed)
Auto routine sequence logic
Constructor hardware config
Timer-based eject sequence
← click an item to see 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.

IntakeSubsystem.java
public class IntakeSubsystem extends SubsystemBase {

  // ── 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());
  }
}
← click a highlighted token
🔍 LRI Perspective: "Stator current in periodic() has saved robots"

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.

⚠️ Three levels — only the middle one is right

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.

🔌 System Check — Subsystem Verification Checklist

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 in periodic() 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?

  • A The method name is too long — subsystem methods must be under 20 characters.
  • B The subsystem now depends on an XboxController — it can never be called from autonomous, a simulation, or any input source other than that specific controller. The decision of when to shoot belongs in a command, not the subsystem.
  • C The method should be private, not public, since only the subsystem should know when to shoot.
  • D This is acceptable — subsystems can accept controller arguments if the subsystem directly controls the shooter.

2. You want to set the motor to brake mode and configure current limiting. Where in the subsystem does this code belong?

  • A In periodic(), so the configuration is refreshed every 20 ms in case it gets reset.
  • B In the constructor — configuration runs once at startup. Putting it in periodic() wastes CPU and repeatedly overwrites any intentional changes made at runtime.
  • C In robotInit() of Robot.java, after the subsystem is instantiated.
  • D In Constants.java, as static initializer blocks.

3. Your DriveSubsystem has a public method public WPI_TalonFX getLeftMotor() that returns the motor controller object. Why is this a design problem?

  • A Returning an object reference from a subsystem always throws a NullPointerException.
  • B Any code that calls this method can directly configure or command the motor, bypassing the subsystem's current limiting, inversion settings, and safety logic. Encapsulation is broken — the subsystem no longer fully owns its hardware.
  • C Motor controllers are final and cannot be returned by reference.
  • D This is fine as long as the calling command never actually changes motor configuration.
💪 Practice Prompt

Build the IntakeSubsystem and DriveSubsystem

  1. Create IntakeSubsystem.java in your subsystems/ 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). Expose setSpeed(double), stop(), and hasGamePiece() as public methods. Add SmartDashboard output in periodic().
  2. Create DriveSubsystem.java. Move both drive motors and the DifferentialDrive into it. In the constructor, configure both motors (current limit, brake mode). Expose arcadeDrive(double speed, double turn) and stop(). In periodic(), publish encoder distance, encoder rate, and motor outputs.
  3. Move all port numbers and CAN IDs into Constants.java if you haven't already. Ensure every hardcoded number in both subsystem files is replaced with a Constants.kXxx reference.
  4. Instantiate both subsystems in RobotContainer. Deploy and enable in Test mode. Confirm SmartDashboard values appear and update for both subsystems — this verifies construction and periodic() run correctly before any commands are attached.
  5. Stretch goal: Add a third subsystem, GyroSubsystem, that wraps the ADIS16470 or NavX gyro. Expose getAngleDegrees(), getRotation2d(), and reset(). In periodic(), publish the heading. Write a comment in the constructor explaining why you reset the gyro there vs. in autonomousInit().