Unit 9 · Lesson 5

Auto Selection with SendableChooser

You can't redeploy code on the field. SendableChooser is how your drive team picks the right autonomous routine for each match — without touching a line of code between the pit and the alliance station.

By the end of this lesson, you will:

  • Implement a SendableChooser<Command> in RobotContainer and publish it to SmartDashboard
  • Explain the difference between setDefaultOption() and addOption() and why the default matters at competition
  • Trace the call chain from SmartDashboard selection → getAutonomousCommand()schedule()
  • Design a competition-ready auto menu with appropriate options, a safe default, and a timed fallback
  • Describe three common SendableChooser bugs and how to detect and prevent them
  • Explain when PathPlanner's AutoBuilder.buildAutoChooser() is preferable to a manual SendableChooser

Why You Can't Just Hardcode a Routine

Autonomous strategy in FRC isn't fixed. It changes based on which alliance partners you're paired with, which scoring zones are contested, whether you're playing offense or defense, and what the score differential is heading into auto. A team that can adapt its auto routine between matches without redeploying code has a meaningful strategic advantage over one that can't.

More practically: at competition, you get somewhere between 6 and 10 minutes between matches. Redeploying robot code takes 30–60 seconds under ideal conditions — finding the laptop, opening VS Code, building, deploying, reconnecting to the Driver Station. That's not impossible, but it burns time you may not have, and it introduces the risk of deploying broken code under pressure.

SendableChooser solves this by creating a dropdown widget on the Driver Station's SmartDashboard. The drive team picks the strategy for the upcoming match in the queue, and the robot uses exactly that routine when autonomous begins.

How SendableChooser Works

SendableChooser<T> is a generic WPILib class that pairs human-readable strings with values of type T. For autonomous selection, T is Command — each string label maps to an autonomous command. When the robot code asks getSelected(), it gets back the Command object associated with whatever string the driver chose.

The chooser publishes itself to SmartDashboard (or Shuffleboard) automatically when you call SmartDashboard.putData(). Once published, the Driver Station laptop displays it as a dropdown widget. The selection persists — it doesn't reset when you disable and re-enable the robot.

Full Implementation

Everything lives in RobotContainer. The chooser is a field, populated in the constructor, published once, and read in getAutonomousCommand(). Nothing goes in Robot.java except the existing call to getAutonomousCommand().

RobotContainer.java — complete SendableChooser implementation
public class RobotContainer {

    private final DriveSubsystem  m_drive  = new DriveSubsystem();
    private final IntakeSubsystem m_intake = new IntakeSubsystem();
    private final ArmSubsystem    m_arm    = new ArmSubsystem();

    // ── Autonomous chooser ─────────────────────────────────────────────────
    // Generic type is Command — each option is a complete auto Command.
    private final SendableChooser<Command> m_autoChooser =
        new SendableChooser<>();

    public RobotContainer() {
        configureAutoChooser();
        configureButtonBindings();
    }

    private void configureAutoChooser() {

        // ── Default option: always safe, always works ──────────────────────
        // setDefaultOption() is what runs if the drive team forgets to select.
        // It must be something the robot can execute reliably from ANY starting position.
        // "Drive forward" meets this bar. A complex multi-piece routine does not.
        m_autoChooser.setDefaultOption(
            "Drive Forward (Safe Default)",
            new DriveDistanceCommand(m_drive, 2.0, 0.4).withTimeout(4.0));

        // ── Additional options: strategy-specific ──────────────────────────
        m_autoChooser.addOption(
            "Two Piece — Left Side",
            getTwoPieceLeftAuto());

        m_autoChooser.addOption(
            "Two Piece — Right Side",
            getTwoPieceRightAuto());

        m_autoChooser.addOption(
            "Score One + Play Defense",
            getScoreAndDefenseAuto());

        m_autoChooser.addOption(
            "Timed Fallback (Sensor Failure)",
            getTimedFallbackAuto());

        m_autoChooser.addOption(
            "Do Nothing",
            Commands.none());

        // ── Publish to SmartDashboard ──────────────────────────────────────
        // This creates the dropdown widget the drive team sees.
        // The string key "Auto Mode" must be consistent — if you change it,
        // saved Shuffleboard layouts won't find the widget anymore.
        SmartDashboard.putData("Auto Mode", m_autoChooser);
    }

