blob: a5ea527b881668cc8d5e4ad32b024f5a1d6dfa1e [file] [log] [blame]
// Copyright 2024 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
#include "modules/buttons/manager.h"
#include "modules/worker/test_worker.h"
#include "pw_digital_io/digital_io.h"
#include "pw_status/status.h"
#include "pw_sync/interrupt_spin_lock.h"
#include "pw_sync/timed_thread_notification.h"
#include "pw_unit_test/framework.h"
using ::pw::chrono::SystemClock;
using ::pw::digital_io::DigitalIn;
using ::pw::digital_io::State;
using ::pw::sync::InterruptSpinLock;
using namespace std::literals::chrono_literals;
namespace sense {
constexpr const auto kMaxWaitOnFailedTest = 3s;
using pw::digital_io::DigitalInOut;
using pw::digital_io::State;
class TestDigitalInOut : public DigitalInOut {
public:
TestDigitalInOut() : state_(State::kInactive) {}
void NoisySetState(int settle_iterations, State final_state) {
std::lock_guard lock(lock_);
settle_iterations_ = settle_iterations;
final_state_ = final_state;
}
private:
pw::Status DoEnable(bool) override { return pw::OkStatus(); }
pw::Result<State> DoGetState() override {
std::lock_guard lock(lock_);
if (settle_iterations_ <= 0) {
state_ = final_state_;
} else {
state_ = state_ == State::kActive ? State::kInactive : State::kActive;
settle_iterations_ -= 1;
}
return state_;
}
pw::Status DoSetState(State state) override {
NoisySetState(0, state);
return pw::OkStatus();
}
InterruptSpinLock lock_;
State state_ PW_GUARDED_BY(lock_) = State::kInactive;
State final_state_ PW_GUARDED_BY(lock_) = State::kInactive;
int settle_iterations_ PW_GUARDED_BY(lock_) = 0;
};
// A test harness for writing tests that use pubsub.
class ManagerTest : public ::testing::Test {
public:
using PubSub = sense::GenericPubSub<Event>;
protected:
virtual void SetUp() override {
last_event_ = {};
events_processed_ = 0;
ASSERT_EQ(pw::OkStatus(), io_a_.SetState(State::kInactive));
ASSERT_EQ(pw::OkStatus(), io_b_.SetState(State::kInactive));
ASSERT_EQ(pw::OkStatus(), io_x_.SetState(State::kInactive));
ASSERT_EQ(pw::OkStatus(), io_y_.SetState(State::kInactive));
}
/// Expects that a button was pressed.
/// If `false`, the test must abort.
template <typename Event>
[[nodiscard]] bool AssertPressed(bool pressed = true) {
bool acquired = notification_.try_acquire_for(kMaxWaitOnFailedTest);
EXPECT_TRUE(acquired);
if (!acquired) {
return false;
}
bool has_value = last_event_.has_value();
EXPECT_TRUE(has_value);
if (!has_value) {
return false;
}
bool holds_alternative = std::holds_alternative<Event>(last_event_.value());
EXPECT_TRUE(holds_alternative);
if (!holds_alternative) {
return false;
}
EXPECT_EQ(std::get<Event>(last_event_.value()).pressed(), pressed);
if (std::get<Event>(last_event_.value()).pressed() != pressed) {
return false;
}
return true;
}
pw::InlineDeque<Event, 4> event_queue_;
std::array<typename PubSub::Subscriber, 4> subscribers_buffer_;
std::optional<Event> last_event_;
int events_processed_ = 0;
pw::sync::TimedThreadNotification notification_;
TestDigitalInOut io_a_;
TestDigitalInOut io_b_;
TestDigitalInOut io_x_;
TestDigitalInOut io_y_;
};
TEST(DebounceTest, SingleEdgePropagatesAfterDelay) {
Debouncer debouncer(State::kInactive);
// Arbitrary start time. Debouncer does not sample clock.
auto time = SystemClock::now();
EXPECT_EQ(debouncer.UpdateState(time, State::kInactive), State::kInactive);
// Line stays inactive for 10ms.
time += 10ms;
EXPECT_EQ(debouncer.UpdateState(time, State::kInactive), State::kInactive);
// 10ms later, the line transitions to active but the output of the debouncer
// remains inactive.
time += 10ms;
EXPECT_EQ(debouncer.UpdateState(time, State::kActive), State::kInactive);
// The output of the debounces remains active 1ms before it's debounce
// interval.
EXPECT_EQ(debouncer.UpdateState(time + Debouncer::kDebounceInterval - 1ms,
State::kActive),
State::kInactive);
// After the full interval has eleapse, the output state becomes active.
EXPECT_EQ(debouncer.UpdateState(time + Debouncer::kDebounceInterval,
State::kActive),
State::kActive);
}
TEST(DebounceTest, TwoEdgesPropagateAfterDelay) {
Debouncer debouncer(State::kInactive);
// Arbitrary start time. Debouncer does not sample clock.
auto time = SystemClock::now();
EXPECT_EQ(debouncer.UpdateState(time, State::kInactive), State::kInactive);
// Signal stays inactive.
time += 10ms;
EXPECT_EQ(debouncer.UpdateState(time, State::kInactive), State::kInactive);
// Signal becomes active and propagates through debouncer correctly.
time += 10ms;
EXPECT_EQ(debouncer.UpdateState(time, State::kActive), State::kInactive);
EXPECT_EQ(debouncer.UpdateState(time + Debouncer::kDebounceInterval - 1ms,
State::kActive),
State::kInactive);
EXPECT_EQ(debouncer.UpdateState(time + Debouncer::kDebounceInterval,
State::kActive),
State::kActive);
// Signal becomes inactive and propagates through debouncer correctly.
time += Debouncer::kDebounceInterval + 10ms;
EXPECT_EQ(debouncer.UpdateState(time, State::kInactive), State::kActive);
EXPECT_EQ(debouncer.UpdateState(time + Debouncer::kDebounceInterval - 1ms,
State::kInactive),
State::kActive);
EXPECT_EQ(debouncer.UpdateState(time + Debouncer::kDebounceInterval,
State::kInactive),
State::kInactive);
}
TEST(DebounceTest, RapidStateChangesAreDebounced) {
Debouncer debouncer(State::kInactive);
// Arbitrary start time. Debouncer does not sample clock.
auto time = SystemClock::now();
EXPECT_EQ(debouncer.UpdateState(time, pw::digital_io::State::kInactive),
pw::digital_io::State::kInactive);
// Signal stays inactive.
time += 10ms;
EXPECT_EQ(debouncer.UpdateState(time, State::kInactive), State::kInactive);
// Signal changes onces every ms for 10ms and the output remains inactive.
for (int i = 0; i < 10; ++i) {
time += 1ms;
auto state = i % 2 == 1 ? State::kActive : State::kInactive;
EXPECT_EQ(debouncer.UpdateState(time, state), State::kInactive);
}
// Signal transitions to active a final time an remains steady.
time += 1ms;
EXPECT_EQ(debouncer.UpdateState(time, State::kInactive), State::kInactive);
time += 1ms;
EXPECT_EQ(debouncer.UpdateState(time, State::kActive), State::kInactive);
EXPECT_EQ(debouncer.UpdateState(time + Debouncer::kDebounceInterval - 1ms,
State::kActive),
State::kInactive);
EXPECT_EQ(debouncer.UpdateState(time + Debouncer::kDebounceInterval,
State::kActive),
State::kActive);
// Signal changes onces every ms for 10ms and the output remains active.
for (int i = 0; i < 10; ++i) {
time += 1ms;
auto state = i % 2 == 1 ? State::kActive : State::kInactive;
EXPECT_EQ(debouncer.UpdateState(time, state), State::kActive);
}
// Signal transitions to inactive a final time an remains steady.
time += 1ms;
EXPECT_EQ(debouncer.UpdateState(time, State::kActive), State::kActive);
time += 1ms;
EXPECT_EQ(debouncer.UpdateState(time, State::kInactive), State::kActive);
EXPECT_EQ(debouncer.UpdateState(time + Debouncer::kDebounceInterval - 1ms,
State::kInactive),
State::kActive);
EXPECT_EQ(debouncer.UpdateState(time + Debouncer::kDebounceInterval,
State::kInactive),
State::kInactive);
}
TEST(EdgeDetectorTest, EdgesDetected) {
EdgeDetector edge_detector(State::kInactive);
// Inactive -> Inactive = None.
EXPECT_EQ(edge_detector.UpdateState(State::kInactive),
EdgeDetector::StateChange::kNone);
// Inactive -> Active = Activate.
EXPECT_EQ(edge_detector.UpdateState(State::kActive),
EdgeDetector::StateChange::kActivate);
// Active -> Active = None.
EXPECT_EQ(edge_detector.UpdateState(State::kActive),
EdgeDetector::StateChange::kNone);
// Active -> Inactive = Deactivate.
EXPECT_EQ(edge_detector.UpdateState(State::kInactive),
EdgeDetector::StateChange::kDeactivate);
// Inactive -> Inactive = None.
EXPECT_EQ(edge_detector.UpdateState(State::kInactive),
EdgeDetector::StateChange::kNone);
}
TEST_F(ManagerTest, AllButtonsTurnOnAndOffEvents) {
sense::TestWorker<> worker;
PubSub pubsub(worker, event_queue_, subscribers_buffer_);
ButtonManager manager(io_a_, io_b_, io_x_, io_y_);
manager.Init(pubsub, worker);
ASSERT_TRUE(pubsub.Subscribe([this](Event event) {
last_event_ = event;
events_processed_ += 1;
notification_.release();
}));
ASSERT_EQ(pw::OkStatus(), io_a_.SetState(State::kActive));
ASSERT_TRUE(AssertPressed<sense::ButtonA>());
ASSERT_EQ(pw::OkStatus(), io_b_.SetState(State::kActive));
ASSERT_TRUE(AssertPressed<sense::ButtonB>());
ASSERT_EQ(pw::OkStatus(), io_x_.SetState(State::kActive));
ASSERT_TRUE(AssertPressed<sense::ButtonX>());
ASSERT_EQ(pw::OkStatus(), io_y_.SetState(State::kActive));
ASSERT_TRUE(AssertPressed<sense::ButtonY>());
ASSERT_EQ(pw::OkStatus(), io_a_.SetState(State::kInactive));
ASSERT_TRUE(AssertPressed<sense::ButtonA>(false));
ASSERT_EQ(pw::OkStatus(), io_b_.SetState(State::kInactive));
ASSERT_TRUE(AssertPressed<sense::ButtonB>(false));
ASSERT_EQ(pw::OkStatus(), io_x_.SetState(State::kInactive));
ASSERT_TRUE(AssertPressed<sense::ButtonX>(false));
ASSERT_EQ(pw::OkStatus(), io_y_.SetState(State::kInactive));
ASSERT_TRUE(AssertPressed<sense::ButtonY>(false));
worker.Stop();
}
TEST_F(ManagerTest, DebouncingWorksOnNoisyIo) {
sense::TestWorker<> worker;
PubSub pubsub(worker, event_queue_, subscribers_buffer_);
ASSERT_TRUE(pubsub.Subscribe([this](Event event) {
last_event_ = event;
events_processed_ += 1;
notification_.release();
}));
ButtonManager manager(io_a_, io_b_, io_x_, io_y_);
manager.Init(pubsub, worker);
// Set line active with 10 noisy transition and assert that we only
// receive one event.
io_a_.NoisySetState(10, State::kActive);
ASSERT_TRUE(AssertPressed<sense::ButtonA>());
EXPECT_EQ(events_processed_, 1);
// Set line inactive with 10 noisy transition and assert that we only
// receive one event.
io_a_.NoisySetState(10, State::kInactive);
ASSERT_TRUE(AssertPressed<sense::ButtonA>(false));
EXPECT_EQ(events_processed_, 2);
worker.Stop();
}
} // namespace sense