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