mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-07-02 02:51:42 +00:00
[wpilib] Make BooleanEvent more consistent (#5436)
Co-authored-by: Tyler Veness <calcmogul@gmail.com> Co-authored-by: Joseph Eng <91924258+KangarooKoala@users.noreply.github.com>
This commit is contained in:
@@ -7,56 +7,61 @@
|
||||
using namespace frc;
|
||||
|
||||
BooleanEvent::BooleanEvent(EventLoop* loop, std::function<bool()> condition)
|
||||
: m_loop(loop), m_condition(std::move(condition)) {}
|
||||
: m_loop(loop), m_condition(std::move(condition)) {
|
||||
m_state = std::make_shared<bool>(m_condition());
|
||||
m_loop->Bind(
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
[condition = m_condition, state = m_state] { *state = condition(); });
|
||||
}
|
||||
|
||||
BooleanEvent::operator std::function<bool()>() {
|
||||
return m_condition;
|
||||
return [state = m_state] { return *state; };
|
||||
}
|
||||
|
||||
bool BooleanEvent::GetAsBoolean() const {
|
||||
return m_condition();
|
||||
return *m_state;
|
||||
}
|
||||
|
||||
void BooleanEvent::IfHigh(std::function<void()> action) {
|
||||
m_loop->Bind([condition = m_condition, action = std::move(action)] {
|
||||
if (condition()) {
|
||||
m_loop->Bind([state = m_state, action = std::move(action)] {
|
||||
if (*state) {
|
||||
action();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
BooleanEvent BooleanEvent::operator!() {
|
||||
return BooleanEvent(this->m_loop, [lhs = m_condition] { return !lhs(); });
|
||||
return BooleanEvent(this->m_loop, [state = m_state] { return !*state; });
|
||||
}
|
||||
|
||||
BooleanEvent BooleanEvent::operator&&(std::function<bool()> rhs) {
|
||||
return BooleanEvent(this->m_loop,
|
||||
[lhs = m_condition, rhs] { return lhs() && rhs(); });
|
||||
[state = m_state, rhs] { return *state && rhs(); });
|
||||
}
|
||||
|
||||
BooleanEvent BooleanEvent::operator||(std::function<bool()> rhs) {
|
||||
return BooleanEvent(this->m_loop,
|
||||
[lhs = m_condition, rhs] { return lhs() || rhs(); });
|
||||
[state = m_state, rhs] { return *state || rhs(); });
|
||||
}
|
||||
|
||||
BooleanEvent BooleanEvent::Rising() {
|
||||
return BooleanEvent(
|
||||
this->m_loop, [lhs = m_condition, m_previous = m_condition()]() mutable {
|
||||
bool present = lhs();
|
||||
bool past = m_previous;
|
||||
m_previous = present;
|
||||
return !past && present;
|
||||
});
|
||||
return BooleanEvent(this->m_loop,
|
||||
[state = m_state, m_previous = *m_state]() mutable {
|
||||
bool present = *state;
|
||||
bool past = m_previous;
|
||||
m_previous = present;
|
||||
return !past && present;
|
||||
});
|
||||
}
|
||||
|
||||
BooleanEvent BooleanEvent::Falling() {
|
||||
return BooleanEvent(
|
||||
this->m_loop, [lhs = m_condition, m_previous = m_condition()]() mutable {
|
||||
bool present = lhs();
|
||||
bool past = m_previous;
|
||||
m_previous = present;
|
||||
return past && !present;
|
||||
});
|
||||
return BooleanEvent(this->m_loop,
|
||||
[state = m_state, m_previous = *m_state]() mutable {
|
||||
bool present = *state;
|
||||
bool past = m_previous;
|
||||
m_previous = present;
|
||||
return past && !present;
|
||||
});
|
||||
}
|
||||
|
||||
BooleanEvent BooleanEvent::Debounce(units::second_t debounceTime,
|
||||
@@ -64,5 +69,5 @@ BooleanEvent BooleanEvent::Debounce(units::second_t debounceTime,
|
||||
return BooleanEvent(
|
||||
this->m_loop,
|
||||
[debouncer = frc::Debouncer(debounceTime, type),
|
||||
lhs = m_condition]() mutable { return debouncer.Calculate(lhs()); });
|
||||
state = m_state]() mutable { return debouncer.Calculate(*state); });
|
||||
}
|
||||
|
||||
@@ -40,9 +40,10 @@ class BooleanEvent {
|
||||
BooleanEvent(EventLoop* loop, std::function<bool()> condition);
|
||||
|
||||
/**
|
||||
* Check whether this event is active or not.
|
||||
* Check whether this event is active or not as of the last loop poll.
|
||||
*
|
||||
* @return true if active.
|
||||
* @return true if active, false if not active. If the event was never polled,
|
||||
* it returns the state at event construction.
|
||||
*/
|
||||
bool GetAsBoolean() const;
|
||||
|
||||
@@ -69,7 +70,7 @@ class BooleanEvent {
|
||||
[](EventLoop* loop, std::function<bool()> condition) {
|
||||
return T(loop, condition);
|
||||
}) {
|
||||
return ctor(m_loop, m_condition);
|
||||
return ctor(m_loop, [state = m_state] { return *state; });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +85,8 @@ class BooleanEvent {
|
||||
* Composes this event with another event, returning a new event that is
|
||||
* active when both events are active.
|
||||
*
|
||||
* <p>The new event will use this event's polling loop.
|
||||
* <p>The events must use the same event loop. If the events use different
|
||||
* event loops, the composed signal won't update until both loops are polled.
|
||||
*
|
||||
* @param rhs the event to compose with
|
||||
* @return the event that is active when both events are active
|
||||
@@ -95,7 +97,8 @@ class BooleanEvent {
|
||||
* Composes this event with another event, returning a new event that is
|
||||
* active when either event is active.
|
||||
*
|
||||
* <p>The new event will use this event's polling loop.
|
||||
* <p>The events must use the same event loop. If the events use different
|
||||
* event loops, the composed signal won't update until both loops are polled.
|
||||
*
|
||||
* @param rhs the event to compose with
|
||||
* @return the event that is active when either event is active
|
||||
@@ -131,5 +134,6 @@ class BooleanEvent {
|
||||
private:
|
||||
EventLoop* m_loop;
|
||||
std::function<bool()> m_condition;
|
||||
std::shared_ptr<bool> m_state; // A programmer's worst nightmare.
|
||||
};
|
||||
} // namespace frc
|
||||
|
||||
487
wpilibc/src/test/native/cpp/event/BooleanEventTest.cpp
Normal file
487
wpilibc/src/test/native/cpp/event/BooleanEventTest.cpp
Normal file
@@ -0,0 +1,487 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "frc/event/BooleanEvent.h"
|
||||
#include "frc/event/EventLoop.h"
|
||||
|
||||
using namespace frc;
|
||||
|
||||
TEST(BooleanEventTest, BinaryCompositions) {
|
||||
EventLoop loop;
|
||||
int andCounter = 0;
|
||||
int orCounter = 0;
|
||||
|
||||
EXPECT_EQ(0, andCounter);
|
||||
EXPECT_EQ(0, orCounter);
|
||||
|
||||
(BooleanEvent(&loop, [] { return true; }) && BooleanEvent(&loop, [] {
|
||||
return false;
|
||||
})).IfHigh([&] { ++andCounter; });
|
||||
(BooleanEvent(&loop, [] { return true; }) || BooleanEvent(&loop, [] {
|
||||
return false;
|
||||
})).IfHigh([&] { ++orCounter; });
|
||||
|
||||
loop.Poll();
|
||||
|
||||
EXPECT_EQ(0, andCounter);
|
||||
EXPECT_EQ(1, orCounter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that composed edge events only execute on edges (two
|
||||
* rising edge events composed with and() should only execute when both signals
|
||||
* are on the rising edge)
|
||||
*/
|
||||
TEST(BooleanEventTest, BinaryCompositionsWithEdgeDecorators) {
|
||||
EventLoop loop;
|
||||
bool boolean1 = false;
|
||||
bool boolean2 = false;
|
||||
bool boolean3 = false;
|
||||
bool boolean4 = false;
|
||||
int counter = 0;
|
||||
|
||||
auto event1 = BooleanEvent(&loop, [&] { return boolean1; }).Rising();
|
||||
auto event2 = BooleanEvent(&loop, [&] { return boolean2; }).Rising();
|
||||
auto event3 = BooleanEvent(&loop, [&] { return boolean3; }).Rising();
|
||||
auto event4 = BooleanEvent(&loop, [&] { return boolean4; }).Rising();
|
||||
(event1 && event2).IfHigh([&] { ++counter; });
|
||||
(event3 || event4).IfHigh([&] { ++counter; });
|
||||
EXPECT_EQ(0, counter);
|
||||
|
||||
boolean1 = true;
|
||||
boolean2 = true;
|
||||
boolean3 = true;
|
||||
boolean4 = true;
|
||||
loop.Poll(); // Both actions execute
|
||||
|
||||
EXPECT_EQ(2, counter);
|
||||
|
||||
loop.Poll(); // Nothing should happen since nothing is on rising edge
|
||||
|
||||
EXPECT_EQ(2, counter);
|
||||
|
||||
boolean1 = false;
|
||||
boolean2 = false;
|
||||
boolean3 = false;
|
||||
boolean4 = false;
|
||||
loop.Poll(); // Nothing should happen
|
||||
|
||||
EXPECT_EQ(2, counter);
|
||||
|
||||
boolean1 = true;
|
||||
loop.Poll(); // Nothing should happen since only Bool 1 is on rising edge
|
||||
|
||||
EXPECT_EQ(2, counter);
|
||||
|
||||
boolean2 = true;
|
||||
loop.Poll(); // Bool 2 is on rising edge, but Bool 1 isn't, nothing should
|
||||
// happen
|
||||
|
||||
EXPECT_EQ(2, counter);
|
||||
|
||||
boolean1 = false;
|
||||
boolean2 = false;
|
||||
loop.Poll(); // Nothing should happen
|
||||
|
||||
EXPECT_EQ(2, counter);
|
||||
|
||||
boolean1 = true;
|
||||
boolean2 = true;
|
||||
loop.Poll(); // Bool 1 and 2 are on rising edge, increments counter once
|
||||
|
||||
EXPECT_EQ(3, counter);
|
||||
|
||||
boolean3 = true;
|
||||
loop.Poll(); // Bool 3 is on rising edge, increments counter once
|
||||
|
||||
EXPECT_EQ(4, counter);
|
||||
|
||||
loop.Poll(); // Nothing should happen, Bool 3 isn't on rising edge
|
||||
|
||||
EXPECT_EQ(4, counter);
|
||||
|
||||
boolean4 = true;
|
||||
loop.Poll(); // Bool 4 is on rising edge, increments counter once
|
||||
|
||||
EXPECT_EQ(5, counter);
|
||||
|
||||
loop.Poll(); // Nothing should happen, Bool 4 isn't on rising edge
|
||||
|
||||
EXPECT_EQ(5, counter);
|
||||
}
|
||||
|
||||
TEST(BooleanEventTest, BinaryCompositionLoopSemantics) {
|
||||
EventLoop loop1;
|
||||
EventLoop loop2;
|
||||
bool boolean1 = true;
|
||||
bool boolean2 = true;
|
||||
int counter1 = 0;
|
||||
int counter2 = 0;
|
||||
|
||||
(BooleanEvent(&loop1, [&] { return boolean1; }) && BooleanEvent(&loop2, [&] {
|
||||
return boolean2;
|
||||
})).IfHigh([&] { ++counter1; });
|
||||
(BooleanEvent(&loop2, [&] { return boolean2; }) && BooleanEvent(&loop1, [&] {
|
||||
return boolean1;
|
||||
})).IfHigh([&] { ++counter2; });
|
||||
|
||||
EXPECT_EQ(0, counter1);
|
||||
EXPECT_EQ(0, counter2);
|
||||
|
||||
loop1
|
||||
.Poll(); // 1st event executes, Bool 1 and 2 are true, increments counter
|
||||
|
||||
EXPECT_EQ(1, counter1);
|
||||
EXPECT_EQ(0, counter2);
|
||||
|
||||
loop2
|
||||
.Poll(); // 2nd event executes, Bool 1 and 2 are true, increments counter
|
||||
|
||||
EXPECT_EQ(1, counter1);
|
||||
EXPECT_EQ(1, counter2);
|
||||
|
||||
boolean2 = false;
|
||||
loop1.Poll(); // 1st event executes, Bool 2 is still true because loop 2
|
||||
// hasn't updated it, increments counter
|
||||
|
||||
EXPECT_EQ(2, counter1);
|
||||
EXPECT_EQ(1, counter2);
|
||||
|
||||
loop2.Poll(); // 2nd event executes, Bool 2 is now false because this loop
|
||||
// updated it, does nothing
|
||||
|
||||
EXPECT_EQ(2, counter1);
|
||||
EXPECT_EQ(1, counter2);
|
||||
|
||||
loop1.Poll(); // All bools are updated at this point, nothing should happen
|
||||
|
||||
EXPECT_EQ(2, counter1);
|
||||
EXPECT_EQ(1, counter2);
|
||||
|
||||
boolean2 = true;
|
||||
loop2.Poll(); // 2nd event executes, Bool 2 is true because this loop updated
|
||||
// it, increments counter
|
||||
|
||||
EXPECT_EQ(2, counter1);
|
||||
EXPECT_EQ(2, counter2);
|
||||
|
||||
loop1.Poll(); // 1st event executes, Bool 2 is true because loop 2 updated
|
||||
// it, increments counter
|
||||
|
||||
EXPECT_EQ(3, counter1);
|
||||
EXPECT_EQ(2, counter2);
|
||||
|
||||
boolean1 = false;
|
||||
loop2.Poll(); // 2nd event executes, Bool 1 is still true because loop 1
|
||||
// hasn't updated it, increments counter
|
||||
|
||||
EXPECT_EQ(3, counter1);
|
||||
EXPECT_EQ(3, counter2);
|
||||
|
||||
loop1.Poll(); // 1st event executes, Bool 1 is false because this loop
|
||||
// updated it, does nothing
|
||||
|
||||
EXPECT_EQ(3, counter1);
|
||||
EXPECT_EQ(3, counter2);
|
||||
|
||||
loop2.Poll(); // All bools are updated at this point, nothing should happen
|
||||
|
||||
EXPECT_EQ(3, counter1);
|
||||
EXPECT_EQ(3, counter2);
|
||||
}
|
||||
|
||||
/** Tests the order of actions bound to an event loop. */
|
||||
TEST(BooleanEventTest, PollOrdering) {
|
||||
EventLoop loop;
|
||||
bool boolean1 = true;
|
||||
bool boolean2 = true;
|
||||
bool enableAssert = false;
|
||||
int counter = 0;
|
||||
|
||||
(BooleanEvent( // This event binds an action to the event loop first
|
||||
&loop,
|
||||
[&] {
|
||||
if (enableAssert) {
|
||||
++counter;
|
||||
EXPECT_EQ(1, counter % 3);
|
||||
}
|
||||
return boolean1;
|
||||
}) && // The composed event binds an action to the event loop third
|
||||
// This event binds an action to the event loop second
|
||||
BooleanEvent(&loop, [&] {
|
||||
if (enableAssert) {
|
||||
++counter;
|
||||
EXPECT_EQ(2, counter % 3);
|
||||
}
|
||||
return boolean2;
|
||||
// This binds an action to the event loop fourth
|
||||
})).IfHigh([&] {
|
||||
if (enableAssert) {
|
||||
++counter;
|
||||
EXPECT_EQ(0, counter % 3);
|
||||
}
|
||||
});
|
||||
enableAssert = true;
|
||||
loop.Poll();
|
||||
loop.Poll();
|
||||
loop.Poll();
|
||||
loop.Poll();
|
||||
}
|
||||
|
||||
TEST(BooleanEventTest, EdgeDecorators) {
|
||||
EventLoop loop;
|
||||
bool boolean = false;
|
||||
int counter = 0;
|
||||
|
||||
BooleanEvent(&loop, [&] { return boolean; }).Falling().IfHigh([&] {
|
||||
--counter;
|
||||
});
|
||||
BooleanEvent(&loop, [&] { return boolean; }).Rising().IfHigh([&] {
|
||||
++counter;
|
||||
});
|
||||
|
||||
EXPECT_EQ(0, counter);
|
||||
|
||||
boolean = false;
|
||||
loop.Poll();
|
||||
|
||||
EXPECT_EQ(0, counter);
|
||||
|
||||
boolean = true;
|
||||
loop.Poll();
|
||||
|
||||
EXPECT_EQ(1, counter);
|
||||
|
||||
boolean = true;
|
||||
loop.Poll();
|
||||
|
||||
EXPECT_EQ(1, counter);
|
||||
|
||||
boolean = false;
|
||||
loop.Poll();
|
||||
|
||||
EXPECT_EQ(0, counter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that binding actions to the same edge event will result in all actions
|
||||
* executing.
|
||||
*/
|
||||
TEST(BooleanEventTest, EdgeReuse) {
|
||||
EventLoop loop;
|
||||
bool boolean = false;
|
||||
int counter = 0;
|
||||
|
||||
auto event = BooleanEvent(&loop, [&] { return boolean; }).Rising();
|
||||
event.IfHigh([&] { ++counter; });
|
||||
event.IfHigh([&] { ++counter; });
|
||||
|
||||
EXPECT_EQ(0, counter);
|
||||
|
||||
loop.Poll();
|
||||
|
||||
EXPECT_EQ(0, counter);
|
||||
|
||||
boolean = true;
|
||||
loop.Poll();
|
||||
|
||||
EXPECT_EQ(2, counter);
|
||||
|
||||
loop.Poll();
|
||||
|
||||
EXPECT_EQ(2, counter);
|
||||
|
||||
boolean = false;
|
||||
loop.Poll();
|
||||
|
||||
EXPECT_EQ(2, counter);
|
||||
|
||||
boolean = true;
|
||||
loop.Poll();
|
||||
|
||||
EXPECT_EQ(4, counter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that all actions execute on separate edge events constructed from the
|
||||
* original event.
|
||||
*/
|
||||
TEST(BooleanEventTest, EdgeReconstruct) {
|
||||
EventLoop loop;
|
||||
bool boolean = false;
|
||||
int counter = 0;
|
||||
|
||||
auto event = BooleanEvent(&loop, [&] { return boolean; });
|
||||
event.Rising().IfHigh([&] { ++counter; });
|
||||
event.Rising().IfHigh([&] { ++counter; });
|
||||
|
||||
EXPECT_EQ(0, counter);
|
||||
|
||||
loop.Poll();
|
||||
|
||||
EXPECT_EQ(0, counter);
|
||||
|
||||
boolean = true;
|
||||
loop.Poll();
|
||||
|
||||
EXPECT_EQ(2, counter);
|
||||
|
||||
loop.Poll();
|
||||
|
||||
EXPECT_EQ(2, counter);
|
||||
|
||||
boolean = false;
|
||||
loop.Poll();
|
||||
|
||||
EXPECT_EQ(2, counter);
|
||||
|
||||
boolean = true;
|
||||
loop.Poll();
|
||||
|
||||
EXPECT_EQ(4, counter);
|
||||
}
|
||||
|
||||
/** Tests that all actions bound to an event will still execute even if the
|
||||
* signal is changed during the loop poll */
|
||||
TEST(BooleanEventTest, MidLoopBooleanChange) {
|
||||
EventLoop loop;
|
||||
bool boolean = false;
|
||||
int counter = 0;
|
||||
|
||||
auto event = BooleanEvent(&loop, [&] { return boolean; }).Rising();
|
||||
event.IfHigh([&] {
|
||||
boolean = false;
|
||||
++counter;
|
||||
});
|
||||
event.IfHigh([&] { ++counter; });
|
||||
|
||||
EXPECT_EQ(0, counter);
|
||||
|
||||
loop.Poll();
|
||||
|
||||
EXPECT_EQ(0, counter);
|
||||
|
||||
boolean = true;
|
||||
loop.Poll();
|
||||
|
||||
EXPECT_EQ(2, counter);
|
||||
|
||||
loop.Poll();
|
||||
|
||||
EXPECT_EQ(2, counter);
|
||||
|
||||
boolean = false;
|
||||
loop.Poll();
|
||||
|
||||
EXPECT_EQ(2, counter);
|
||||
|
||||
boolean = true;
|
||||
loop.Poll();
|
||||
|
||||
EXPECT_EQ(4, counter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that all actions bound to composed events will still execute even if
|
||||
* the composed signal changes during the loop poll.
|
||||
*/
|
||||
TEST(BooleanEventTest, MidLoopBooleanChangeWithComposedEvents) {
|
||||
EventLoop loop;
|
||||
bool boolean1 = false;
|
||||
bool boolean2 = false;
|
||||
bool boolean3 = false;
|
||||
bool boolean4 = false;
|
||||
int counter = 0;
|
||||
|
||||
auto event1 = BooleanEvent(&loop, [&] { return boolean1; });
|
||||
auto event2 = BooleanEvent(&loop, [&] { return boolean2; });
|
||||
auto event3 = BooleanEvent(&loop, [&] { return boolean3; });
|
||||
auto event4 = BooleanEvent(&loop, [&] { return boolean4; });
|
||||
event1.IfHigh([&] {
|
||||
boolean2 = false;
|
||||
boolean3 = false;
|
||||
++counter;
|
||||
});
|
||||
(event3 || event4).IfHigh([&] {
|
||||
boolean1 = false;
|
||||
++counter;
|
||||
});
|
||||
(event1 && event2).IfHigh([&] {
|
||||
boolean4 = false;
|
||||
++counter;
|
||||
});
|
||||
|
||||
EXPECT_EQ(0, counter);
|
||||
|
||||
boolean1 = true;
|
||||
boolean2 = true;
|
||||
boolean3 = true;
|
||||
boolean4 = true;
|
||||
loop.Poll(); // All three actions execute, incrementing the counter three
|
||||
// times and setting all booleans to false
|
||||
|
||||
EXPECT_EQ(3, counter);
|
||||
|
||||
loop.Poll(); // Nothing should happen since everything was set to false
|
||||
|
||||
EXPECT_EQ(3, counter);
|
||||
|
||||
boolean1 = true;
|
||||
boolean2 = true;
|
||||
loop.Poll(); // Bool 1 and 2 are true, increments counter twice, Bool 2 gets
|
||||
// set to false
|
||||
|
||||
EXPECT_EQ(5, counter);
|
||||
|
||||
boolean1 = false;
|
||||
loop.Poll(); // Nothing should happen
|
||||
|
||||
EXPECT_EQ(5, counter);
|
||||
|
||||
boolean1 = true;
|
||||
boolean3 = true;
|
||||
loop.Poll(); // Bool 1 and 3 are true, increments counter twice, Bool 3 gets
|
||||
// set to false
|
||||
|
||||
EXPECT_EQ(7, counter);
|
||||
|
||||
boolean1 = false;
|
||||
boolean4 = true;
|
||||
loop.Poll(); // Bool 4 is true, increments counter once
|
||||
|
||||
EXPECT_EQ(8, counter);
|
||||
}
|
||||
|
||||
TEST(BooleanEventTest, Negation) {
|
||||
EventLoop loop;
|
||||
bool boolean = false;
|
||||
int counter = 0;
|
||||
|
||||
(!BooleanEvent(&loop, [&] { return boolean; })).IfHigh([&] { ++counter; });
|
||||
|
||||
EXPECT_EQ(0, counter);
|
||||
|
||||
loop.Poll();
|
||||
|
||||
EXPECT_EQ(1, counter);
|
||||
|
||||
boolean = true;
|
||||
loop.Poll();
|
||||
|
||||
EXPECT_EQ(1, counter);
|
||||
|
||||
boolean = false;
|
||||
loop.Poll();
|
||||
|
||||
EXPECT_EQ(2, counter);
|
||||
|
||||
boolean = true;
|
||||
loop.Poll();
|
||||
|
||||
EXPECT_EQ(2, counter);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ package edu.wpi.first.wpilibj.event;
|
||||
import static edu.wpi.first.util.ErrorMessages.requireNonNullParam;
|
||||
|
||||
import edu.wpi.first.math.filter.Debouncer;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
@@ -28,6 +29,9 @@ public class BooleanEvent implements BooleanSupplier {
|
||||
/** Condition. */
|
||||
private final BooleanSupplier m_signal;
|
||||
|
||||
/** The state of the condition in the current loop poll. Nightmare to manage. */
|
||||
private final AtomicBoolean m_state = new AtomicBoolean(false);
|
||||
|
||||
/**
|
||||
* Creates a new event with the given signal determining whether it is active.
|
||||
*
|
||||
@@ -37,16 +41,19 @@ public class BooleanEvent implements BooleanSupplier {
|
||||
public BooleanEvent(EventLoop loop, BooleanSupplier signal) {
|
||||
m_loop = requireNonNullParam(loop, "loop", "BooleanEvent");
|
||||
m_signal = requireNonNullParam(signal, "signal", "BooleanEvent");
|
||||
m_state.set(m_signal.getAsBoolean());
|
||||
m_loop.bind(() -> m_state.set(m_signal.getAsBoolean()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the state of this signal (high or low).
|
||||
* Check the state of this signal (high or low) as of the last loop poll.
|
||||
*
|
||||
* @return true for the high state, false for the low state.
|
||||
* @return true for the high state, false for the low state. If the event was never polled, it
|
||||
* returns the state at event construction.
|
||||
*/
|
||||
@Override
|
||||
public final boolean getAsBoolean() {
|
||||
return m_signal.getAsBoolean();
|
||||
return m_state.get();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,7 +64,7 @@ public class BooleanEvent implements BooleanSupplier {
|
||||
public final void ifHigh(Runnable action) {
|
||||
m_loop.bind(
|
||||
() -> {
|
||||
if (m_signal.getAsBoolean()) {
|
||||
if (m_state.get()) {
|
||||
action.run();
|
||||
}
|
||||
});
|
||||
@@ -72,11 +79,11 @@ public class BooleanEvent implements BooleanSupplier {
|
||||
return new BooleanEvent(
|
||||
m_loop,
|
||||
new BooleanSupplier() {
|
||||
private boolean m_previous = m_signal.getAsBoolean();
|
||||
private boolean m_previous = m_state.get();
|
||||
|
||||
@Override
|
||||
public boolean getAsBoolean() {
|
||||
boolean present = m_signal.getAsBoolean();
|
||||
boolean present = m_state.get();
|
||||
boolean ret = !m_previous && present;
|
||||
m_previous = present;
|
||||
return ret;
|
||||
@@ -93,11 +100,11 @@ public class BooleanEvent implements BooleanSupplier {
|
||||
return new BooleanEvent(
|
||||
m_loop,
|
||||
new BooleanSupplier() {
|
||||
private boolean m_previous = m_signal.getAsBoolean();
|
||||
private boolean m_previous = m_state.get();
|
||||
|
||||
@Override
|
||||
public boolean getAsBoolean() {
|
||||
boolean present = m_signal.getAsBoolean();
|
||||
boolean present = m_state.get();
|
||||
boolean ret = m_previous && !present;
|
||||
m_previous = present;
|
||||
return ret;
|
||||
@@ -132,7 +139,7 @@ public class BooleanEvent implements BooleanSupplier {
|
||||
|
||||
@Override
|
||||
public boolean getAsBoolean() {
|
||||
return m_debouncer.calculate(m_signal.getAsBoolean());
|
||||
return m_debouncer.calculate(m_state.get());
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -144,35 +151,37 @@ public class BooleanEvent implements BooleanSupplier {
|
||||
* @return the negated event
|
||||
*/
|
||||
public BooleanEvent negate() {
|
||||
return new BooleanEvent(m_loop, () -> !m_signal.getAsBoolean());
|
||||
return new BooleanEvent(m_loop, () -> !m_state.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Composes this event with another event, returning a new signal that is in the high state when
|
||||
* both signals are in the high state.
|
||||
*
|
||||
* <p>The new event will use this event's polling loop.
|
||||
* <p>The events must use the same event loop. If the events use different event loops, the
|
||||
* composed signal won't update until both loops are polled.
|
||||
*
|
||||
* @param other the event to compose with
|
||||
* @return the event that is active when both events are active
|
||||
*/
|
||||
public BooleanEvent and(BooleanSupplier other) {
|
||||
requireNonNullParam(other, "other", "and");
|
||||
return new BooleanEvent(m_loop, () -> m_signal.getAsBoolean() && other.getAsBoolean());
|
||||
return new BooleanEvent(m_loop, () -> m_state.get() && other.getAsBoolean());
|
||||
}
|
||||
|
||||
/**
|
||||
* Composes this event with another event, returning a new signal that is high when either signal
|
||||
* is high.
|
||||
*
|
||||
* <p>The new event will use this event's polling loop.
|
||||
* <p>The events must use the same event loop. If the events use different event loops, the
|
||||
* composed signal won't update until both loops are polled.
|
||||
*
|
||||
* @param other the event to compose with
|
||||
* @return a signal that is high when either signal is high.
|
||||
*/
|
||||
public BooleanEvent or(BooleanSupplier other) {
|
||||
requireNonNullParam(other, "other", "or");
|
||||
return new BooleanEvent(m_loop, () -> m_signal.getAsBoolean() || other.getAsBoolean());
|
||||
return new BooleanEvent(m_loop, () -> m_state.get() || other.getAsBoolean());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,6 +194,6 @@ public class BooleanEvent implements BooleanSupplier {
|
||||
* @return an instance of the subclass.
|
||||
*/
|
||||
public <T extends BooleanSupplier> T castTo(BiFunction<EventLoop, BooleanSupplier, T> ctor) {
|
||||
return ctor.apply(m_loop, m_signal);
|
||||
return ctor.apply(m_loop, m_state::get);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,43 +30,218 @@ class BooleanEventTest {
|
||||
assertEquals(1, orCounter.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that composed edge events only execute on edges (two rising edge events composed with
|
||||
* and() should only execute when both signals are on the rising edge).
|
||||
*/
|
||||
@Test
|
||||
void testBinaryCompositionsWithEdgeDecorators() {
|
||||
var loop = new EventLoop();
|
||||
var bool1 = new AtomicBoolean(false);
|
||||
var bool2 = new AtomicBoolean(false);
|
||||
var bool3 = new AtomicBoolean(false);
|
||||
var bool4 = new AtomicBoolean(false);
|
||||
var counter = new AtomicInteger(0);
|
||||
|
||||
var event1 = new BooleanEvent(loop, bool1::get).rising();
|
||||
var event2 = new BooleanEvent(loop, bool2::get).rising();
|
||||
var event3 = new BooleanEvent(loop, bool3::get).rising();
|
||||
var event4 = new BooleanEvent(loop, bool4::get).rising();
|
||||
event1.and(event2).ifHigh(counter::incrementAndGet);
|
||||
event3.or(event4).ifHigh(counter::incrementAndGet);
|
||||
assertEquals(0, counter.get());
|
||||
|
||||
bool1.set(true);
|
||||
bool2.set(true);
|
||||
bool3.set(true);
|
||||
bool4.set(true);
|
||||
loop.poll(); // Both actions execute
|
||||
|
||||
assertEquals(2, counter.get());
|
||||
|
||||
loop.poll(); // Nothing should happen since nothing is on rising edge
|
||||
|
||||
assertEquals(2, counter.get());
|
||||
|
||||
bool1.set(false);
|
||||
bool2.set(false);
|
||||
bool3.set(false);
|
||||
bool4.set(false);
|
||||
loop.poll(); // Nothing should happen
|
||||
|
||||
assertEquals(2, counter.get());
|
||||
|
||||
bool1.set(true);
|
||||
loop.poll(); // Nothing should happen since only Bool 1 is on rising edge
|
||||
|
||||
assertEquals(2, counter.get());
|
||||
|
||||
bool2.set(true);
|
||||
loop.poll(); // Bool 2 is on rising edge, but Bool 1 isn't, nothing should happen
|
||||
|
||||
assertEquals(2, counter.get());
|
||||
|
||||
bool1.set(false);
|
||||
bool2.set(false);
|
||||
loop.poll(); // Nothing should happen
|
||||
|
||||
assertEquals(2, counter.get());
|
||||
|
||||
bool1.set(true);
|
||||
bool2.set(true);
|
||||
loop.poll(); // Bool 1 and 2 are on rising edge, increments counter once
|
||||
|
||||
assertEquals(3, counter.get());
|
||||
|
||||
bool3.set(true);
|
||||
loop.poll(); // Bool 3 is on rising edge, increments counter once
|
||||
|
||||
assertEquals(4, counter.get());
|
||||
|
||||
loop.poll(); // Nothing should happen, Bool 3 isn't on rising edge
|
||||
|
||||
assertEquals(4, counter.get());
|
||||
|
||||
bool4.set(true);
|
||||
loop.poll(); // Bool 4 is on rising edge, increments counter once
|
||||
|
||||
assertEquals(5, counter.get());
|
||||
|
||||
loop.poll(); // Nothing should happen, Bool 4 isn't on rising edge
|
||||
|
||||
assertEquals(5, counter.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBinaryCompositionLoopSemantics() {
|
||||
var loop1 = new EventLoop();
|
||||
var loop2 = new EventLoop();
|
||||
|
||||
var bool1 = new AtomicBoolean(true);
|
||||
var bool2 = new AtomicBoolean(true);
|
||||
var counter1 = new AtomicInteger(0);
|
||||
var counter2 = new AtomicInteger(0);
|
||||
|
||||
new BooleanEvent(loop1, () -> true)
|
||||
.and(new BooleanEvent(loop2, () -> true))
|
||||
new BooleanEvent(loop1, bool1::get)
|
||||
.and(new BooleanEvent(loop2, bool2::get))
|
||||
.ifHigh(counter1::incrementAndGet);
|
||||
|
||||
new BooleanEvent(loop2, () -> true)
|
||||
.and(new BooleanEvent(loop1, () -> true))
|
||||
new BooleanEvent(loop2, bool2::get)
|
||||
.and(new BooleanEvent(loop1, bool1::get))
|
||||
.ifHigh(counter2::incrementAndGet);
|
||||
|
||||
assertEquals(0, counter1.get());
|
||||
assertEquals(0, counter2.get());
|
||||
|
||||
loop1.poll();
|
||||
loop1.poll(); // 1st event executes, Bool 1 and 2 are true, increments counter
|
||||
|
||||
assertEquals(1, counter1.get());
|
||||
assertEquals(0, counter2.get());
|
||||
|
||||
loop2.poll();
|
||||
loop2.poll(); // 2nd event executes, Bool 1 and 2 are true, increments counter
|
||||
|
||||
assertEquals(1, counter1.get());
|
||||
assertEquals(1, counter2.get());
|
||||
|
||||
bool2.set(false);
|
||||
loop1.poll(); // 1st event executes, Bool 2 is still true because loop 2 hasn't updated it,
|
||||
// increments counter
|
||||
|
||||
assertEquals(2, counter1.get());
|
||||
assertEquals(1, counter2.get());
|
||||
|
||||
loop2.poll(); // 2nd event executes, Bool 2 is now false because this loop updated it, does
|
||||
// nothing
|
||||
|
||||
assertEquals(2, counter1.get());
|
||||
assertEquals(1, counter2.get());
|
||||
|
||||
loop1.poll(); // All bools are updated at this point, nothing should happen
|
||||
|
||||
assertEquals(2, counter1.get());
|
||||
assertEquals(1, counter2.get());
|
||||
|
||||
bool2.set(true);
|
||||
loop2.poll(); // 2nd event executes, Bool 2 is true because this loop updated it, increments
|
||||
// counter
|
||||
|
||||
assertEquals(2, counter1.get());
|
||||
assertEquals(2, counter2.get());
|
||||
|
||||
loop1
|
||||
.poll(); // 1st event executes, Bool 2 is true because loop 2 updated it, increments counter
|
||||
|
||||
assertEquals(3, counter1.get());
|
||||
assertEquals(2, counter2.get());
|
||||
|
||||
bool1.set(false);
|
||||
loop2.poll(); // 2nd event executes, Bool 1 is still true because loop 1 hasn't updated it,
|
||||
// increments counter
|
||||
|
||||
assertEquals(3, counter1.get());
|
||||
assertEquals(3, counter2.get());
|
||||
|
||||
loop1.poll(); // 1st event executes, Bool 1 is false because this loop updated it, does nothing
|
||||
|
||||
assertEquals(3, counter1.get());
|
||||
assertEquals(3, counter2.get());
|
||||
|
||||
loop2.poll(); // All bools are updated at this point, nothing should happen
|
||||
|
||||
assertEquals(3, counter1.get());
|
||||
assertEquals(3, counter2.get());
|
||||
}
|
||||
|
||||
/** Tests the order of actions bound to an event loop. */
|
||||
@Test
|
||||
void testPollOrdering() {
|
||||
var loop = new EventLoop();
|
||||
var bool1 = new AtomicBoolean(true);
|
||||
var bool2 = new AtomicBoolean(true);
|
||||
var enableAssert = new AtomicBoolean(false);
|
||||
var counter = new AtomicInteger(0);
|
||||
// This event binds an action to the event loop first
|
||||
new BooleanEvent(
|
||||
loop,
|
||||
() -> {
|
||||
if (enableAssert.get()) {
|
||||
counter.incrementAndGet();
|
||||
assertEquals(1, counter.get() % 3);
|
||||
}
|
||||
return bool1.get();
|
||||
})
|
||||
// The composed event binds an action to the event loop third
|
||||
.and(
|
||||
// This event binds an action to the event loop second
|
||||
new BooleanEvent(
|
||||
loop,
|
||||
() -> {
|
||||
if (enableAssert.get()) {
|
||||
counter.incrementAndGet();
|
||||
assertEquals(2, counter.get() % 3);
|
||||
}
|
||||
return bool2.get();
|
||||
}))
|
||||
// This binds an action to the event loop fourth
|
||||
.ifHigh(
|
||||
() -> {
|
||||
if (enableAssert.get()) {
|
||||
counter.incrementAndGet();
|
||||
assertEquals(0, counter.get() % 3);
|
||||
}
|
||||
});
|
||||
enableAssert.set(true);
|
||||
loop.poll();
|
||||
loop.poll();
|
||||
loop.poll();
|
||||
loop.poll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEdgeDecorators() {
|
||||
var loop = new EventLoop();
|
||||
var bool = new AtomicBoolean(false);
|
||||
var counter = new AtomicInteger(0);
|
||||
|
||||
var loop = new EventLoop();
|
||||
|
||||
new BooleanEvent(loop, bool::get).falling().ifHigh(counter::decrementAndGet);
|
||||
new BooleanEvent(loop, bool::get).rising().ifHigh(counter::incrementAndGet);
|
||||
|
||||
@@ -93,6 +268,7 @@ class BooleanEventTest {
|
||||
assertEquals(0, counter.get());
|
||||
}
|
||||
|
||||
/** Tests that binding actions to the same edge event will result in all actions executing. */
|
||||
@Test
|
||||
void testEdgeReuse() {
|
||||
var loop = new EventLoop();
|
||||
@@ -112,23 +288,24 @@ class BooleanEventTest {
|
||||
bool.set(true);
|
||||
loop.poll();
|
||||
|
||||
assertEquals(1, counter.get()); // FIXME?: natural sense dictates counter == 2!!
|
||||
assertEquals(2, counter.get());
|
||||
|
||||
loop.poll();
|
||||
|
||||
assertEquals(1, counter.get());
|
||||
assertEquals(2, counter.get());
|
||||
|
||||
bool.set(false);
|
||||
loop.poll();
|
||||
|
||||
assertEquals(1, counter.get());
|
||||
assertEquals(2, counter.get());
|
||||
|
||||
bool.set(true);
|
||||
loop.poll();
|
||||
|
||||
assertEquals(2, counter.get());
|
||||
assertEquals(4, counter.get());
|
||||
}
|
||||
|
||||
/** Tests that all actions execute on separate edge events constructed from the original event. */
|
||||
@Test
|
||||
void testEdgeReconstruct() {
|
||||
var loop = new EventLoop();
|
||||
@@ -148,8 +325,7 @@ class BooleanEventTest {
|
||||
bool.set(true);
|
||||
loop.poll();
|
||||
|
||||
// unlike the previous test ...
|
||||
assertEquals(2, counter.get()); // as natural sense dictates, counter == 2
|
||||
assertEquals(2, counter.get());
|
||||
|
||||
loop.poll();
|
||||
|
||||
@@ -165,4 +341,155 @@ class BooleanEventTest {
|
||||
|
||||
assertEquals(4, counter.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that all actions bound to an event will still execute even if the signal is changed
|
||||
* during the loop poll.
|
||||
*/
|
||||
@Test
|
||||
void testMidLoopBooleanChange() {
|
||||
var loop = new EventLoop();
|
||||
var bool = new AtomicBoolean(false);
|
||||
var counter = new AtomicInteger(0);
|
||||
|
||||
var event = new BooleanEvent(loop, bool::get).rising();
|
||||
event.ifHigh(
|
||||
() -> {
|
||||
bool.set(false);
|
||||
counter.incrementAndGet();
|
||||
});
|
||||
event.ifHigh(counter::incrementAndGet);
|
||||
|
||||
assertEquals(0, counter.get());
|
||||
|
||||
loop.poll();
|
||||
|
||||
assertEquals(0, counter.get());
|
||||
|
||||
bool.set(true);
|
||||
loop.poll();
|
||||
|
||||
assertEquals(2, counter.get());
|
||||
|
||||
loop.poll();
|
||||
|
||||
assertEquals(2, counter.get());
|
||||
|
||||
bool.set(false);
|
||||
loop.poll();
|
||||
|
||||
assertEquals(2, counter.get());
|
||||
|
||||
bool.set(true);
|
||||
loop.poll();
|
||||
|
||||
assertEquals(4, counter.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that all actions bound to composed events will still execute even if the composed signal
|
||||
* changes during the loop poll.
|
||||
*/
|
||||
@Test
|
||||
void testMidLoopBooleanChangeWithComposedEvents() {
|
||||
var loop = new EventLoop();
|
||||
var bool1 = new AtomicBoolean(false);
|
||||
var bool2 = new AtomicBoolean(false);
|
||||
var bool3 = new AtomicBoolean(false);
|
||||
var bool4 = new AtomicBoolean(false);
|
||||
var counter = new AtomicInteger(0);
|
||||
|
||||
var event1 = new BooleanEvent(loop, bool1::get);
|
||||
var event2 = new BooleanEvent(loop, bool2::get);
|
||||
var event3 = new BooleanEvent(loop, bool3::get);
|
||||
var event4 = new BooleanEvent(loop, bool4::get);
|
||||
event1.ifHigh(
|
||||
() -> {
|
||||
bool2.set(false);
|
||||
bool3.set(false);
|
||||
counter.incrementAndGet();
|
||||
});
|
||||
event3
|
||||
.or(event4)
|
||||
.ifHigh(
|
||||
() -> {
|
||||
bool1.set(false);
|
||||
counter.incrementAndGet();
|
||||
});
|
||||
event1
|
||||
.and(event2)
|
||||
.ifHigh(
|
||||
() -> {
|
||||
bool4.set(false);
|
||||
counter.incrementAndGet();
|
||||
});
|
||||
|
||||
assertEquals(0, counter.get());
|
||||
|
||||
bool1.set(true);
|
||||
bool2.set(true);
|
||||
bool3.set(true);
|
||||
bool4.set(true);
|
||||
loop.poll(); // All three actions execute, incrementing the counter three times and setting all
|
||||
// booleans to false
|
||||
|
||||
assertEquals(3, counter.get());
|
||||
|
||||
loop.poll(); // Nothing should happen since everything was set to false
|
||||
|
||||
assertEquals(3, counter.get());
|
||||
|
||||
bool1.set(true);
|
||||
bool2.set(true);
|
||||
loop.poll(); // Bool 1 and 2 are true, increments counter twice, Bool 2 gets set to false
|
||||
|
||||
assertEquals(5, counter.get());
|
||||
|
||||
bool1.set(false);
|
||||
loop.poll(); // Nothing should happen
|
||||
|
||||
assertEquals(5, counter.get());
|
||||
|
||||
bool1.set(true);
|
||||
bool3.set(true);
|
||||
loop.poll(); // Bool 1 and 3 are true, increments counter twice, Bool 3 gets set to false
|
||||
|
||||
assertEquals(7, counter.get());
|
||||
|
||||
bool1.set(false);
|
||||
bool4.set(true);
|
||||
loop.poll(); // Bool 4 is true, increments counter once
|
||||
|
||||
assertEquals(8, counter.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNegation() {
|
||||
var loop = new EventLoop();
|
||||
var bool = new AtomicBoolean(false);
|
||||
var counter = new AtomicInteger(0);
|
||||
|
||||
new BooleanEvent(loop, bool::get).negate().ifHigh(counter::incrementAndGet);
|
||||
|
||||
assertEquals(0, counter.get());
|
||||
|
||||
loop.poll();
|
||||
|
||||
assertEquals(1, counter.get());
|
||||
|
||||
bool.set(true);
|
||||
loop.poll();
|
||||
|
||||
assertEquals(1, counter.get());
|
||||
|
||||
bool.set(false);
|
||||
loop.poll();
|
||||
|
||||
assertEquals(2, counter.get());
|
||||
|
||||
bool.set(true);
|
||||
loop.poll();
|
||||
|
||||
assertEquals(2, counter.get());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user