AdvantageKit: Logging and Replay Framework

advanced40 min

An architecture, not just a tool

AdvantageKit is listed among external tools, but adopting it is an architectural decision rather than an add-on. Your robot class changes, your subsystems get structured around the IO layer, and logging stops being something you remember to do.

That is the trade: more structure up front, in exchange for being able to reproduce any match on a laptop afterwards. This lesson covers the framework; Hardware Abstraction with IO Layers covers the subsystem structure it expects, and Simulation Basics covers replay.

What is AdvantageKit?

AdvantageKit is a logging, telemetry, and replay framework developed by Team 6328 (Mechanical Advantage). It records every input to your robot code—every sensor reading, button press, and joystick value—during a match. After the match, you can replay these recorded inputs through your robot code in simulation to debug issues and verify fixes.

Unlike traditional logging that records selected outputs, AdvantageKit records all inputs. This enables deterministic log replay: your robot code runs exactly as it did during the match, using the same sensor values in the same order.

AdvantageKit is free, open-source, and works with any vendor hardware. Learn more: AdvantageKit Documentation | AdvantageKit GitHub

How It Works

AdvantageKit requires you to structure your code using the IO Layer pattern. Instead of reading sensors directly, your subsystems interact with an IO interface that can be swapped between real hardware, simulation, or a log file player.

During a match, the IO layer reads real hardware and AdvantageKit automatically logs all input values. After the match, you can run your code in replay mode, where the IO layer reads from the log file instead of hardware. Your robot code runs unchanged, receiving the exact same inputs it received during the match.

LoggedRobot

The robot class extends LoggedRobot instead of TimedRobot:

public class Robot extends LoggedRobot {
    private Command autonomousCommand;
    private RobotContainer robotContainer;

It is a drop-in replacement — same robotPeriodic, same autonomousInit, same 20 ms loop. The difference is that loop timing is handed to the logger, which is what makes replay possible: in replay mode the loop runs as fast as the log can be read rather than in real time.

Setup happens in the constructor, before anything else exists:

public Robot() {
    // Record metadata — stamped into every log
    Logger.recordMetadata("ProjectName", BuildConstants.MAVEN_NAME);
    Logger.recordMetadata("BuildDate", BuildConstants.BUILD_DATE);
    Logger.recordMetadata("GitSHA", BuildConstants.GIT_SHA);
    Logger.recordMetadata("GitBranch", BuildConstants.GIT_BRANCH);
    Logger.recordMetadata(
        "GitDirty",
        switch (BuildConstants.DIRTY) {
            case 0 -> "All changes committed";
            case 1 -> "Uncommitted changes";
            default -> "Unknown";
        });

    switch (Constants.currentMode) {
        case REAL:
            // Log to a USB stick ("/U/logs") and publish live
            Logger.addDataReceiver(new WPILOGWriter());
            Logger.addDataReceiver(new NT4Publisher());
            break;
        case SIM:
            Logger.addDataReceiver(new NT4Publisher());
            break;
        case REPLAY:
            setUseTiming(false); // Run as fast as possible
            String logPath = LogFileUtil.findReplayLog();
            Logger.setReplaySource(new WPILOGReader(logPath));
            Logger.addDataReceiver(new WPILOGWriter(LogFileUtil.addPathSuffix(logPath, "_sim")));
            break;
    }

    Logger.start();

    robotContainer = new RobotContainer();
}

BuildConstants is generated by the build, not written by hand. It stamps the git commit, branch, and whether the working tree was dirty into every log file.

Why the metadata matters

When a log shows behaviour nobody can explain, the first question is always “what code was actually on the robot?”

GitSHA answers it exactly, and GitDirty answers whether the code on the robot corresponded to a commit at all. Check both before spending an hour analysing a log.

Also: plug in the USB stick. On a real robot WPILOGWriter writes to /U/logs. No stick, no log, and the post-match analysis you were counting on does not exist.

What each data receiver does

ReceiverEffect
WPILOGWriterWrites a .wpilog file — on the robot, to the USB stick
NT4PublisherPublishes live to NetworkTables for dashboards and AdvantageScope
WPILOGReaderReplay only — feeds a recorded log back in as if it were hardware

AdvantageKit Subsystem Structure

public class Flywheel extends SubsystemBase {
    // 1. Define the inputs that will be logged
    // API: @AutoLog annotation - https://docs.advantagekit.org/javadoc/org/littletonrobotics/junction/AutoLog.html
    @AutoLog
    public static class FlywheelInputs {
        public double velocityRadPerSec = 0.0;
        public double appliedVolts = 0.0;
        public double currentAmps = 0.0;
    }

