Unit 6 · Lesson 10

Console Output & Debugging

System.out.println() is the world's most underrated debugging tool — and the most misused one in FRC. This lesson covers how to use console output strategically, how it interacts with SmartDashboard, what to do when the output floods the console, and how to clean up before competition so your logs stay useful.

By the end of this lesson, you will:

  • Place console output strategically to verify program flow, variable values, and state transitions
  • Explain why printing in periodic() without throttling floods the console and how to prevent it
  • Access the robot's live console output from VS Code's RioLog panel
  • Decide for each print statement whether it belongs in production code, should be removed, or should be migrated to SmartDashboard or WPILOG
  • Apply the 2910 standard: no temporary debug prints in competition code

Three Questions Console Output Answers

Every print statement exists to answer one question. If you can't state the question, you don't need the print statement. The three useful questions are:

  1. "Did this code path run?" — Verifying control flow. A print in an if branch confirms whether the branch executed.
  2. "What is the value of this variable right now?" — Verifying data. Printing an encoder value, a sensor state, or a calculated output to confirm it's what you expect.
  3. "When did this event happen?" — Verifying timing. Printing at state transitions, command start/end, or loop boundaries to understand sequencing.
❌ Vague — answers no question
if (hasPiece()) {
  System.out.println("here");
  setState(HOLDING);
}
✅ Specific — confirms the transition
if (hasPiece()) {
  System.out.println(
    "[Intake] Piece detected → HOLDING");
  setState(HOLDING);
}

The Flood Problem: Printing in periodic()

teleopPeriodic() runs 50 times per second. A single System.out.println() inside it generates 3,000 log entries per minute. The Driver Station console can't display them all. Recent messages scroll off the screen before you can read them. The system also has to serialize and transmit all that data, which consumes loop time.

❌ Floods at 50 Hz
public void teleopPeriodic() {
  // Generates 3000 lines/minute
  System.out.println(
    "Encoder: " + getDistance());
}
✅ Use SmartDashboard instead
public void periodic() {
  // Updates live, no console flood
  SmartDashboard.putNumber(
    "Drive/Distance", getDistance());
}

The rule: if you need to watch a value continuously, use SmartDashboard. If you need to know when a specific event occurred (one-time state change, error condition), use a targeted print statement. Print statements in periodic() are almost always the wrong tool unless they're gated by a state-change condition.

💡 Counter-gating: print once every N loops

Sometimes you want to sample a value periodically without flooding. A counter field works: declare private int m_printCounter = 0;, then inside periodic(): if (++m_printCounter % 50 == 0) { System.out.println(...); }. This fires once per second (every 50 loops). This is a development tool — remove it before competition. SmartDashboard is better for anything you want to see long-term.

Strategic Print Placement: Debug Scenarios

Select a debugging scenario to see the recommended console strategy.

Debug scenario advisor — select a situation
← select a debugging scenario

Accessing the Console: RioLog in VS Code

The Driver Station console (Messages tab) shows a filtered, formatted view. For raw output from System.out.println(), the VS Code RioLog panel gives you direct access to the robot's stdout stream. Access it via Ctrl+Shift+P → WPILib: Start RioLog while tethered to the robot.

The difference matters: the DS console may delay or truncate long messages. RioLog shows everything, unformatted, in real time — useful during initial bringup and for high-frequency debugging where the DS console is too slow.

The Console Cleanup Standard

The rule on Team 2910: no temporary debug prints in code pushed to main. This is enforced at pull request review. The reasoning: a flooded console in competition hides real errors. If every loop prints "running" and an actual WARNING fires, it scrolls off before anyone reads it.

The system for this:

Print Statement TypeWhere It BelongsBefore Competition
Temporary variable check during developmentLocal dev branch onlyDelete before merge
State machine transition confirmationOK in feature branchMigrate to SmartDashboard.putString()
Error condition messageAny branchConvert to DriverStation.reportWarning()
Startup/init confirmationOK in mainKeep — fires once, not in a loop
Sensor value sampling in periodic()Local dev onlyRemove — use SmartDashboard or DataLog
🔍 LRI Perspective: "I can tell how mature a team's code is by the console"

