├── CMakeLists.txt ├── Examples └── InterruptButtonExample.ino ├── InterruptButton - Flow Diagram.pdf ├── InterruptButton.cpp ├── InterruptButton.h ├── LICENSE ├── README.md ├── images └── flowDiagram.png ├── library.json └── library.properties /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | 3 | FILE(GLOB_RECURSE app_sources "*.cpp") 4 | 5 | # Build InterruptButton as an ESP-IDF component 6 | if(ESP_PLATFORM) 7 | idf_component_register( 8 | SRCS ${app_sources} 9 | INCLUDE_DIRS "." 10 | #REQUIRES ${depends} 11 | #PRIV_REQUIRES 12 | ) 13 | return() 14 | endif() 15 | 16 | project(InterruptButton VERSION 1.0.0) 17 | #target_compile_options(${COMPONENT_TARGET} PRIVATE -fno-rtti) 18 | -------------------------------------------------------------------------------- /Examples/InterruptButtonExample.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include "InterruptButton.h" 3 | 4 | #define BUTTON_1 0 // Top Left or Bottom Right button 5 | #define BUTTON_2 35 // Bottom Left or Top Right button 6 | 7 | 8 | //-- BUTTON VARIABLES --------------------------------------------------------- 9 | InterruptButton button1(BUTTON_1, LOW); // Statically allocated button (safest) 10 | InterruptButton* button2; // Dynamically allocated button (if you know what you are doing) 11 | 12 | 13 | //-- EXAMPLE ACTION FUCTION TO BIND TO AN EVENT LATER ------------------------- 14 | void menu0Button1keyDown(void) { 15 | Serial.printf("Menu 0, Button 1: Key Down: %lu ms\n", millis()); 16 | } 17 | 18 | //== MAIN SETUP FUNCTION =========================================================================== 19 | //================================================================================================== 20 | 21 | void setup() { 22 | Serial.begin(115200); // Remember to match platformio.ini setting here 23 | while(!Serial); // Wait for serial port to start up 24 | 25 | // SETUP THE BUTTONS ----------------------------------------------------------------------------- 26 | button2 = new InterruptButton(BUTTON_2, LOW); // Create dynamically allocated button 27 | uint8_t numMenus = 3; // Maximum number of menus/pages (think relative to gui menus) a single button will have 28 | InterruptButton::setMenuCount(numMenus); 29 | InterruptButton::setMenuLevel(0); // Use the functions bound to the first menu associated with the button 30 | //InterruptButton::m_RTOSservicerStackDepth = 4096; // Use larger values for more memory intensive functions if using Asynchronous mode. 31 | InterruptButton::setMode(Mode_Asynchronous); // Defaults to Asynchronous (immediate like an ISR and not actioned in main loop) 32 | 33 | // -- Menu/UI Page 00 Functions ------------------------------------------------- 34 | // ------------------------------------------------------------------------------ 35 | 36 | // BIND BUTTON NUMBER 1 ACTIONS 37 | // Bind the address of a predefined function to an event as per below 38 | uint8_t thisMenuLevel = 0; 39 | button1.bind(Event_KeyDown, thisMenuLevel, &menu0Button1keyDown); 40 | 41 | // Or simply bind LAMBDA functions (with no name) directly to the event as per the remaining examples 42 | button1.bind(Event_KeyUp, thisMenuLevel, [](){ Serial.printf("Menu 0, Button 1: Key Up: %lu ms\n", millis()); }); 43 | button1.bind(Event_KeyPress, thisMenuLevel, [](){ Serial.printf("Menu 0, Button 1: Key Press: %lu ms\n", millis()); }); 44 | button1.bind(Event_LongKeyPress, thisMenuLevel, [](){ Serial.printf("Menu 0, Button 1: Long Key Press: %lu ms\n", millis()); }); 45 | button1.bind(Event_AutoRepeatPress, thisMenuLevel, [](){ Serial.printf("Menu 0, Button 1: Auto Repeat Key Press: %lu ms\n", millis()); }); 46 | button1.bind(Event_DoubleClick, thisMenuLevel, [](){ 47 | Serial.printf("Menu 0, Button 1: Double Click: %lu ms - ", millis()); 48 | switch(InterruptButton::getMode()){ 49 | case Mode_Asynchronous: 50 | InterruptButton::setMode(Mode_Hybrid); 51 | Serial.println("Changing to HYBRID mode."); 52 | break; 53 | case Mode_Hybrid: 54 | InterruptButton::setMode(Mode_Synchronous); 55 | Serial.println("Changing to SYNCHRONOUS mode."); 56 | break; 57 | case Mode_Synchronous: 58 | InterruptButton::setMode(Mode_Asynchronous); 59 | Serial.println("Changing to ASYNCHRONOUS mode."); 60 | break; 61 | } }); 62 | 63 | 64 | // BIND BUTTON NUMBER 2 ACTIONS 65 | button2->bind(Event_KeyDown, thisMenuLevel, [](){ Serial.printf("Menu 0, Button 2: Key Down: %lu ms\n", millis()); }); 66 | button2->bind(Event_KeyUp, thisMenuLevel, [](){ Serial.printf("Menu 0, Button 2: Key Up : %lu ms\n", millis()); }); 67 | button2->bind(Event_KeyPress, thisMenuLevel, [](){ Serial.printf("Menu 0, Button 2: Key Press: %lu ms\n", millis()); }); 68 | 69 | button2->bind(Event_LongKeyPress, thisMenuLevel, [](){ 70 | Serial.printf("Menu 0, Button 2: Long Press: %lu ms - Disabling doubleclicks and switing to menu level ", millis()); 71 | button2->disableEvent(Event_DoubleClick); 72 | InterruptButton::setMenuLevel(1); 73 | Serial.println(InterruptButton::getMenuLevel()); 74 | }); 75 | 76 | button2->bind(Event_AutoRepeatPress, thisMenuLevel, [](){ Serial.printf("Menu 0, Button 2: Auto Repeat Key Press: %lu ms\n", millis()); }); 77 | button2->bind(Event_DoubleClick, thisMenuLevel, [](){ Serial.printf("Menu 0, Button 2: Double Click: %lu ms - Changing to Menu Level ", millis()); 78 | InterruptButton::setMenuLevel(1); 79 | Serial.println(InterruptButton::getMenuLevel()); 80 | }); 81 | 82 | 83 | 84 | 85 | // -- Menu/UI Page 01 Functions ------------------------------------------------- 86 | // ------------------------------------------------------------------------------ 87 | thisMenuLevel = 1; 88 | button1.bind(Event_KeyDown, thisMenuLevel, [](){ Serial.printf("Menu 1, Button 1: Key Down: %lu ms\n", millis()); }); 89 | button1.bind(Event_KeyUp, thisMenuLevel, [](){ Serial.printf("Menu 1, Button 1: Key Up: %lu ms\n", millis()); }); 90 | button1.bind(Event_KeyPress, thisMenuLevel, [](){ Serial.printf("Menu 1, Button 1: Key Press: %lu ms\n", millis()); }); 91 | button1.bind(Event_LongKeyPress, thisMenuLevel, [](){ Serial.printf("Menu 1, Button 1: Long Key Press: %lu ms\n", millis()); }); 92 | button1.bind(Event_AutoRepeatPress, thisMenuLevel, [](){ Serial.printf("Menu 1, Button 1: Auto Repeat Key Press: %lu ms\n", millis());}); 93 | button1.bind(Event_DoubleClick, thisMenuLevel, [](){ 94 | Serial.printf("Menu 1, Button 1: Double Click: %lu ms - Changing to ASYNCHRONOUS mode and to menu level ", millis()); 95 | InterruptButton::setMode(Mode_Asynchronous); 96 | InterruptButton::setMenuLevel(0); 97 | Serial.println(InterruptButton::getMenuLevel()); 98 | }); 99 | 100 | 101 | // Button 2 actions are defined as LAMDA functions which have no name (for exaample purposes) 102 | button2->bind(Event_KeyDown, thisMenuLevel, [](){ Serial.printf("Menu 1, Button 2: Key Down: %lu ms\n", millis()); }); 103 | button2->bind(Event_KeyUp, thisMenuLevel, [](){ Serial.printf("Menu 1, Button 2: Key Up : %lu ms\n", millis()); }); 104 | button2->bind(Event_KeyPress, thisMenuLevel, [](){ Serial.printf("Menu 1, Button 2: Key Press: %lu ms\n", millis()); }); 105 | 106 | button2->bind(Event_LongKeyPress, thisMenuLevel, [](){ 107 | Serial.printf("Menu 1, Button 2: Long Press: %lu ms - [Re-enabling doubleclick - NOTE FASTER KEYPRESS RESPONSE SINCE DOUBLECLICK WAS DISABLED] Changing back to Menu Level ", millis()); 108 | button2->enableEvent(Event_DoubleClick); 109 | InterruptButton::setMenuLevel(0); 110 | Serial.println(InterruptButton::getMenuLevel()); 111 | }); 112 | 113 | button2->bind(Event_AutoRepeatPress, thisMenuLevel, [](){ Serial.printf("Menu 1, Button 2: Auto Repeat Key Press: %lu ms\n", millis()); }); 114 | 115 | button2->bind(Event_DoubleClick, thisMenuLevel, [](){ 116 | Serial.print("Menu 1, Button 2: Double Click - Changing Back to Menu Level "); 117 | //button2->disableEvent(Event_DoubleClick); 118 | InterruptButton::setMenuLevel(2); 119 | Serial.println(InterruptButton::getMenuLevel()); 120 | }); 121 | 122 | 123 | 124 | 125 | 126 | // -- Menu/UI Page 02 Functions ------------------------------------------------- 127 | // ------------------------------------------------------------------------------ 128 | thisMenuLevel = 2; 129 | button1.bind(Event_KeyPress, thisMenuLevel, [](){ 130 | Serial.printf("Menu 2, Button 1: keyPress: %lu ms\n", millis()); 131 | }); 132 | button1.bind(Event_DoubleClick, thisMenuLevel, [](){ 133 | Serial.printf("Menu 2, Button 1: Double Click: %lu ms - Deleting Button 2, press Button 2 quick to test for crash!\n", millis()); 134 | delay(2000); 135 | if(button2 != nullptr) { 136 | delete button2; 137 | button2 = nullptr; 138 | } 139 | }); 140 | 141 | button2->bind(Event_KeyDown, thisMenuLevel, [](){ Serial.printf("Menu 2, Button 2: Key Down: %lu ms\n", millis()); }); 142 | button2->bind(Event_KeyUp, thisMenuLevel, [](){ Serial.printf("Menu 2, Button 2: Key Up: %lu ms\n", millis()); }); 143 | button2->bind(Event_KeyPress, thisMenuLevel, [](){ 144 | Serial.printf("Menu 2, Button 2: keyPress: %lu ms - Didn't seem to Crash!\n", millis()); 145 | }); 146 | } 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | //== MAIN LOOP FUNCTION ===================================================================================== 162 | //=========================================================================================================== 163 | 164 | void loop() { 165 | if(InterruptButton::getMode() != Mode_Asynchronous) InterruptButton::processSyncEvents(); 166 | 167 | // Normally main program will run here and cause various inconsistant loop timing in syncronous events. 168 | 169 | delay(2000); 170 | } 171 | -------------------------------------------------------------------------------- /InterruptButton - Flow Diagram.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwmingis/InterruptButton/d68cf266edf367cfd8aaca513405dc9c86e13d5a/InterruptButton - Flow Diagram.pdf -------------------------------------------------------------------------------- /InterruptButton.cpp: -------------------------------------------------------------------------------- 1 | #include "InterruptButton.h" 2 | 3 | // Include reference req'd for debugging and warnings across serial port. 4 | #ifdef ARDUINO 5 | #include "esp32-hal-log.h" 6 | #else 7 | #include "esp_log.h" 8 | #endif 9 | 10 | 11 | #define ESP_INTR_FLAG_DEFAULT 0 12 | #define EVENT_TASK_PRIORITY 2 // One level higher than arduino's loop() which is priority level 1 13 | #define EVENT_TASK_STACK 4096 // Stack size associated with the queue servicer 14 | #define EVENT_TASK_NAME "BTN_ACTN" 15 | #define EVENT_TASK_CORE 1 // Same core as setup() and loop() 16 | 17 | static const char* TAG = "IBTN"; // IDF log tag 18 | 19 | 20 | 21 | /* ToDo 22 | 1. Need to confirm if any ISR's need to blocked/disabled from other ISR entry, ie portMUX highlevel/lowlevel, etc. 23 | 2. Consider Adding button eventTypes such as momentary, latching, etc. 24 | 3. Consider adding chord combinations (2 or more buttons pressed concurently) as a event. Added to static class member, when any one button is pushed. 25 | corresponding buttons are checked to see if they are in waiting for release state (will be a factorial type check to minimise redundant checks) 26 | 27 | 4. Consider allowing a single button to follow it's own menu level and depart from the global menulevel. 28 | This would be usefull when you have a powerbutton that doesn't change function and menu buttons that do 29 | change their function based on what the current gui menu level is (like a soft key) 30 | */ 31 | 32 | //-- STATIC CLASS MEMBERS AND METHODS (COMMON ACROSS ALL INSTANCES TO SAVE MEMORY) ----------------------- 33 | //-------------------------------------------------------------------------------------------------------- 34 | //-------------------------------------------------------------------------------------------------------- 35 | //-------------------------------------------------------------------------------------------------------- 36 | 37 | //-- Initialise Static Member Variables ------------------------------------------------------------------ 38 | //-------------------------------------------------------------------------------------------------------- 39 | uint32_t InterruptButton::m_RTOSservicerStackDepth { 2048 }; 40 | uint8_t InterruptButton::m_numMenus { 0 }; // 0 Means not initialised, can be set by user; once set it can't be changed. 41 | uint8_t InterruptButton::m_menuLevel { 0 }; 42 | modes InterruptButton::m_mode { Mode_Asynchronous }; 43 | bool InterruptButton::m_classInitialised { false }; 44 | func_ptr_t InterruptButton::m_asyncEventQueue[ASYNC_EVENT_QUEUE_DEPTH] = { nullptr }; 45 | func_ptr_t InterruptButton::m_syncEventQueue[SYNC_EVENT_QUEUE_DEPTH] = { nullptr }; 46 | TaskHandle_t InterruptButton::m_asyncQueueServicerHandle { nullptr }; 47 | bool InterruptButton::m_deleteInProgress { false }; 48 | 49 | // This is used to initialise the queue(s) and also switch between them. 50 | bool InterruptButton::setMode(modes mode){ 51 | // Flush both queues 52 | for(uint8_t i = 0; i < ASYNC_EVENT_QUEUE_DEPTH; i++) m_asyncEventQueue[i] = nullptr; 53 | for(uint8_t i = 0; i < SYNC_EVENT_QUEUE_DEPTH; i++) m_syncEventQueue[i] = nullptr; 54 | 55 | if(mode == Mode_Asynchronous || mode == Mode_Hybrid) { 56 | m_mode = mode; 57 | 58 | // Start the RTOS queue action service/task 59 | bool retVal; 60 | if(m_asyncQueueServicerHandle != nullptr) { 61 | vTaskResume(m_asyncQueueServicerHandle); // Assuming it may have been paused earlier. 62 | retVal = true; 63 | } else { 64 | retVal = xTaskCreatePinnedToCore(asyncQueueServicer, EVENT_TASK_NAME, m_RTOSservicerStackDepth, NULL, 65 | EVENT_TASK_PRIORITY, &m_asyncQueueServicerHandle, EVENT_TASK_CORE) == pdPASS; 66 | } 67 | if(!retVal) ESP_LOGE(TAG, "setMode(): Failed to create RTOS queue servicing task!"); 68 | return retVal; 69 | 70 | } else if(mode == Mode_Synchronous) { 71 | m_mode = mode; 72 | if(m_asyncQueueServicerHandle != nullptr) vTaskSuspend(m_asyncQueueServicerHandle); 73 | return true; 74 | 75 | } else { 76 | ESP_LOGE(TAG, "setMode(): Invalid mode specified!"); 77 | return false; 78 | } 79 | } 80 | 81 | modes InterruptButton::getMode(){ 82 | return m_mode; 83 | } 84 | 85 | void InterruptButton::asyncQueueServicer(void* pvParams){ 86 | while(1){ 87 | if(m_asyncEventQueue[0] != nullptr) { 88 | m_asyncEventQueue[0](); // Action the first entry 89 | 90 | for(uint8_t i = 1; i < ASYNC_EVENT_QUEUE_DEPTH; i++) { // Shift the rest down or clear this entry. 91 | m_asyncEventQueue[i - 1] = m_asyncEventQueue[i]; 92 | if(m_asyncEventQueue[i] == nullptr) break; 93 | if(i == ASYNC_EVENT_QUEUE_DEPTH - 1) m_asyncEventQueue[i] = nullptr; 94 | } 95 | } 96 | vTaskDelay(10 / ((TickType_t)1000 / configTICK_RATE_HZ)); // Required to yield to RTOS scheduler during idle times 97 | } 98 | vTaskDelete(NULL); // Only reached if we put a condition in the primary while loop based on mode 99 | } 100 | 101 | void InterruptButton::processSyncEvents() { 102 | while(m_syncEventQueue[0] != nullptr) { 103 | m_syncEventQueue[0](); // Action the first entry 104 | 105 | for(uint8_t i = 1; i < SYNC_EVENT_QUEUE_DEPTH; i++) { // Shift the rest down or clear this entry. 106 | m_syncEventQueue[i - 1] = m_syncEventQueue[i]; 107 | if(m_syncEventQueue[i] == nullptr) break; 108 | if(i == SYNC_EVENT_QUEUE_DEPTH - 1) m_syncEventQueue[i] = nullptr; 109 | } 110 | } 111 | } 112 | 113 | 114 | //-- Method to monitor button, called by button change and various timer interrupts ---------------------- 115 | void IRAM_ATTR InterruptButton::readButton(void *arg){ 116 | if(m_deleteInProgress) return; 117 | InterruptButton* btn = reinterpret_cast(arg); 118 | 119 | switch(btn->m_state){ 120 | case Released: // Was sitting released but just detected a signal from the button 121 | gpio_intr_disable(btn->m_pin); // Ignore change inputs while we poll for a valid press 122 | btn->m_validPolls = 1; btn->m_totalPolls = 1; // Was released, just detected a change, must be a valid press so count it. 123 | btn->m_blockKeyPress = false; 124 | startTimer(btn->m_buttonPollTimer, btn->m_pollIntervalUS, &readButton, btn, "DB_begin_"); // Begin debouncing the button input 125 | btn->m_state = ConfirmingPress; 126 | 127 | break; 128 | 129 | case ConfirmingPress: // we get here each time the debounce timer expires (onchange interrupt disabled remember) 130 | btn->m_totalPolls++; // Count the number of total reads 131 | if(gpio_get_level(btn->m_pin) == btn->m_pressedState) btn->m_validPolls++; // Count the number of valid 'PRESSED' reads 132 | if(btn->m_totalPolls >= TARGET_POLLS){ // If we have checked the button enough times, then make a decision on key state 133 | if(btn->m_validPolls * 2 <= btn->m_totalPolls) { // Then it was a false alarm 134 | btn->m_state = Released; 135 | gpio_intr_enable(btn->m_pin); 136 | return; 137 | } // Otherwise, spill over to "Pressing" 138 | } else { // Not yet enough polls to confirm state 139 | startTimer(btn->m_buttonPollTimer, btn->m_pollIntervalUS, &readButton, btn, "CP2_"); // Keep sampling pin state 140 | return; 141 | } 142 | [[fallthrough]]; // Planned spill through here (no break) if logic requires, ie keyDown confirmed. 143 | case Pressing: // VALID KEYDOWN, assumed pressed if it had valid polls more than half the time 144 | btn->action(btn, Event_KeyDown); // Add the keyDown action to the relevant queue 145 | if(btn->eventEnabled(Event_LongKeyPress) && btn->eventActions[m_menuLevel][Event_LongKeyPress] != nullptr){ 146 | startTimer(btn->m_buttonLPandRepeatTimer, uint64_t(btn->m_longKeyPressMS * 1000), &longPressEvent, btn, "CP1_"); 147 | } else if (btn->eventEnabled(Event_AutoRepeatPress)) { 148 | startTimer(btn->m_buttonLPandRepeatTimer, uint64_t(btn->m_autoRepeatMS * 1000), &autoRepeatPressEvent, btn, "CP1_"); 149 | } 150 | 151 | btn->m_state = Pressed; 152 | gpio_intr_enable(btn->m_pin); // Begin monitoring pin again 153 | break; 154 | 155 | case Pressed: // Currently pressed until now, but there was a change on the pin 156 | gpio_intr_disable(btn->m_pin); // Turn off this interrupt to ignore inputs while we wait to check if valid release 157 | startTimer(btn->m_buttonPollTimer, btn->m_pollIntervalUS, &readButton, btn, "PR_"); // Start timer and start polling the button to debounce it 158 | btn->m_validPolls = 1; btn->m_totalPolls = 1; // This is first poll and it was just released by definition of state 159 | btn->m_state = WaitingForRelease; 160 | break; 161 | 162 | case WaitingForRelease: // we get here when debounce timer or doubleclick timeout timer alarms (onchange interrupt disabled remember) 163 | // stay in this state until released, because button could remain locked down if release missed. 164 | btn->m_totalPolls++; 165 | if(gpio_get_level(btn->m_pin) != btn->m_pressedState){ 166 | btn->m_validPolls++; 167 | if(btn->m_totalPolls < TARGET_POLLS || btn->m_validPolls * 2 <= btn->m_totalPolls) { // If we haven't polled enough or not high enough success rate 168 | startTimer(btn->m_buttonPollTimer, btn->m_pollIntervalUS, &readButton, btn, "W4R_polling_"); // Then keep sampling pin state until release is confirmed 169 | return; 170 | } // Otherwise, spill through to "Releasing" 171 | } else { 172 | if(btn->m_validPolls > 0) { 173 | btn->m_validPolls--; 174 | } else { 175 | btn->m_totalPolls = 0; // Key is being held down, don't let total polls get too far ahead. 176 | } 177 | startTimer(btn->m_buttonPollTimer, btn->m_pollIntervalUS, &readButton, btn, "W4R_invalidPoll"); // Keep sampling pin state until released 178 | } 179 | [[fallthrough]]; // Intended spill through here (no break) to "Releasing" once keyUp confirmed. 180 | 181 | case Releasing: 182 | killTimer(btn->m_buttonLPandRepeatTimer); 183 | btn->action(btn, Event_KeyUp); // Add the keyUp action to the relevant queue 184 | 185 | if(btn->eventEnabled(Event_DoubleClick) && btn->eventEnabled(Event_All) && // If double-clicks are enabled and defined 186 | btn->eventActions[m_menuLevel][Event_DoubleClick] != nullptr) { 187 | 188 | if(btn->m_wtgForDblClick) { // VALID DOUBLE-CLICK (second keyup without a timeout, would normally check 189 | killTimer(btn->m_buttonDoubleClickTimer); // esp_timer_is_active, but function not available in esp32 arduino core. 190 | btn->m_wtgForDblClick = false; 191 | btn->action(btn, Event_DoubleClick); // Add the double-click action to the relevant queue 192 | 193 | } else if (!btn->m_blockKeyPress) { // Commence double-click detection process 194 | btn->m_wtgForDblClick = true; 195 | btn->m_doubleClickMenuLevel = m_menuLevel; // Save menuLevel in case this is converted to a keyPress later 196 | startTimer(btn->m_buttonDoubleClickTimer, uint64_t(btn->m_doubleClickMS * 1000), &doubleClickTimeout, btn, "W4R_DCsetup_"); 197 | } 198 | } else if(!btn->m_blockKeyPress) { // Otherwise, treat as a basic keyPress 199 | btn->action(btn, Event_KeyPress); // Then treat as a normal keyPress 200 | } 201 | btn->m_state = Released; 202 | gpio_intr_enable(btn->m_pin); 203 | break; 204 | } // End of SWITCH statement 205 | 206 | return; 207 | } // End of readButton function 208 | 209 | 210 | //-- Method to handle longKeyPresses (called by timer)---------------------------------------------------- 211 | void InterruptButton::longPressEvent(void *arg){ 212 | if(m_deleteInProgress) return; 213 | InterruptButton* btn = reinterpret_cast(arg); 214 | 215 | btn->action(btn, Event_LongKeyPress); // Add the long keypress action to the relevant queue 216 | btn->m_blockKeyPress = true; // Used to prevent regular keypress or doubleclick later on in procedure. 217 | 218 | //Initiate the autorepeat function 219 | if(btn->eventEnabled(Event_AutoRepeatPress) && gpio_get_level(btn->m_pin) == btn->m_pressedState) { // Sanity check to stop autorepeats in case we somehow missed button release 220 | startTimer(btn->m_buttonLPandRepeatTimer, uint64_t(btn->m_autoRepeatMS * 1000), &autoRepeatPressEvent, btn, "LPD_"); 221 | } 222 | } 223 | 224 | //-- Method to handle autoRepeatPresses (called by timer)------------------------------------------------- 225 | void InterruptButton::autoRepeatPressEvent(void *arg){ 226 | if(m_deleteInProgress) return; 227 | InterruptButton* btn = reinterpret_cast(arg); 228 | btn->m_blockKeyPress = true; // Used to prevent regular keypress or doubleclick later on in procedure. 229 | 230 | if(btn->eventActions[m_menuLevel][Event_AutoRepeatPress] != nullptr) { 231 | btn->action(btn, Event_AutoRepeatPress); // Action the Async Auto Repeat KeyPress Event if defined 232 | } else { 233 | btn->action(btn, Event_KeyPress); // Action the Async KeyPress Event otherwise 234 | } 235 | if(btn->eventEnabled(Event_AutoRepeatPress) && gpio_get_level(btn->m_pin) == btn->m_pressedState) { // Sanity check to stop autorepeats in case we somehow missed button release 236 | startTimer(btn->m_buttonLPandRepeatTimer, uint64_t(btn->m_autoRepeatMS * 1000), &autoRepeatPressEvent, btn, "LPD_"); 237 | } 238 | } 239 | 240 | //-- Method to return to interpret previous keyUp as a keyPress instead of a doubleClick if it times out. 241 | void InterruptButton::doubleClickTimeout(void *arg){ 242 | if(m_deleteInProgress) return; 243 | InterruptButton* btn = reinterpret_cast(arg); 244 | btn->m_wtgForDblClick = false; 245 | if(gpio_get_level(btn->m_pin) != btn->m_pressedState) 246 | btn->action(btn, Event_KeyPress, btn->m_doubleClickMenuLevel); // Then treat as a normal keyPress at the menuLevel when first click occurred 247 | // Note, this timer is never started if previous press was a longpress 248 | } 249 | 250 | //-- Helper method to simplify starting a timer ---------------------------------------------------------- 251 | void IRAM_ATTR InterruptButton::startTimer(esp_timer_handle_t &timer, uint32_t duration_US, void (*callBack)(void* arg), InterruptButton* btn, const char *msg){ 252 | if(m_deleteInProgress) return; 253 | esp_timer_create_args_t tmrConfig; 254 | tmrConfig.arg = reinterpret_cast(btn); 255 | tmrConfig.callback = callBack; 256 | tmrConfig.dispatch_method = ESP_TIMER_TASK; 257 | tmrConfig.name = msg; 258 | // this line crashes the esp if button was created dynamically. 259 | killTimer(timer); 260 | esp_timer_create(&tmrConfig, &timer); 261 | esp_timer_start_once(timer, duration_US); 262 | } 263 | 264 | //-- Helper method to kill a timer ----------------------------------------------------------------------- 265 | void IRAM_ATTR InterruptButton::killTimer(esp_timer_handle_t &timer){ 266 | if(timer){ 267 | esp_timer_stop(timer); 268 | esp_timer_delete(timer); 269 | timer = nullptr; 270 | } 271 | } 272 | 273 | void IRAM_ATTR InterruptButton::action(InterruptButton* btn, events event, uint8_t menuLevel){ 274 | if(m_deleteInProgress) return; 275 | if(menuLevel >= m_numMenus) return; // Invalid menu level 276 | if(!btn->eventEnabled(event) || !btn->eventEnabled(Event_All)) return; // Specific event is or all events are disabled 277 | if(btn->eventActions[menuLevel][event] == nullptr) return; // Event is not defined 278 | 279 | if(m_mode == Mode_Asynchronous || (m_mode == Mode_Hybrid && (event == Event_KeyDown || event == Event_KeyUp))) { 280 | for(uint8_t i = 0; i < ASYNC_EVENT_QUEUE_DEPTH; i++){ // Action immediatley using RTOS asynchronous Queue 281 | //ESP_LOGD(TAG,"Searching for free spot to add action: %d", i); 282 | if(m_asyncEventQueue[i] == nullptr){ 283 | //ESP_LOGD(TAG,"\tAdding entry at position: %d", i); 284 | m_asyncEventQueue[i] = btn->eventActions[menuLevel][event]; 285 | break; 286 | } 287 | } 288 | } else { // Action when called in main loop hook using synchronous Queue 289 | for(uint8_t i = 0; i < SYNC_EVENT_QUEUE_DEPTH; i++){ 290 | if(m_syncEventQueue[i] == nullptr){ 291 | m_syncEventQueue[i] = btn->eventActions[menuLevel][event]; 292 | break; 293 | } 294 | } 295 | } 296 | } 297 | 298 | 299 | 300 | //-- CLASS MEMBERS AND METHODS SPECIFIC TO A SINGLE INSTANCE (BUTTON) ------------------------------------ 301 | //-------------------------------------------------------------------------------------------------------- 302 | //-------------------------------------------------------------------------------------------------------- 303 | //-------------------------------------------------------------------------------------------------------- 304 | 305 | // Class object control and setup functions ------------------------------------- 306 | // ------------------------------------------------------------------------------ 307 | 308 | // Constructor ------------------------------------------------------------------ 309 | InterruptButton::InterruptButton(uint8_t pin, uint8_t pressedState, gpio_mode_t pinMode, 310 | uint16_t longKeyPressMS, uint16_t autoRepeatMS, 311 | uint16_t doubleClickMS, uint32_t debounceUS) : 312 | m_pressedState(pressedState), 313 | m_pinMode(pinMode), 314 | m_longKeyPressMS(longKeyPressMS), 315 | m_autoRepeatMS(autoRepeatMS), 316 | m_doubleClickMS(doubleClickMS) { 317 | 318 | if (GPIO_IS_VALID_GPIO(pin)) // Check for a valid pin first 319 | m_pin = static_cast(pin); 320 | else { 321 | ESP_LOGW(TAG, "%d is not valid gpio on this platform", pin); 322 | m_pin = static_cast(-1); //GPIO_NUM_NC (enum not showing up as defined); 323 | } 324 | m_pollIntervalUS = (debounceUS / TARGET_POLLS > 65535) ? 65535 : debounceUS / TARGET_POLLS; 325 | } 326 | 327 | // Destructor -------------------------------------------------------------------- 328 | InterruptButton::~InterruptButton() { 329 | m_deleteInProgress = true; 330 | gpio_isr_handler_remove(m_pin); 331 | killTimer(m_buttonPollTimer); killTimer(m_buttonLPandRepeatTimer); killTimer(m_buttonDoubleClickTimer); 332 | gpio_reset_pin(m_pin); 333 | 334 | for(int menu = 0; menu < m_numMenus; menu++) delete [] eventActions[menu]; 335 | delete [] eventActions; 336 | m_deleteInProgress = false; 337 | } 338 | 339 | // Initialiser ------------------------------------------------------------------- 340 | void InterruptButton::initialiseInstance(void){ 341 | if(m_thisButtonInitialised) return; 342 | 343 | if(!m_classInitialised){ // We must be initialising the first button 344 | if(m_numMenus == 0) m_numMenus = 1; // Default to a single menu level if not set prior to initialising first button 345 | esp_err_t err = gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT); 346 | if(err != ESP_OK) ESP_LOGD(TAG, "GPIO ISR service installed with exit status: %d", err); 347 | m_classInitialised = setMode(m_mode) && (err == ESP_OK || err == ESP_ERR_INVALID_STATE); 348 | } 349 | 350 | eventActions = new func_ptr_t*[m_numMenus]; // Define the array of actions associated with each button 351 | for(int menu = 0; menu < m_numMenus; menu++){ 352 | eventActions[menu] = new func_ptr_t[NumEventTypes]; 353 | for(int evt = 0; evt < NumEventTypes; evt++){ 354 | eventActions[menu][evt] = nullptr; 355 | } 356 | } 357 | gpio_config_t gpio_conf = {}; // Configure the interrupt associated with the pin 358 | gpio_conf.mode = m_pinMode; 359 | gpio_conf.pin_bit_mask = BIT64(static_cast(m_pin)); 360 | gpio_conf.pull_down_en = (m_pressedState) ? GPIO_PULLDOWN_ENABLE : GPIO_PULLDOWN_DISABLE; 361 | gpio_conf.pull_up_en = (m_pressedState) ? GPIO_PULLUP_DISABLE : GPIO_PULLUP_ENABLE; 362 | gpio_conf.intr_type = GPIO_INTR_ANYEDGE; 363 | gpio_config(&gpio_conf); 364 | gpio_isr_handler_add(m_pin, InterruptButton::readButton, reinterpret_cast(this)); 365 | m_state = (gpio_get_level(m_pin) == m_pressedState) ? Pressed : Released; // Set to current state when initialising 366 | m_thisButtonInitialised = true; 367 | } 368 | 369 | 370 | //-- TIMING INTERVAL GETTERS AND SETTERS ----------------------------------------------------------------- 371 | void InterruptButton::setLongPressInterval(uint16_t intervalMS) { m_longKeyPressMS = intervalMS; } 372 | uint16_t InterruptButton::getLongPressInterval(void) { return m_longKeyPressMS; } 373 | void InterruptButton::setAutoRepeatInterval(uint16_t intervalMS) { m_autoRepeatMS = intervalMS; } 374 | uint16_t InterruptButton::getAutoRepeatInterval(void) { return m_autoRepeatMS; } 375 | void InterruptButton::setDoubleClickInterval(uint16_t intervalMS) { m_doubleClickMS = intervalMS; } 376 | uint16_t InterruptButton::getDoubleClickInterval(void) { return m_doubleClickMS; } 377 | 378 | 379 | // -- FUNCTIONS RELATED TO EXTERNAL ACTIONS -------------------------------------------------------------- 380 | // ------------------------------------------------------------------------------------------------------- 381 | void InterruptButton::bind(events event, uint8_t menuLevel, func_ptr_t action){ 382 | if(!m_thisButtonInitialised) initialiseInstance(); // Auto initialisation (typical begin() function) 383 | 384 | if(menuLevel >= m_numMenus) { 385 | ESP_LOGE(TAG, "Specified menu level is greater than the number of menus!"); 386 | } else if(event >= NumEventTypes) { 387 | ESP_LOGE(TAG, "Specified event is invalid!"); 388 | } else { 389 | eventActions[menuLevel][event] = action; // Bind external action to button 390 | if(!eventEnabled(event)) enableEvent(event); // Assume if we are binding it, we want it enabled. 391 | } 392 | } 393 | 394 | void InterruptButton::unbind(events event, uint8_t menuLevel){ 395 | if(m_numMenus == 0){ 396 | ESP_LOGE(TAG, "You must have bound at least one function prior to unbinding it from a button!"); 397 | } else if(menuLevel >= m_numMenus) { 398 | ESP_LOGE(TAG, "Specified menu level is greater than the number of menus!"); 399 | } else if(event >= NumEventTypes) { 400 | ESP_LOGE(TAG, "Specified event is invalid!"); 401 | } else { 402 | eventActions[menuLevel][event] = nullptr; 403 | } 404 | return; 405 | } 406 | 407 | void InterruptButton::enableEvent(events event){ 408 | if(event <= Event_All && event != NumEventTypes) eventMask |= (1UL << (event)); // Set the relevant bit 409 | } 410 | void InterruptButton::disableEvent(events event){ 411 | if(event <= Event_All && event != NumEventTypes) eventMask &= ~(1UL << (event)); // Clear the relevant bit 412 | } 413 | bool InterruptButton::eventEnabled(events event) { 414 | return ((eventMask >> event) & 0x01) == 0x01; 415 | } 416 | 417 | void InterruptButton::setMenuCount(uint8_t numberOfMenus){ // This can only be set before initialising first button 418 | if(!m_classInitialised && numberOfMenus >= 1) m_numMenus = numberOfMenus; 419 | } 420 | uint8_t InterruptButton::getMenuCount(void) { 421 | return m_numMenus; 422 | } 423 | 424 | void InterruptButton::setMenuLevel(uint8_t level) { 425 | if(level < m_numMenus) { 426 | m_menuLevel = level; 427 | } else { 428 | ESP_LOGE(TAG, "Menu level '%d' must be >= 0 AND < number of menus (zero origin): ", level); 429 | } 430 | } 431 | 432 | uint8_t InterruptButton::getMenuLevel(){ 433 | return m_menuLevel; 434 | } 435 | -------------------------------------------------------------------------------- /InterruptButton.h: -------------------------------------------------------------------------------- 1 | // New in version 2.0.1 2 | // Moved RTOS task servicer to Core 1 3 | // Blocked doubleclicks and key presses in the event of an autokeypress event. 4 | 5 | #ifndef INTERRUPTBUTTON_H_ 6 | #define INTERRUPTBUTTON_H_ 7 | 8 | 9 | #include "driver/gpio.h" 10 | #include "esp_timer.h" 11 | #include "freertos/FreeRTOS.h" 12 | #include "freertos/task.h" 13 | #include "freertos/queue.h" 14 | #include 15 | 16 | #define ASYNC_EVENT_QUEUE_DEPTH 5 // This queue is serviced very quickly so can be short 17 | #define SYNC_EVENT_QUEUE_DEPTH 10 // This queue is limited to mainloop frequency so actions can backup 18 | #define TARGET_POLLS 10 // Number of times to poll a button to determine it's state 19 | 20 | 21 | typedef std::function func_ptr_t; // Typedef to faciliate managing pointers to external action functions 22 | 23 | enum modes { 24 | Mode_Asynchronous, // All actions performed via Asynchronous RTOS queue 25 | Mode_Hybrid, // keyUp and keyDown performed by RTOS queue, remaining actions by Static Synchronous Queue. 26 | Mode_Synchronous // All actions performed by Synchronous Queue (static class member array, FIFO). 27 | }; 28 | 29 | enum events:uint8_t { 30 | Event_KeyDown = 0, 31 | Event_KeyUp, 32 | Event_KeyPress, 33 | Event_LongKeyPress, 34 | Event_AutoRepeatPress, 35 | Event_DoubleClick, 36 | NumEventTypes, // Not an event, but this value used to size the number of columns in event/action array. 37 | Event_All // Used to enable or disable all events 38 | }; 39 | 40 | 41 | // -- Interrupt Button and Debouncer --------------------------------------------------------------------------------------- 42 | // -- ---------------------------------------------------------------------------------------------------------------------- 43 | class InterruptButton { 44 | private: 45 | enum buttonStates { // Enumeration to assist with program flow at state machine for reading button 46 | Released, 47 | ConfirmingPress, 48 | Pressing, 49 | Pressed, 50 | WaitingForRelease, 51 | Releasing 52 | }; 53 | 54 | // STATIC class members shared by all instances of this object (common across all instances of the class) 55 | // ------------------------------------------------------------------------------------------------------ 56 | static void asyncQueueServicer(void* pvParams); // Function used as RTOS task to receive and process action from RTOS message queue. 57 | static void readButton(void* arg); // function to read button state (must be static to bind to GPIO and timer ISR) 58 | static void longPressEvent(void *arg); // Callback to excecute a longPress event, called by timer 59 | static void autoRepeatPressEvent(void *arg); // Callback to excecute a autoRepeatPress event, called by timer 60 | static void doubleClickTimeout(void *arg); // Callback used to separate double-clicks from regular keyPress's, called by timer 61 | static void startTimer(esp_timer_handle_t &timer, // Helper func to start timer. 62 | uint32_t duration_US, 63 | void (*callBack)(void* arg), 64 | InterruptButton* btn, 65 | const char *msg); 66 | static void killTimer(esp_timer_handle_t &timer); // Helper function to kill a timer 67 | 68 | static void action(InterruptButton *btn, // Helper function to simplify calling actions at specified menulevel 69 | events event, 70 | uint8_t menuLevel); 71 | inline static void action(InterruptButton* btn, events event) { action(btn, event, m_menuLevel); }; 72 | 73 | static bool m_classInitialised; // Boolean flag to control class initialisation 74 | static bool m_firstButtonInitialised; // Used to block any further changes to m_numMenus 75 | static TaskHandle_t m_asyncQueueServicerHandle; // Pointer/handle to the RTOS task that actions the RTOS Queue messages 76 | static func_ptr_t m_asyncEventQueue[]; // Array used as the Static Synchronous Event Queue 77 | static func_ptr_t m_syncEventQueue[]; // Array used as the Static Synchronous Event Queue 78 | 79 | static uint8_t m_numMenus; // Total number of menu sets, can be set by user, but only before initialising first button 80 | static uint8_t m_menuLevel; // Current menulevel for all buttons (global in class so common across all buttons) 81 | static modes m_mode; 82 | static bool m_deleteInProgress; // Precautionary blocker to prevent asyc calls of object methods while they are being deleted 83 | 84 | // Non-static instance specific member declarations 85 | // ------------------------------------------------ 86 | void initialiseInstance(void); // Setup interrupts and event-action array 87 | bool m_thisButtonInitialised = false; // Allows us to intialise when binding functions (ie detect if already done) 88 | gpio_num_t m_pin; // Button gpio 89 | uint8_t m_pressedState; // State of button when it is pressed (LOW or HIGH) 90 | gpio_mode_t m_pinMode; // GPIO mode: IDF's input/output mode 91 | volatile buttonStates m_state; // Instance specific state machine variable (intialised when intialising button) 92 | volatile bool m_wtgForDblClick = false; 93 | esp_timer_handle_t m_buttonPollTimer = nullptr; // Instance specific timer for button debouncing 94 | esp_timer_handle_t m_buttonLPandRepeatTimer = nullptr; // Instance specific timer for button longPress and autoRepeat timing 95 | esp_timer_handle_t m_buttonDoubleClickTimer = nullptr; // Instance specific timer for discerning double-clicks from regular keyPresses 96 | 97 | volatile uint8_t m_doubleClickMenuLevel; // Stores current menulevel while differentiating between regular keyPress or a double-click 98 | uint16_t m_pollIntervalUS; // Timing variables 99 | uint16_t m_longKeyPressMS; 100 | uint16_t m_autoRepeatMS; 101 | uint16_t m_doubleClickMS; 102 | 103 | volatile bool m_blockKeyPress; // Boolean flag to prevent firing a keypress if a longPress or AutoRepeatPress occurred (outside of polling fuction) 104 | volatile uint16_t m_validPolls = 0; // Variables to conduct debouncing algoritm 105 | volatile uint16_t m_totalPolls = 0; 106 | 107 | func_ptr_t** eventActions = nullptr; // Pointer to 2D array, event actions by row, menu levels by column. 108 | uint16_t eventMask = 0b0000010000111; // Default to keyUp, keyDown, and keyPress enabled, and no blanket disable 109 | // When binding functions, longKeyPress, autoKeyPresses, & double-clicks are automatically enabled. 110 | 111 | public: 112 | // Static class members shared by all instances of this object ----------------------- 113 | static bool setMode(modes mode); // Toggle between Synchronous (Static Queue), Hybrid, or Asynchronous modes (RTOS Queue) 114 | static modes getMode(void); 115 | static void processSyncEvents(void); // Process Sync Events, called from main looop 116 | static void setMenuCount(uint8_t numberOfMenus); // Sets number of menus/pages that each button has (can only be done before intialising first button) 117 | static uint8_t getMenuCount(void); // Retrieves total number of menus. 118 | static void setMenuLevel(uint8_t level); // Sets menu level across all buttons (ie buttons mean something different each page) 119 | static uint8_t getMenuLevel(); // Retrieves menu level 120 | static uint32_t m_RTOSservicerStackDepth; // Allows the user to set the depth of RTOS servicer function (for bound functions) 121 | // Must be set before initialsing/binding first button or calling setMode(). 122 | 123 | // Non-static instance specific member declarations ---------------------------------- 124 | InterruptButton(uint8_t pin, // Class Constructor, pin to monitor 125 | uint8_t pressedState, // State of the pin when pressed (HIGH or LOW) 126 | gpio_mode_t pinMode = GPIO_MODE_INPUT, 127 | uint16_t longKeyPressMS = 750, 128 | uint16_t autoRepeatMS = 250, 129 | uint16_t doubleClickMS = 333, 130 | uint32_t debounceUS = 8000); 131 | ~InterruptButton(); // Class Destructor 132 | 133 | void enableEvent(events event); // Enable the event passed as argument (updates bitmask) 134 | void disableEvent(events event); // Disable the event passed as argument (updates bitmask) 135 | bool eventEnabled(events event); // Read bitmask and determine if event is enabled 136 | void setLongPressInterval(uint16_t intervalMS); // Updates LongPress Interval 137 | uint16_t getLongPressInterval(void); 138 | void setAutoRepeatInterval(uint16_t intervalMS); // Updates autoRepeat Interval 139 | uint16_t getAutoRepeatInterval(void); 140 | void setDoubleClickInterval(uint16_t intervalMS); // Updates autoRepeat Interval 141 | uint16_t getDoubleClickInterval(void); 142 | 143 | 144 | // Routines to manage interface with external action functions associated with each event --- 145 | void bind(events event, // Used to bind an action to an event at a given menulevel 146 | uint8_t menuLevel, 147 | func_ptr_t action); 148 | inline void bind(events event, func_ptr_t action) { bind(event, m_menuLevel, action); } // Above function defaulting to current menulevel 149 | 150 | void unbind(events event, // Used to unbind an action to an event at a given menulevel 151 | uint8_t menuLevel); 152 | inline void unbind(events event) { unbind(event, m_menuLevel); }; // Above function defaulting to current menulevel 153 | }; 154 | 155 | #endif // INTERRUPTBUTTON_H_ 156 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 rwmingis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # InterruptButton for ESP32 (Arduino / ESP IDF) 2 | This is highly responsive and simple to use interrupt-based button event library for the ESP32 suitable in the Arduino framework as well as the ESP IDF framework. It uses the 'onChange' interrupt (rising or falling) for a given pin and the ESP high precision timer to carry the necessary pin-polling to facilitate a simple, but reliable debouncing and timing routines. Once de-bounced, actions bound to certain button events including 'Key Down', 'Key Up', 'Key Press', 'Long Key Press', 'Auto-Repeat Press', and 'Double-Clicks' are available to bind your own functions to. 3 | 4 | How the bound functions are executed depends on the mode you have set the library to: 'Mode_Asynchronous' (actioned immediately), 'Mode_Synchronous' (actioned in main loop), or 'Mode_Hybrid' where 'Key Up' and 'Key Down' are Asynchronous and the remainder of events are executed Synchronously. 5 | 6 | This makes employing extended button functions VERY. EASY. TO. DO. Only the first 2 lines below are required to get it going: 7 | 8 | ``` 9 | // ----- Global variable -------- 10 | InterruptButton button1(32, LOW); // Monitor pin 35, LOW when pressed 11 | 12 | // ----- setup() function ------- 13 | button1.bind(Event_KeyPress, &menu0Button1keyPress); // Bind a predefined function to the event 14 | 15 | // Alternatively you may bind a lambda function directy which saves having to define it elsewhere 16 | button1.bind(Event_DoubleClick, [](){ /* do stuff here */ }); 17 | 18 | // ------------------------------------------------------------------------------------------------------ 19 | // If you have selected Mode_Synchronous, then you'll need to action the event in the main loops as below 20 | // ------------------------------------------------------------------------------------------------------ 21 | 22 | // ----- Main loop() function ---- 23 | button1.processSyncEvents(); // Only required if using sync events 24 | ``` 25 | 26 | With a built in user-defined menu/paging system, each button can be bound to multiple layers of different functionality depending on what the user interface is displaying at a given moment. This means it can work as a simple button, all the way up to a full user interface using a single button with different combinations of special key actions for navigation. 27 | 28 | The use of interrupts instead of laborious button polling means that actions bound to the button are NOT limited to the main loop frequency which significantly reduces the chance of missed presses with long main loop durations. 29 | 30 | There are 6 events and a menu/paging structure which is only limited to your memory on chip (which is huge for the ESP32). This means you could attach 6 different user-defined action functions to a single button per menu level per page. Ie if you have a 4 page gui menu, one button associated with that interface could have up to 24 actions bound to it! 31 | 32 | ## It allows for the following: 33 | 34 | ### Event Types 35 | Events are actioned by calling user defined functions attached to specific button events Asynchronously via RTOS or Synchronously via main loop hook. 36 | * **Event_KeyDown** - Happens anytime key is depressed (even if held), be it a keyPress, longKeyPress, or a double-click 37 | * **Event_KeyUp** - Happens anytime key is released, be it a keyPress, longKeyPress, end of an AutoRepeatPress, or a double-click 38 | * **Event_KeyPress** - Occurs upon keyUp only if it is not a longKeyPress, AutoRepeatPress, or double-click 39 | * **Event_LongKeyPress** (required press time is user configurable) 40 | * **Event_AutoRepeatPress** (Rapid fire, if enabled, but not defined, then the standard keyPress action is used) 41 | * **Event_DoubleClick** (max time between clicks is user configurable) 42 | 43 | ### Multi-page/level events 44 | This is handy if you have several different GUI pages where all the buttons mean something different on a different page. 45 | You can change the menu level of all buttons at once using the static member function 'setMenuLevel(level)'. Note that you must set the desired number of menus before initialising your first button, as this cannot be changed later (this may be improved later subject to user requests) 46 | 47 | ### Other Features 48 | * Each event (or all events) can enabled or disabled on a per-button basis 49 | * The timing for debounce, longPress, AutoRepeatPress and doubleClick can be set on a per-button basis. 50 | * Asynchronous events are called *Immediately* after debouncing 51 | * Synchronous events are invoked by calling the 'processSyncEvents()' member function in the main loop and *are subject to the main loop timing.* 52 | 53 | ### Example Usage 54 | This is an output of the serial port from the example file. Here just the Serial.Println() function is called, but you can replace that with your own code to do what you need. 55 | 56 | ``` 57 | Menu 0, Button 1: Key Down: 5371114 ms 58 | Menu 0, Button 1: Key Up: 5371340 ms 59 | Menu 0, Button 1: Key Press: 5371540 ms 60 | Menu 0, Button 1: Key Down: 5373335 ms 61 | Menu 0, Button 1: Long Key Press: 5374085 ms 62 | Menu 0, Button 1: Auto Repeat Key Press: 5374335 ms 63 | Menu 0, Button 1: Key Up: 5374472 ms 64 | Menu 0, Button 1: Key Down: 5376200 ms 65 | Menu 0, Button 1: Key Up: 5376380 ms 66 | Menu 0, Button 1: Key Press: 5376580 ms 67 | Menu 0, Button 1: Key Down: 5377305 ms 68 | Menu 0, Button 1: Long Key Press: 5378055 ms 69 | Menu 0, Button 1: Auto Repeat Key Press: 5378305 ms 70 | Menu 0, Button 1: Auto Repeat Key Press: 5378555 ms 71 | Menu 0, Button 1: Auto Repeat Key Press: 5378805 ms 72 | Menu 0, Button 1: Auto Repeat Key Press: 5379055 ms 73 | Menu 0, Button 1: Key Up: 5379145 ms 74 | Menu 0, Button 2: Key Down: 5380088 ms 75 | Menu 0, Button 2: Key Up : 5380378 ms 76 | Menu 0, Button 2: Key Press: 5380578 ms 77 | Menu 0, Button 2: Key Down: 5381342 ms 78 | Menu 0, Button 2: Long Press: 5382092 ms - Disabling doubleclicks and switing to menu level 1 79 | Menu 1, Button 2: Auto Repeat Key Press: 5382343 ms 80 | Menu 1, Button 2: Key Up : 5382440 ms 81 | Menu 1, Button 1: Key Down: 5389981 ms 82 | Menu 1, Button 1: Key Up: 5390070 ms 83 | Menu 1, Button 1: Key Down: 5390149 ms 84 | Menu 1, Button 1: Key Up: 5390264 ms 85 | Menu 1, Button 1: Double Click: 5390265 ms - Changing to ASYNCHRONOUS mode and to menu level 0 86 | ``` 87 | 88 | ## Functional Flow Diagram ## 89 | The flow diagram below shows the basic function of the library. It is pending an update to include some recent updates and additions such as 'autoRepeatPress' 90 | 91 | ![Flow Diagram](images/flowDiagram.png) 92 | 93 | 94 | ## Roadmap Forward ## 95 | * Consider Adding button modes such as momentary, latching, etc. 96 | 97 | ## Known Limitations: 98 | * The Synchronous routines can be much more robust than Asynchronous routies (depending on value set for 'm_RTOSservicerStackDepth'), but are limited to the main loop frequency 99 | 100 | ### See the example file, as it covers most interesting things, but I believe it is fairly self-explanatory. 101 | 102 | * This libary should not be used for mission critical or mass deployments. The developer should satisfy themselves that this library is stable for their purpose. I feel the code works great and I generated this library because I couldn't find anything similar and the ones based on loop polling didn't work at all with long loop times. Interrupts can be a bit cantankerous, but this seems to work nearly flawlessly in my experience, but I imagine maybe not so for everyone and welcome any suggestions for improvements.* 103 | 104 | Special thanks to @vortigont for all his input and feedback, particuarly with respect to methodology, implementing the ESP IDF functions to allow this library to work on both platforms, and the suggestion of RTOS queues. 105 | -------------------------------------------------------------------------------- /images/flowDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwmingis/InterruptButton/d68cf266edf367cfd8aaca513405dc9c86e13d5a/images/flowDiagram.png -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "InterruptButton", 3 | "description": "Interrupt driven events for hardware buttons", 4 | "keywords": "ESP32, ïnterrupt, button, event, action, bind, synchronous, asynchronous, RTOS", 5 | "authors": [ 6 | { 7 | "name": "R. Mingis", 8 | "maintainer": true 9 | } 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/rwmingis/InterruptButton" 14 | }, 15 | "version": "2.0.2", 16 | "frameworks": "Arduino", 17 | "platforms": ["espressif32"], 18 | "examples": [ 19 | "examples/*.ino" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=InterruptButton 2 | version=1.0.0 3 | author=R. Mingis 4 | maintainer=R. Mingis 5 | sentence=Interrupt driven events for hardware buttons 6 | paragraph=This is an interrupt based button event and debounce library for the ESP32. It allows for synchronous keyDown, keyUp, keyPress, longKeyPress and doubleClick events as well as asynchronous keyPress, longKeyPress, and doubleClick events 7 | category=Buttons 8 | url=http://notAvailable/ 9 | architectures=ESP32 10 | --------------------------------------------------------------------------------