    public Command getAutonomousCommand() {
        // getSelected() returns the Command mapped to the current dropdown selection.
        // If no selection was made (shouldn't happen with setDefaultOption), returns null.
        // Robot.java's null check before scheduling handles this edge case.
        return m_autoChooser.getSelected();
    }

    // Private builder methods — each returns a fresh Command instance.
    private Command getTwoPieceLeftAuto() {
        return Commands.sequence( /* ... */ );
    }
    private Command getTwoPieceRightAuto() {
        return Commands.sequence( /* ... */ );
    }
    private Command getScoreAndDefenseAuto() {
        return Commands.sequence( /* ... */ );
    }
    private Command getTimedFallbackAuto() {
        return Commands.run(
            () -> m_drive.arcadeDrive(0.4, 0), m_drive)
            .withTimeout(1.5);
    }
}
💡 Use builder methods, not field assignments, for auto commands

Notice that each option is built by calling a private method (getTwoPieceLeftAuto()) rather than storing the command in a field. This is intentional: each call to the builder method creates a fresh Command instance. If you stored a command as a field and passed that same object to addOption(), redeploying or reinitializing the robot during testing could leave the chooser holding a stale command object. Builder methods guarantee a fresh instance every time the chooser is populated.

The Pre-Match Workflow: From Selection to Execution

Use the mock Driver Station below to walk through exactly what happens from auto selection to the first command running. Select a routine, enable autonomous, and trace the code path in real time.

Mock Driver Station — auto selection simulator DISABLED
SmartDashboard — Auto Mode widget
📋 Auto Mode ● CONNECTED
Code execution trace
1
DS enables autonomous mode FMS sends mode packet → roboRIO
2
Robot.java: autonomousInit() Called once at mode transition
3
getAutonomousCommand() RobotContainer asked for selected auto
4
m_autoChooser.getSelected() Returns selected Command object
5
m_autoCommand.schedule() Handed to CommandScheduler
6
Command.initialize() First loop — command begins executing
Event log
[0.00s] Robot code initialized — chooser published to SmartDashboard
[0.02s] Default selection active: "Drive Forward (Safe Default)"

Designing a Competition-Ready Auto Menu

The options you offer matter as much as the implementation. A menu with 12 options nobody can remember under match pressure is worse than one with 5 clearly named, tested choices. Here's a recommended structure for a typical competition robot.

Recommended auto menu structure example — adapt to your robot and game
DEFAULT
"Drive Forward (Safe)" Gets the robot out of the starting zone for mobility points. Works from any starting position. Sensor-based with a generous timeout. This should always be the default — not your highest-scoring routine, because a complex routine that fails silently is worse than a simple routine that succeeds reliably.
OPTION
"Two Piece — Left" Full competition auto from the left starting position. Sensor-based throughout. Only add this when it's been tested and works reliably at least 8 of 10 consecutive attempts. Include the starting position in the name — your drive coach needs to match the selection to the robot's actual placement.
OPTION
"Two Piece — Right" Mirror of the left routine. Keep these as separate named options rather than a single "Two Piece" with a position parameter — it's clearer and less error-prone when selecting under pressure.
OPTION
"Score One + Defense" Strategic option for when your alliance doesn't need you to score a second piece. Score the preloaded piece, then drive to disrupt an opponent's auto path. Requires coordination with alliance partners in the queue — your drive coach should confirm this selection before locking it in.
FALLBACK
"Timed Fallback (Sensor Failure)" Pure timed routine — no sensors required. Select this if an encoder or gyro failed in the pit and can't be fixed before the match. It will drift, but it will move the robot and it will never hang. Always keep this option ready and re-test it every event. It should be visible and accessible, not hidden.
SAFETY
"Do Nothing" Returns Commands.none(). Use when your alliance partners have asked you to stay still because their auto path crosses your starting zone, or when any movement would interfere with a more important alliance action. Never leave this as the default — it scores zero and helps zero — but having it available prevents accidental interference.
🔍 Name your options for the drive coach, not the programmer

"twoPieceLeft_v3_withTimeout_FINAL" is a reasonable code method name. It's a terrible SmartDashboard option label. Your drive coach is reading this under time pressure, possibly from 10 feet away, trying to confirm the selection before the match clock starts. Use clear, descriptive names that include starting position, rough piece count, and any strategic intent: "Two Piece — Left Side," "Score + Defense — Center," "Drive Forward Only." If the string doesn't tell a non-programmer what the robot will do, rename it.

