RobotCore 1.0
WPILib port to raspberry Pi
Loading...
Searching...
No Matches
Trigger.java
1// Copyright (c) FIRST and other WPILib contributors.
2// Open Source Software; you can modify and/or share it under the terms of
3// the WPILib BSD license file in the root directory of this project.
4
5package org.wpilib.command3;
6
7import static org.wpilib.units.Units.Seconds;
8import static org.wpilib.util.ErrorMessages.requireNonNullParam;
9
10import java.util.ArrayDeque;
11import java.util.ArrayList;
12import java.util.Deque;
13import java.util.EnumMap;
14import java.util.List;
15import java.util.Map;
16import java.util.function.BooleanSupplier;
17import org.wpilib.event.EventLoop;
18import org.wpilib.math.filter.Debouncer;
19import org.wpilib.system.Timer;
20import org.wpilib.units.measure.Time;
21
76public class Trigger implements BooleanSupplier {
77 private final BooleanSupplier m_condition;
78 private final EventLoop m_loop;
79 private final Scheduler m_scheduler;
80 private final BindingScope m_lifetimeScope;
81
83 private Signal m_previousSignal;
84
86 private Signal m_cachedSignal;
87
88 private final Map<BindingType, List<Binding>> m_bindings = new EnumMap<>(BindingType.class);
89 private final Runnable m_eventLoopCallback = this::poll;
90
95 enum Signal {
100 }
101
109 public Trigger(Scheduler scheduler, BooleanSupplier condition) {
110 this(
111 requireNonNullParam(scheduler, "scheduler", "Trigger"),
112 scheduler.getDefaultEventLoop(),
113 requireNonNullParam(condition, "condition", "Trigger"));
114 }
115
124 public Trigger(BooleanSupplier condition) {
125 this(Scheduler.getDefault(), condition);
126 }
127
135 @SuppressWarnings("this-escape")
136 public Trigger(Scheduler scheduler, EventLoop loop, BooleanSupplier condition) {
137 m_scheduler = requireNonNullParam(scheduler, "scheduler", "Trigger");
138 m_loop = requireNonNullParam(loop, "loop", "Trigger");
139 m_condition = requireNonNullParam(condition, "condition", "Trigger");
140
141 // Treat robot mode scopes as globals for the purposes of trigger object lifetimes.
142 // Any bindings made to the trigger will still use the appropriate scopes, but this prevents
143 // bugs where a trigger created in eg `autonomousInit()` and cached, then later rebound in
144 // `teleopInit()`, would get removed from the scheduler _after_ the new binding was added in
145 // `teleopInit()` due to init functions running _before_ `robotPeriodic()`.
146 // This does open the possibility for these triggers to never be cleaned up, but robot mode
147 // changes are so infrequent that it's not a big deal.
148 m_lifetimeScope =
149 switch (BindingScope.createNarrowestScope(m_scheduler)) {
151 case BindingScope useMe -> useMe;
152 };
153
154 m_scheduler.addBoundTrigger(this);
155 m_loop.bindWeak(m_eventLoopCallback);
156 }
157
164 public Trigger onTrue(Command command) {
165 requireNonNullParam(command, "command", "onTrue");
166 addBinding(BindingType.SCHEDULE_ON_RISING_EDGE, command);
167 return this;
168 }
169
180 public Trigger ifTrue(Command command) {
181 requireNonNullParam(command, "command", "ifTrue");
182 addBinding(BindingType.SCHEDULE_WHILE_HIGH, command);
183 return this;
184 }
185
192 public Trigger onFalse(Command command) {
193 requireNonNullParam(command, "command", "onFalse");
194 addBinding(BindingType.SCHEDULE_ON_FALLING_EDGE, command);
195 return this;
196 }
197
208 public Trigger whileTrue(Command command) {
209 requireNonNullParam(command, "command", "whileTrue");
210 addBinding(BindingType.RUN_WHILE_HIGH, command);
211 return this;
212 }
213
224 public Trigger whileFalse(Command command) {
225 requireNonNullParam(command, "command", "whileFalse");
226 addBinding(BindingType.RUN_WHILE_LOW, command);
227 return this;
228 }
229
242 requireNonNullParam(command, "command", "retryWhileTrue");
244 return this;
245 }
246
259 requireNonNullParam(command, "command", "retryWhileFalse");
260 addBinding(BindingType.CONTINUOUSLY_SCHEDULE_WHILE_LOW, command);
261 return this;
262 }
263
270 public Trigger toggleOnTrue(Command command) {
271 requireNonNullParam(command, "command", "toggleOnTrue");
272 addBinding(BindingType.TOGGLE_ON_RISING_EDGE, command);
273 return this;
274 }
275
282 public Trigger toggleOnFalse(Command command) {
283 requireNonNullParam(command, "command", "toggleOnFalse");
284 addBinding(BindingType.TOGGLE_ON_FALLING_EDGE, command);
285 return this;
286 }
287
299 @Override
300 public boolean getAsBoolean() {
301 return m_cachedSignal == Signal.HIGH;
302 }
303
310 public Trigger and(BooleanSupplier trigger) {
311 return new Trigger(m_scheduler, m_loop, () -> getAsBoolean() && trigger.getAsBoolean());
312 }
313
320 public Trigger or(BooleanSupplier trigger) {
321 return new Trigger(m_scheduler, m_loop, () -> getAsBoolean() || trigger.getAsBoolean());
322 }
323
330 public Trigger negate() {
331 return new Trigger(m_scheduler, m_loop, () -> !getAsBoolean());
332 }
333
341 public Trigger debounce(Time duration) {
342 return debounce(duration, Debouncer.DebounceType.RISING);
343 }
344
353 public Trigger debounce(Time duration, Debouncer.DebounceType type) {
354 var debouncer = new Debouncer(duration.in(Seconds), type);
355 return new Trigger(m_scheduler, m_loop, () -> debouncer.calculate(getAsBoolean()));
356 }
357
369 return new Trigger(
370 m_scheduler, m_loop, () -> m_cachedSignal == Signal.HIGH && m_previousSignal == Signal.LOW);
371 }
372
384 return new Trigger(
385 m_scheduler, m_loop, () -> m_cachedSignal == Signal.LOW && m_previousSignal == Signal.HIGH);
386 }
387
398 public Trigger multiPress(int pressCount, Time duration) {
399 requireNonNullParam(duration, "duration", "multiTap");
400
401 // Short circuits to avoid unnecessary state tracking and object allocations
402 if (duration.baseUnitMagnitude() <= 0) {
403 // A nonpositive window size can never be met
404 return new Trigger(m_scheduler, m_loop, () -> false);
405 } else if (pressCount <= 0) {
406 // A nonpositive press count is always met
407 return new Trigger(m_scheduler, m_loop, () -> true);
408 }
409
410 final double durationSeconds = duration.in(Seconds);
411
412 return new Trigger(
413 m_scheduler,
414 m_loop,
415 new BooleanSupplier() {
416 // Note: unlike EdgeCounterFilter, this implementation tracks timestamps directly
417 // and remains high even if the signal is low, just as long as the number of rising edges
418 // meets the criteria. EdgeCounterFilter is only high when the requisite number of edges
419 // have been seen _and_ the signal is high.
420 private final Deque<Double> m_timestamps = new ArrayDeque<>();
421 private boolean m_risingEdgeOccurred = false;
422
423 @Override
424 public boolean getAsBoolean() {
425 if (m_cachedSignal == Signal.HIGH && m_previousSignal != Signal.HIGH) {
426 if (!m_risingEdgeOccurred) {
427 m_timestamps.addLast(Timer.getTimestamp());
428 m_risingEdgeOccurred = true;
429 }
430 } else if (m_cachedSignal != Signal.HIGH) {
431 m_risingEdgeOccurred = false;
432 }
433
434 double currentTime = Timer.getTimestamp();
435 while (!m_timestamps.isEmpty()
436 && currentTime - m_timestamps.peekFirst() > durationSeconds + 1e-9) {
437 m_timestamps.removeFirst();
438 }
439
440 return m_timestamps.size() >= pressCount;
441 }
442 });
443 }
444
445 private void poll() {
446 // Clear bindings that no longer need to run
447 // This should always be checked, regardless of signal change, since bindings may be scoped
448 // and those scopes may become inactive.
449 clearStaleBindings();
450
451 m_previousSignal = m_cachedSignal;
452 m_cachedSignal = readSignal();
453
454 // Always attempt to schedule bindings based on the current signal
455 if (m_cachedSignal == Signal.HIGH) {
456 scheduleBindings(BindingType.SCHEDULE_WHILE_HIGH);
458 } else if (m_cachedSignal == Signal.LOW) {
459 scheduleBindings(BindingType.CONTINUOUSLY_SCHEDULE_WHILE_LOW);
460 }
461
462 if (m_cachedSignal == m_previousSignal) {
463 // No change in the signal. Nothing to do
464 return;
465 }
466
467 if (m_cachedSignal == Signal.HIGH) {
468 // Signal is now high when it wasn't before - a rising edge
469 scheduleBindings(BindingType.SCHEDULE_ON_RISING_EDGE);
470 scheduleBindings(BindingType.RUN_WHILE_HIGH);
471 cancelBindings(BindingType.RUN_WHILE_LOW);
472 cancelBindings(BindingType.CONTINUOUSLY_SCHEDULE_WHILE_LOW);
473 toggleBindings(BindingType.TOGGLE_ON_RISING_EDGE);
474 }
475
476 if (m_cachedSignal == Signal.LOW) {
477 // Signal is now low when it wasn't before - a falling edge
478 scheduleBindings(BindingType.SCHEDULE_ON_FALLING_EDGE);
479 scheduleBindings(BindingType.RUN_WHILE_LOW);
480 cancelBindings(BindingType.RUN_WHILE_HIGH);
481 cancelBindings(BindingType.CONTINUOUSLY_SCHEDULE_WHILE_HIGH);
482 toggleBindings(BindingType.TOGGLE_ON_FALLING_EDGE);
483 }
484 }
485
486 private Signal readSignal() {
487 if (m_condition.getAsBoolean()) {
488 return Signal.HIGH;
489 } else {
490 return Signal.LOW;
491 }
492 }
493
495 private void clearStaleBindings() {
496 m_bindings.forEach(
497 (_bindingType, bindings) -> {
498 for (var iterator = bindings.iterator(); iterator.hasNext(); ) {
499 var binding = iterator.next();
500 if (binding.scope().active()) {
501 // This binding's scope is still active, leave it running
502 continue;
503 }
504
505 // The scope is no longer active. Remove the binding and immediately cancel its command.
506 iterator.remove();
507 m_scheduler.cancel(binding.command());
508 }
509 });
510 }
511
517 private void scheduleBindings(BindingType bindingType) {
518 m_bindings.getOrDefault(bindingType, List.of()).forEach(m_scheduler::schedule);
519 }
520
526 private void cancelBindings(BindingType bindingType) {
527 m_bindings
528 .getOrDefault(bindingType, List.of())
529 .forEach(binding -> m_scheduler.cancel(binding.command()));
530 }
531
538 private void toggleBindings(BindingType bindingType) {
539 m_bindings
540 .getOrDefault(bindingType, List.of())
541 .forEach(
542 binding -> {
543 var command = binding.command();
544 if (m_scheduler.isScheduledOrRunning(command)) {
545 m_scheduler.cancel(command);
546 } else {
547 m_scheduler.schedule(binding);
548 }
549 });
550 }
551
558 Signal getCachedSignal() {
559 return m_cachedSignal;
560 }
561
568 Signal getPreviousSignal() {
569 return m_previousSignal;
570 }
571
572 // package-private for Scheduler
573 boolean isScopeActive() {
574 return m_lifetimeScope.active();
575 }
576
577 // package-private for testing
578 void addBinding(BindingScope scope, BindingType bindingType, Command command) {
579 // Note: we use a throwable here instead of Thread.currentThread().getStackTrace() for easier
580 // stack frame filtering and modification.
581 m_bindings
582 .computeIfAbsent(bindingType, _k -> new ArrayList<>())
583 .add(new Binding(scope, bindingType, command, new Throwable()));
584 }
585
586 private void addBinding(BindingType bindingType, Command command) {
587 var scope = BindingScope.createNarrowestScope(m_scheduler);
588 addBinding(scope, bindingType, command);
589 }
590}
static Scheduler getDefault()
Trigger onFalse(Command command)
Definition Trigger.java:192
Trigger whileTrue(Command command)
Definition Trigger.java:208
Trigger ifTrue(Command command)
Definition Trigger.java:180
Trigger debounce(Time duration, Debouncer.DebounceType type)
Definition Trigger.java:353
Trigger onTrue(Command command)
Definition Trigger.java:164
Trigger whileFalse(Command command)
Definition Trigger.java:224
Trigger toggleOnTrue(Command command)
Definition Trigger.java:270
Trigger toggleOnFalse(Command command)
Definition Trigger.java:282
Trigger(BooleanSupplier condition)
Definition Trigger.java:124
Trigger(Scheduler scheduler, BooleanSupplier condition)
Definition Trigger.java:109
Trigger retryWhileFalse(Command command)
Definition Trigger.java:258
Trigger or(BooleanSupplier trigger)
Definition Trigger.java:320
Trigger multiPress(int pressCount, Time duration)
Definition Trigger.java:398
Trigger debounce(Time duration)
Definition Trigger.java:341
Trigger and(BooleanSupplier trigger)
Definition Trigger.java:310
Trigger retryWhileTrue(Command command)
Definition Trigger.java:241
Debouncer(double debounceTime, DebounceType type)
static double getTimestamp()
Definition Timer.java:26
static BindingScope createNarrowestScope(Scheduler scheduler)