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}