When I connect to a robot and open the DS console, if I see thousands of lines per minute of "encoder: 0.00, encoder: 0.00, encoder: 0.00," I know this team is going to miss a real error in competition because it'll be buried. When I see a clean console — startup messages, then silence punctuated only by meaningful warnings — I know this team has thought about what information they actually need. Debugging output is a development tool. A cluttered console in competition is a liability.

🔌 System Check — Console Hygiene Before Competition

Run this checklist before any competition deploy:

  • Search the entire project for System.out.println (Ctrl+Shift+F in VS Code). For every occurrence: decide whether it's temporary debug output (delete it), a permanent condition (convert to reportWarning()), or a startup message (keep it).
  • Enable the robot and watch the DS console for 30 seconds during TeleOp. Count how many lines per second appear. The target for competition code: fewer than 1 line per second during normal operation.
  • If you see "Loop time of Xs overran the Ys period" warnings, this is an emergency — find and fix the slow call before competing. Run the robot in Test mode and watch for the warning to identify which periodic method is slow.

Knowledge Check

1. Your robot's arm isn't reaching its target position. You add System.out.println("arm angle: " + m_arm.getAngle()) to teleopPeriodic(). What problem does this create, and what's the better approach?

  • A No problem — printing in periodic is standard debugging practice.
  • B This generates 3,000 lines per minute, flooding the DS console so fast that useful error messages scroll off before they're seen. Better: use SmartDashboard.putNumber("Arm/Angle", m_arm.getAngle()) in periodic() — shows the live value without cluttering the console.
  • C The print statement itself causes the loop to overrun by adding too much processing time.
  • D The angle value returned by getAngle() is unreliable when printed to the console.

2. You want to confirm that the EJECTING state is only entered once per B button press (not multiple times per press). Where should you place the print statement, and what should it say?

  • A Inside the EJECTING case of the switch statement, printing every loop while ejecting.
  • B On the transition line: m_state = EJECTING; — add the print immediately after, so it fires once when the state changes. Message: "[Intake] → EJECTING at " + Timer.getFPGATimestamp().
  • C In robotPeriodic(), checking for state EJECTING every loop.
  • D In disabledInit(), printing a summary of all state transitions that occurred.

3. A teammate pushes code to main that contains 15 System.out.println() statements in three different periodic() methods. What is the right response?

  • A Leave them — they might be useful if something breaks at competition.
  • B Review each one: delete temporary debug prints, convert error conditions to DriverStation.reportWarning(), and migrate continuous value monitoring to SmartDashboard. Enforce this as a pre-merge checklist item on your team.
  • C Comment them all out so they're available if needed later.
  • D Keep the ones in teleopPeriodic() and remove only the ones in robotPeriodic().
💪 Practice Prompt

Console Audit and Strategic Debug Placement

  1. Run a project-wide search for System.out.println. Categorize every result: temporary debug (delete), permanent condition (convert to reportWarning), or startup message (keep). Make the changes and deploy. Watch the DS console for 30 seconds and count lines per second — target under 1/sec.
  2. In your IntakeSubsystem state machine, add a targeted print on every state transition: System.out.printf("[Intake] %s → %s%n", m_state, newState). Run through a full intake/eject cycle and confirm each transition fires exactly once. Then migrate this to SmartDashboard.putString("Intake/State", m_state.toString()) and remove the print.
  3. Deliberately add a 25 ms blocking call to teleopPeriodic(): Timer.delay(0.025). Enable and watch the DS console for the loop overrun warning. Confirm it appears. Remove the delay. This lets you recognize a loop overrun warning in a real scenario before you encounter it accidentally.
  4. Stretch goal: Access the RioLog panel in VS Code (Ctrl+Shift+P → WPILib: Start RioLog) while connected to the robot. Compare the output to the DS Messages tab. Note the differences in format, timing, and completeness. Add a startup message with the current date and WPILib version (System.out.println("Robot started: " + edu.wpi.first.wpilibj.RobotBase.class.getPackage().getImplementationVersion())) and verify it appears in both views.