Unit 11 · Lesson 2

Introduction to AdvantageKit

SmartDashboard tells you what your robot is doing right now. AdvantageKit tells you what your robot did during every loop of every match you've ever run — and lets you re-run that match's exact logic offline, after the fact, as many times as you need. This lesson shows you what AdvantageKit is, how its Logger works, and why logging structured inputs before using them is what makes everything else in Unit 11 possible.

By the end of this lesson, you will:

  • Explain the difference between SmartDashboard-style logging and AdvantageKit's structured input logging
  • Add AdvantageKit to a robot project as a vendor dependency and configure Robot.java to start the Logger
  • Use Logger.recordOutput() to log any value — double, boolean, Pose2d, enum, or custom type
  • Understand why Logger.processInputs() must be called immediately after updateInputs() and what it does in each mode
  • Describe AdvantageKit's three output backends: real robot (WPILOG file), simulation (console/NT), and replay (log overwrite)
  • Locate and download a .wpilog file from the roboRIO after a match

What AdvantageKit Is

AdvantageKit is an open-source logging framework for FRC robots, created and maintained by Team 6328 (Mechanical Advantage). It runs on top of WPILib and provides two capabilities that WPILib's built-in logging does not: structured input capture and deterministic replay.

Structured input capture means AdvantageKit records every sensor input that enters the robot's subsystems — velocity readings, encoder positions, current draws, motor temperatures — at every loop iteration, in a format that ties each value to the specific subsystem and IO layer that produced it. Not just "shooter velocity was 3450 RPM at some point" but "in loop 1,247 of the match, Shooter/velocityRpm was 3450.2 and the subsystem considered itself at-speed, which is why the eject command started."

Deterministic replay means you can take that log file and re-run your robot code's logic against it, offline, on a laptop, without any hardware — and get the exact same subsystem outputs that the robot produced during the match. If you add a System.out.println or a new SmartDashboard value after the fact and replay, you can see what that value would have been during the match. This is the most powerful debugging capability available to an FRC team.

🔍 The 11pm debugging session this makes possible

After a frustrating match where the robot missed its last shot, the team downloads the .wpilog file from the roboRIO, opens it in AdvantageScope, and replays the autonomous period. They add a new Logger.recordOutput("Shooter/pidError", error) line to the shooter subsystem and replay again — the log now shows them the PID error at every loop during the sequence. The robot's shooter was still spinning down when the shot was triggered. It took 20 minutes to find, with no robot present, at 11pm. Without AdvantageKit, this would have required reproducing the exact scenario on the physical robot the next morning — if it was even reproducible.

AdvantageKit vs. SmartDashboard

Capability SmartDashboard / Shuffleboard AdvantageKit Logger
Live monitoring Yes — values visible in real time during teleop/auto Yes — also visible live via NetworkTables when configured
Post-match review Limited — only what Driver Station logs capture Full — every logged value at every loop since match start
Historical replay No — values are not stored in a format that supports replay Yes — entire subsystem logic re-runs on historical inputs
Input logging Manual — programmer decides what to log each time Automatic for IO layer inputs via processInputs()
Log format Driver Station log (.dsevents) — limited data types WPILOG (.wpilog) — typed, timestamped, all WPILib types
Offline analysis Very limited Full AdvantageScope visualization and replay
Replay-based debugging Not possible Core capability — add new log statements and replay to see what they would have shown

How Data Flows Through AdvantageKit

Select a mode below to see how data travels from your subsystem's IO layer through the Logger to its destination. The path changes depending on whether the robot is running on real hardware, in simulation, or in replay mode.

AdvantageKit data flow — select a mode select a tab
Real robot
Simulation
Replay
Real robot mode: The IO implementation reads from hardware and populates the inputs object. Logger.processInputs() writes all inputs to the WPILOG file on the roboRIO's USB drive. Logger.recordOutput() calls append computed values (PID error, target RPM, state enum) to the same file. After the match, the WPILOG file can be downloaded and opened in AdvantageScope.