    // 2. Define the IO Interface
    public interface FlywheelIO {
        default void updateInputs(FlywheelInputs inputs) {}
        default void setVoltage(double volts) {}
    }

    private final FlywheelIO io;
    private final FlywheelInputsAutoLogged inputs = new FlywheelInputsAutoLogged();

    // Constructor injects the implementation (Real, Sim, or Replay)
    public Flywheel(FlywheelIO io) {
        this.io = io;
    }

    @Override
    public void periodic() {
        // 3. Update inputs from the IO layer
        io.updateInputs(inputs);
        
        // 4. Log the inputs automatically
        // API: Logger.processInputs() - https://docs.advantagekit.org/javadoc/org/littletonrobotics/junction/Logger.html#processInputs(java.lang.String,java.lang.Object)
        Logger.processInputs("Flywheel", inputs);

        // Your logic uses 'inputs.velocityRadPerSec' instead of 'motor.getVelocity()'
    }

    public void runVolts(double volts) {
        io.setVoltage(volts);
    }
}

Logging

AdvantageKit automatically logs all fields in classes annotated with @AutoLog. It uses a high-performance binary format (WPILOG/RLOG) that records data every loop cycle with minimal overhead. (Logging Documentation)

You can also log custom outputs using Logger.recordOutput():

Logger.recordOutput("MyState/Target", targetValue);

Logger.recordOutput("Odometry/Pose", pose);

Inputs vs Outputs

There are three logging mechanisms and they are easy to confuse:

MechanismRecorded asUsed for
@AutoLog on an inputs classInputAnything read from hardware — makes replay possible
@AutoLogOutput on a methodOutputValues your code computed or commanded
Logger.recordOutput(key, value)OutputOne-off values from inside a command

The distinction is the whole trick. Inputs are replayed back into the code; outputs are what the code produced. If a sensor reading is recorded as an output, replay will not feed it back in, and the replay is meaningless.

// input — read from hardware, replayed
io.updateInputs(inputs);
Logger.processInputs("Drive/Module0", inputs);

// output — computed by us, not replayed
Logger.recordOutput("Odometry/Trajectory", activePath.toArray(new Pose2d[0]));

Log Replay

Log replay is AdvantageKit’s core feature. After a match, download the log file and run your robot code in replay mode. The IO layer reads from the log file instead of hardware, feeding the recorded sensor values into your code in the same order they occurred during the match.

This lets you debug match issues without the robot. You can step through code, inspect variables, modify logic, and verify that fixes work against real match data. The replay is deterministic: if your code is deterministic, the replay will match the robot’s behavior exactly.

Learn more: Log Replay Documentation

Getting Started

Best practices:

  • Use the IO Layer pattern for all subsystems that interact with hardware.
  • Log all inputs using @AutoLog annotations.
  • Log important outputs and states using Logger.recordOutput().
  • Keep input classes simple: use primitives and basic data structures.
  • Use AdvantageScope to visualize and analyze log files.
  • Keep your code deterministic: avoid random numbers, timestamps, or other non-deterministic sources in logic.

@AutoLogOutput

Not every subsystem needs a full IO layer. For a simple mechanism, @AutoLogOutput logs the return value of a method with no other setup:

@AutoLogOutput(key = "Conveyor/velocity")
public double getConveyorVelocity() {
    return conveyor.getVelocity().getValueAsDouble();
}

Because the annotation logs a return value, write setters that return the value they set. That is what puts commanded values in the log alongside measured ones, with no extra logging code anywhere:

@AutoLogOutput(key = "Intake/targetPos")
public double setIntakePosition(double pos) {
    pivot.setControl(motionMagic.withPosition(pos));
    return pos;
}

Now Intake/targetPos and Intake/Position — commanded and actual — can be dragged onto the same graph. A void setter would leave half of that picture missing.

Remember these are outputs: visible in AdvantageScope, but not fed back in during replay. Anything read from hardware that your logic depends on belongs in an @AutoLog inputs class instead.

Resources