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