001// Copyright (c) Choreo contributors
002
003package choreo.auto;
004
005import static edu.wpi.first.wpilibj.Alert.AlertType.kError;
006
007import choreo.util.ChoreoAlert;
008import edu.wpi.first.util.sendable.Sendable;
009import edu.wpi.first.util.sendable.SendableBuilder;
010import edu.wpi.first.wpilibj.Alert;
011import edu.wpi.first.wpilibj.DriverStation;
012import edu.wpi.first.wpilibj.DriverStation.Alliance;
013import edu.wpi.first.wpilibj.RobotBase;
014import edu.wpi.first.wpilibj2.command.Command;
015import edu.wpi.first.wpilibj2.command.Commands;
016import java.util.HashMap;
017import java.util.Optional;
018import java.util.function.Supplier;
019
020/**
021 * An Choreo specific {@code SendableChooser} that allows for the selection of {@link AutoRoutine}s
022 * at runtime via a <a
023 * href="https://docs.wpilib.org/en/stable/docs/software/dashboards/index.html#dashboards">Dashboard</a>.
024 *
025 * <p>This chooser takes a <a href="https://en.wikipedia.org/wiki/Lazy_loading">lazy loading</a>
026 * approach to {@link AutoRoutine}s, only generating the {@link AutoRoutine} when it is selected.
027 * This approach has the benefit of not loading all autos on startup, but also not loading the auto
028 * during auto start causing a delay.
029 *
030 * <p>Once the {@link AutoChooser} is made you can add {@link AutoRoutine}s to it using {@link
031 * #addRoutine} or add {@link Command}s to it using {@link #addCmd}. Similar to {@code
032 * SendableChooser} this chooser can be added to the {@link
033 * edu.wpi.first.wpilibj.smartdashboard.SmartDashboard} using {@code
034 * SmartDashboard.putData(Sendable)}.
035 *
036 * <p>You can set the Robot's autonomous command to the chooser's chosen auto routine via <code>
037 * RobotModeTriggers.autonomous.whileTrue(chooser.autoSchedulingCmd());</code>
038 */
039public class AutoChooser implements Sendable {
040  private final String DO_NOTHING_NAME;
041  private static final Alert selectedNonexistentAuto =
042      ChoreoAlert.alert("Selected an auto that isn't an option", kError);
043
044  private final HashMap<String, Supplier<Command>> autoRoutines = new HashMap<>();
045
046  private String selected;
047  private String[] options = new String[] {};
048
049  private Optional<Alliance> allianceAtGeneration = Optional.empty();
050  private String nameAtGeneration;
051  private Command generatedCommand = Commands.none();
052
053  /** Constructs a new {@link AutoChooser}. */
054  public AutoChooser() {
055    this("Nothing");
056  }
057
058  /**
059   * Constructs a new {@link AutoChooser} with the given name for the do-nothing default option.
060   *
061   * @param doNothingName The option name for the default choice.
062   */
063  public AutoChooser(String doNothingName) {
064    DO_NOTHING_NAME = doNothingName;
065    nameAtGeneration = DO_NOTHING_NAME;
066    generatedCommand = Commands.none();
067    addCmd(DO_NOTHING_NAME, Commands::none);
068    select(DO_NOTHING_NAME);
069  }
070
071  /**
072   * Returns the name of the default do-nothing option.
073   *
074   * @return the name of the default do-nothing option.
075   */
076  public String getDefaultName() {
077    return DO_NOTHING_NAME;
078  }
079
080  /**
081   * Select a new option in the chooser.
082   *
083   * <p>This method is called automatically when published as a sendable.
084   *
085   * @param selectStr The name of the option to select.
086   * @return The name of the selected option.
087   */
088  public String select(String selectStr) {
089    return select(selectStr, false);
090  }
091
092  private String select(String selectStr, boolean force) {
093    selected = selectStr;
094    if (selected.equals(nameAtGeneration)
095        && allianceAtGeneration.equals(DriverStation.getAlliance())) {
096      // early return if the selected auto matches the active auto
097      return nameAtGeneration;
098    }
099    boolean dsValid = DriverStation.isDisabled() && DriverStation.getAlliance().isPresent();
100    if (dsValid || force) {
101      if (!autoRoutines.containsKey(selected) && !selected.equals(DO_NOTHING_NAME)) {
102        selected = DO_NOTHING_NAME;
103        selectedNonexistentAuto.set(true);
104      } else {
105        selectedNonexistentAuto.set(false);
106      }
107      allianceAtGeneration = DriverStation.getAlliance();
108      nameAtGeneration = selected;
109      generatedCommand = autoRoutines.get(nameAtGeneration).get().withName(nameAtGeneration);
110    } else {
111      allianceAtGeneration = Optional.empty();
112      nameAtGeneration = DO_NOTHING_NAME;
113      generatedCommand = Commands.none();
114    }
115    return nameAtGeneration;
116  }
117
118  /**
119   * Add an AutoRoutine to the chooser.
120   *
121   * <p>This is done to load AutoRoutines when and only when they are selected, in order to save
122   * memory and file loading time for unused AutoRoutines.
123   *
124   * <p>The generators are only run when the DriverStation is disabled and the alliance is known.
125   *
126   * <p>One way to keep this clean is to make an `Autos` class that all of your subsystems/resources
127   * are <a href="https://en.wikipedia.org/wiki/Dependency_injection">dependency injected</a> into.
128   * Then create methods inside that class that take an {@link AutoFactory} and return an {@link
129   * AutoRoutine}.
130   *
131   * <h3>Example:</h3>
132   *
133   * <pre><code>
134   * AutoChooser chooser;
135   * Autos autos = new Autos(swerve, shooter, intake, feeder);
136   * public Robot() {
137   *   chooser = new AutoChooser("/Choosers");
138   *   SmartDashboard.putData(chooser);
139   *   // fourPieceRight is a method that accepts an AutoFactory and returns an AutoRoutine.
140   *   chooser.addRoutine("4 Piece right", autos::fourPieceRight);
141   *   chooser.addRoutine("4 Piece Left", autos::fourPieceLeft);
142   *   chooser.addRoutine("3 Piece Close", autos::threePieceClose);
143   * }
144   * </code></pre>
145   *
146   * @param name The name of the auto routine.
147   * @param generator The function that generates the auto routine.
148   * @return This {@link AutoChooser} instance, to allow for method chaining.
149   */
150  public AutoChooser addRoutine(String name, Supplier<AutoRoutine> generator) {
151    autoRoutines.put(name, () -> generator.get().cmd());
152    options = autoRoutines.keySet().toArray(new String[0]);
153    return this;
154  }
155
156  /**
157   * Adds a Command to the auto chooser.
158   *
159   * <p>This is done to load autonomous commands when and only when they are selected, in order to
160   * save memory and file loading time for unused autonomous commands.
161   *
162   * <p>The generators are only run when the DriverStation is disabled and the alliance is known.
163   *
164   * <h3>Example:</h3>
165   *
166   * <pre><code>
167   * AutoChooser chooser;
168   * Autos autos = new Autos(swerve, shooter, intake, feeder);
169   * public Robot() {
170   *   chooser = new AutoChooser("/Choosers");
171   *   SmartDashboard.putData(chooser);
172   *   // fourPieceLeft is a method that accepts an AutoFactory and returns a command.
173   *   chooser.addCmd("4 Piece left", autos::fourPieceLeft);
174   *   chooser.addCmd("Just Shoot", shooter::shoot);
175   * }
176   * </code></pre>
177   *
178   * @param name The name of the autonomous command.
179   * @param generator The function that generates an autonomous command.
180   * @return This {@link AutoChooser} instance, to allow for method chaining.
181   * @see AutoChooser#addRoutine
182   */
183  public AutoChooser addCmd(String name, Supplier<Command> generator) {
184    autoRoutines.put(name, generator);
185    options = autoRoutines.keySet().toArray(new String[0]);
186    return this;
187  }
188
189  /**
190   * Gets a Command that schedules the selected auto routine. This Command shares the lifetime of
191   * the scheduled Command. This Command can directly be bound to a trigger, like so:
192   *
193   * <pre><code>
194   *     AutoChooser chooser = ...;
195   *
196   *     public Robot() {
197   *         RobotModeTriggers.autonomous().whileTrue(chooser.selectedCommandScheduler());
198   *     }
199   * </code></pre>
200   *
201   * @return A command that runs the selected {@link AutoRoutine}
202   */
203  public Command selectedCommandScheduler() {
204    return Commands.deferredProxy(() -> selectedCommand());
205  }
206
207  /**
208   * Returns the currently selected command.
209   *
210   * <p>If you plan on using this {@link Command} in a {@code Trigger} it is recommended to use
211   * {@link #selectedCommandScheduler()} instead.
212   *
213   * @return The currently selected command.
214   */
215  public Command selectedCommand() {
216    if (RobotBase.isSimulation() && nameAtGeneration == DO_NOTHING_NAME) {
217      select(selected, true);
218    }
219    return generatedCommand;
220  }
221
222  @Override
223  public void initSendable(SendableBuilder builder) {
224    builder.setSmartDashboardType("String Chooser");
225    builder.publishConstBoolean(".controllable", true);
226    builder.publishConstString("default", DO_NOTHING_NAME);
227    builder.addStringArrayProperty("options", () -> options, null);
228    builder.addStringProperty("selected", null, this::select);
229    builder.addStringProperty("active", () -> select(selected), null);
230  }
231}