Skip to content

Adding subsystems

This is the page I expect new mechanism work to follow. It is written to be practical: make the mechanism easy to command, easy to simulate or replay, and easy to diagnose from logs.

Start with the behavior

Before creating files, describe the mechanism in robot terms:

  • What can it do?
  • What states does it report?
  • What commands should other code schedule?
  • What should happen if a sensor disconnects?
  • What should be logged every loop?

For example, an intake might expose:

public void runVolts(double volts)
public void stop()
public boolean hasGamePiece()
public Command intakeUntilDetected()

That is a better interface than exposing motor controllers or raw beam-break voltages.

Use an IO boundary when hardware matters

For mechanisms with motors, sensors, simulation needs, or replay value, use the AdvantageKit IO pattern:

Intake
IntakeIO
IntakeIOInputs
IntakeIOTalonFX
IntakeIOSim

The subsystem owns behavior. The IO implementation owns hardware calls. Replay mode can use an empty IO implementation.

Example subsystem shape

public class Intake extends SubsystemBase {
  private final IntakeIO io;
  private final IntakeIOInputsAutoLogged inputs = new IntakeIOInputsAutoLogged();

  public Intake(IntakeIO io) {
    this.io = io;
  }

  @Override
  public void periodic() {
    io.updateInputs(inputs);
    Logger.processInputs("Intake", inputs);
  }

  public void runVolts(double volts) {
    io.setVoltage(volts);
    Logger.recordOutput("Intake/SetpointVolts", volts);
  }

  public void stop() {
    runVolts(0.0);
  }

  public boolean hasGamePiece() {
    return inputs.hasGamePiece;
  }
}

The key idea is that the command can call intake.runVolts(6.0) without knowing which motor controller exists.

Example IO shape

public interface IntakeIO {
  @AutoLog
  public static class IntakeIOInputs {
    public boolean connected = true;
    public double appliedVolts = 0.0;
    public double currentAmps = 0.0;
    public boolean hasGamePiece = false;
  }

  public default void updateInputs(IntakeIOInputs inputs) {}

  public default void setVoltage(double volts) {}
}

Keep the input struct factual. It should describe what hardware measured, not what a command hoped would happen.

Construct it in RobotContainer

Choose the IO implementation based on robot mode:

private Intake createIntake() {
  return switch (Constants.currentMode) {
    case REAL -> new Intake(new IntakeIOTalonFX());
    case SIM -> new Intake(new IntakeIOSim());
    case REPLAY -> new Intake(new IntakeIO() {});
  };
}

This keeps mode decisions in one place. It also prevents replay from accidentally opening hardware devices.

Commands

Mechanism commands should be small and composable:

public Command intakeCommand() {
  return Commands.startEnd(
      () -> runVolts(6.0),
      this::stop,
      this);
}

public Command intakeUntilDetected() {
  return intakeCommand().until(this::hasGamePiece).withTimeout(2.0);
}

Use timeouts on event commands. An autonomous path event that waits forever is a field failure, not a controls feature.

Driver bindings

Bind controls in RobotContainer. Keep controls explicit and easy to audit.

Good:

controller.rightBumper().whileTrue(intake.intakeCommand());

Avoid hidden behavior where a subsystem changes drive or navigation policy without a scheduled command. If the robot is doing something, it should be visible in the command structure and logs.

Autonomous event registration

Register mechanism events through the auto manager or event registry:

autoManager.registerEvent("intake", () -> intake.intakeUntilDetected().withTimeout(2.0));
autoManager.registerEvent("score", () -> scorer.scoreCommand().withTimeout(1.0));

Then use those event keys from BLine JSON. The string in the path file and the string in code must match.

Keep event commands defensive:

  • add timeouts
  • prefer commands that end cleanly
  • log important state
  • avoid blocking on perfect sensor behavior

Logging rules

Use AdvantageKit for subsystem telemetry:

  • Logger.processInputs("SubsystemName", inputs) for IO inputs.
  • Logger.recordOutput("SubsystemName/Setpoint", value) for requested state.
  • Alert for driver-relevant hardware faults.

Do not publish normal telemetry directly to NetworkTables. AdvantageKit will publish logged outputs to NT4 when the NT4 publisher is configured.

Simulation

A simple simulation is better than none if it catches command mistakes.

Sim does not need to model every physical detail. It should at least let you test:

  • commands schedule
  • commands finish
  • sensors change plausibly
  • logs update
  • autonomous events do not crash

For mechanisms with closed-loop control, make the simulation good enough to tune command sequencing, not necessarily final gains.

Review checklist

Before merging a new subsystem:

  • Commands do not touch vendor device objects.
  • Real, sim, and replay construction paths exist.
  • IO inputs are logged.
  • Setpoints or requested states are logged.
  • Driver bindings are obvious.
  • Autonomous events have timeouts.
  • Unit tests cover pure logic where practical.
  • The subsystem can be disabled safely.