The Three Logger Backends

Real Robot — WPILOG file
On-robot USB data logger

Writes a .wpilog binary log file to a USB drive plugged into the roboRIO, or to the roboRIO's internal storage. Captures every processInputs() call and every recordOutput() call at full loop rate (50 Hz). File grows at roughly 1–5 MB per minute depending on what's logged.

Simulation — NetworkTables + console
Visible live during development

In simulation mode, logged values are published to NetworkTables and visible in SmartDashboard or AdvantageScope in real time. No file is written by default in sim (configurable). This lets you observe all the same logged values during sim development that you'd see replaying a real match log.

Replay — log file as input source
Offline re-execution of match logic

In replay mode, Logger.processInputs() reads inputs from the log file instead of from hardware. The subsystem logic runs on historical data. Computed outputs — everything from recordOutput() — are written to a new log file. Open both in AdvantageScope to compare original vs. new computed values.

Installation and Setup

Adding AdvantageKit to Your Project

Download the AdvantageKit vendordep JSON from github.com/Mechanical-Advantage/AdvantageKit/releases. In VS Code: W key → "Manage Vendor Libraries" → "Install new libraries (online)" and paste the URL. Alternatively, place the JSON file directly in your project's vendordeps/ directory and run ./gradlew dependencies to confirm it resolved.

Configuring Robot.java

AdvantageKit requires the Logger to be configured and started before any subsystems are created. The configuration happens in Robot.java before new RobotContainer() is called.

Robot.java — AdvantageKit Logger setup
import org.littletonrobotics.junction.Logger;
import org.littletonrobotics.junction.LoggedRobot;
import org.littletonrobotics.junction.wpilog.WPILOGWriter;
import org.littletonrobotics.junction.wpilog.WPILOGReader;
import org.littletonrobotics.junction.networktables.NT4Publisher;

// Extend LoggedRobot instead of TimedRobot
// LoggedRobot is a drop-in replacement that handles Logger tick timing
public class Robot extends LoggedRobot {

    private RobotContainer m_robotContainer;

    @Override
    public void robotInit() {
        // ── Logger configuration: MUST happen before RobotContainer is created ────

        // Metadata is stored in the log file for later identification
        Logger.recordMetadata("ProjectName", BuildConstants.MAVEN_NAME);
        Logger.recordMetadata("GitSHA",     BuildConstants.GIT_SHA);
        Logger.recordMetadata("BuildDate",  BuildConstants.BUILD_DATE);

        if (RobotBase.isReal()) {
            // On real robot: write to USB drive (preferred) or roboRIO internal storage
            Logger.addDataReceiver(new WPILOGWriter("/U/logs"));  // USB drive path
            Logger.addDataReceiver(new NT4Publisher());       // also publish to NT for live view
        } else if (Constants.getMode() == Constants.Mode.REPLAY) {
            // Replay mode: read from a previous log file, write a new one with
            // computed outputs from the re-run. The input log path is configured
            // in Constants.java.
            Logger.setReplaySource(new WPILOGReader(Constants.REPLAY_LOG_PATH));
            Logger.addDataReceiver(new WPILOGWriter(Constants.REPLAY_OUTPUT_PATH));
        } else {
            // Simulation: publish to NT only (no file)
            Logger.addDataReceiver(new NT4Publisher());
        }

        // Start the Logger — must be called before any subsystem is constructed
        Logger.start();

        // Now safe to create subsystems (which reference Logger in their periodic())
        m_robotContainer = new RobotContainer();
    }

    @Override
    public void robotPeriodic() {
        CommandScheduler.getInstance().run();
    }
}
💡 BuildConstants — automatic git metadata in your log

