001// Copyright (c) FIRST and other WPILib contributors. 002// Open Source Software; you can modify and/or share it under the terms of 003// the WPILib BSD license file in the root directory of this project. 004 005package org.wpilib.commands3; 006 007import static edu.wpi.first.units.Units.Seconds; 008import static edu.wpi.first.util.ErrorMessages.requireNonNullParam; 009 010import edu.wpi.first.math.filter.Debouncer; 011import edu.wpi.first.units.measure.Time; 012import edu.wpi.first.wpilibj.event.EventLoop; 013import java.util.ArrayList; 014import java.util.EnumMap; 015import java.util.List; 016import java.util.Map; 017import java.util.function.BooleanSupplier; 018 019/** 020 * Triggers allow users to specify conditions for when commands should run. Triggers can be set up 021 * to read from joystick and controller buttons (eg {@link 022 * org.wpilib.commands3.button.CommandXboxController#x()}) or be customized to read sensor values or 023 * any other arbitrary true/false condition. 024 * 025 * <p>It is very easy to link a button to a command. For instance, you could link the trigger button 026 * of a joystick to a "score" command. 027 * 028 * <p>Triggers can easily be composed for advanced functionality using the {@link 029 * #and(BooleanSupplier)}, {@link #or(BooleanSupplier)}, {@link #negate()} operators. 030 * 031 * <p>Trigger bindings created inside a running command will only be active while that command is 032 * running. This is useful for defining trigger-based behavior only in a certain scope and avoids 033 * needing to create dozens of global triggers. Any commands scheduled by these triggers will be 034 * canceled when the enclosing command exits. 035 * 036 * <pre>{@code 037 * Command shootWhileAiming = Command.noRequirements().executing(co -> { 038 * turret.atTarget.onTrue(shooter.shootOnce()); 039 * co.await(turret.lockOnGoal()); 040 * }).named("Shoot While Aiming"); 041 * controller.rightBumper().whileTrue(shootWhileAiming); 042 * }</pre> 043 */ 044public class Trigger implements BooleanSupplier { 045 private final BooleanSupplier m_condition; 046 private final EventLoop m_loop; 047 private final Scheduler m_scheduler; 048 private Signal m_previousSignal; 049 private final Map<BindingType, List<Binding>> m_bindings = new EnumMap<>(BindingType.class); 050 private final Runnable m_eventLoopCallback = this::poll; 051 private boolean m_isBoundToEventLoop; // used for lazily binding to the event loop 052 053 /** 054 * Represents the state of a signal: high or low. Used instead of a boolean for nullity on the 055 * first run, when the previous signal value is undefined and unknown. 056 */ 057 private enum Signal { 058 /** The signal is high. */ 059 HIGH, 060 /** The signal is low. */ 061 LOW 062 } 063 064 /** 065 * Creates a new trigger based on the given condition. Polled by the scheduler's {@link 066 * Scheduler#getDefaultEventLoop() default event loop}. 067 * 068 * @param scheduler The scheduler that should execute triggered commands. 069 * @param condition the condition represented by this trigger 070 */ 071 public Trigger(Scheduler scheduler, BooleanSupplier condition) { 072 m_scheduler = requireNonNullParam(scheduler, "scheduler", "Trigger"); 073 m_loop = scheduler.getDefaultEventLoop(); 074 m_condition = requireNonNullParam(condition, "condition", "Trigger"); 075 } 076 077 /** 078 * Creates a new trigger based on the given condition. Triggered commands are executed by the 079 * {@link Scheduler#getDefault() default scheduler}. 080 * 081 * <p>Polled by the default scheduler button loop. 082 * 083 * @param condition the condition represented by this trigger 084 */ 085 public Trigger(BooleanSupplier condition) { 086 this(Scheduler.getDefault(), condition); 087 } 088 089 /** 090 * Creates a new trigger based on the given condition. 091 * 092 * @param scheduler The scheduler that should execute triggered commands. 093 * @param loop The event loop to poll the trigger. 094 * @param condition the condition represented by this trigger 095 */ 096 public Trigger(Scheduler scheduler, EventLoop loop, BooleanSupplier condition) { 097 m_scheduler = requireNonNullParam(scheduler, "scheduler", "Trigger"); 098 m_loop = requireNonNullParam(loop, "loop", "Trigger"); 099 m_condition = requireNonNullParam(condition, "condition", "Trigger"); 100 } 101 102 /** 103 * Starts the given command whenever the condition changes from `false` to `true`. 104 * 105 * @param command the command to start 106 * @return this trigger, so calls can be chained 107 */ 108 public Trigger onTrue(Command command) { 109 requireNonNullParam(command, "command", "onTrue"); 110 addBinding(BindingType.SCHEDULE_ON_RISING_EDGE, command); 111 return this; 112 } 113 114 /** 115 * Starts the given command whenever the condition changes from `true` to `false`. 116 * 117 * @param command the command to start 118 * @return this trigger, so calls can be chained 119 */ 120 public Trigger onFalse(Command command) { 121 requireNonNullParam(command, "command", "onFalse"); 122 addBinding(BindingType.SCHEDULE_ON_FALLING_EDGE, command); 123 return this; 124 } 125 126 /** 127 * Starts the given command when the condition changes to `true` and cancels it when the condition 128 * changes to `false`. 129 * 130 * <p>Doesn't re-start the command if it ends while the condition is still `true`. 131 * 132 * @param command the command to start 133 * @return this trigger, so calls can be chained 134 */ 135 public Trigger whileTrue(Command command) { 136 requireNonNullParam(command, "command", "whileTrue"); 137 addBinding(BindingType.RUN_WHILE_HIGH, command); 138 return this; 139 } 140 141 /** 142 * Starts the given command when the condition changes to `false` and cancels it when the 143 * condition changes to `true`. 144 * 145 * <p>Doesn't re-start the command if it ends while the condition is still `false`. 146 * 147 * @param command the command to start 148 * @return this trigger, so calls can be chained 149 */ 150 public Trigger whileFalse(Command command) { 151 requireNonNullParam(command, "command", "whileFalse"); 152 addBinding(BindingType.RUN_WHILE_LOW, command); 153 return this; 154 } 155 156 /** 157 * Toggles a command when the condition changes from `false` to `true`. 158 * 159 * @param command the command to toggle 160 * @return this trigger, so calls can be chained 161 */ 162 public Trigger toggleOnTrue(Command command) { 163 requireNonNullParam(command, "command", "toggleOnTrue"); 164 addBinding(BindingType.TOGGLE_ON_RISING_EDGE, command); 165 return this; 166 } 167 168 /** 169 * Toggles a command when the condition changes from `true` to `false`. 170 * 171 * @param command the command to toggle 172 * @return this trigger, so calls can be chained 173 */ 174 public Trigger toggleOnFalse(Command command) { 175 requireNonNullParam(command, "command", "toggleOnFalse"); 176 addBinding(BindingType.TOGGLE_ON_FALLING_EDGE, command); 177 return this; 178 } 179 180 @Override 181 public boolean getAsBoolean() { 182 return m_condition.getAsBoolean(); 183 } 184 185 /** 186 * Composes two triggers with logical AND. 187 * 188 * @param trigger the condition to compose with 189 * @return A trigger which is active when both component triggers are active. 190 */ 191 public Trigger and(BooleanSupplier trigger) { 192 return new Trigger( 193 m_scheduler, m_loop, () -> m_condition.getAsBoolean() && trigger.getAsBoolean()); 194 } 195 196 /** 197 * Composes two triggers with logical OR. 198 * 199 * @param trigger the condition to compose with 200 * @return A trigger which is active when either component trigger is active. 201 */ 202 public Trigger or(BooleanSupplier trigger) { 203 return new Trigger( 204 m_scheduler, m_loop, () -> m_condition.getAsBoolean() || trigger.getAsBoolean()); 205 } 206 207 /** 208 * Creates a new trigger that is active when this trigger is inactive, i.e. that acts as the 209 * negation of this trigger. 210 * 211 * @return the negated trigger 212 */ 213 public Trigger negate() { 214 return new Trigger(m_scheduler, m_loop, () -> !m_condition.getAsBoolean()); 215 } 216 217 /** 218 * Creates a new debounced trigger from this trigger - it will become active when this trigger has 219 * been active for longer than the specified period. 220 * 221 * @param duration The debounce period. 222 * @return The debounced trigger (rising edges debounced only) 223 */ 224 public Trigger debounce(Time duration) { 225 return debounce(duration, Debouncer.DebounceType.kRising); 226 } 227 228 /** 229 * Creates a new debounced trigger from this trigger - it will become active when this trigger has 230 * been active for longer than the specified period. 231 * 232 * @param duration The debounce period. 233 * @param type The debounce type. 234 * @return The debounced trigger. 235 */ 236 public Trigger debounce(Time duration, Debouncer.DebounceType type) { 237 var debouncer = new Debouncer(duration.in(Seconds), type); 238 return new Trigger(m_scheduler, m_loop, () -> debouncer.calculate(m_condition.getAsBoolean())); 239 } 240 241 private void poll() { 242 // Clear bindings that no longer need to run 243 // This should always be checked, regardless of signal change, since bindings may be scoped 244 // and those scopes may become inactive. 245 clearStaleBindings(); 246 247 var signal = readSignal(); 248 249 if (signal == m_previousSignal) { 250 // No change in the signal. Nothing to do 251 return; 252 } 253 254 if (signal == Signal.HIGH) { 255 // Signal is now high when it wasn't before - a rising edge 256 scheduleBindings(BindingType.SCHEDULE_ON_RISING_EDGE); 257 scheduleBindings(BindingType.RUN_WHILE_HIGH); 258 cancelBindings(BindingType.RUN_WHILE_LOW); 259 toggleBindings(BindingType.TOGGLE_ON_RISING_EDGE); 260 } 261 262 if (signal == Signal.LOW) { 263 // Signal is now low when it wasn't before - a falling edge 264 scheduleBindings(BindingType.SCHEDULE_ON_FALLING_EDGE); 265 scheduleBindings(BindingType.RUN_WHILE_LOW); 266 cancelBindings(BindingType.RUN_WHILE_HIGH); 267 toggleBindings(BindingType.TOGGLE_ON_FALLING_EDGE); 268 } 269 270 m_previousSignal = signal; 271 } 272 273 private Signal readSignal() { 274 if (m_condition.getAsBoolean()) { 275 return Signal.HIGH; 276 } else { 277 return Signal.LOW; 278 } 279 } 280 281 /** Removes bindings in inactive scopes. Running commands tied to those bindings are canceled. */ 282 private void clearStaleBindings() { 283 m_bindings.forEach( 284 (_bindingType, bindings) -> { 285 for (var iterator = bindings.iterator(); iterator.hasNext(); ) { 286 var binding = iterator.next(); 287 if (binding.scope().active()) { 288 // This binding's scope is still active, leave it running 289 continue; 290 } 291 292 // The scope is no long active. Remove the binding and immediately cancel its command. 293 iterator.remove(); 294 m_scheduler.cancel(binding.command()); 295 } 296 }); 297 } 298 299 /** 300 * Schedules all commands bound to the given binding type. 301 * 302 * @param bindingType the binding type to schedule 303 */ 304 private void scheduleBindings(BindingType bindingType) { 305 m_bindings.getOrDefault(bindingType, List.of()).forEach(m_scheduler::schedule); 306 } 307 308 /** 309 * Cancels all commands bound to the given binding type. 310 * 311 * @param bindingType the binding type to cancel 312 */ 313 private void cancelBindings(BindingType bindingType) { 314 m_bindings 315 .getOrDefault(bindingType, List.of()) 316 .forEach(binding -> m_scheduler.cancel(binding.command())); 317 } 318 319 /** 320 * Toggles all commands bound to the given binding type. If a command is currently scheduled or 321 * running, it will be canceled; otherwise, it will be scheduled. 322 * 323 * @param bindingType the binding type to cancel 324 */ 325 private void toggleBindings(BindingType bindingType) { 326 m_bindings 327 .getOrDefault(bindingType, List.of()) 328 .forEach( 329 binding -> { 330 var command = binding.command(); 331 if (m_scheduler.isScheduledOrRunning(command)) { 332 m_scheduler.cancel(command); 333 } else { 334 m_scheduler.schedule(binding); 335 } 336 }); 337 } 338 339 // package-private for testing 340 void addBinding(BindingScope scope, BindingType bindingType, Command command) { 341 // Note: we use a throwable here instead of Thread.currentThread().getStackTrace() for easier 342 // stack frame filtering and modification. 343 m_bindings 344 .computeIfAbsent(bindingType, _k -> new ArrayList<>()) 345 .add(new Binding(scope, bindingType, command, new Throwable().getStackTrace())); 346 347 if (!m_isBoundToEventLoop) { 348 m_loop.bind(m_eventLoopCallback); 349 m_isBoundToEventLoop = true; 350 } 351 } 352 353 private void addBinding(BindingType bindingType, Command command) { 354 BindingScope scope = 355 switch (m_scheduler.currentCommand()) { 356 case Command c -> { 357 // A command is creating a binding - make it scoped to that specific command 358 yield BindingScope.forCommand(m_scheduler, c); 359 } 360 case null -> { 361 // Creating a binding outside a command - it's global in scope 362 yield BindingScope.global(); 363 } 364 }; 365 366 addBinding(scope, bindingType, command); 367 } 368}