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}