AdvantageKit includes a Gradle plugin that generates a BuildConstants.java file at compile time containing the current Git SHA, branch name, build date, and project name. When logged as metadata at robot startup, every .wpilog file carries an exact record of which version of the code produced it. After a competition, you can correlate a log file to a specific commit in your repository — which is essential when debugging whether a problem was introduced between matches. To enable it, add id "org.littletonrobotics.junction.toolsPlugin" to the plugins block in your build.gradle.

The Logger API: recordOutput and processInputs

Two Logger methods cover nearly all of the API you'll use day to day.

Logger.recordOutput() — log any computed value

Call this anywhere in periodic() to record any value. The first argument is the log key — a hierarchical string using "/" as a separator. The key becomes the path in AdvantageScope's log viewer. Good key naming conventions make logs searchable and self-documenting.

recordOutput(key, double)

Velocity RPM, PID error, voltage commands, distance estimates

recordOutput(key, boolean)

isAtSpeed, isHomed, beamBreak state, motor connected

recordOutput(key, double[])

Swerve module states, multi-value sensor arrays

recordOutput(key, Pose2d)

Robot field position — renders as robot icon on Field2d in AdvantageScope

recordOutput(key, Pose2d[])

Array of poses — trajectories, vision targets, multiple robots

recordOutput(key, Enum)

State machine enum values — renders as readable string in log

Shooter.java — recording outputs in periodic()
@Override
public void periodic() {
    // 1. Read inputs from IO
    // 2. processInputs — logs inputs AND overwrites them in replay mode
    m_io.updateInputs(m_inputs);
    Logger.processInputs("Shooter", m_inputs);

    // 3. Run control logic using m_inputs (now safe to read)
    // 4. Record ALL computed values — these are what replay adds insight to

    // Scalars
    Logger.recordOutput("Shooter/TargetRpm",      m_targetRpm);
    Logger.recordOutput("Shooter/VelocityRpm",    m_inputs.velocityRpm);
    Logger.recordOutput("Shooter/VelocityError",  m_targetRpm - m_inputs.velocityRpm);
    Logger.recordOutput("Shooter/PIDOutput",      m_pid.calculate(m_inputs.velocityRpm));

    // Booleans
    Logger.recordOutput("Shooter/AtTargetSpeed",  isAtTargetSpeed());
    Logger.recordOutput("Shooter/MotorConnected", m_inputs.motorConnected);

    // State enum (logs as string: "IDLE", "SPINNING_UP", "READY", "EJECTING")
    Logger.recordOutput("Shooter/State",          m_currentState);
}

Logger.processInputs() — the critical call

As described in Lesson 1, Logger.processInputs(prefix, inputs) does two things simultaneously:

  • In real robot / simulation mode: logs the current state of every field in the inputs object to the log file / NetworkTables under the given prefix. This is how sensor readings become part of the permanent match record.
  • In replay mode: reads the values for these fields from the log file at the current timestamp and writes them into the inputs object. The subsystem then runs its logic on historical data from the match.

This dual behavior is what makes replay deterministic. The subsystem code doesn't need to know which mode it's in — it just calls processInputs() and the framework handles the rest.

💡 The @AutoLog annotation reduces boilerplate

AdvantageKit provides an @AutoLog annotation that automatically generates the code needed to make a class's fields loggable with processInputs(). Instead of manually implementing the logging interface, annotate your inputs class with @AutoLogOutput (for output recording) or use the generated ShooterIOInputsAutoLogged class that AdvantageKit creates at compile time when @AutoLog is applied to your ShooterIOInputs inner class. This removes the need to write custom serialization code for each inputs type. Refer to the AdvantageKit documentation for the current annotation and generated class pattern — the details change slightly between versions.

Getting Log Files Off the Robot

