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 edu.wpi.first.wpilibj2.command.button;
006
007import static edu.wpi.first.util.ErrorMessages.requireNonNullParam;
008
009import edu.wpi.first.math.filter.Debouncer;
010import edu.wpi.first.math.filter.EdgeCounterFilter;
011import edu.wpi.first.wpilibj.event.EventLoop;
012import edu.wpi.first.wpilibj2.command.Command;
013import edu.wpi.first.wpilibj2.command.CommandScheduler;
014import java.util.function.BooleanSupplier;
015
016/**
017 * This class provides an easy way to link commands to conditions.
018 *
019 * <p>It is very easy to link a button to a command. For instance, you could link the trigger button
020 * of a joystick to a "score" command.
021 *
022 * <p>Triggers can easily be composed for advanced functionality using the {@link
023 * #and(BooleanSupplier)}, {@link #or(BooleanSupplier)}, {@link #negate()} operators.
024 *
025 * <p>This class is provided by the NewCommands VendorDep
026 */
027public class Trigger implements BooleanSupplier {
028  /** Functional interface for the body of a trigger binding. */
029  @FunctionalInterface
030  private interface BindingBody {
031    /**
032     * Executes the body of the binding.
033     *
034     * @param previous The previous state of the condition.
035     * @param current The current state of the condition.
036     */
037    void run(boolean previous, boolean current);
038  }
039
040  private final BooleanSupplier m_condition;
041  private final EventLoop m_loop;
042
043  /**
044   * Creates a new trigger based on the given condition.
045   *
046   * @param loop The loop instance that polls this trigger.
047   * @param condition the condition represented by this trigger
048   */
049  public Trigger(EventLoop loop, BooleanSupplier condition) {
050    m_loop = requireNonNullParam(loop, "loop", "Trigger");
051    m_condition = requireNonNullParam(condition, "condition", "Trigger");
052  }
053
054  /**
055   * Creates a new trigger based on the given condition.
056   *
057   * <p>Polled by the default scheduler button loop.
058   *
059   * @param condition the condition represented by this trigger
060   */
061  public Trigger(BooleanSupplier condition) {
062    this(CommandScheduler.getInstance().getDefaultButtonLoop(), condition);
063  }
064
065  /**
066   * Adds a binding to the EventLoop.
067   *
068   * @param body The body of the binding to add.
069   */
070  private void addBinding(BindingBody body) {
071    m_loop.bind(
072        new Runnable() {
073          private boolean m_previous = m_condition.getAsBoolean();
074
075          @Override
076          public void run() {
077            boolean current = m_condition.getAsBoolean();
078
079            body.run(m_previous, current);
080
081            m_previous = current;
082          }
083        });
084  }
085
086  /**
087   * Starts the command when the condition changes.
088   *
089   * @param command the command to start
090   * @return this trigger, so calls can be chained
091   */
092  public Trigger onChange(Command command) {
093    requireNonNullParam(command, "command", "onChange");
094    addBinding(
095        (previous, current) -> {
096          if (previous != current) {
097            CommandScheduler.getInstance().schedule(command);
098          }
099        });
100    return this;
101  }
102
103  /**
104   * Starts the given command whenever the condition changes from `false` to `true`.
105   *
106   * @param command the command to start
107   * @return this trigger, so calls can be chained
108   */
109  public Trigger onTrue(Command command) {
110    requireNonNullParam(command, "command", "onTrue");
111    addBinding(
112        (previous, current) -> {
113          if (!previous && current) {
114            CommandScheduler.getInstance().schedule(command);
115          }
116        });
117    return this;
118  }
119
120  /**
121   * Starts the given command whenever the condition changes from `true` to `false`.
122   *
123   * @param command the command to start
124   * @return this trigger, so calls can be chained
125   */
126  public Trigger onFalse(Command command) {
127    requireNonNullParam(command, "command", "onFalse");
128    addBinding(
129        (previous, current) -> {
130          if (previous && !current) {
131            CommandScheduler.getInstance().schedule(command);
132          }
133        });
134    return this;
135  }
136
137  /**
138   * Starts the given command when the condition changes to `true` and cancels it when the condition
139   * changes to `false`.
140   *
141   * <p>Doesn't re-start the command if it ends while the condition is still `true`. If the command
142   * should restart, see {@link edu.wpi.first.wpilibj2.command.RepeatCommand}.
143   *
144   * @param command the command to start
145   * @return this trigger, so calls can be chained
146   */
147  public Trigger whileTrue(Command command) {
148    requireNonNullParam(command, "command", "whileTrue");
149    addBinding(
150        (previous, current) -> {
151          if (!previous && current) {
152            CommandScheduler.getInstance().schedule(command);
153          } else if (previous && !current) {
154            command.cancel();
155          }
156        });
157    return this;
158  }
159
160  /**
161   * Starts the given command when the condition changes to `false` and cancels it when the
162   * condition changes to `true`.
163   *
164   * <p>Doesn't re-start the command if it ends while the condition is still `false`. If the command
165   * should restart, see {@link edu.wpi.first.wpilibj2.command.RepeatCommand}.
166   *
167   * @param command the command to start
168   * @return this trigger, so calls can be chained
169   */
170  public Trigger whileFalse(Command command) {
171    requireNonNullParam(command, "command", "whileFalse");
172    addBinding(
173        (previous, current) -> {
174          if (previous && !current) {
175            CommandScheduler.getInstance().schedule(command);
176          } else if (!previous && current) {
177            command.cancel();
178          }
179        });
180    return this;
181  }
182
183  /**
184   * Toggles a command when the condition changes from `false` to `true`.
185   *
186   * @param command the command to toggle
187   * @return this trigger, so calls can be chained
188   */
189  public Trigger toggleOnTrue(Command command) {
190    requireNonNullParam(command, "command", "toggleOnTrue");
191    addBinding(
192        (previous, current) -> {
193          if (!previous && current) {
194            if (command.isScheduled()) {
195              command.cancel();
196            } else {
197              CommandScheduler.getInstance().schedule(command);
198            }
199          }
200        });
201    return this;
202  }
203
204  /**
205   * Toggles a command when the condition changes from `true` to `false`.
206   *
207   * @param command the command to toggle
208   * @return this trigger, so calls can be chained
209   */
210  public Trigger toggleOnFalse(Command command) {
211    requireNonNullParam(command, "command", "toggleOnFalse");
212    addBinding(
213        (previous, current) -> {
214          if (previous && !current) {
215            if (command.isScheduled()) {
216              command.cancel();
217            } else {
218              CommandScheduler.getInstance().schedule(command);
219            }
220          }
221        });
222    return this;
223  }
224
225  @Override
226  public boolean getAsBoolean() {
227    return m_condition.getAsBoolean();
228  }
229
230  /**
231   * Composes two triggers with logical AND.
232   *
233   * @param trigger the condition to compose with
234   * @return A trigger which is active when both component triggers are active.
235   */
236  public Trigger and(BooleanSupplier trigger) {
237    return new Trigger(m_loop, () -> m_condition.getAsBoolean() && trigger.getAsBoolean());
238  }
239
240  /**
241   * Composes two triggers with logical OR.
242   *
243   * @param trigger the condition to compose with
244   * @return A trigger which is active when either component trigger is active.
245   */
246  public Trigger or(BooleanSupplier trigger) {
247    return new Trigger(m_loop, () -> m_condition.getAsBoolean() || trigger.getAsBoolean());
248  }
249
250  /**
251   * Creates a new trigger that is active when this trigger is inactive, i.e. that acts as the
252   * negation of this trigger.
253   *
254   * @return the negated trigger
255   */
256  public Trigger negate() {
257    return new Trigger(m_loop, () -> !m_condition.getAsBoolean());
258  }
259
260  /**
261   * Creates a new debounced trigger from this trigger - it will become active when this trigger has
262   * been active for longer than the specified period.
263   *
264   * @param seconds The debounce period.
265   * @return The debounced trigger (rising edges debounced only)
266   */
267  public Trigger debounce(double seconds) {
268    return debounce(seconds, Debouncer.DebounceType.kRising);
269  }
270
271  /**
272   * Creates a new debounced trigger from this trigger - it will become active when this trigger has
273   * been active for longer than the specified period.
274   *
275   * @param seconds The debounce period.
276   * @param type The debounce type.
277   * @return The debounced trigger.
278   */
279  public Trigger debounce(double seconds, Debouncer.DebounceType type) {
280    return new Trigger(
281        m_loop,
282        new BooleanSupplier() {
283          final Debouncer m_debouncer = new Debouncer(seconds, type);
284
285          @Override
286          public boolean getAsBoolean() {
287            return m_debouncer.calculate(m_condition.getAsBoolean());
288          }
289        });
290  }
291
292  /**
293   * Creates a new multi-press trigger from this trigger - it will become active when this trigger
294   * has been activated the required number of times within the specified time window.
295   *
296   * <p>This is useful for implementing "double-click" style functionality.
297   *
298   * <p>Input for this must be stable, consider using a Debouncer before this to avoid counting
299   * noise as multiple presses.
300   *
301   * @param requiredPresses The number of presses required.
302   * @param windowTime The number of seconds in which the presses must occur.
303   * @return The multi-press trigger.
304   */
305  public Trigger multiPress(int requiredPresses, double windowTime) {
306    return new Trigger(
307        m_loop,
308        new BooleanSupplier() {
309          final EdgeCounterFilter m_edgeCounterFilter =
310              new EdgeCounterFilter(requiredPresses, windowTime);
311
312          @Override
313          public boolean getAsBoolean() {
314            return m_edgeCounterFilter.calculate(m_condition.getAsBoolean());
315          }
316        });
317  }
318}