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 {@AutoLogpublic 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);}
@Overridepublic void updateInputs(FlywheelIOInputs inputs) { inputs.positionRad = motor.getPosition().getValueAsDouble(); inputs.velocityRadPerSec = motor.getVelocity().getValueAsDouble();}
@Overridepublic 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);
@Overridepublic 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
- FRC Log Replay and Simulation - Mechanical Advantage's definitive 2024 Championship presentation on the why behind the IO-Layer abstraction.
- AdvantageKit Architecture - The core repository governing deterministic replay loops on the RIO.
- AdvantageScope Documentation - Visualizing 3D logs natively in real-time.