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