From f964a3e14a3fcf0a96af3bcc002eb50e0e40ad5e Mon Sep 17 00:00:00 2001 From: Michael Adams Date: Sun, 21 Jun 2026 14:11:25 +1200 Subject: [PATCH] Add CI, linter, library.json, etc --- .clang-format | 13 +++ .github/workflows/ci.yml | 69 ++++++++++++++ .gitignore | 6 ++ Button.h | 33 ------- examples/basic_usage/basic_usage.ino | 19 ++-- keywords.txt | 1 + library.json | 19 ++++ library.properties | 8 +- platformio.ini | 18 ++++ Button.cpp => src/Button.cpp | 22 ++--- src/Button.h | 33 +++++++ test/mock/Arduino.h | 27 ++++++ test/test_button/test_main.cpp | 130 +++++++++++++++++++++++++++ 13 files changed, 341 insertions(+), 57 deletions(-) create mode 100644 .clang-format create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore delete mode 100644 Button.h create mode 100644 library.json create mode 100644 platformio.ini rename Button.cpp => src/Button.cpp (78%) create mode 100644 src/Button.h create mode 100644 test/mock/Arduino.h create mode 100644 test/test_button/test_main.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..427feac --- /dev/null +++ b/.clang-format @@ -0,0 +1,13 @@ +--- +Language: Cpp +BasedOnStyle: LLVM +UseTab: ForIndentation +TabWidth: 4 +IndentWidth: 4 +BreakBeforeBraces: Allman +ColumnLimit: 0 +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +SortIncludes: false +... diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..54b4acc --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,69 @@ +name: CI + +on: + push: + pull_request: + +jobs: + format: + name: clang-format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check formatting + uses: jidicula/clang-format-action@v4.13.0 + with: + clang-format-version: "17" + check-path: "." + + lint: + name: arduino-lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Lint library + uses: arduino/arduino-lint-action@v1 + with: + # Button is already in the Library Manager index, so lint against the + # rules for libraries being updated. + library-manager: update + compliance: strict + + compile: + name: compile examples + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - fqbn: arduino:avr:uno + platforms: | + - name: arduino:avr + - fqbn: esp8266:esp8266:generic + platforms: | + - name: esp8266:esp8266 + source-url: https://arduino.esp8266.com/stable/package_esp8266com_index.json + steps: + - uses: actions/checkout@v4 + - name: Compile example sketches + uses: arduino/compile-sketches@v1 + with: + fqbn: ${{ matrix.fqbn }} + platforms: ${{ matrix.platforms }} + libraries: | + - source-path: ./ + sketch-paths: | + - examples/basic_usage + + test: + name: native unit tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install PlatformIO + run: pip install --upgrade platformio + - name: Run tests + run: pio test -e native diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de99bcb --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.pio/ +.pioenvs/ +.piolibdeps/ +.vscode/ +*.o +*.a diff --git a/Button.h b/Button.h deleted file mode 100644 index 6a23b29..0000000 --- a/Button.h +++ /dev/null @@ -1,33 +0,0 @@ -/* - Button - a small library for Arduino to handle button debouncing - - MIT licensed. -*/ - -#ifndef Button_h -#define Button_h -#include "Arduino.h" - -class Button -{ - public: - Button(uint8_t pin, uint16_t debounce_ms = 100); - void begin(); - bool read(); - bool toggled(); - bool pressed(); - bool released(); - bool has_changed(); - - const static bool PRESSED = LOW; - const static bool RELEASED = HIGH; - - private: - uint8_t _pin; - uint16_t _delay; - bool _state; - uint32_t _ignore_until; - bool _has_changed; -}; - -#endif diff --git a/examples/basic_usage/basic_usage.ino b/examples/basic_usage/basic_usage.ino index 4f66e29..79c52c0 100644 --- a/examples/basic_usage/basic_usage.ino +++ b/examples/basic_usage/basic_usage.ino @@ -4,23 +4,28 @@ Button button1(2); // Connect your button between pin 2 and GND Button button2(3); // Connect your button between pin 3 and GND Button button3(4); // Connect your button between pin 4 and GND -void setup() { +void setup() +{ button1.begin(); button2.begin(); button3.begin(); - - while (!Serial) { }; // for Leos + + while (!Serial) + { + }; // for Leos Serial.begin(9600); } -void loop() { +void loop() +{ if (button1.pressed()) Serial.println("Button 1 pressed"); - + if (button2.released()) Serial.println("Button 2 released"); - - if (button3.toggled()) { + + if (button3.toggled()) + { if (button3.read() == Button::PRESSED) Serial.println("Button 3 has been pressed"); else diff --git a/keywords.txt b/keywords.txt index 1372b4a..5ba7851 100644 --- a/keywords.txt +++ b/keywords.txt @@ -12,6 +12,7 @@ Button KEYWORD1 # Methods and Functions (KEYWORD2) ####################################### +begin KEYWORD2 read KEYWORD2 toggled KEYWORD2 pressed KEYWORD2 diff --git a/library.json b/library.json new file mode 100644 index 0000000..98c8e23 --- /dev/null +++ b/library.json @@ -0,0 +1,19 @@ +{ + "name": "Button", + "version": "1.2.0", + "description": "Button is a tiny library to make reading buttons very simple. It handles debouncing automatically, and monitoring of state.", + "keywords": "button, debounce, input, switch", + "repository": { + "type": "git", + "url": "https://github.com/madleech/Button.git" + }, + "authors": [ + { + "name": "Michael Adams", + "url": "https://github.com/madleech" + } + ], + "license": "MIT", + "frameworks": "arduino", + "platforms": "*" +} diff --git a/library.properties b/library.properties index 37c08a7..b962170 100644 --- a/library.properties +++ b/library.properties @@ -1,9 +1,9 @@ name=Button -version=1.1.0 -author=Michael Adams -maintainer=Michael Adams +version=1.2.0 +author=Michael Adams +maintainer=Michael Adams sentence=Button is a tiny library to make reading buttons very simple. paragraph=It handles debouncing automatically, and monitoring of state. category=Signal Input/Output -url=http://utrainia.com/ +url=https://github.com/madleech/Button architectures=* diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..4f2f249 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,18 @@ +; PlatformIO project configuration for Button. +; +; pio test -e native -> run the native unit tests (mocks millis()/digitalRead()) +; pio run -e uno -> compile-check against AVR (Arduino Uno) + +[platformio] +src_dir = src + +[env:native] +platform = native +lib_compat_mode = off +test_build_src = true +build_flags = -I test/mock + +[env:uno] +platform = atmelavr +board = uno +framework = arduino diff --git a/Button.cpp b/src/Button.cpp similarity index 78% rename from Button.cpp rename to src/Button.cpp index 746ce21..2722edf 100644 --- a/Button.cpp +++ b/src/Button.cpp @@ -1,18 +1,14 @@ /* - Button - a small library for Arduino to handle button debouncing - - MIT licensed. + Button - a small library for Arduino to handle button debouncing + + MIT licensed. */ #include "Button.h" #include Button::Button(uint8_t pin, uint16_t debounce_ms) -: _pin(pin) -, _delay(debounce_ms) -, _state(HIGH) -, _ignore_until(0) -, _has_changed(false) + : _pin(pin), _delay(debounce_ms), _state(HIGH), _ignore_until(0), _has_changed(false) { } @@ -21,9 +17,9 @@ void Button::begin() pinMode(_pin, INPUT_PULLUP); } -// +// // public methods -// +// bool Button::read() { @@ -32,15 +28,15 @@ bool Button::read() { // ignore any changes during this period } - - // pin has changed + + // pin has changed else if (digitalRead(_pin) != _state) { _ignore_until = millis() + _delay; _state = !_state; _has_changed = true; } - + return _state; } diff --git a/src/Button.h b/src/Button.h new file mode 100644 index 0000000..9f0fa09 --- /dev/null +++ b/src/Button.h @@ -0,0 +1,33 @@ +/* + Button - a small library for Arduino to handle button debouncing + + MIT licensed. +*/ + +#ifndef Button_h +#define Button_h +#include "Arduino.h" + +class Button +{ + public: + Button(uint8_t pin, uint16_t debounce_ms = 100); + void begin(); + bool read(); + bool toggled(); + bool pressed(); + bool released(); + bool has_changed(); + + const static bool PRESSED = LOW; + const static bool RELEASED = HIGH; + + private: + uint8_t _pin; + uint16_t _delay; + bool _state; + uint32_t _ignore_until; + bool _has_changed; +}; + +#endif diff --git a/test/mock/Arduino.h b/test/mock/Arduino.h new file mode 100644 index 0000000..cc02b48 --- /dev/null +++ b/test/mock/Arduino.h @@ -0,0 +1,27 @@ +/* + Minimal Arduino.h mock for native unit tests. + + Provides just enough of the Arduino runtime for Button to build and run + off-device. The test controls time via _mock_millis and the (single) pin + level via _mock_pin_state, both defined in the test translation unit. +*/ + +#ifndef _Button_test_Arduino_h +#define _Button_test_Arduino_h + +#include +#include + +#define LOW 0 +#define HIGH 1 +#define INPUT_PULLUP 2 + +// Controllable test state, defined by the test translation unit. +extern unsigned long _mock_millis; +extern int _mock_pin_state; + +unsigned long millis(); +void pinMode(uint8_t pin, uint8_t mode); +int digitalRead(uint8_t pin); + +#endif diff --git a/test/test_button/test_main.cpp b/test/test_button/test_main.cpp new file mode 100644 index 0000000..eaa5258 --- /dev/null +++ b/test/test_button/test_main.cpp @@ -0,0 +1,130 @@ +/* + Native unit tests for Button. + + These run off-device via PlatformIO's `native` platform. millis() and + digitalRead() are mocked (see test/mock/Arduino.h) so we can drive time and + the pin level deterministically and exercise the debounce logic. + + Wiring model: the button is between the pin and GND with INPUT_PULLUP, so + the raw pin reads HIGH (RELEASED) when up and LOW (PRESSED) when down. +*/ + +#include + +#include "Arduino.h" +#include "Button.h" + +// The mock state declared in test/mock/Arduino.h. +unsigned long _mock_millis = 0; +int _mock_pin_state = HIGH; + +unsigned long millis() +{ + return _mock_millis; +} + +void pinMode(uint8_t, uint8_t) +{ +} + +int digitalRead(uint8_t) +{ + return _mock_pin_state; +} + +void setUp(void) +{ + _mock_millis = 0; + _mock_pin_state = HIGH; // released +} + +void tearDown(void) +{ +} + +// A freshly constructed button reads as released. +void test_starts_released(void) +{ + Button button(2); + TEST_ASSERT_EQUAL(Button::RELEASED, button.read()); +} + +// pressed() fires exactly once per press. +void test_pressed_fires_once(void) +{ + Button button(2); + + _mock_pin_state = LOW; // button pushed down + TEST_ASSERT_TRUE(button.pressed()); + TEST_ASSERT_FALSE(button.pressed()); // no new edge +} + +// released() fires exactly once when the button comes back up. +void test_released_fires_once(void) +{ + Button button(2); + + _mock_pin_state = LOW; + button.pressed(); // register the press (debounce window: 0..100) + + _mock_millis = 150; // past the debounce window + _mock_pin_state = HIGH; + TEST_ASSERT_TRUE(button.released()); + TEST_ASSERT_FALSE(button.released()); +} + +// Bounces inside the debounce window are ignored. +void test_debounce_ignores_bounce(void) +{ + Button button(2); + + _mock_pin_state = LOW; // press at t=0, ignore changes until t=100 + TEST_ASSERT_TRUE(button.pressed()); + + _mock_millis = 50; // still inside the window + _mock_pin_state = HIGH; // a contact bounce back up + TEST_ASSERT_FALSE(button.released()); + TEST_ASSERT_EQUAL(Button::PRESSED, button.read()); // state unchanged +} + +// toggled() reports any debounced change of state, once per change. +void test_toggled_on_each_change(void) +{ + Button button(2); + + _mock_pin_state = LOW; // pressed + TEST_ASSERT_TRUE(button.toggled()); + TEST_ASSERT_FALSE(button.toggled()); + + _mock_millis = 200; // past debounce window + _mock_pin_state = HIGH; // released + TEST_ASSERT_TRUE(button.toggled()); +} + +// A custom debounce time is honoured. +void test_custom_debounce_time(void) +{ + Button button(2, 500); + + _mock_pin_state = LOW; + TEST_ASSERT_TRUE(button.pressed()); // ignore changes until t=500 + + _mock_millis = 400; // a real release, but still inside the 500ms window + _mock_pin_state = HIGH; + TEST_ASSERT_FALSE(button.released()); + + _mock_millis = 500; // window elapsed + TEST_ASSERT_TRUE(button.released()); +} + +int main(int, char **) +{ + UNITY_BEGIN(); + RUN_TEST(test_starts_released); + RUN_TEST(test_pressed_fires_once); + RUN_TEST(test_released_fires_once); + RUN_TEST(test_debounce_ignores_bounce); + RUN_TEST(test_toggled_on_each_change); + RUN_TEST(test_custom_debounce_time); + return UNITY_END(); +}