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