Skip to main content

Subsystems

Our robots are composed of subsystems: intakes, shooters, turrets, arms, etc. We organize the code we use to control each subsystem into a class that implements Subsystem. Each subsystem is built upon and integrates some existing patterns. I recommend reading through these pages, first.

Here's a bare-bones subsystem
public class DriveBase implements Loopable {

private static final Logger log = LoggerFactory.getLogger(DriveBase.class);

private static final DriveBase mInstance = new DriveBase();
public static DriveBase getInstance() {
return mInstance;
}

public enum DriveWantedAction {
_NOTHING,
OPEN_LOOP,
}

public enum DriveState {
UN_INIT,
OPEN_LOOP,
}

private final ReentrantLock mLock = new ReentrantLock();

private TalonFX leftMotor1;
private TalonFX leftMotor2;
private TalonFX rightMotor1;
private TalonFX rightMotor2;

private DriveState mDriveState = DriveState.UN_INIT;
private DriveWantedAction mDriveWantedAction = DriveWantedAction._NOTHING;
private double mCurrentStateStartedTimestamp;
private double mLeftDriveOpenLoopDemand;
private double mRightDriveOpenLoopDemand;

private DriveBase() {

}

private DriveState init() {
leftMotor1 = new TalonFX(8);
leftMotor2 = new TalonFX(9);
rightMotor1 = new TalonFX(17);
rightMotor2 = new TalonFX(19);

rightMotor2.follow(rightMotor1);
leftMotor2.follow(leftMotor1);
leftMotor1.setInverted(TalonFXInvertType.CounterClockwise);
leftMotor2.setInverted(TalonFXInvertType.CounterClockwise);
rightMotor1.setInverted(TalonFXInvertType.Clockwise);
rightMotor2.setInverted(TalonFXInvertType.Clockwise);

commandOpenLoop(0, 0);
return DriveState.OPEN_LOOP;
}


@Override
public void loop(double timestamp) {
mLock.lock();
try {
DriveState newState = handleStateTransitions(mDriveState);

switch (newState) {
case UN_INIT:
newState = init();
break;
case OPEN_LOOP:
updateDriveOpenLoop();
break;
}

if (newState != mDriveState) {
mCurrentStateStartedTimestamp = timestamp;
mDriveState = newState;
}
} finally {
mLock.unlock();
}
}

public void setOpenLoop(double leftDemand, double rightDemand) {
mLock.lock();
try {
mDriveWantedAction = DriveWantedAction.OPEN_LOOP;
mLeftDriveOpenLoopDemand = leftDemand;
mRightDriveOpenLoopDemand = rightDemand;
} finally {
mLock.unlock();
}
}

private DriveState handleStateTransitions(DriveState currentState) {
switch (currentState) {
case UN_INIT:
// I'm in init - no one can change that
return DriveState.UN_INIT;
case OPEN_LOOP:
// Fallthrough intended
if (mDriveWantedAction == DriveWantedAction.OPEN_LOOP) {
return DriveState.OPEN_LOOP;
}
}
// Should never happen, but when in doubt return the current state
return currentState;
}

private void updateDriveOpenLoop() {
commandOpenLoop(mLeftDriveOpenLoopDemand, mRightDriveOpenLoopDemand);
}

private void commandOpenLoop(double leftDemand, double rightDemand) {
leftMotor1.set(TalonFXControlMode.PercentOutput, leftDemand);
rightMotor1.set(TalonFXControlMode.PercentOutput, rightDemand);
}

}
Here's a somewhat-pruned-down subsystem from 2023 (messier, but more feature-complete):
public class Wrist implements AcesSubsystem<Wrist.Telemetry>, BufferedTelemetryProvider<Wrist.Telemetry>, UnbufferedTelemetryProvider<Wrist.Telemetry> {

// Set up logger (see logging docs)
private static final Logger log = LoggerFactory.getLogger(Wrist.class);
// Define slot IDs for particular control loop gains
private static final int kOpenLoopSlotId = 0;
private static final int kVelocitySlotId = 0;
private static final int kPositionSlotId = 1;


// This class is a Singleton (see Singleton docs)
private static final Wrist mInstance = new Wrist();
public static Wrist getInstance() {
return mInstance;
}

private Wrist() {

}

public enum WristWantedAction {
_NOTHING,
OPEN_LOOP,
VELOCITY,
RAW_DEGREE,
}

public enum WristState {
UN_INIT,
OPEN_LOOP,
VELOCITY,
RAW_DEGREE,
}

public enum GainSet {
CLOSE,
FAR
}

private GainSet mCurrentGainSet;


private final Telemetry mTelemetry = new Telemetry();
private final DataStoreCircularBuffer<GroupDataStore> mTelemetryBuffer = new DataStoreCircularBuffer<>(mTelemetry, Telemetry::new, Constants.loopRates.kWristHz, Constants.kTelemetryDefaultSecondsToBuffer);
private final ReentrantLock mLock = new ReentrantLock();

private AcesSparkMax mWristLeader;

private WristWantedAction mWristWantedAction = WristWantedAction._NOTHING;
private WristState mWristState = WristState.UN_INIT;

private double mCurrentStateStartedTimestamp;
private double mPreviousRawDegreeSetpoint;
private double mWristOpenLoopSetpoint = 0;
private double mWristVelocitySetpoint = 0;
private double mWristRawDegreeSetpoint = 0;

private WristState init() {
mWristLeader = new AcesSparkMax(Constants.wrist.motorConfig.kWristLeaderId, "Wrist wristLeader");
mWristLeader.setEngineeringUnitOffset(0);

CANSparkMax spark = (CANSparkMax) mWristLeader.getNativeHandle();
spark.restoreFactoryDefaults(true);
configureSettings();

mTelemetry.wristLeaderStatus = mWristLeader.getMotorDataStore();
mTelemetry.wristLeaderMotionProfile = mWristLeader.getMotionProfileDataStore();
mTelemetry.fillContainedDataStoresMap();

commandOpenLoop(0);
return WristState.OPEN_LOOP;
}

public void configureSettings() {
mLock.lock();
try {
configureWristAcesSparkMax(mWristLeader);
} finally {
mLock.unlock();
}
}

private void configureWristAcesSparkMax(AcesSparkMax sparkMax) {
// Put all of your configuration code here (omitted for brevity)
}

@Override
public void loop(double timestamp) {
// TODO Auto-generated method stub
mLock.lock();
try {
WristState newState = handleStateTransitions(mWristState);

mTelemetry.currentState.put(mWristState);
mTelemetry.wristOpenLoopSetpoint.put(mWristOpenLoopSetpoint);

switch (newState) {
case UN_INIT:
newState = init();
break;
case OPEN_LOOP:
updateWristOpenLoop();
break;
case RAW_DEGREE:
updateWristRawDegree();
break;
case VELOCITY:
updateWristVelocity();
break;
}

if (newState != mWristState) {
mCurrentStateStartedTimestamp = timestamp;
mWristState = newState;
}

mWristLeader.loop(timestamp);

mWristLeader.outputTelemetry();

mTelemetry.timestamp.put(timestamp);
mTelemetryBuffer.put();
mTelemetryContainer.put();
} finally {
mLock.unlock();
}

}

// Provide methods for the rest of the code to interface with this subsystem
public double getPositionDegrees() {
return mWristLeader.getPosition();
}

public void setOpenLoop(double wristOpenLoopSetpoint) {
mLock.lock();
try {
mWristWantedAction = WristWantedAction.OPEN_LOOP;
mWristOpenLoopSetpoint = wristOpenLoopSetpoint;
} finally {
mLock.unlock();
}
}

public void setVelocitySetpoint(double wristVelocitySetpoint) {
mLock.lock();
try {
mWristWantedAction = WristWantedAction.VELOCITY;
mWristVelocitySetpoint = wristVelocitySetpoint;
} finally {
mLock.unlock();
}
}

public void setDegreeSetpoint(double wristDegreeSetpoint) {
mLock.lock();
try {
mWristWantedAction = WristWantedAction.RAW_DEGREE;

mWristRawDegreeSetpoint = wristDegreeSetpoint;
} finally {
mLock.unlock();
}
}

private WristState handleStateTransitions(WristState currentState) {
switch (currentState) {
case UN_INIT:
// I'm in init - no one can change that
return WristState.UN_INIT;
case OPEN_LOOP:
// Fallthrough intended
case RAW_DEGREE:
// Fallthrough intended
case VELOCITY:
if (mWristWantedAction == WristWantedAction.OPEN_LOOP) {
return WristState.OPEN_LOOP;
}
if (mWristWantedAction == WristWantedAction.VELOCITY) {
return WristState.VELOCITY;
}
if (mWristWantedAction == WristWantedAction.RAW_DEGREE) {
return WristState.RAW_DEGREE;
}
}
// Should never happen, but when in doubt return the current state
return currentState;
}

// Methods to handle each state
private void updateWristOpenLoop() {
commandOpenLoop(mWristOpenLoopSetpoint);
}

private void updateWristVelocity() {
commandVelocitySetpoint(mWristVelocitySetpoint);
}

private void updateWristRawDegree() {
commandRawDegreeSetpoint(mWristRawDegreeSetpoint);
}

// Methods that handle setting outputs to each motor
private void commandOpenLoop(double wristSetpoint) {
mWristLeader.setSignal(new MotorSignalWithSlotId(wristSetpoint, true, MotorSignal.Type.OPEN_LOOP, kOpenLoopSlotId));
}

private void commandVelocitySetpoint(double wristSetpoint) {
mWristLeader.setSignal(new MotorSignalWithSlotId(wristSetpoint, true, MotorSignal.Type.VELOCITY, kVelocitySlotId));
}

private void commandRawDegreeSetpoint(double setpoint) { //TODO: fix
mWristLeader.setSignal(new MotorSignalWithSlotId(
Util.limit(setpoint, Constants.wrist.kMinDegrees, Constants.wrist.kMaxDegrees),
true, MotorSignal.Type.POSITION, kPositionSlotId)); //basic
// System.out.println("commanding raw degree setpoint of " + setpoint);
mTelemetry.wristDegreeSetpoint.put(setpoint);
double rawDegreeError = Math.abs(getCurrentRawRotationDegrees() - setpoint);
mPreviousRawDegreeSetpoint = setpoint;
}

private double getCurrentRawRotationDegrees() {
return mWristLeader.getPosition();
}

public boolean isAtSetpoint() {
return Util.epsilonEquals(mWristRawDegreeSetpoint, getPositionDegrees(), Constants.wrist.kWristAtPositionEpsilon);
}

@Override
public void checkForFaults() {
}

@Override
public Telemetry getTelemetry() {
return mTelemetry;
}

@Override
public void outputTelemetry(double timestamp) {
mWristLeader.outputTelemetry();
}


@Override
public void registerTelemetryBuffersTo(BufferedTelemetryConsumer<GroupDataStore> consumer) {
consumer.registerTelemetryBuffer("wrist", mTelemetryBuffer);
}

public class Telemetry extends GroupDataStore {
public DataStore_Float timestamp = new DataStore_Float();


public DataStore_AcesMotor wristLeaderStatus = new DataStore_AcesMotor();
public DataStore_MotionProfile wristLeaderMotionProfile = new DataStore_MotionProfile();

public DataStore_Enum<WristState> currentState = new DataStore_Enum<>(WristState.UN_INIT);
public DataStore_Float wristOpenLoopSetpoint = new DataStore_Float();

public DataStore_Float wristDegreeSetpoint = new DataStore_Float();
}
}

Notes:

  • The reason "update" and "command" methods are separate is that:
    • Turret 2022: Uses the same commandRawDegreeSetpoint from four different states
    • Arm 2023: Consolidates "safety checks" to a common function