The subsystem execution concept discussed on this page are part of our custom MWLib library developed specifically for MARS/WARS teams. They are not part of the official WPILib distribution but are designed to work seamlessly with it.
It is recommended to read through the Mechanism Library documentation before diving into Subsystems, as our subsystem implementation heavily utilizes mechanisms. You can find the Mechanism Library documentation here
It is recommended to read through the State Machine documentation before diving into Subsystems, as our subsystem implementation heavily utilizes state machines. You can find the State Machine documentation here.
Subsystems
Subsystems are a fundamental concept in robot programming, representing distinct functional units of the robot. Each subsystem encapsulates specific hardware components and their associated behaviors, allowing for organized and modular code.
Architecture
Subsystems follow a read, update, write architecture, where sensor data is read, processed, and then commands are sent to actuators. This architecture ensures that all subsystems operate in a synchronized manner, reducing the chances of conflicts and improving overall robot performance.
The readInputs and writeOutputs methods hidden and are called automatically by the subsystem framework. The updateLogic method is where the main logic of the subsystem is implemented. The subsystem framework ensures that these methods are called in the correct order during each robot cycle. However for our subsystems to we need to implement a couple of methods defined in the template MWSubsystem class.
Update Logic:
The updateLogic() method is where the main behavior of the subsystem is defined. This method is called periodically during each robot cycle, and it is responsible for processing sensor data, making decisions based on that data, and preparing commands to be sent to actuators. This method should implement the state machine that defines the subsystem's behavior.
Reset:
The reset() method is called to reset the subsystem to a known state. This is typically done at the start of a match or when the robot is disabled. In this method, you should reset any internal state variables, stop motors, and ensure that the subsystem is in a safe state.
Get IOs:
The getIOs() method is used to retrieve the input/output (IO) objects associated with the subsystem. These IO objects are typically instances of mechanism classes that represent the hardware components of the subsystem. This method allows the subsystem to access and control its hardware components.
Creating a Subsystem
Creating a new subsystem can be a little daunting at first, but by following a structured approach, you can simplify the process. Our process is unique to MARS/WARS teams so be sure to follow these steps closely. ChatGPT or Goggle searches may not yield the correct results for our custom implementation. The following steps outline how to create a new subsystem from scratch:
Create the Subsystem Package
Start by creating a new package within the frc.robot.subsystems directory. Name the package according to the subsystem you are creating, such as drivetrain, shooter, or intake. Remember a package is just a folder in this context.
Create the Subsystem Constants Class
Within the newly created package, create a constants class to hold all the configuration values for the subsystem. This class should define constants for motor IDs, sensor ports, PID values, and any other configuration parameters needed for the subsystem. For example, if you are creating a shooter subsystem, you might create a class named ShooterConstants. Remember that any numbers that you may use in your code that are not arbitrary should be defined as constants in this class. The only exception to this rule is using 0 for stopping motors or etc.
It is important to define a new enum at the top of the class called ShooterStates in this example. This enum will define all the states that the shooter subsystem state machine can be in.
Below is an example of what the constants class might look like:
public class ShooterConstants extends MwConstants {
// =============================================================================
// ENUMS AND STATE DEFINITIONS
// =============================================================================
public enum ShooterStates {
IDLE,
SPIN_UP,
SHOOTING
}
// =============================================================================
// CAN IDS AND HARDWARE CONFIGURATION
// =============================================================================
// Motor CAN IDs
public final int SHOOTER_ID = 10;
public final int INDEXER_ID = 11;
// =============================================================================
// MECHANICAL CONSTANTS - FLYWHEEL
// =============================================================================
public final boolean SHOOTER_INVERTED = false;
public final double FLYWHEEL_GEAR_RATIO = 1.0;
public final double FLYWHEEL_WHEEL_RADIUS_METERS = Units.inchesToMeters(3);
public final double FLYWHEEL_WHEEL_MASS_KG = 2.3; // kg, approximate
public final double FLYWHEEL_INERTIA = 0.5 * FLYWHEEL_WHEEL_MASS_KG
* Math.pow(FLYWHEEL_WHEEL_RADIUS_METERS, 2.0); // kg m^2
// =============================================================================
// MECHANICAL CONSTANTS - INDEXER
// =============================================================================
public final boolean INDEXER_INVERTED = false;
public final double INDEXER_GEAR_RATIO = 1.0;
// =============================================================================
// CONTROL SETPOINTS
// =============================================================================
public final double SHOOT_VELOCITY = Units.rotationsPerMinuteToRadiansPerSecond(5000);
public final double INDEXER_DUTY_CYCLE = 0.3; // 30% power for indexing
// =============================================================================
// MOTOR CONFIGURATION OBJECTS
// =============================================================================
public final MotorConfig SHOOTER_MOTOR_CONFIG = new MotorConfig();
public final MotorConfig INDEXER_MOTOR_CONFIG = new MotorConfig();
// =============================================================================
// CONSTRUCTOR - MOTOR CONFIGURATION INITIALIZATION
// =============================================================================
public ShooterConstants() {
// Configure Shooter Motor
SHOOTER_MOTOR_CONFIG.can_id = SHOOTER_ID;
SHOOTER_MOTOR_CONFIG.motor_type = FxMotorType.X60;
SHOOTER_MOTOR_CONFIG.canbus_name = "rio";
SHOOTER_MOTOR_CONFIG.config = new TalonFXConfiguration();
SHOOTER_MOTOR_CONFIG.config.MotorOutput.Inverted =
PhoenixUtil.toInvertedValue(SHOOTER_INVERTED);
// Configure Indexer Motor
INDEXER_MOTOR_CONFIG.can_id = INDEXER_ID;
INDEXER_MOTOR_CONFIG.motor_type = FxMotorType.X60;
INDEXER_MOTOR_CONFIG.canbus_name = "rio";
INDEXER_MOTOR_CONFIG.config = new TalonFXConfiguration();
INDEXER_MOTOR_CONFIG.config.MotorOutput.Inverted =
PhoenixUtil.toInvertedValue(INDEXER_INVERTED);
}
}
Ensure that your constants class extends MwConstants to inherit necessary functionality. The subsystem framework relies on this inheritance for proper operation.
Use section comments (like the ones shown above) to organize your constants into logical groups. This makes the file much easier to navigate and maintain as your subsystem grows in complexity.
Motor configurations should be created as public final fields and initialized in the constructor. This pattern allows you to configure all motor parameters (CAN ID, inversion, current limits, PID gains, etc.) in one place before passing them to the mechanism constructors.
Create the Subsystem Class
Next, create the subsystem class itself within the same package. Below is an example implementation of a ShooterSubsystem class that uses the ShooterConstants defined above.
public class ShooterSubsystem extends MwSubsystem<ShooterStates, ShooterConstants> {
private static ShooterSubsystem instance_ = null;
// Mechanisms
private FlywheelMech flywheel_;
private RollerMech indexer_;
public static ShooterSubsystem getInstance() {
if (instance_ == null) {
instance_ = new ShooterSubsystem();
}
return instance_;
}
public ShooterSubsystem() {
super(ShooterStates.IDLE, new ShooterConstants());
// Initialize mechanisms
flywheel_ = new FlywheelMech(
getSubsystemKey(),
"Flywheel",
List.of(CONSTANTS.SHOOTER_MOTOR_CONFIG),
CONSTANTS.FLYWHEEL_GEAR_RATIO,
CONSTANTS.FLYWHEEL_INERTIA,
CONSTANTS.FLYWHEEL_WHEEL_RADIUS_METERS
);
indexer_ = new RollerMech(
getSubsystemKey(),
"Indexer",
List.of(CONSTANTS.INDEXER_MOTOR_CONFIG),
CONSTANTS.INDEXER_GEAR_RATIO
);
}
@Override
public void handleStateTransition(ShooterStates wanted) {
// Add custom state transition logic if needed
// Otherwise, default transition occurs
system_state_ = wanted;
}
@Override
public void updateLogic(double timestamp) {
switch (system_state_) {
case IDLE:
flywheel_.setTargetDutyCycle(0.0);
indexer_.setTargetDutyCycle(0.0);
break;
case SPIN_UP:
flywheel_.setTargetVelocity(CONSTANTS.SHOOT_VELOCITY);
indexer_.setTargetDutyCycle(0.0);
break;
case SHOOTING:
flywheel_.setTargetVelocity(CONSTANTS.SHOOT_VELOCITY);
indexer_.setTargetDutyCycle(CONSTANTS.INDEXER_DUTY_CYCLE);
break;
}
// Log Additional Data Here!
}
@Override
public List<SubsystemIoBase> getIos() {
return Arrays.asList(flywheel_, indexer_);
}
@Override
public void reset() {
system_state_ = ShooterStates.IDLE;
}
}
The @Override annotations indicate that these methods are overriding methods defined in the superclass MwSubsystem. This is important for ensuring that the subsystem behaves correctly within the framework. If you forget to include these methods, the subsystem will likely fail to compile.
Notice how mechanisms are initialized in the constructor with getSubsystemKey() as the logging prefix and a descriptive name. This ensures all mechanism logs are properly organized under the subsystem.
This class extends MwSubsystem, specifying the state enum and constants class as type parameters. Make sure to state and constants type define in the class declaration:
public class ShooterSubsystem extends MwSubsystem<ShooterStates, ShooterConstants>
We use a singleton pattern to ensure that only one instance of the subsystem exists. This is done using a private static variable and a public static getInstance() method. This means that anywhere in the code we need to access the ShooterSubsystem, we call ShooterSubsystem.getInstance() to get the single instance of the subsystem.
private static ShooterSubsystem instance_ = null;
public static ShooterSubsystem getInstance() {
if (instance_ == null) {
instance_ = new ShooterSubsystem();
}
return instance_;
}
Anytime we need to access the subsystem we call ShooterSubsystem.getInstance(). This prevents us from accidentally creating multiple instances of the subsystem which would lead to unexpected behavior.
Make sure to call the superclass constructor in the subsystem constructor, passing in the initial state and a new instance of the constants class. This will make sure the subsystem is properly initialized using our custom framework. You can then add any additional initialization code needed for your subsystem after the superclass constructor call.
public ShooterSubsystem() {
super(ShooterStates.IDLE, new ShooterConstants());
}
Make sure to implement the updateLogic() method to define the behavior of the subsystem based on its current state. You can use a switch statement to handle different states and implement the corresponding logic for each state. See Adding the State Machine below for more details.
The reset method should set the subsystem back to its initial state. This is important for ensuring that the subsystem starts in a known state when the robot is enabled.
@Override
public void reset() {
system_state_ = ShooterStates.IDLE;
}
Register the Subsystem
Finally, ensure that the new subsystem is registered within the robot's main subsystem manager. This typically involves adding an instance of the subsystem to a list or map of subsystems that the robot manages. This step is crucial for the subsystem to be recognized and updated during the robot's operation. This is done in the RobotContainer class, which is also the robot's super state machine (RobotContainer extends MwSuperStateMachine<RobotStates, RobotConstants>). Here is an example of how to register the ShooterSubsystem:
private RobotContainer() {
super(RobotStates.IDLE, new RobotConstants(), BuildConstants.class);
// !!!!!! ALL SUBSYSTEMS MUST BE REGISTERED HERE TO RUN !!!!!!!
registerSubsystem(ShooterSubsystem.getInstance());
// Add other subsystems here
// !!!!! LEAVE THESE LINES AS THE LAST LINE IN THE CONSTRUCTOR !!!!!!
reset();
}
Failing to register the subsystem will result in it not being updated or functioning correctly during robot operation.
Adding Mechanisms
Mechanisms are the building blocks of subsystems, representing individual hardware components such as motors, sensors, and actuators. To add mechanisms to your subsystem, follow these steps:
Declare Mechanism IOs
In your subsystem class, declare the mechanism IO objects that represent the hardware components of your subsystem. These objects should be instances of the appropriate mechanism classes from the Mechanism Library. Remember you can have multiple mechanisms in a single subsystem.
private RollerMech indexer_;
Initialize Mechanism IOs
In the subsystem constructor, initialize the mechanism IO objects using the configuration values defined in the constants class. This typically involves creating new instances of the mechanism classes and passing in the necessary parameters such as motor configurations, gear ratios, and physical properties.
public IntakeSubsystem() {
super(IntakeStates.STORE, new IntakeConstants());
// Initialize roller mechanism
roller_ = new RollerMech(
getSubsystemKey(), // Logging prefix
"Roller", // Mechanism name
List.of(CONSTANTS.ROLLER_MOTOR_CONFIG), // Motor configs
CONSTANTS.ROLLER_GEAR_RATIO // Gear ratio
);
// Initialize pivot arm mechanism
pivot_ = new ArmMech(
getSubsystemKey(), // Logging prefix
"Pivot", // Mechanism name
List.of(CONSTANTS.PIVOT_MOTOR_CONFIG), // Motor configs
CONSTANTS.PIVOT_GEAR_RATIO, // Gear ratio
CONSTANTS.PIVOT_LENGTH, // Arm length (m)
CONSTANTS.PIVOT_MASS, // Arm mass (kg)
CONSTANTS.PIVOT_MIN, // Min angle (rad)
CONSTANTS.PIVOT_MAX // Max angle (rad)
);
// Set initial position for mechanisms that need zeroing
pivot_.setCurrentPosition(CONSTANTS.PIVOT_HOME_POSITION);
}
The getSubsystemKey() method provides the logging prefix for the subsystem. This ensures that all mechanism logs are organized under the correct subsystem in the log viewer. Each mechanism should be initialized with this prefix to maintain proper log organization.
When you have multiple instances of the same mechanism type in a subsystem (e.g., left and right arms), provide unique names as the second parameter. This helps distinguish between them in logs. If you only have one instance, you can still provide a descriptive name or pass null to use the default.
Motor configurations must be provided as List.of(...) containing one or more motor config objects. The first motor in the list is treated as the leader, and subsequent motors automatically follow it.
Update the Subsystem IOs
Override the getIos() method in your subsystem class to return a list of the mechanism IO objects. This allows the subsystem framework to access and manage the hardware components of the subsystem.
@Override
public List<SubsystemIoBase> getIos() {
return Arrays.asList(roller_, pivot_);
}
If your subsystem contains multiple mechanisms, include all of them in the list returned by the getIOs() method. Simply add them as parameters to the Arrays.asList() call:
// Example with three mechanisms
@Override
public List<SubsystemIoBase> getIos() {
return Arrays.asList(indexer_, flywheel_, hood_);
}
The subsystem framework uses this method to automatically call readInputs(), writeOutputs(), and logData() on each mechanism during the robot loop. If this method returns an empty list or is not implemented correctly, none of your mechanisms will be updated. Ensure that you return all mechanism IO objects that correspond to the hardware components of your subsystem.
Using the Mechanisms
With the mechanisms added to your subsystem, you can now use them within the updateLogic() method to implement the desired behavior. This may involve reading sensor data, controlling motor outputs, and implementing logic based on the subsystem's needs. See Mechanisms for more details on the interworkings of mechanisms.
@Override
public void updateLogic(double timestamp) {
// Example: Control mechanisms based on button inputs or sensor readings
// Set pivot to a specific angle
pivot_.setTargetPosition(Units.degreesToRadians(45));
// Run roller at a specific speed
roller_.setTargetDutyCycle(0.5); // 50% power
// Or use velocity control
roller_.setTargetVelocity(Units.rotationsPerMinuteToRadiansPerSecond(500));
// Stop mechanisms
roller_.setTargetDutyCycle(0.0);
pivot_.setTargetDutyCycle(0.0);
}
In practice, you'll typically use a state machine to organize your subsystem's behavior. The example above shows basic mechanism control, but for complete subsystem implementation with state machines, see Adding the State Machine below.
You can do more than just send target requests to mechanisms. Sometimes we need to see what our mechanisms are doing to make changes to our requests. All mechanisms provide getter methods to query their current state:
// Query mechanism state
double current_angle = pivot_.getCurrentPosition();
double current_velocity = pivot_.getCurrentVelocity();
double motor_current = pivot_.getLeaderCurrent();
// Use state information for conditional logic
if (MathUtil.isNear(target_angle, current_angle, tolerance)) {
// Pivot has reached target position
// Can now proceed with next action
}
// Check if mechanism is moving too fast
if (Math.abs(current_velocity) > max_safe_velocity) {
// Slow down for safety
pivot_.setTargetDutyCycle(0.0);
}
// Monitor motor health
if (motor_current > current_threshold) {
// High current detected - possible jam or mechanical issue
MwLog.log(getSubsystemKey() + "/Warning/HighCurrent", motor_current);
}
This is particularly useful for:
- Waiting for completion: Check if a mechanism has reached a target before proceeding
- Conditional control: Adjust outputs based on current state
- Safety checks: Monitor currents, velocities, or positions to prevent damage
- Debugging: Log mechanism state to diagnose issues
See the Mechanisms documentation for the complete list of getter methods available for each mechanism type.
Adding the State Machine
The state machine is a crucial part of the subsystem, defining its behavior based on different states. Our subsystem framework provides a structured way to implement state machines and it is expected that each subsystem will have one. Follow these steps to add a state machine to your subsystem:
Define States
Ensure that you have defined an enum in your constants class that lists all the possible states for your subsystem. This enum will be used to represent the current state of the subsystem. This should be in the constants file associated with the subsystem. Here is an example of a state enum for a shooter subsystem:
public enum ShooterStates {
IDLE,
SPIN_UP,
SHOOTING
}
Implement State Logic
In the updateLogic() method of your subsystem class, implement the logic for each state using a switch statement. This is where you define the behavior of the subsystem based on its current state.
@Override
public void updateLogic(double timestamp) {
switch (system_state_) {
case IDLE:
// Logic for IDLE state
break;
case SPIN_UP:
// Logic for SPIN_UP state
break;
case SHOOTING:
// Logic for SHOOTING state
break;
}
}
Implement State Transitions
If you wish to handle state transitions explicitly, you can override the handleStateTransition() method. This method is called every loop and attempts to set the current state to the wanted state. If left unimplemented, the subsystem will automatically transition to the wanted state immediately.
@Override
protected void handleStateTransition(ShooterStates wanted) {
// Simple immediate transition (this is the default behavior)
system_state_ = wanted;
}
When you override it, write it as the transition table: an outer switch on system_state_ (where the
subsystem is now) and an inner switch on wanted (what was asked). Each arm says where to go next. A request with
no arm is ignored, which is how you forbid a move. Step states (a timed settle, a homing move) are states
nothing requests directly: an arm of the table enters them, and their own arm decides when they are done.
Here is the 2026 intake. Asking for SQUEEZE from DEPLOYED settles in SQUEEZE_WAIT first; while it settles,
every request is ignored:
@Override
protected void handleStateTransition(IntakeStates wanted) {
switch (system_state_) {
case DEPLOYED -> {
switch (wanted) {
// Settle before the squeeze engages (a timed step state)
case SQUEEZE -> {
system_state_ = IntakeStates.SQUEEZE_WAIT;
timer.restart();
}
case STORE, INTAKE, OUTTAKE, IDLE, TUNING -> system_state_ = wanted;
default -> {}
}
}
case SQUEEZE_WAIT -> {
// Ignores every request until the settle time is up
if (timer.get() > CONSTANTS.SQUEEZE_WAIT_TIME) {
system_state_ = IntakeStates.SQUEEZE;
}
}
case STORE, INTAKE, OUTTAKE, IDLE, SQUEEZE, TUNING -> {
switch (wanted) {
case STORE, DEPLOYED, INTAKE, OUTTAKE, IDLE, SQUEEZE, TUNING -> system_state_ = wanted;
default -> {}
}
}
}
}
The same shape handles a condition on the way in. A shooter that may only shoot once it is at speed keeps the
request in SPIN_UP until isAtShootingSpeed():
case SPIN_UP -> {
switch (wanted) {
case SHOOTING -> { if (isAtShootingSpeed()) system_state_ = ShooterStates.SHOOTING; }
case IDLE -> system_state_ = wanted;
default -> {}
}
}
The rules that make this work with updateLogic:
- Every condition lives here.
updateLogicis aswitchwith noifin it. If a state should behave differently depending on a sensor or a setting, split it into two states and pick between them here. - Decided values become fields. A target computed from a sensor (the flywheel speed for this distance) is
computed here into a field that
updateLogicreads. - No enter or exit actions. A state commands its mechanisms every loop, so there is nothing to undo when it
leaves. Calculations such as filters and feedforward math are fine in
updateLogic; decisions are not. - Group states that behave the same in one arm (
case STORE, INTAKE, ... ->) so the table stays readable. Keep the arms in the enum's order.
Omit it while every request can be honoured immediately. Add the table as soon as one move needs a guard, a timed settle or a step state. Most real subsystems end up with one.
The wanted state is sticky, meaning that until it is updated the subsystem will continue to attempt to transition to that state.
If you setup any sort of complex transition logic in handleStateTransition(), make sure to account for the fact that the subsystem will keep trying to transition to the wanted state every loop.
Updating the State
To change the state of the subsystem, use the setWantedState() method. This method sets the desired state for the subsystem, which will be processed in the next update cycle.
shooter_.setWantedState(ShooterStates.SHOOTING);
This will request the subsystem to transition to the SHOOTING state during the next update cycle, provided any conditions in handleStateTransition() are met. It helps ensure that state changes are managed in a controlled manner.
The code that asks a subsystem for a state is the robot's super state machine in RobotContainer. Buttons and autonomous routines don't call a subsystem's setWantedState() themselves: they ask RobotContainer for a robot state, and it asks each subsystem. RobotContainer runs before the subsystems every loop, so the subsystem sees the request in the same loop.
Logging
Logging is an important aspect of subsystem development, as it allows you to monitor the behavior of the subsystem and diagnose issues. Our subsystem framework provides built-in logging capabilities that can be easily integrated into your subsystem. To log data from your subsystem, you can use the MwLog class to log relevant information such as the current state, sensor readings, and actuator outputs. Here is an example of logging the current state of the subsystem in the updateLogic() method:
MwLog.log(getSubsystemKey() + "/TargetRotation", target_.getRotation());
This will log the current state of the subsystem under a key that includes the subsystem's name, making it easy to identify in the logs. Make sure to log any other relevant data that will help you understand the subsystem's behavior during operation. This could include variables or internal subsystem states that are important for debugging or performance analysis. See more about logging and telemetry on the Telemetry and Logging page.
Each mechanism also has its own logging capabilities. They will automatically log relevant data such as motor outputs, sensor readings, and internal state variables. Make sure to set the logging prefix for each mechanism to ensure that the log data is organized correctly. This is required in the mechanism constructor.