Supporting a button library.

This commit is contained in:
Eric Ratliff
2026-02-22 07:44:41 -06:00
parent 131ca648b5
commit 970479f4b6
8 changed files with 922 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
[library]
name = "button"
version = "0.1.0"
description = "Momentary pushbutton with debounce support"
[requires]
bus = "digital"
pins = ["signal"]
[provides]
interface = "button.h"
implementation = "button_digital.h"
mock = "button_mock.h"
simulation = "button_sim.h"
test = "test_button.cpp"

View File

@@ -0,0 +1,52 @@
#ifndef BUTTON_H
#define BUTTON_H
/*
* Momentary Pushbutton -- Abstract Interface
*
* This is the contract that your application code depends on. It does
* not know or care whether the button state comes from a real GPIO
* pin, a test mock with canned presses, or a simulation with
* realistic contact bounce.
*
* Three implementations ship with this driver:
* button_digital.h -- Real hardware via Hal::digitalRead()
* button_mock.h -- Test mock with programmable state
* button_sim.h -- Simulation with configurable bounce
*
* Usage in application code:
* #include "button.h"
*
* class ToggleApp {
* public:
* ToggleApp(Hal* hal, Button* btn)
* : hal_(hal), btn_(btn) {}
*
* void update() {
* if (btn_->isPressed()) {
* hal_->digitalWrite(LED_PIN, HIGH);
* } else {
* hal_->digitalWrite(LED_PIN, LOW);
* }
* }
* };
*
* Generated by Anvil -- https://nxgit.dev/nexus-workshops/anvil
*/
class Button {
public:
virtual ~Button() = default;
/// Is the button currently pressed?
/// Returns true when pressed, false when released.
/// Implementations handle active-low vs active-high internally.
virtual bool isPressed() = 0;
/// Read the raw digital state (HIGH or LOW).
/// No inversion -- returns exactly what the pin reads.
/// Useful for debugging or custom logic.
virtual int readState() = 0;
};
#endif // BUTTON_H

View File

@@ -0,0 +1,57 @@
#ifndef BUTTON_DIGITAL_H
#define BUTTON_DIGITAL_H
#include "button.h"
#include "hal.h"
/*
* Button -- Real hardware implementation (digital read).
*
* Reads a physical pushbutton connected to a digital pin.
* Supports both wiring configurations:
*
* Active-low (most common, use INPUT_PULLUP):
* Pin -> Button -> GND
* Reads LOW when pressed, HIGH when released.
* Set active_low = true (default).
*
* Active-high (external pull-down resistor):
* Pin -> Button -> VCC, with pull-down to GND
* Reads HIGH when pressed, LOW when released.
* Set active_low = false.
*
* Wiring (active-low, no external resistor needed):
* Digital pin -> one leg of button
* GND -> other leg of button
* Set pin mode to INPUT_PULLUP in your sketch.
*
* Note: This implementation does NOT debounce. If your application
* needs debounce (most do), implement it in your app logic -- that
* way you can test it with the mock and simulator.
*/
class ButtonDigital : public Button {
public:
/// Create a button on the given digital pin.
/// active_low: true if pressed reads LOW (default, use with INPUT_PULLUP).
ButtonDigital(Hal* hal, uint8_t pin, bool active_low = true)
: hal_(hal)
, pin_(pin)
, active_low_(active_low)
{}
bool isPressed() override {
int state = hal_->digitalRead(pin_);
return active_low_ ? (state == LOW) : (state == HIGH);
}
int readState() override {
return hal_->digitalRead(pin_);
}
private:
Hal* hal_;
uint8_t pin_;
bool active_low_;
};
#endif // BUTTON_DIGITAL_H

View File