After a match, the .wpilog file is on the roboRIO (or the USB drive plugged into it). There are three ways to retrieve it:

  • USB drive (preferred): Plug a USB drive into the roboRIO before matches. AdvantageKit writes logs directly to /U/logs/ on the drive. After the match, pull the drive and plug it into a laptop. Logs are immediately accessible. This is the fastest method and doesn't require network access.
  • FTP over USB tether: Connect the laptop to the roboRIO via USB-B cable. Use an FTP client (FileZilla, or the built-in sftp in Terminal/PowerShell) to connect to 172.22.11.2 and navigate to /home/lvuser/. Download the most recent .wpilog file.
  • FTP over robot network: Connect to the robot's WiFi. FTP to 10.TE.AM.2. Same path as above. Slower than USB, but works wirelessly in the pit during between-match review.
🔍 USB drive in the robot at all times

Keep a USB drive permanently plugged into the roboRIO throughout competition. Do not rely on FTP during a match queue — you won't have time. The USB drive should be dedicated to logs (nothing else on it), labeled with the team number and year, and have at least 8 GB of capacity (one competition generates roughly 200–500 MB of logs across 10+ matches). After each event, archive the drive's contents to a shared team folder organized by event name and date. These logs are your debugging history for the entire season. Teams that lose them lose the ability to diagnose intermittent failures that only appear at events.

Log Key Naming Conventions

Log keys are hierarchical strings that become the navigation tree in AdvantageScope. Consistent naming conventions matter — a messy log namespace is as hard to navigate as messy code.

Recommended log key naming patterns
// ── Pattern: SubsystemName/Category/SpecificValue ─────────────────────────

// IO layer inputs (set by processInputs prefix)
// These appear under: Shooter/velocityRpm, Shooter/currentAmps, etc.
Logger.processInputs("Shooter", m_inputs);      // creates Shooter/* namespace
Logger.processInputs("Drive/FrontLeft", inputs); // Drive/FrontLeft/*

// Computed outputs (use SubsystemName/ValueName)
Logger.recordOutput("Shooter/TargetRpm",     m_targetRpm);
Logger.recordOutput("Shooter/VelocityError", error);
Logger.recordOutput("Shooter/State",         m_state);

// Robot-level pose and field visualization
Logger.recordOutput("Odometry/Robot",        m_drive.getPose());
Logger.recordOutput("Odometry/VisionPose",   visionPose);
Logger.recordOutput("Odometry/Trajectory",   trajectoryPoses);

// Command scheduler (AdvantageKit logs active commands automatically)
// These appear under: ActiveCommands/SchedulerActions

// Avoid: vague names, no namespace, duplicate keys across subsystems
Logger.recordOutput("rpm",         value); // ❌ which subsystem?
Logger.recordOutput("Shooter/rpm", value); // ✓ clear and findable

🔌 System Check

⚙️ Verifying AdvantageKit Is Working

After installing AdvantageKit and configuring Robot.java, verify each of these before relying on it:

  • Robot.java extends LoggedRobot, not TimedRobot. Check the class declaration. If it still extends TimedRobot, AdvantageKit's timing integration isn't active. This won't prevent the code from compiling, but the Logger won't tick correctly and log timestamps will be wrong.
  • Logger.start() is called before new RobotContainer(). Check the order in robotInit(). If RobotContainer is constructed before Logger.start(), any processInputs() or recordOutput() calls during subsystem construction will fail silently or throw exceptions.
  • A .wpilog file is created after running on the real robot. After running auto or teleop on the physical robot, connect via FTP or pull the USB drive and confirm a .wpilog file exists in /U/logs/ (or /home/lvuser/ if no USB is connected). If no file exists, verify the WPILOGWriter path is correct and the robot has write permissions.
  • Log keys appear in AdvantageScope when the .wpilog is opened. Open the file in AdvantageScope (Lesson 4). Expand the subsystem namespaces. Confirm that IO input fields (e.g., Shooter/velocityRpm) and output fields (e.g., Shooter/TargetRpm) are present. If only some keys appear, verify that processInputs() is called before any logic reads m_inputs fields (Lesson 1 order requirement).
  • Git SHA appears in the log metadata. Open the .wpilog in AdvantageScope and look for the metadata fields. Confirm GitSHA and BuildDate are present. If they show as empty strings, the toolsPlugin is not active in build.gradle and BuildConstants wasn't generated.