Three Common SendableChooser Bugs

Bug 1: Calling getSelected() in the constructor

This is the most frequent mistake. A team calls m_autoChooser.getSelected() in the RobotContainer constructor and stores the result. The problem: the constructor runs at robotInit(), before the Driver Station has sent any selection. The selection returns the default option (or null) regardless of what's shown on the dashboard.

The fix is always to call getSelected() in getAutonomousCommand(), which is called in autonomousInit()after the match has started and the Driver Station selection is final.

The getSelected() timing mistake and its fix
// ❌ WRONG: captured at construction time, ignores dashboard selection
public RobotContainer() {
    configureAutoChooser();
    m_selectedAuto = m_autoChooser.getSelected(); // always returns default
}
public Command getAutonomousCommand() {
    return m_selectedAuto;  // always the default — selection ignored
}

// ✅ CORRECT: called at autonomousInit() time, after selection is final
public Command getAutonomousCommand() {
    return m_autoChooser.getSelected();
}

Bug 2: Forgetting setDefaultOption() and relying on addOption() ordering

addOption() options don't have a guaranteed default — if no option has been explicitly set as default and the dashboard widget loads for the first time, getSelected() may return null. Always use setDefaultOption() exactly once for the option that should run when no selection is made.

Bug 3: Using a command object reference instead of a builder call

If you write m_autoChooser.addOption("Two Piece", m_twoPieceCommand) where m_twoPieceCommand is a field, the chooser holds a reference to that specific object. If that object was already scheduled once (during a previous auto test), it may be in a completed or stale state when the match runs it. Use builder methods that return a fresh instance each time getAutonomousCommand() is called.

💡 Log the selected auto name to SmartDashboard

Add this to robotPeriodic() so you can always confirm what's selected without looking at the dropdown widget: SmartDashboard.putString("Selected Auto", m_autoChooser.getSelected() != null ? m_autoChooser.getSelected().getName() : "None"). At competition, this gives you a real-time readback of the active selection that you can verify with AdvantageScope logs after a match. If the robot ran the wrong auto, the log will tell you exactly what was selected.

PathPlanner's AutoBuilder: The Upgrade Path

Once you start using PathPlanner (covered starting in Lesson 7), you gain access to AutoBuilder.buildAutoChooser() — a method that automatically scans the robot's deploy directory for .auto files and builds a SendableChooser from them. This eliminates the need to manually register every auto option in code.

RobotContainer.java — PathPlanner AutoBuilder chooser (preview)
// After configuring AutoBuilder in your drivetrain subsystem:

private final SendableChooser<Command> m_autoChooser =
    AutoBuilder.buildAutoChooser();
// ↑ Scans deploy/pathplanner/autos/ for .auto files automatically.
//   Every .auto file appears as a chooser option without any addOption() calls.

public RobotContainer() {
    // Register named commands so PathPlanner event markers
    // can trigger robot actions at specific path waypoints.
    NamedCommands.registerCommand("intake", new IntakeUntilSensorCommand(m_intake));
    NamedCommands.registerCommand("shoot", new ShootCommand(m_shooter));

    SmartDashboard.putData("Auto Mode", m_autoChooser);
}

public Command getAutonomousCommand() {
    return m_autoChooser.getSelected();
}
🔍 Which approach does Team 2910 use?

Team 2910 uses PathPlanner for all competition autonomous routines and relies on AutoBuilder.buildAutoChooser() for selection. The manual SendableChooser pattern from this lesson is what you use when you're writing sensor-based command groups directly in Java — which is exactly where you are right now in the curriculum. Lesson 7 introduces PathPlanner and the auto builder. Both approaches use the same SmartDashboard dropdown mechanism and the same getSelected() call — so everything you're learning in this lesson transfers directly.

🔌 System Check

⚙️ Before Relying on SendableChooser at Competition