@@ -0,0 +1,60 @@
#ifndef BUTTON_MOCK_H
#define BUTTON_MOCK_H
#include "button.h"
/*
* Button -- Test mock with programmable state.
*
* Use this in tests to control exactly what button state your
* application sees, without any real hardware.
*
* Example:
* ButtonMock btn;
* btn.setPressed(true);
*
* ToggleApp app(&sim, &btn);
* app.update();
*
* EXPECT_EQ(sim.getPin(LED_PIN), HIGH); // LED should be on
*
* You can also track how many times the button was read:
* btn.resetReadCount();
* app.update();
* EXPECT_EQ(btn.readCount(), 1);
*/
class ButtonMock : public Button {
public:
/// Set whether the button appears pressed.
void setPressed(bool pressed) {
pressed_ = pressed;
}
/// Set the raw digital state returned by readState().
void setRaw(int value) {
raw_ = value;
}
bool isPressed() override {
++read_count_;
return pressed_;
}
int readState() override {
++read_count_;
return raw_;
}
/// How many times has the button been read?
int readCount() const { return read_count_; }
/// Reset the read counter.
void resetReadCount() { read_count_ = 0; }
private:
bool pressed_ = false; // Default: not pressed
int raw_ = HIGH; // Default: HIGH (active-low, not pressed)
int read_count_ = 0;
};
#endif // BUTTON_MOCK_H

View File

@@ -0,0 +1,100 @@
#ifndef BUTTON_SIM_H
#define BUTTON_SIM_H
#include "button.h"
#include <cstdlib>
/*
* Button -- Simulation with realistic contact bounce.
*
* Unlike the mock (which returns exact states), the simulator models
* contact bounce during press/release transitions. This is useful
* for system tests that verify your debounce logic, such as:
*
* - Software debounce algorithms (delay-based, counter-based)
* - Edge detection that should not double-trigger
* - State machine transitions that must be bounce-tolerant
*
* Example:
* ButtonSim btn;
* btn.setBounceReads(5); // 5 noisy reads per transition
*
* btn.press(); // start a press
* // First few reads may bounce between pressed/released
* // After bounce_reads, settles to pressed
*
* btn.release(); // start a release
* // Same bounce behavior during release
*
* For tests that do not care about bounce:
* ButtonSim btn;
* btn.setBounceReads(0); // no bounce, instant transitions
* btn.press();
* EXPECT_TRUE(btn.isPressed()); // always true immediately
*/
class ButtonSim : public Button {
public:
/// Create a simulated button.
/// bounce_reads: how many noisy reads occur after each transition
ButtonSim(int bounce_reads = 0)
: pressed_(false)
, bounce_reads_(bounce_reads)
, reads_since_change_(0)
, seed_(42)
{}
/// Press the button (start a press transition).
void press() {
if (!pressed_) {
pressed_ = true;
reads_since_change_ = 0;
}
}
/// Release the button (start a release transition).
void release() {
if (pressed_) {
pressed_ = false;
reads_since_change_ = 0;
}
}
/// Set the number of bouncy reads after each transition.
void setBounceReads(int count) { bounce_reads_ = count; }
/// Seed the random number generator for repeatable bounce patterns.
void setSeed(unsigned int seed) { seed_ = seed; }
bool isPressed() override {
if (reads_since_change_ < bounce_reads_) {
++reads_since_change_;
// During bounce, randomly return wrong state
bool bounce_flip = (nextRandom() > 0.5f);
return bounce_flip ? !pressed_ : pressed_;
}
reads_since_change_ = bounce_reads_; // clamp
return pressed_;
}
int readState() override {
// Map pressed state to pin level (active-low convention)
bool p = isPressed();
return p ? LOW : HIGH;
}
private:
bool pressed_;
int bounce_reads_;
int reads_since_change_;
unsigned int seed_;
/// Simple LCG random in [0.0, 1.0).
/// Deterministic -- same seed gives same sequence.
float nextRandom() {
seed_ = seed_ * 1103515245 + 12345;
return (float)((seed_ >> 16) & 0x7FFF) / 32768.0f;
}
};
#endif // BUTTON_SIM_H

View File