Knowledge Check

1. A robot runs a match and the team finds the shooter missed shots repeatedly. They download the .wpilog file. The file contains Shooter/velocityRpm and Shooter/TargetRpm but they want to also see the PID correction voltage at each loop — a value they forgot to log before the match. Can they recover this information, and if so, how?

  • A No — values not logged during the match cannot be recovered; they must reproduce the scenario on the robot
  • B Yes — add Logger.recordOutput("Shooter/PIDVoltage", pidVoltage) to the subsystem, then replay the log file; the subsystem logic re-runs on the original sensor inputs and now computes and logs the PID voltage at every historical loop
  • C Only if they also have the Driver Station log from that match
  • D Only with Limelight — AdvantageKit doesn't support post-hoc computed values

2. A programmer adds Logger.start() at the bottom of robotInit(), after new RobotContainer(). The code compiles and runs. What is likely broken?

  • A Nothing — Logger.start() can be called at any point before the first periodic() runs
  • B Any processInputs() or recordOutput() calls made during subsystem construction (in constructors or initialization blocks) will fail silently or throw, and the Logger's timestamps may be wrong; Logger must be started before any subsystem references it
  • C The log file won't be created until the second match
  • D Only replay mode is affected — real robot logging works fine with any start order

3. A team logs Logger.recordOutput("rpm", m_inputs.velocityRpm) in their shooter subsystem and Logger.recordOutput("rpm", m_inputs.angleRad) in their arm subsystem. Both use the same key "rpm". What happens in the log file, and how should this be fixed?

  • A AdvantageKit detects the collision and automatically creates "rpm_1" and "rpm_2"
  • B Both subsystems write to the same key — the log entry for "rpm" is overwritten each loop by whichever subsystem runs second; the arm's angle data (mislabeled as "rpm") overwrites the shooter's velocity; fix by using namespaced keys: "Shooter/VelocityRpm" and "Arm/AngleRad"
  • C The Logger throws a key conflict exception at startup
  • D Both values are stored under "rpm" as an array with two elements
💪 Practice Prompt

Add AdvantageKit to Your Robot Project

  1. Install AdvantageKit using the vendor library URL from github.com/Mechanical-Advantage/AdvantageKit/releases. Change Robot.java to extend LoggedRobot. Configure the Logger in robotInit() with a WPILOGWriter for real robot mode and NT4Publisher for simulation. Call Logger.start() before new RobotContainer(). Confirm the code builds without errors.
  2. Add Logger.recordOutput() calls to one subsystem's periodic(): at minimum, log the target value, the actual sensor value, and the error. Use namespaced keys ("SubsystemName/ValueName"). Deploy to simulation and confirm the keys appear in SmartDashboard or AdvantageScope.
  3. Connect a USB drive to the roboRIO. Deploy and run the robot in auto or teleop for at least 30 seconds. Pull the USB drive and confirm a .wpilog file was created in /U/logs/. Open it in AdvantageScope (covered fully in Lesson 4) and confirm your logged keys appear.
  4. Update your subsystem from Lesson 1's Practice Prompt (the IO layer refactor) to use Logger.processInputs() after updateInputs(). Confirm the order is correct: updateInputs()processInputs() → logic reads m_inputs. Verify in a .wpilog file that the IO layer inputs appear under the correct namespace.
  5. Bonus: Add Git metadata logging using BuildConstants. Enable the toolsPlugin in build.gradle. Rebuild. After running, verify that the .wpilog file's metadata section contains your current Git SHA and build date. Commit a change to your code, rebuild, and run again — confirm the SHA in the new log file is different from the previous one.