mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-19 00:41:43 +00:00
[cmd3] Add rising and falling edge trigger factories (#8366)
`Trigger.getAsBoolean()` behavior has been changed from passing through the underlying boolean supplier to returning the latest cached signal as determined by the most recent call to `poll()`. This allows rising and falling edge triggers to have a consistent return value over an entire polling cycle, rather than only being high for the _first_ check in a cycle. Closes #8309
This commit is contained in:
@@ -4,12 +4,14 @@
|
||||
|
||||
package org.wpilib.command3;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertAll;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class TriggerTest extends CommandTestBase {
|
||||
@@ -276,4 +278,369 @@ class TriggerTest extends CommandTestBase {
|
||||
assertEquals(List.of(), m_scheduler.getRunningCommands().stream().map(Command::name).toList());
|
||||
assertFalse(triggeredCommandRan.get(), "Command was unexpectedly triggered");
|
||||
}
|
||||
|
||||
@Test
|
||||
void risingEdge() {
|
||||
var signal = new AtomicBoolean(false);
|
||||
var baseTrigger = new Trigger(m_scheduler, signal::get);
|
||||
var risingEdgeTrigger = baseTrigger.risingEdge();
|
||||
|
||||
assertAll(
|
||||
"Signals start null",
|
||||
() -> assertEquals(null, baseTrigger.getCachedSignal()),
|
||||
() -> assertEquals(null, baseTrigger.getPreviousSignal()),
|
||||
() -> assertEquals(null, risingEdgeTrigger.getCachedSignal()),
|
||||
() -> assertEquals(null, risingEdgeTrigger.getPreviousSignal()));
|
||||
|
||||
m_scheduler.run();
|
||||
assertAll(
|
||||
"First run (base signal stays low)",
|
||||
() -> assertEquals(Trigger.Signal.LOW, baseTrigger.getCachedSignal()),
|
||||
() -> assertEquals(null, baseTrigger.getPreviousSignal()),
|
||||
() -> assertEquals(Trigger.Signal.LOW, risingEdgeTrigger.getCachedSignal()),
|
||||
() -> assertEquals(null, risingEdgeTrigger.getPreviousSignal()));
|
||||
|
||||
signal.set(true);
|
||||
m_scheduler.run();
|
||||
assertAll(
|
||||
"Second run (base signal goes high)",
|
||||
() -> assertEquals(Trigger.Signal.HIGH, baseTrigger.getCachedSignal()),
|
||||
() -> assertEquals(Trigger.Signal.LOW, baseTrigger.getPreviousSignal()),
|
||||
() ->
|
||||
assertEquals(
|
||||
Trigger.Signal.HIGH,
|
||||
risingEdgeTrigger.getCachedSignal(),
|
||||
"Rising edge trigger did not go high"),
|
||||
() -> assertEquals(Trigger.Signal.LOW, risingEdgeTrigger.getPreviousSignal()));
|
||||
|
||||
m_scheduler.run();
|
||||
assertAll(
|
||||
"Third run (base signal stays high)",
|
||||
() -> assertEquals(Trigger.Signal.HIGH, baseTrigger.getCachedSignal()),
|
||||
() -> assertEquals(Trigger.Signal.HIGH, baseTrigger.getPreviousSignal()),
|
||||
() ->
|
||||
assertEquals(
|
||||
Trigger.Signal.LOW,
|
||||
risingEdgeTrigger.getCachedSignal(),
|
||||
"Rising edge trigger did not go low"),
|
||||
() ->
|
||||
assertEquals(
|
||||
Trigger.Signal.HIGH,
|
||||
risingEdgeTrigger.getPreviousSignal(),
|
||||
"Rising edge trigger was not previously high"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallingEdge() {
|
||||
var signal = new AtomicBoolean(false);
|
||||
var baseTrigger = new Trigger(m_scheduler, signal::get);
|
||||
var fallingEdgeTrigger = baseTrigger.fallingEdge();
|
||||
|
||||
assertAll(
|
||||
"Signals start null",
|
||||
() -> assertEquals(null, baseTrigger.getCachedSignal()),
|
||||
() -> assertEquals(null, baseTrigger.getPreviousSignal()),
|
||||
() -> assertEquals(null, fallingEdgeTrigger.getCachedSignal()),
|
||||
() -> assertEquals(null, fallingEdgeTrigger.getPreviousSignal()));
|
||||
|
||||
m_scheduler.run();
|
||||
assertAll(
|
||||
"First run (base signal stays low)",
|
||||
() -> assertEquals(Trigger.Signal.LOW, baseTrigger.getCachedSignal()),
|
||||
() -> assertEquals(null, baseTrigger.getPreviousSignal()),
|
||||
() -> assertEquals(Trigger.Signal.LOW, fallingEdgeTrigger.getCachedSignal()),
|
||||
() -> assertEquals(null, fallingEdgeTrigger.getPreviousSignal()));
|
||||
|
||||
signal.set(true);
|
||||
m_scheduler.run();
|
||||
assertAll(
|
||||
"Second run (base signal goes high)",
|
||||
() -> assertEquals(Trigger.Signal.HIGH, baseTrigger.getCachedSignal()),
|
||||
() -> assertEquals(Trigger.Signal.LOW, baseTrigger.getPreviousSignal()),
|
||||
() -> assertEquals(Trigger.Signal.LOW, fallingEdgeTrigger.getCachedSignal()),
|
||||
() -> assertEquals(Trigger.Signal.LOW, fallingEdgeTrigger.getPreviousSignal()));
|
||||
|
||||
signal.set(false);
|
||||
m_scheduler.run();
|
||||
assertAll(
|
||||
"Third run (base signal goes low)",
|
||||
() -> assertEquals(Trigger.Signal.LOW, baseTrigger.getCachedSignal()),
|
||||
() -> assertEquals(Trigger.Signal.HIGH, baseTrigger.getPreviousSignal()),
|
||||
() ->
|
||||
assertEquals(
|
||||
Trigger.Signal.HIGH,
|
||||
fallingEdgeTrigger.getCachedSignal(),
|
||||
"Falling edge trigger did not go high"),
|
||||
() ->
|
||||
assertEquals(
|
||||
Trigger.Signal.LOW,
|
||||
fallingEdgeTrigger.getPreviousSignal(),
|
||||
"Falling edge trigger was not previously low"));
|
||||
|
||||
m_scheduler.run();
|
||||
assertAll(
|
||||
"Fourth run (base signal stays low)",
|
||||
() -> assertEquals(Trigger.Signal.LOW, baseTrigger.getCachedSignal()),
|
||||
() -> assertEquals(Trigger.Signal.LOW, baseTrigger.getPreviousSignal()),
|
||||
() ->
|
||||
assertEquals(
|
||||
Trigger.Signal.LOW,
|
||||
fallingEdgeTrigger.getCachedSignal(),
|
||||
"Falling edge trigger did not go low"),
|
||||
() ->
|
||||
assertEquals(
|
||||
Trigger.Signal.HIGH,
|
||||
fallingEdgeTrigger.getPreviousSignal(),
|
||||
"Falling edge trigger was not previously high"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void ensureBoundBindsDependencies() {
|
||||
var a = new AtomicBoolean(false);
|
||||
var b = new AtomicBoolean(false);
|
||||
|
||||
var baseA = new Trigger(m_scheduler, a::get);
|
||||
var baseB = new Trigger(m_scheduler, b::get);
|
||||
|
||||
// Compose a trigger that depends on an intermediate, unbound risingEdge() trigger
|
||||
var composed = baseA.and(baseB.risingEdge());
|
||||
|
||||
var command = Command.noRequirements(Coroutine::park).named("Cmd");
|
||||
// Bind only the composed trigger; ensureBound() must bind dependencies first so polling order
|
||||
// updates base triggers before evaluating the composed condition.
|
||||
composed.onTrue(command);
|
||||
|
||||
// First run initializes all signals to LOW
|
||||
m_scheduler.run();
|
||||
assertFalse(
|
||||
m_scheduler.isRunning(command), "Command should not run on first initialization run");
|
||||
|
||||
// Cause both conditions to be true in the same cycle: A is true, and B has a rising edge
|
||||
a.set(true);
|
||||
b.set(true);
|
||||
m_scheduler.run();
|
||||
|
||||
assertTrue(
|
||||
m_scheduler.isRunning(command),
|
||||
"Top-level composed trigger did not fire when dependency rising edge occurred");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ensureBoundDeeplyNestedDependencies() {
|
||||
var a = new AtomicBoolean(false);
|
||||
var b = new AtomicBoolean(false);
|
||||
var c = new AtomicBoolean(false);
|
||||
|
||||
var baseA = new Trigger(m_scheduler, a::get);
|
||||
var baseB = new Trigger(m_scheduler, b::get);
|
||||
var baseC = new Trigger(m_scheduler, c::get);
|
||||
|
||||
// Two levels of nesting: baseA AND (baseB.risingEdge() AND baseC.risingEdge())
|
||||
final var nested = baseA.and(baseB.risingEdge().and(baseC.risingEdge()));
|
||||
|
||||
// Initialize signals
|
||||
m_scheduler.run();
|
||||
|
||||
// Trigger both rising edges and set A high in the same cycle
|
||||
a.set(true);
|
||||
b.set(true);
|
||||
c.set(true);
|
||||
m_scheduler.run();
|
||||
|
||||
assertTrue(
|
||||
nested.getAsBoolean(),
|
||||
"Deeply nested composed trigger did not fire; dependencies may not have been bound first");
|
||||
}
|
||||
|
||||
@Test
|
||||
void composedAnd() {
|
||||
var signalA = new AtomicBoolean(false);
|
||||
var signalB = new AtomicBoolean(false);
|
||||
var triggerA = new Trigger(m_scheduler, flickering(signalA));
|
||||
var triggerB = new Trigger(m_scheduler, flickering(signalB));
|
||||
|
||||
var andTrigger = triggerA.and(triggerB);
|
||||
|
||||
m_scheduler.run();
|
||||
assertFalse(andTrigger.getAsBoolean());
|
||||
|
||||
signalA.set(true);
|
||||
m_scheduler.run();
|
||||
assertFalse(andTrigger.getAsBoolean());
|
||||
|
||||
signalA.set(true);
|
||||
signalB.set(true);
|
||||
m_scheduler.run();
|
||||
assertTrue(andTrigger.getAsBoolean());
|
||||
|
||||
signalA.set(false);
|
||||
m_scheduler.run();
|
||||
assertFalse(andTrigger.getAsBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void composedAndWithSupplier() {
|
||||
var signalA = new AtomicBoolean(false);
|
||||
var signalB = new AtomicBoolean(false);
|
||||
var triggerA = new Trigger(m_scheduler, flickering(signalA));
|
||||
|
||||
var andTrigger = triggerA.and(flickering(signalB));
|
||||
|
||||
m_scheduler.run();
|
||||
assertFalse(andTrigger.getAsBoolean());
|
||||
|
||||
signalA.set(true);
|
||||
signalB.set(true);
|
||||
m_scheduler.run();
|
||||
assertTrue(andTrigger.getAsBoolean());
|
||||
|
||||
signalB.set(false);
|
||||
m_scheduler.run();
|
||||
assertFalse(andTrigger.getAsBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void composedOr() {
|
||||
var signalA = new AtomicBoolean(false);
|
||||
var signalB = new AtomicBoolean(false);
|
||||
var triggerA = new Trigger(m_scheduler, flickering(signalA));
|
||||
var triggerB = new Trigger(m_scheduler, flickering(signalB));
|
||||
|
||||
var orTrigger = triggerA.or(triggerB);
|
||||
|
||||
m_scheduler.run();
|
||||
assertFalse(orTrigger.getAsBoolean());
|
||||
|
||||
signalA.set(true);
|
||||
m_scheduler.run();
|
||||
assertTrue(orTrigger.getAsBoolean());
|
||||
|
||||
signalA.set(false);
|
||||
m_scheduler.run();
|
||||
assertFalse(orTrigger.getAsBoolean());
|
||||
|
||||
signalB.set(true);
|
||||
m_scheduler.run();
|
||||
assertTrue(orTrigger.getAsBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void composedOrWithSupplier() {
|
||||
var signalA = new AtomicBoolean(false);
|
||||
var signalB = new AtomicBoolean(false);
|
||||
var triggerA = new Trigger(m_scheduler, flickering(signalA));
|
||||
|
||||
var orTrigger = triggerA.or(flickering(signalB));
|
||||
|
||||
m_scheduler.run();
|
||||
assertFalse(orTrigger.getAsBoolean());
|
||||
|
||||
signalB.set(true);
|
||||
m_scheduler.run();
|
||||
assertTrue(orTrigger.getAsBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void composedNegate() {
|
||||
var signal = new AtomicBoolean(false);
|
||||
var trigger = new Trigger(m_scheduler, flickering(signal));
|
||||
|
||||
var negated = trigger.negate();
|
||||
|
||||
m_scheduler.run();
|
||||
assertTrue(negated.getAsBoolean());
|
||||
|
||||
signal.set(true);
|
||||
m_scheduler.run();
|
||||
assertFalse(negated.getAsBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void selfComposition() {
|
||||
var signal = new AtomicBoolean(false);
|
||||
var trigger = new Trigger(m_scheduler, flickering(signal));
|
||||
|
||||
var selfAnd = trigger.and(trigger);
|
||||
var selfOr = trigger.or(trigger);
|
||||
|
||||
m_scheduler.run();
|
||||
assertFalse(selfAnd.getAsBoolean());
|
||||
assertFalse(selfOr.getAsBoolean());
|
||||
|
||||
signal.set(true);
|
||||
m_scheduler.run();
|
||||
assertTrue(selfAnd.getAsBoolean());
|
||||
assertTrue(selfOr.getAsBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void complexComposition() {
|
||||
var signalA = new AtomicBoolean(false);
|
||||
var signalB = new AtomicBoolean(false);
|
||||
var signalC = new AtomicBoolean(false);
|
||||
var triggerA = new Trigger(m_scheduler, flickering(signalA));
|
||||
var triggerB = new Trigger(m_scheduler, flickering(signalB));
|
||||
var triggerC = new Trigger(m_scheduler, flickering(signalC));
|
||||
|
||||
// (A and B) or (not C)
|
||||
var composed = triggerA.and(triggerB).or(triggerC.negate());
|
||||
|
||||
// Initially A=F, B=F, C=F. (F and F) or (not F) -> F or T -> T
|
||||
m_scheduler.run();
|
||||
assertTrue(composed.getAsBoolean());
|
||||
|
||||
// A=T, B=T, C=T. (T and T) or (not T) -> T or F -> T
|
||||
signalA.set(true);
|
||||
signalB.set(true);
|
||||
signalC.set(true);
|
||||
m_scheduler.run();
|
||||
assertTrue(composed.getAsBoolean());
|
||||
|
||||
// A=F, B=T, C=T. (F and T) or (not T) -> F or F -> F
|
||||
signalA.set(false);
|
||||
signalC.set(true); // Ensure C is high for next run if it flickered
|
||||
m_scheduler.run();
|
||||
assertFalse(composed.getAsBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void triggerUnbindsWhenCommandScopeInactive() {
|
||||
var triggerSignal = new AtomicBoolean(false);
|
||||
var commandRan = new AtomicBoolean(false);
|
||||
var innerCommand = Command.noRequirements(_ -> commandRan.set(true)).named("Inner");
|
||||
|
||||
var outerCommand =
|
||||
Command.noRequirements(
|
||||
co -> {
|
||||
var trigger = new Trigger(m_scheduler, triggerSignal::get);
|
||||
trigger.onTrue(innerCommand);
|
||||
co.park();
|
||||
})
|
||||
.named("Outer");
|
||||
|
||||
m_scheduler.schedule(outerCommand);
|
||||
m_scheduler.run();
|
||||
assertTrue(m_scheduler.isRunning(outerCommand));
|
||||
|
||||
triggerSignal.set(true);
|
||||
m_scheduler.run();
|
||||
assertTrue(commandRan.get());
|
||||
|
||||
// Cancel outer command, trigger should now be out of scope
|
||||
m_scheduler.cancel(outerCommand);
|
||||
m_scheduler.run();
|
||||
assertFalse(m_scheduler.isRunning(outerCommand));
|
||||
|
||||
// The trigger should have unbound itself during the last run() call.
|
||||
}
|
||||
|
||||
private BooleanSupplier flickering(AtomicBoolean signal) {
|
||||
return () -> {
|
||||
boolean val = signal.get();
|
||||
if (val) {
|
||||
signal.set(false);
|
||||
}
|
||||
return val;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user