@@ -0,0 +1,289 @@
/*
* Button Driver Tests
*
* Auto-generated by: anvil add button
* These tests verify the Button driver mock and simulation without
* any hardware. They run alongside your unit and system tests.
*
* To run: ./test.sh (all tests)
* ./test.sh --system (skips these -- use no filter to include)
* ./test.sh --unit (skips these -- use no filter to include)
*/
#include <gtest/gtest.h>
#include "mock_arduino.h"
#include "sim_hal.h"
#include "button.h"
#include "button_digital.h"
#include "button_mock.h"
#include "button_sim.h"
// ---------------------------------------------------------------------------
// Mock: basic functionality
// ---------------------------------------------------------------------------
class ButtonMockTest : public ::testing::Test {
protected:
void SetUp() override {
mock_arduino_reset();
}
ButtonMock btn;
};
TEST_F(ButtonMockTest, DefaultsToNotPressed) {
EXPECT_FALSE(btn.isPressed());
}
TEST_F(ButtonMockTest, SetPressedReturnsTrue) {
btn.setPressed(true);
EXPECT_TRUE(btn.isPressed());
}
TEST_F(ButtonMockTest, SetReleasedReturnsFalse) {
btn.setPressed(true);
btn.setPressed(false);
EXPECT_FALSE(btn.isPressed());
}
TEST_F(ButtonMockTest, DefaultRawIsHigh) {
EXPECT_EQ(btn.readState(), HIGH);
}
TEST_F(ButtonMockTest, SetRawReturnsExactValue) {
btn.setRaw(LOW);
EXPECT_EQ(btn.readState(), LOW);
}
TEST_F(ButtonMockTest, ReadCountTracking) {
EXPECT_EQ(btn.readCount(), 0);
btn.isPressed();
btn.isPressed();
btn.readState();
EXPECT_EQ(btn.readCount(), 3);
}
TEST_F(ButtonMockTest, ReadCountReset) {
btn.isPressed();
btn.isPressed();
btn.resetReadCount();
EXPECT_EQ(btn.readCount(), 0);
}
TEST_F(ButtonMockTest, PressAndRawAreIndependent) {
// setPressed controls isPressed(), setRaw controls readState()
btn.setPressed(true);
btn.setRaw(HIGH);
EXPECT_TRUE(btn.isPressed());
EXPECT_EQ(btn.readState(), HIGH);
}
// ---------------------------------------------------------------------------
// Digital: real implementation via SimHal
// ---------------------------------------------------------------------------
class ButtonDigitalTest : public ::testing::Test {
protected:
void SetUp() override {
mock_arduino_reset();
}
SimHal hal;
};
TEST_F(ButtonDigitalTest, ActiveLowPressedWhenLow) {
ButtonDigital btn(&hal, 2, true); // active-low
hal.setPin(2, LOW);
EXPECT_TRUE(btn.isPressed());
}
TEST_F(ButtonDigitalTest, ActiveLowReleasedWhenHigh) {
ButtonDigital btn(&hal, 2, true); // active-low
hal.setPin(2, HIGH);
EXPECT_FALSE(btn.isPressed());
}
TEST_F(ButtonDigitalTest, ActiveHighPressedWhenHigh) {
ButtonDigital btn(&hal, 2, false); // active-high
hal.setPin(2, HIGH);
EXPECT_TRUE(btn.isPressed());
}
TEST_F(ButtonDigitalTest, ActiveHighReleasedWhenLow) {
ButtonDigital btn(&hal, 2, false); // active-high
hal.setPin(2, LOW);
EXPECT_FALSE(btn.isPressed());
}
TEST_F(ButtonDigitalTest, ReadStateReturnsRawPin) {
ButtonDigital btn(&hal, 2, true);
hal.setPin(2, LOW);
EXPECT_EQ(btn.readState(), LOW);
hal.setPin(2, HIGH);
EXPECT_EQ(btn.readState(), HIGH);
}
TEST_F(ButtonDigitalTest, DifferentPin) {
ButtonDigital btn(&hal, 7, true);
hal.setPin(7, LOW);
EXPECT_TRUE(btn.isPressed());
}
TEST_F(ButtonDigitalTest, DefaultIsActiveLow) {
ButtonDigital btn(&hal, 2); // active_low defaults to true
hal.setPin(2, LOW);
EXPECT_TRUE(btn.isPressed());
}
// ---------------------------------------------------------------------------
// Simulation: bounce and determinism
// ---------------------------------------------------------------------------
class ButtonSimTest : public ::testing::Test {
protected:
void SetUp() override {
mock_arduino_reset();
}
};
TEST_F(ButtonSimTest, DefaultsToNotPressed) {
ButtonSim btn;
EXPECT_FALSE(btn.isPressed());
}
TEST_F(ButtonSimTest, PressWithNoBounceIsImmediate) {
ButtonSim btn(0); // no bounce
btn.press();
EXPECT_TRUE(btn.isPressed());
}
TEST_F(ButtonSimTest, ReleaseWithNoBounceIsImmediate) {
ButtonSim btn(0);
btn.press();
btn.release();
EXPECT_FALSE(btn.isPressed());
}
TEST_F(ButtonSimTest, BounceSettlesAfterEnoughReads) {
ButtonSim btn(5); // 5 bouncy reads
btn.press();
// Read through the bounce window
for (int i = 0; i < 5; ++i) {
btn.isPressed(); // may or may not bounce
}
// After bounce window, should be stable
EXPECT_TRUE(btn.isPressed());
EXPECT_TRUE(btn.isPressed());
EXPECT_TRUE(btn.isPressed());
}
TEST_F(ButtonSimTest, ReleaseBouncesSettleToReleased) {
ButtonSim btn(3);
btn.press();
// Clear press bounce
for (int i = 0; i < 10; ++i) btn.isPressed();
btn.release();
// Read through release bounce
for (int i = 0; i < 3; ++i) btn.isPressed();
// Settled to released
EXPECT_FALSE(btn.isPressed());
}
TEST_F(ButtonSimTest, DeterministicWithSameSeed) {
ButtonSim s1(5);
ButtonSim s2(5);
s1.setSeed(99);
s2.setSeed(99);
s1.press();
s2.press();
for (int i = 0; i < 10; ++i) {
EXPECT_EQ(s1.isPressed(), s2.isPressed())
<< "Reading " << i << " should match with same seed";
}
}
TEST_F(ButtonSimTest, DifferentSeedsDifferentBounce) {
ButtonSim s1(10);
ButtonSim s2(10);
s1.setSeed(1);
s2.setSeed(2);
s1.press();
s2.press();
// At least one reading during bounce should differ
bool any_differ = false;
for (int i = 0; i < 10; ++i) {
if (s1.isPressed() != s2.isPressed()) {
any_differ = true;
break;
}
}
EXPECT_TRUE(any_differ);
}
TEST_F(ButtonSimTest, NoBounceZeroReads) {
ButtonSim btn(0);
btn.press();
// With zero bounce, every read should be stable
for (int i = 0; i < 20; ++i) {
EXPECT_TRUE(btn.isPressed()) << "Read " << i << " should be pressed";
}
}
TEST_F(ButtonSimTest, ReadStateMatchesIsPressed) {
ButtonSim btn(0);
btn.press();
// Active-low convention: pressed -> LOW
EXPECT_EQ(btn.readState(), LOW);
btn.release();
EXPECT_EQ(btn.readState(), HIGH);
}
TEST_F(ButtonSimTest, DoublePressSameState) {
ButtonSim btn(0);
btn.press();
btn.press(); // second press is a no-op
EXPECT_TRUE(btn.isPressed());
}
TEST_F(ButtonSimTest, DoubleReleaseSameState) {
ButtonSim btn(0);
btn.release(); // already released, no-op
EXPECT_FALSE(btn.isPressed());
}
// ---------------------------------------------------------------------------
// Polymorphism: all impls work through Button pointer
// ---------------------------------------------------------------------------
TEST(ButtonPolymorphismTest, AllImplsWorkThroughBasePointer) {
mock_arduino_reset();
SimHal hal;
hal.setDigital(2, LOW); // pressed for active-low
ButtonDigital digital_btn(&hal, 2, true);
ButtonMock mock_btn;
ButtonSim sim_btn(0);
mock_btn.setPressed(true);
sim_btn.press();
Button* buttons[] = { &digital_btn, &mock_btn, &sim_btn };
for (auto* b : buttons) {
bool pressed = b->isPressed();
int raw = b->readState();
EXPECT_TRUE(pressed);
(void)raw; // just verify it compiles and runs
}
}