Skip to content

The IO Layer Pattern

The most important architectural rule in MARSLib is the **separation of hardware from logic**. We use the AdvantageKit IO abstraction pattern to ensure that our robot code is deterministic, testable, and capable of bit-perfect log replay.

Architectural Immunity

If you call motor.getVelocity() directly in your subsystem, your code depends on physical hardware. You can't run it in sim accurately, you can't test it easily, and your logs won't capture what the motor was "actually" doing versus what your code "saw".

1. The Interface

First, we define an interface that lists all inputs the subsystem needs from the hardware. We use @AutoLog from AdvantageKit to automatically generate the logging boilerplate.

public interface FlywheelIO {
@AutoLog
public static class FlywheelIOInputs {
public double positionRad = 0.0;
public double velocityRadPerSec = 0.0;
public double appliedVolts = 0.0;
public double currentAmps = 0.0;
}
public default void updateInputs(FlywheelIOInputs inputs) {}
public default void setVoltage(double volts) {}
}

2. Hardware Implementation

The "Real" implementation talks to actual motors. It has no logic—it only moves data between the hardware and the Inputs object.

public class FlywheelIOTalonFX implements FlywheelIO {
private final <a href="https://v6.docs.ctr-electronics.com/en/stable/docs/api-reference/api-usage/talonfx.html" target="_blank">TalonFX</a> motor;
public FlywheelIOTalonFX(int id) {
motor = new TalonFX(id);
}
@Override
public void updateInputs(FlywheelIOInputs inputs) {
inputs.positionRad = motor.getPosition().getValueAsDouble();
inputs.velocityRadPerSec = motor.getVelocity().getValueAsDouble();
}
@Override
public void setVoltage(double volts) {
motor.setControl(new VoltageOut(volts));
}
}

3. Physics Implementation

The "Sim" implementation uses math to calculate how a motor would behave. MARSLib uses WPILib Physics Sims (like DCMotorSim) and MARSPhysicsWorld for high-fidelity physics.

public class FlywheelIOSim implements FlywheelIO {
private final DCMotorSim sim = new DCMotorSim(DCMotor.getFalcon500(1), 1.0, 0.01);
@Override
public void updateInputs(FlywheelIOInputs inputs) {
sim.update(0.020);
inputs.positionRad = sim.getAngularPositionRad();
inputs.velocityRadPerSec = sim.getAngularVelocityRadPerSec();
}
}

4. Dependency Injection

In RobotContainer, we decide which version to use based on the robot mode. The Subsystem itself only ever sees the FlywheelIO interface!

FlywheelIO io;
if (isReal) {
io = new FlywheelIOTalonFX(1);
} else {
io = new FlywheelIOSim();
}
subsystem = new FlywheelSubsystem(io);

Student Pro-Tip

Always keep your IO implementations "dumb". All logic belongs in the Subsystem and Commands. If you add math to your hardware layer, you've broken the abstraction.




📖 Further Reading & External Resources