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}