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}