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   * @return the name of the default do-nothing option.
073   */
074  public String getDefaultName() {
075    return DO_NOTHING_NAME;
076  }
077
078  /**
079   * Select a new option in the chooser.
080   *
081   * <p>This method is called automatically when published as a sendable.
082   *
083   * @param selectStr The name of the option to select.
084   * @return The name of the selected option.
085   */
086  public String select(String selectStr) {
087    return select(selectStr, false);
088  }
089
090  private String select(String selectStr, boolean force) {
091    selected = selectStr;
092    if (selected.equals(nameAtGeneration)
093        && allianceAtGeneration.equals(DriverStation.getAlliance())) {
094      // early return if the selected auto matches the active auto
095      return nameAtGeneration;
096    }
097    boolean dsValid = DriverStation.isDisabled() && DriverStation.getAlliance().isPresent();
098    if (dsValid || force) {
099      if (!autoRoutines.containsKey(selected) && !selected.equals(DO_NOTHING_NAME)) {
100        selected = DO_NOTHING_NAME;
101        selectedNonexistentAuto.set(true);
102      } else {
103        selectedNonexistentAuto.set(false);
104      }
105      allianceAtGeneration = DriverStation.getAlliance();
106      nameAtGeneration = selected;
107      generatedCommand = autoRoutines.get(nameAtGeneration).get().withName(nameAtGeneration);
108    } else {
109      allianceAtGeneration = Optional.empty();
110      nameAtGeneration = DO_NOTHING_NAME;
111      generatedCommand = Commands.none();
112    }
113    return nameAtGeneration;
114  }
115
116  /**
117   * Add an AutoRoutine to the chooser.
118   *
119   * <p>This is done to load AutoRoutines when and only when they are selected, in order to save
120   * memory and file loading time for unused AutoRoutines.
121   *
122   * <p>The generators are only run when the DriverStation is disabled and the alliance is known.
123   *
124   * <p>One way to keep this clean is to make an `Autos` class that all of your subsystems/resources
125   * are <a href="https://en.wikipedia.org/wiki/Dependency_injection">dependency injected</a> into.
126   * Then create methods inside that class that take an {@link AutoFactory} and return an {@link
127   * AutoRoutine}.
128   *
129   * <h3>Example:</h3>
130   *
131   * <pre><code>
132   * AutoChooser chooser;
133   * Autos autos = new Autos(swerve, shooter, intake, feeder);
134   * public Robot() {
135   *   chooser = new AutoChooser("/Choosers");
136   *   SmartDashboard.putData(chooser);
137   *   // fourPieceRight is a method that accepts an AutoFactory and returns an AutoRoutine.
138   *   chooser.addRoutine("4 Piece right", autos::fourPieceRight);
139   *   chooser.addRoutine("4 Piece Left", autos::fourPieceLeft);
140   *   chooser.addRoutine("3 Piece Close", autos::threePieceClose);
141   * }
142   * </code></pre>
143   *
144   * @param name The name of the auto routine.
145   * @param generator The function that generates the auto routine.
146   * @return This {@link AutoChooser} instance, to allow for method chaining.
147   */
148  public AutoChooser addRoutine(String name, Supplier<AutoRoutine> generator) {
149    autoRoutines.put(name, () -> generator.get().cmd());
150    options = autoRoutines.keySet().toArray(new String[0]);
151    return this;
152  }
153
154  /**
155   * Adds a Command to the auto chooser.
156   *
157   * <p>This is done to load autonomous commands when and only when they are selected, in order to
158   * save memory and file loading time for unused autonomous commands.
159   *
160   * <p>The generators are only run when the DriverStation is disabled and the alliance is known.
161   *
162   * <h3>Example:</h3>
163   *
164   * <pre><code>
165   * AutoChooser chooser;
166   * Autos autos = new Autos(swerve, shooter, intake, feeder);
167   * public Robot() {
168   *   chooser = new AutoChooser("/Choosers");
169   *   SmartDashboard.putData(chooser);
170   *   // fourPieceLeft is a method that accepts an AutoFactory and returns a command.
171   *   chooser.addCmd("4 Piece left", autos::fourPieceLeft);
172   *   chooser.addCmd("Just Shoot", shooter::shoot);
173   * }
174   * </code></pre>
175   *
176   * @param name The name of the autonomous command.
177   * @param generator The function that generates an autonomous command.
178   * @return This {@link AutoChooser} instance, to allow for method chaining.
179   * @see AutoChooser#addRoutine
180   */
181  public AutoChooser addCmd(String name, Supplier<Command> generator) {
182    autoRoutines.put(name, generator);
183    options = autoRoutines.keySet().toArray(new String[0]);
184    return this;
185  }
186
187  /**
188   * Gets a Command that schedules the selected auto routine. This Command shares the lifetime of
189   * the scheduled Command. This Command can directly be bound to a trigger, like so:
190   *
191   * <pre><code>
192   *     AutoChooser chooser = ...;
193   *
194   *     public Robot() {
195   *         RobotModeTriggers.autonomous().whileTrue(chooser.selectedCommandScheduler());
196   *     }
197   * </code></pre>
198   *
199   * @return A command that runs the selected {@link AutoRoutine}
200   */
201  public Command selectedCommandScheduler() {
202    return Commands.deferredProxy(() -> selectedCommand());
203  }
204
205  /**
206   * Returns the currently selected command.
207   *
208   * <p>If you plan on using this {@link Command} in a {@code Trigger} it is recommended to use
209   * {@link #selectedCommandScheduler()} instead.
210   *
211   * @return The currently selected command.
212   */
213  public Command selectedCommand() {
214    if (RobotBase.isSimulation() && nameAtGeneration == DO_NOTHING_NAME) {
215      select(selected, true);
216    }
217    return generatedCommand;
218  }
219
220  @Override
221  public void initSendable(SendableBuilder builder) {
222    builder.setSmartDashboardType("String Chooser");
223    builder.publishConstBoolean(".controllable", true);
224    builder.publishConstString("default", DO_NOTHING_NAME);
225    builder.addStringArrayProperty("options", () -> options, null);
226    builder.addStringProperty("selected", null, this::select);
227    builder.addStringProperty("active", () -> select(selected), null);
228  }
229}