The chooser must be verified to work correctly at full system scale, not just in unit testing. Work through this list before your first practice event:

  • Verify the dropdown appears on SmartDashboard after deploy. Open SmartDashboard (or Shuffleboard), deploy code, connect to the robot. The "Auto Mode" widget must appear as a dropdown with all expected options. If it doesn't appear, confirm SmartDashboard.putData("Auto Mode", m_autoChooser) is being called in the constructor, not in a periodic method.
  • Test each auto option individually before competition. Select each option from the dropdown, enable autonomous, and verify the correct routine runs. Do this on the practice day before your first match, not during the match queue. One incomplete test is one bad auto run waiting to happen.
  • Confirm the default option works from all starting positions. Your default should be "Drive Forward" or equivalent. Confirm it works when the robot is placed at any alliance station position — left, center, and right. If it only works from center, it's not a safe default.
  • Verify getSelected() is called in getAutonomousCommand(), not the constructor. Change the selection on SmartDashboard after robot code is running. Enable auto. Confirm the newly-selected routine ran, not the original default. If you always get the default, you have the timing bug.
  • Confirm the Shuffleboard layout saves and restores correctly. Competition teams save their Shuffleboard layouts to a file and restore them at each event. Test that the "Auto Mode" widget appears in the correct position after restoring a layout, and that the selection made before a match persists through a robot disable/re-enable cycle.

Knowledge Check

1. A team calls m_autoChooser.getSelected() inside the RobotContainer constructor and stores the result in a field called m_selectedAuto. They return m_selectedAuto from getAutonomousCommand(). The drive team changes the dropdown selection right before the match. What routine will the robot run?

  • A The routine the drive team selected — getSelected() always returns the current dashboard value
  • B The default option — getSelected() was called at construction time before any dashboard selection was made, so it returned the default, and that result is permanently stored in m_selectedAuto
  • C The last option added with addOption(), because that overrides the stored default
  • D Nothing — the robot will throw a NullPointerException and the code will crash

2. A team's robot runs a complex two-piece auto when the drive team selects "Drive Forward Only." After investigation they discover they have m_autoChooser.setDefaultOption("Drive Forward Only", m_twoPieceAuto) in their code — they accidentally passed the wrong command to the default option. What is the correct fix?

  • A Add a second setDefaultOption() call with the correct command — the most recent one takes precedence
  • B Change the argument to the correct command: m_autoChooser.setDefaultOption("Drive Forward Only", new DriveDistanceCommand(m_drive, 2.0, 0.4))
  • C Delete and recreate the SendableChooser object, because options cannot be changed after being added
  • D Use removeOption() to delete the wrong entry, then call setDefaultOption() again

3. During a match, a robot's encoder fails mid-auto and the sensor-based drive command hangs. The team didn't include a timed fallback option in their chooser. At the next match, the encoder still isn't working. What is the best available option given their current codebase?

  • A Redeploy with a timed command hardcoded in getAutonomousCommand() — the chooser can be removed for this match
  • B Use the "Do Nothing" option if they have it — running no auto is safer than risking a hang
  • C Add a timed fallback option to the chooser in the pit and redeploy before the match — this requires a full redeploy but takes only 1–2 minutes and is the most correct fix for the actual problem, restoring the ability to score mobility points without sensor dependency
  • D Select the complex two-piece auto and hope the partial sensor data is close enough to complete the routine
💪 Practice Prompt

Build a Competition-Ready Auto Chooser

  1. In your RobotContainer, declare a SendableChooser<Command> field and a private configureAutoChooser() method. Call this method from the constructor. The chooser must have: one setDefaultOption() for a simple drive-forward command with timeout, at least two addOption() entries for more complex routines, one entry for a timed fallback auto, and one entry for Commands.none(). Use human-readable names that include position information where relevant.
  2. Publish the chooser with SmartDashboard.putData("Auto Mode", m_autoChooser) in configureAutoChooser(). In getAutonomousCommand(), return m_autoChooser.getSelected() — nothing else. Verify in Robot.java that the null check before scheduling is present.
  3. Add this line to robotPeriodic(): SmartDashboard.putString("Active Auto Selection", m_autoChooser.getSelected() != null ? "Selected" : "NULL - no selection"). Deploy, open SmartDashboard, and verify the dropdown appears and the readback string updates when you change the selection.
  4. Test the timing bug intentionally: temporarily move m_autoChooser.getSelected() into the constructor and store it. Enable auto without changing the selection, then enable again after changing the selection. Confirm you always get the default regardless of what's displayed. Restore the correct implementation and verify the fix works.
  5. Bonus: Add a SmartDashboard.putString() in autonomousInit() that logs the name of the selected auto alongside the timestamp. Then run two consecutive auto cycles with different selections. Confirm the log shows different selections for each cycle. This is the verification pattern used by competition teams to confirm the right auto ran — and to prove it from the log after a bad match.