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