/ firmware / src / programs / buttons.cpp
buttons.cpp
  1  #include "buttons.h"
  2  #include <config.h>
  3  
  4  #include <OneButton.h>
  5  #include <Arduino.h>
  6  
  7  namespace {
  8  
  9  const int8_t gpios[config::buttons::COUNT] = {
 10    config::buttons::GPIO_1,
 11    config::buttons::GPIO_2,
 12    config::buttons::GPIO_3,
 13  };
 14  
 15  OneButton instances[config::buttons::COUNT];
 16  
 17  ButtonCallback on_press_cb = nullptr;
 18  ButtonCallback on_click_cb = nullptr;
 19  ButtonCallback on_double_click_cb = nullptr;
 20  ButtonCallback on_multi_click_cb = nullptr;
 21  ButtonCallback on_long_press_start_cb = nullptr;
 22  ButtonCallback on_long_press_stop_cb = nullptr;
 23  ButtonCallback on_during_long_press_cb = nullptr;
 24  IdleCallback on_idle_cb = nullptr;
 25  
 26  void route_press(void *p) {
 27    if (on_press_cb) on_press_cb((uint8_t)(uintptr_t)p);
 28  }
 29  
 30  void route_click(void *p) {
 31    if (on_click_cb) on_click_cb((uint8_t)(uintptr_t)p);
 32  }
 33  
 34  void route_double_click(void *p) {
 35    if (on_double_click_cb) on_double_click_cb((uint8_t)(uintptr_t)p);
 36  }
 37  
 38  void route_multi_click(void *p) {
 39    if (on_multi_click_cb) on_multi_click_cb((uint8_t)(uintptr_t)p);
 40  }
 41  
 42  void route_long_press_start(void *p) {
 43    if (on_long_press_start_cb) on_long_press_start_cb((uint8_t)(uintptr_t)p);
 44  }
 45  
 46  void route_long_press_stop(void *p) {
 47    if (on_long_press_stop_cb) on_long_press_stop_cb((uint8_t)(uintptr_t)p);
 48  }
 49  
 50  void route_during_long_press(void *p) {
 51    if (on_during_long_press_cb) on_during_long_press_cb((uint8_t)(uintptr_t)p);
 52  }
 53  
 54  void route_idle() {
 55    if (on_idle_cb) on_idle_cb();
 56  }
 57  
 58  }
 59  
 60  void programs::buttons::initialize() {
 61    uint8_t active = 0;
 62    for (uint8_t i = 0; i < config::buttons::COUNT; i++) {
 63      if (gpios[i] < 0) continue;
 64      instances[i].setup(gpios[i], INPUT, true);
 65      instances[i].setDebounceMs(config::buttons::DEBOUNCE_MS);
 66      instances[i].setPressMs(config::buttons::LONG_PRESS_MS);
 67      instances[i].attachPress(route_press, (void *)(uintptr_t)i);
 68      instances[i].attachClick(route_click, (void *)(uintptr_t)i);
 69      instances[i].attachDoubleClick(route_double_click, (void *)(uintptr_t)i);
 70      instances[i].attachMultiClick(route_multi_click, (void *)(uintptr_t)i);
 71      instances[i].attachLongPressStart(route_long_press_start, (void *)(uintptr_t)i);
 72      instances[i].attachLongPressStop(route_long_press_stop, (void *)(uintptr_t)i);
 73      instances[i].attachDuringLongPress(route_during_long_press, (void *)(uintptr_t)i);
 74      instances[i].attachIdle(route_idle);
 75      active++;
 76    }
 77    Serial.printf("[buttons] %d active (GPIO %d, %d, %d)\n",
 78                  active,
 79                  config::buttons::GPIO_1, config::buttons::GPIO_2, config::buttons::GPIO_3);
 80  }
 81  
 82  void programs::buttons::service() {
 83    for (uint8_t i = 0; i < config::buttons::COUNT; i++) {
 84      if (gpios[i] < 0) continue;
 85      instances[i].tick();
 86    }
 87  }
 88  
 89  void programs::buttons::onPress(ButtonCallback cb)            { on_press_cb = cb; }
 90  void programs::buttons::onClick(ButtonCallback cb)            { on_click_cb = cb; }
 91  void programs::buttons::onDoubleClick(ButtonCallback cb)      { on_double_click_cb = cb; }
 92  void programs::buttons::onMultiClick(ButtonCallback cb)       { on_multi_click_cb = cb; }
 93  void programs::buttons::onLongPressStart(ButtonCallback cb)   { on_long_press_start_cb = cb; }
 94  void programs::buttons::onLongPressStop(ButtonCallback cb)    { on_long_press_stop_cb = cb; }
 95  void programs::buttons::onDuringLongPress(ButtonCallback cb)  { on_during_long_press_cb = cb; }
 96  void programs::buttons::onIdle(IdleCallback cb)               { on_idle_cb = cb; }
 97  
 98  void programs::buttons::setClickMs(unsigned int ms) {
 99    for (uint8_t i = 0; i < config::buttons::COUNT; i++) {
100      if (gpios[i] < 0) continue;
101      instances[i].setClickMs(ms);
102    }
103  }
104  
105  void programs::buttons::setIdleMs(unsigned int ms) {
106    for (uint8_t i = 0; i < config::buttons::COUNT; i++) {
107      if (gpios[i] < 0) continue;
108      instances[i].setIdleMs(ms);
109    }
110  }
111  
112  void programs::buttons::setLongPressIntervalMs(unsigned int ms) {
113    for (uint8_t i = 0; i < config::buttons::COUNT; i++) {
114      if (gpios[i] < 0) continue;
115      instances[i].setLongPressIntervalMs(ms);
116    }
117  }
118  
119  bool programs::buttons::isPressed(uint8_t index) {
120    if (index >= config::buttons::COUNT) return false;
121    if (gpios[index] < 0) return false;
122    return !digitalRead(gpios[index]);
123  }
124  
125  bool programs::buttons::isIdle(uint8_t index) {
126    if (index >= config::buttons::COUNT) return false;
127    if (gpios[index] < 0) return true;
128    return instances[index].isIdle();
129  }
130  
131  bool programs::buttons::isLongPressed(uint8_t index) {
132    if (index >= config::buttons::COUNT) return false;
133    if (gpios[index] < 0) return false;
134    return instances[index].isLongPressed();
135  }
136  
137  unsigned long programs::buttons::getPressedMs(uint8_t index) {
138    if (index >= config::buttons::COUNT) return 0;
139    if (gpios[index] < 0) return 0;
140    return instances[index].getPressedMs();
141  }
142  
143  int programs::buttons::getNumberClicks(uint8_t index) {
144    if (index >= config::buttons::COUNT) return 0;
145    if (gpios[index] < 0) return 0;
146    return instances[index].getNumberClicks();
147  }
148  
149  void programs::buttons::reset(uint8_t index) {
150    if (index >= config::buttons::COUNT) return;
151    if (gpios[index] < 0) return;
152    instances[index].reset();
153  }
154  
155  #ifdef PIO_UNIT_TESTING
156  
157  #include <testing/utils.h>
158  
159  static void test_buttons_config_valid(void) {
160    GIVEN("the button GPIO configuration");
161    THEN("it matches the expected layout");
162  
163    TEST_ASSERT_EQUAL_INT_MESSAGE(3, config::buttons::COUNT,
164        "device: should have 3 button slots");
165    TEST_ASSERT_EQUAL_INT_MESSAGE(-1, config::buttons::GPIO_1,
166        "device: GPIO_1 should be -1 (reserved for PSRAM)");
167    TEST_ASSERT_GREATER_OR_EQUAL_INT_MESSAGE(0, config::buttons::GPIO_2,
168        "device: GPIO_2 should be a valid pin");
169    TEST_ASSERT_GREATER_OR_EQUAL_INT_MESSAGE(0, config::buttons::GPIO_3,
170        "device: GPIO_3 should be a valid pin");
171  
172    TEST_PRINTF("debounce=%dms long_press=%dms",
173             config::buttons::DEBOUNCE_MS, config::buttons::LONG_PRESS_MS);
174  }
175  
176  static void test_buttons_disabled_gpio_rejected(void) {
177    WHEN("isPressed is called on a disabled GPIO");
178    THEN("it returns false");
179    TEST_ASSERT_FALSE_MESSAGE(programs::buttons::isPressed(0),
180        "device: button 0 (GPIO -1) should always return false");
181  }
182  
183  static void test_buttons_out_of_range_rejected(void) {
184    WHEN("isPressed is called with an out-of-range index");
185    THEN("it returns false");
186    TEST_ASSERT_FALSE_MESSAGE(programs::buttons::isPressed(255),
187        "device: index 255 should return false");
188  }
189  
190  void programs::buttons::test() {
191    MODULE("Buttons");
192    RUN_TEST(test_buttons_config_valid);
193    RUN_TEST(test_buttons_disabled_gpio_rejected);
194    RUN_TEST(test_buttons_out_of_range_rejected);
195  }
196  
197  #endif