diff --git a/.gitignore b/.gitignore index af56f61..4048809 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .DS_Store .idea/ +.vscode +_* \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4da4241 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,50 @@ +# Changelog + +All notable changes to this project will be documented in this file starting 2021. + +## [2.0.0] - 2021-01-22 + +* CHANGELOG created. +* Many thanks to the improvements included from #27 (**@aslobodyanuk**), #59 (**@ShaggyDog18**) and #73 (**@geeksville**). + +This is a major update with breaking changes. + +The **states** are re-factored to support counting the clicks. + +By design only one of the events (click, doubleClick, MultiClick) are triggered within one interaction. +As a consequence a single-click interaction is detected after waiting some milliseconds (see setClickTicks()) without another click happening; +Only if you have not attached any double-click event function the waiting time can be skipped. + +Detecting a long 'down' not only works with the first but always as the last click. + +The number of actual clicks can be retrieved from the library any time. + +The function **getPressedTicks()** was removed. See example SimpleOneButton on how to get that time by using attachLongPressStart to save starting time. + +The function **attachPressStart()** is removed as **attachLongPressStart()** does the same but also supports parameters. + +One additional feature has been added not to call the event functions from the interrupt routine and detect +the need for event functions to be called only when the tick() function is called from the main loop() method. +This is because some boards and processors do not support timing or Serial functions (among others) from interrupt routines. + +The function **isIdle()** was added to allow detect a current interaction. + +The library now supports to detect multiple (>2) clicks in a row using **attachMultiClick()** . + + +* The internal _state is using enum instead of plain numbers to make the library more readable. +* functions that had been marked deprecated are now removed. (attachPress->attachLongPressXXX) +* added const to constant parameters to enable meaningful compiler warnings. +* added code for de-bouncing double clicks from pull 27. +* added isIdle() function to find out that the internal state is `init`. + + +### Examples + +* Examples run on NodeMCU boards. (the library worked already). + +* The **SimpleOneButton.ino** example got some cleanup and definition to be used with ESP8266 boards as well. + +* The **InterruptOneButton.ino** example now is using attachInterrupt instead of UNO specific register modifications. + +* The **SpecialInput.ino** example was added to shpow how to use the OneButton algorythm and input pattern recognition with your own source of input. diff --git a/README.md b/README.md index 29f73eb..1193592 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -Arduino OneButton Library -=== +# Arduino OneButton Library This Arduino library is improving the usage of a singe button for input. It shows how to use an digital input pin with a single pushbutton attached @@ -11,6 +10,8 @@ This is also a sample for implementing simple finite-state machines by using the You can find more details on this library at http://www.mathertel.de/Arduino/OneButtonLibrary.aspx +The change log of this library can be found in [CHANGELOG](CHANGELOG.md). + ## Getting Started @@ -123,11 +124,14 @@ the following functions to change the timing. click event is not attached, the library will assume a valid single click after one click duration, otherwise it must wait for the double click timeout to pass. -| Function | Default (ms) | Description | -| ----------------------- | ------------ | ------------------------------------------------------------- | -| `setDebounceTicks(int)` | `50` | Period of time in which to ignore additional level changes. | -| `setClickTicks(int)` | `600` | Timeout used to distinguish single clicks from double clicks. | -| `setPressTicks(int)` | `1000` | Duration to hold a button to trigger a long press. | +| Function | Default | Description | +| ----------------------- | ---------- | ------------------------------------------------------------- | +| `setDebounceTicks(int)` | `50 msec` | Period of time in which to ignore additional level changes. | +| `setClickTicks(int)` | `500 msec` | Timeout used to distinguish single clicks from double clicks. | +| `setPressTicks(int)` | `800 msec` | Duration to hold a button to trigger a long press. | + +You may change these default values but be aware that when you specify too short times +it is hard to click twice or you will create a press instead of a click. ### Additional Functions diff --git a/examples/BlinkMachine/BlinkMachine.ino b/examples/BlinkMachine/BlinkMachine.ino index cd88ccf..02f8502 100644 --- a/examples/BlinkMachine/BlinkMachine.ino +++ b/examples/BlinkMachine/BlinkMachine.ino @@ -11,8 +11,8 @@ http://www.mathertel.de/Arduino/OneButtonLibrary.aspx Setup a test circuit: - * Connect a pushbutton to pin A1 (ButtonPin) and ground. - * The pin 13 (StatusPin) is used for output attach a led and resistor to ground + * Connect a pushbutton to pin A1 (Uno) or D3 (ESP8266) and ground. + * The pin 13 (UNO) or D4 (ESP8266) is used for output attach a led and resistor to ground or see the built-in led on the standard arduino board. The Sketch shows how to setup the library and bind a "machine" that can blink the LED slow or fast. @@ -58,8 +58,21 @@ typedef enum { } MyActions; +#if defined(ARDUINO_AVR_UNO) || defined(ARDUINO_AVR_NANO_EVERY) +// Example for Arduino UNO with input button on pin 2 and builtin LED on pin 13 +#define PIN_INPUT A1 +#define PIN_LED 13 + +#elif defined(ESP8266) +// Example for NodeMCU with input button using FLASH button on D3 and using the led on -12 module (D4). +// This LED is lighting on output level LOW. +#define PIN_INPUT D3 +#define PIN_LED D4 + +#endif + // Setup a new OneButton on pin A1. -OneButton button(A1, true); +OneButton button(PIN_INPUT, true); MyActions nextAction = ACTION_OFF; // no action when starting @@ -67,7 +80,7 @@ MyActions nextAction = ACTION_OFF; // no action when starting // setup code here, to run once. void setup() { // enable the standard led on pin 13. - pinMode(13, OUTPUT); // sets the digital pin as output + pinMode(PIN_LED, OUTPUT); // sets the digital pin as output // link the myClickFunction function to be called on a click event. button.attachClick(myClickFunction); @@ -91,26 +104,26 @@ void loop() { if (nextAction == ACTION_OFF) { // do nothing. - digitalWrite(13, LOW); + digitalWrite(PIN_LED, LOW); } else if (nextAction == ACTION_ON) { // turn LED on - digitalWrite(13, HIGH); + digitalWrite(PIN_LED, HIGH); } else if (nextAction == ACTION_SLOW) { // do a slow blinking if (now % 1000 < 500) { - digitalWrite(13, LOW); + digitalWrite(PIN_LED, LOW); } else { - digitalWrite(13, HIGH); + digitalWrite(PIN_LED, HIGH); } // if } else if (nextAction == ACTION_FAST) { // do a fast blinking if (now % 200 < 100) { - digitalWrite(13, LOW); + digitalWrite(PIN_LED, LOW); } else { - digitalWrite(13, HIGH); + digitalWrite(PIN_LED, HIGH); } // if } // if } // loop @@ -139,4 +152,4 @@ void myDoubleClickFunction() { } // myDoubleClickFunction // End - + diff --git a/examples/InterruptOneButton/InterruptOneButton.ino b/examples/InterruptOneButton/InterruptOneButton.ino index 551f2e7..d726f8b 100644 --- a/examples/InterruptOneButton/InterruptOneButton.ino +++ b/examples/InterruptOneButton/InterruptOneButton.ino @@ -14,55 +14,164 @@ * The pin 13 (StatusPin) is used for output attach a led and resistor to ground or see the built-in led on the standard arduino board. - The Sketch shows how to setup the library and bind a special function to the doubleclick event. - By using interrupts the doubleclick function will be called not from the main program. + The sketch shows how to setup the library and bind the functions (singleClick, doubleClick) to the events. + In the loop function the button.tick function must be called as often as you like. + By using interrupts the internal state advances even when for longer time the button.tick is not called. */ - + // 03.03.2011 created by Matthias Hertel // 01.12.2011 extension changed to work with the Arduino 1.0 environment // 04.11.2017 Interrupt version created. +// 04.11.2017 Interrupt version using attachInterrupt. #include "OneButton.h" -// Setup a new OneButton on pin A1. -OneButton button(A1, true); +#if defined(ARDUINO_AVR_UNO) || defined(ARDUINO_AVR_NANO_EVERY) +// Example for Arduino UNO with input button on pin 2 and builtin LED on pin 13 +// attachInterrupt only supports pin 2 and 3 on UNO. +// See https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/ +#define PIN_INPUT 2 +#define PIN_LED 13 + +#elif defined(ESP8266) +// Example for NodeMCU with input button using FLASH button on D3 and using the led on -12 module (D4). +// This LED is lighting on output level LOW. +#define PIN_INPUT D3 +#define PIN_LED D4 + +#endif + +// Setup a new OneButton on pin PIN_INPUT +// The 2. parameter activeLOW is true, because external wiring sets the button to LOW when pressed. +OneButton button(PIN_INPUT, true); + +// current LED state, staring with LOW (0) +int ledState = LOW; + +// save the millis when a press has started. +unsigned long pressStartTime; + +// In case the momentary button puts the input to HIGH when pressed: +// The 2. parameter activeLOW is false when the external wiring sets the button to HIGH when pressed. +// The 3. parameter can be used to disable the PullUp . +// OneButton button(PIN_INPUT, false, false); + + +// This function is called from the interrupt when the signal on the PIN_INPUT has changed. +// do not use Serial in here. +#if defined(ARDUINO_AVR_UNO) || defined (ARDUINO_AVR_NANO_EVERY) +void checkTicks() +{ + // include all buttons here to be checked + button.tick(); // just call tick() to check the state. +} + +#elif defined(ESP8266) +ICACHE_RAM_ATTR void checkTicks() +{ + // include all buttons here to be checked + button.tick(); // just call tick() to check the state. +} + +#endif + + +// this function will be called when the button was pressed 1 time only. +void singleClick() +{ + Serial.println("singleClick() detected."); +} // singleClick + + +// this function will be called when the button was pressed 2 times in a short timeframe. +void doubleClick() +{ + Serial.println("doubleClick() detected."); + + ledState = !ledState; // reverse the LED + digitalWrite(PIN_LED, ledState); +} // doubleClick + + +// this function will be called when the button was pressed 2 times in a short timeframe. +void multiClick() +{ + Serial.print("multiClick("); + Serial.print(button.getNumberClicks()); + Serial.println(") detected."); + + ledState = !ledState; // reverse the LED + digitalWrite(PIN_LED, ledState); +} // multiClick + + +// this function will be called when the button was pressed 2 times in a short timeframe. +void pressStart() +{ + Serial.println("pressStart()"); + pressStartTime = millis() - 1000; // as set in setPressTicks() +} // pressStart() + + +// this function will be called when the button was pressed 2 times in a short timeframe. +void pressStop() +{ + Serial.print("pressStop("); + Serial.print(millis() - pressStartTime); + Serial.println(") detected."); +} // pressStop() // setup code here, to run once: -void setup() { - // enable the standard led on pin 13. - pinMode(13, OUTPUT); // sets the digital pin as output - - // link the doubleclick function to be called on a doubleclick event. - button.attachDoubleClick(doubleclick); +void setup() +{ + Serial.begin(115200); + Serial.println("One Button Example with interrupts."); + + // enable the led output. + pinMode(PIN_LED, OUTPUT); // sets the digital pin as output + digitalWrite(PIN_LED, ledState); + // setup interrupt routine + // when not registering to the interrupt the sketch also works when the tick is called frequently. + attachInterrupt(digitalPinToInterrupt(PIN_INPUT), checkTicks, CHANGE); + + // link the xxxclick functions to be called on xxxclick event. + button.attachClick(singleClick); + button.attachDoubleClick(doubleClick); + button.attachMultiClick(multiClick); + + button.setPressTicks(1000); // that is the time when pressStart is called + button.attachLongPressStart(pressStart); + button.attachLongPressStop(pressStop); + + // A1-Option for UNO: + // it is possible to use e.g. A1 but then some direct register modifications and an ISR has to be used: // You may have to modify the next 2 lines if using another pin than A1 - PCICR |= (1 << PCIE1); // This enables Pin Change Interrupt 1 that covers the Analog input pins or Port C. - PCMSK1 |= (1 << PCINT9); // This enables the interrupt for pin 1 of Port C: This is A1. -} // setup + // PCICR |= (1 << PCIE1); // This enables Pin Change Interrupt 1 that covers the Analog input pins or Port C. + // PCMSK1 |= (1 << PCINT9); // This enables the interrupt for pin 1 of Port C: This is A1. +} // setup +// A1-Option for UNO: // The Interrupt Service Routine for Pin Change Interrupt 1 // This routine will only be called on any signal change on A1: exactly where we need to check. -ISR(PCINT1_vect) { - // keep watching the push button: - button.tick(); // just call tick() to check the state. -} +// ISR(PCINT1_vect) +// { +// // keep watching the push button: +// button.tick(); // just call tick() to check the state. +// } + +// main code here, to run repeatedly: +void loop() +{ + // keep watching the push button, even when no interrupt happens: + button.tick(); -// main code here, to run repeatedly: -void loop() { - // You can implement other code in here or just wait a while + // You can implement other code in here or just wait a while delay(10); } // loop -// this function will be called when the button was pressed 2 times in a short timeframe. -void doubleclick() { - static int m = LOW; - // reverse the LED - m = !m; - digitalWrite(13, m); -} // doubleclick - // End diff --git a/examples/SimpleOneButton/SimpleOneButton.ino b/examples/SimpleOneButton/SimpleOneButton.ino index 3ce5022..8607dac 100644 --- a/examples/SimpleOneButton/SimpleOneButton.ino +++ b/examples/SimpleOneButton/SimpleOneButton.ino @@ -9,8 +9,8 @@ * The pin 13 (StatusPin) is used for output attach a led and resistor to ground or see the built-in led on the standard arduino board. - The Sketch shows how to setup the library and bind a special function to the doubleclick event. - In the loop function the button.tick function has to be called as often as you like. + The sketch shows how to setup the library and bind the functions (singleClick, doubleClick) to the events. + In the loop function the button.tick function must be called as often as you like. */ // 03.03.2011 created by Matthias Hertel @@ -18,16 +18,20 @@ #include "OneButton.h" -// Example for Arduino UNO with input button on A1 -#define PIN_INPUT A1 +#if defined(ARDUINO_AVR_UNO) || defined(ARDUINO_AVR_NANO_EVERY) +// Example for Arduino UNO with input button on pin 2 and builtin LED on pin 13 +#define PIN_INPUT 2 #define PIN_LED 13 -// Example for ESP8266 with input button on D5 and using the led on -12 module. -// #define PIN_INPUT D5 -// #define PIN_LED D4 +#else if defined(ESP8266) +// Example for NodeMCU with input button using FLASH button on D3 and using the led on -12 module (D4). +// This LED is lighting on output level LOW. +#define PIN_INPUT D3 +#define PIN_LED D4 -// Setup a new OneButton on pin PIN_INPUT +#endif +// Setup a new OneButton on pin PIN_INPUT // The 2. parameter activeLOW is true, because external wiring sets the button to LOW when pressed. OneButton button(PIN_INPUT, true); @@ -36,19 +40,23 @@ OneButton button(PIN_INPUT, true); // The 3. parameter can be used to disable the PullUp . // OneButton button(PIN_INPUT, false, false); +// current LED state, staring with LOW (0) +int ledState = LOW; // setup code here, to run once: void setup() { Serial.begin(115200); + Serial.println("One Button Example with polling."); // enable the standard led on pin 13. pinMode(PIN_LED, OUTPUT); // sets the digital pin as output - // link the doubleclick function to be called on a doubleclick event. - button.attachDoubleClick(doubleclick); - + // enable the standard led on pin 13. + digitalWrite(PIN_LED, ledState); + // link the doubleclick function to be called on a doubleclick event. + button.attachDoubleClick(doubleClick); } // setup @@ -64,12 +72,12 @@ void loop() // this function will be called when the button was pressed 2 times in a short timeframe. -void doubleclick() +void doubleClick() { - static int m = LOW; - // reverse the LED - m = !m; - digitalWrite(PIN_LED, m); -} // doubleclick + Serial.println("x2"); + + ledState = !ledState; // reverse the LED + digitalWrite(PIN_LED, ledState); +} // doubleClick // End \ No newline at end of file diff --git a/examples/SpecialInput/SpecialInput.ino b/examples/SpecialInput/SpecialInput.ino new file mode 100644 index 0000000..335df36 --- /dev/null +++ b/examples/SpecialInput/SpecialInput.ino @@ -0,0 +1,70 @@ +/* + * SpecialInput.ino - Example for the OneButtonLibrary library. + * This is a sample sketch to show how to use the OneClick library on other input sources than standard digital pins. + * + * The library internals are explained at + * http://www.mathertel.de/Arduino/OneButtonLibrary.aspx + * + * Setup a test circuit: + * * Connect a pushbutton to pin 2 (ButtonPin) and ground. + * + * The sketch shows how to setup the library and bind the functions (singleClick, doubleClick) to the events. + * In the loop function the button.tick function must be called as often as you like. + * + * * 22.01.2021 created by Matthias Hertel +*/ + +#include "OneButton.h" + +// This is an example on how to use the OneClick library on other input sources than standard digital pins. +// 1. do not use a pin in the initialization of the OneClick library. +// 2. pass the input state to the tick function. + +// You can also find how to create an instance in setup and not by declaration. +// You can also find how to use inline callback functions. + +#if defined(ARDUINO_AVR_UNO) || defined(ARDUINO_AVR_NANO_EVERY) +#define PIN_INPUT 2 + +#else if defined(ESP8266) +#define PIN_INPUT D3 + +#endif + + +// OneButton instance will be created in setup. +OneButton *button; + +void fClicked(void *s) +{ + Serial.print("Click:"); + Serial.println((char *)s); +} + +void setup() +{ + Serial.begin(115200); + Serial.println("One Button Example with custom input."); + +// create the OneButton instance without a pin. + button = new OneButton(); + + // Here is an example on how to use a parameter to the registered function: + button->attachClick(fClicked, "me"); + + // Here is an example on how to use an inline function: + button->attachDoubleClick([]() { Serial.println("DoubleClick"); }); + + // setup your own source of input: + pinMode(PIN_INPUT, INPUT_PULLUP); + +} // setup() + +void loop() +{ + // read your own source of input: + bool isPressed = (digitalRead(PIN_INPUT) == LOW); + + // call tick frequently with current push-state of the input + button->tick(isPressed); +} // loop() diff --git a/examples/TwoButtons/TwoButtons.ino b/examples/TwoButtons/TwoButtons.ino index b18c532..f0ec8b7 100644 --- a/examples/TwoButtons/TwoButtons.ino +++ b/examples/TwoButtons/TwoButtons.ino @@ -46,7 +46,7 @@ OneButton button2(A2, true); // setup code here, to run once: void setup() { // Setup the Serial port. see http://arduino.cc/en/Serial/IfSerial - Serial.begin(9600); + Serial.begin(115200); while (!Serial) { ; // wait for serial port to connect. Needed for Leonardo only } @@ -139,4 +139,4 @@ void longPressStop2() { // End - + diff --git a/library.properties b/library.properties index a675be7..eca4f30 100644 --- a/library.properties +++ b/library.properties @@ -1,9 +1,9 @@ name=OneButton -version=1.6.0 +version=2.0.0 author=Matthias Hertel, mathertel@hotmail.com maintainer=Matthias Hertel sentence=Arduino library for improving the usage of a singe input button. -paragraph=It supports detecting events like single clicks, double clicks and long-time pressing. This enables you to reuse the same button for multiple functions and lowers the hardware invests. +paragraph=It supports detecting events like single, double, multiple clicks and long-time pressing. This enables you to reuse the same button for multiple functions and lowers the hardware invests. category=Signal Input/Output url=https://github.com/mathertel/OneButton architectures=* diff --git a/src/OneButton.cpp b/src/OneButton.cpp index d398807..d6e0aff 100644 --- a/src/OneButton.cpp +++ b/src/OneButton.cpp @@ -34,7 +34,7 @@ OneButton::OneButton() * @param activeLow Set to true when the input level is LOW when the button is pressed, Default is true. * @param pullupActive Activate the internal pullup when available. Default is true. */ -OneButton::OneButton(int pin, boolean activeLow, bool pullupActive) +OneButton::OneButton(const int pin, const boolean activeLow, const bool pullupActive) { // OneButton(); _pin = pin; @@ -58,24 +58,22 @@ OneButton::OneButton(int pin, boolean activeLow, bool pullupActive) } // OneButton -// explicitly set the number of millisec that have to pass by before a click is -// assumed as safe. -void OneButton::setDebounceTicks(int ticks) +// explicitly set the number of millisec that have to pass by before a click is assumed stable. +void OneButton::setDebounceTicks(const int ticks) { _debounceTicks = ticks; } // setDebounceTicks -// explicitly set the number of millisec that have to pass by before a click is -// detected. -void OneButton::setClickTicks(int ticks) + +// explicitly set the number of millisec that have to pass by before a click is detected. +void OneButton::setClickTicks(const int ticks) { _clickTicks = ticks; } // setClickTicks -// explicitly set the number of millisec that have to pass by before a long -// button press is detected. -void OneButton::setPressTicks(int ticks) +// explicitly set the number of millisec that have to pass by before a long button press is detected. +void OneButton::setPressTicks(const int ticks) { _pressTicks = ticks; } // setPressTicks @@ -100,6 +98,7 @@ void OneButton::attachClick(parameterizedCallbackFunction newFunction, void *par void OneButton::attachDoubleClick(callbackFunction newFunction) { _doubleClickFunc = newFunction; + _maxClicks = max(_maxClicks, 2); } // attachDoubleClick @@ -108,21 +107,26 @@ void OneButton::attachDoubleClick(parameterizedCallbackFunction newFunction, voi { _paramDoubleClickFunc = newFunction; _doubleClickFuncParam = parameter; + _maxClicks = max(_maxClicks, 2); } // attachDoubleClick -// save function for press event -// DEPRECATED, is replaced by attachLongPressStart, attachLongPressStop, -// attachDuringLongPress, -void OneButton::attachPress(callbackFunction newFunction) +// save function for multiClick event +void OneButton::attachMultiClick(callbackFunction newFunction) { - _pressFunc = newFunction; -} // attachPress + _multiClickFunc = newFunction; + _maxClicks = max(_maxClicks, 100); +} // attachMultiClick + -void OneButton::attachPressStart(callbackFunction newFunction) +// save function for parameterized MultiClick event +void OneButton::attachMultiClick(parameterizedCallbackFunction newFunction, void *parameter) { - _pressStartFunc = newFunction; -} // attachPressStart + _paramMultiClickFunc = newFunction; + _multiClickFuncParam = parameter; + _maxClicks = max(_maxClicks, 100); +} // attachMultiClick + // save function for longPressStart event void OneButton::attachLongPressStart(callbackFunction newFunction) @@ -130,6 +134,7 @@ void OneButton::attachLongPressStart(callbackFunction newFunction) _longPressStartFunc = newFunction; } // attachLongPressStart + // save function for parameterized longPressStart event void OneButton::attachLongPressStart(parameterizedCallbackFunction newFunction, void *parameter) { @@ -137,12 +142,14 @@ void OneButton::attachLongPressStart(parameterizedCallbackFunction newFunction, _longPressStartFuncParam = parameter; } // attachLongPressStart + // save function for longPressStop event void OneButton::attachLongPressStop(callbackFunction newFunction) { _longPressStopFunc = newFunction; } // attachLongPressStop + // save function for parameterized longPressStop event void OneButton::attachLongPressStop(parameterizedCallbackFunction newFunction, void *parameter) { @@ -150,12 +157,14 @@ void OneButton::attachLongPressStop(parameterizedCallbackFunction newFunction, v _longPressStopFuncParam = parameter; } // attachLongPressStop + // save function for during longPress event void OneButton::attachDuringLongPress(callbackFunction newFunction) { _duringLongPressFunc = newFunction; } // attachDuringLongPress + // save function for parameterized during longPress event void OneButton::attachDuringLongPress(parameterizedCallbackFunction newFunction, void *parameter) { @@ -163,23 +172,20 @@ void OneButton::attachDuringLongPress(parameterizedCallbackFunction newFunction, _duringLongPressFuncParam = parameter; } // attachDuringLongPress -// function to get the current long pressed state -bool OneButton::isLongPressed() -{ - return _isLongPressed; -} -int OneButton::getPressedTicks() +void OneButton::reset(void) { - return _stopTime - _startTime; + _state = OneButton::OCS_INIT; + _lastState = OneButton::OCS_INIT; + _nClicks = 0; + _startTime = 0; } -void OneButton::reset(void) + +// ShaggyDog ---- return number of clicks in any case: single or multiple clicks +int OneButton::getNumberClicks(void) { - _state = 0; // restart. - _startTime = 0; - _stopTime = 0; - _isLongPressed = false; + return _nClicks; } @@ -196,106 +202,126 @@ void OneButton::tick(void) /** - * @brief Advance the finite state machine (FSM) using the given level. + * @brief Advance to a new state and save the last one to come back in cas of bouncing detection. + */ +void OneButton::_newState(stateMachine_t nextState) +{ + _lastState = _state; + _state = nextState; +} // _newState() + + +/** + * @brief Run the finite state machine (FSM) using the given level. */ void OneButton::tick(bool activeLevel) { unsigned long now = millis(); // current (relative) time in msecs. + unsigned long waitTime = (now - _startTime); // Implementation of the state machine - - if (_state == 0) { // waiting for menu pin being pressed. + switch (_state) { + case OneButton::OCS_INIT: + // waiting for level to become active. if (activeLevel) { - _state = 1; // step to state 1 + _newState(OneButton::OCS_DOWN); _startTime = now; // remember starting time + _nClicks = 0; } // if + break; - } else if (_state == 1) { // waiting for menu pin being released. + case OneButton::OCS_DOWN: + // waiting for level to become inactive. - if ((!activeLevel) && - ((unsigned long)(now - _startTime) < _debounceTicks)) { - // button was released to quickly so I assume some debouncing. - // go back to state 0 without calling a function. - _state = 0; + if ((!activeLevel) && (waitTime < _debounceTicks)) { + // button was released to quickly so I assume some bouncing. + _newState(_lastState); } else if (!activeLevel) { - _state = 2; // step to state 2 - _stopTime = now; // remember stopping time - - } else if ((activeLevel) && - ((unsigned long)(now - _startTime) > _pressTicks)) { - _stopTime = now; // remember stopping time - _isLongPressed = true; // Keep track of long press state - if (_pressFunc) - _pressFunc(); - if (_longPressStartFunc) - _longPressStartFunc(); - if (_paramLongPressStartFunc) - _paramLongPressStartFunc(_longPressStartFuncParam); - if (_duringLongPressFunc) - _duringLongPressFunc(); - if (_paramDuringLongPressFunc) - _paramDuringLongPressFunc(_duringLongPressFuncParam); - _state = 6; // step to state 6 - } else { - // Button was pressed down. wait. Stay in this state. - // if a pressStart event is registered, call it: - if (_pressStartFunc) - _pressStartFunc(); + _newState(OneButton::OCS_UP); + _startTime = now; // remember starting time + + } else if ((activeLevel) && (waitTime > _pressTicks)) { + if (_longPressStartFunc) _longPressStartFunc(); + if (_paramLongPressStartFunc) _paramLongPressStartFunc(_longPressStartFuncParam); + _newState(OneButton::OCS_PRESS); } // if + break; - } else if (_state == 2) { - // waiting for menu pin being pressed the second time or timeout. - if ((_doubleClickFunc == NULL && _paramDoubleClickFunc == NULL) || - (unsigned long)(now - _startTime) > _clickTicks) { - // this was only a single short click - if (_clickFunc) - _clickFunc(); - if (_paramClickFunc) - _paramClickFunc(_clickFuncParam); - _state = 0; // restart. - - } else if ((activeLevel) && - ((unsigned long)(now - _stopTime) > _debounceTicks)) { - _state = 3; // step to state 3 - _startTime = now; // remember starting time + case OneButton::OCS_UP: + // level is inactive + + if ((activeLevel) && (waitTime < _debounceTicks)) { + // button was pressed to quickly so I assume some bouncing. + _newState(_lastState); // go back + + } else if (waitTime >= _debounceTicks) { + // count as a short button down + _nClicks++; + _newState(OneButton::OCS_COUNT); } // if + break; + + case OneButton::OCS_COUNT: + // dobounce time is over, count clicks + + if (activeLevel) { + // button is down again + _newState(OneButton::OCS_DOWN); + _startTime = now; // remember starting time + + } else if ((waitTime > _clickTicks) || (_nClicks == _maxClicks)) { + // now we know how many clicks have been made. + + if (_nClicks == 1) { + // this was 1 click only. + if (_clickFunc) _clickFunc(); + if (_paramClickFunc) _paramClickFunc(_clickFuncParam); + + } else if (_nClicks == 2) { + // this was a 2 click sequence. + if (_doubleClickFunc) _doubleClickFunc(); + if (_paramDoubleClickFunc) _paramDoubleClickFunc(_doubleClickFuncParam); + + } else { + // this was a multi click sequence. + if (_multiClickFunc) _multiClickFunc(); + if (_paramMultiClickFunc) _paramMultiClickFunc(_doubleClickFuncParam); + } // if - } else if (_state == 3) { // waiting for menu pin being released finally. - // Stay here for at least _debounceTicks because else we might end up in - // state 1 if the button bounces for too long. - if ((!activeLevel) && - ((unsigned long)(now - _startTime) > _debounceTicks)) { - _stopTime = now; // remember stopping time - // this was a 2 click sequence. - if (_doubleClickFunc) - _doubleClickFunc(); - if (_paramDoubleClickFunc) - _paramDoubleClickFunc(_doubleClickFuncParam); - _state = 0; // restart. + reset(); } // if + break; - } else if (_state == 6) { + case OneButton::OCS_PRESS: // waiting for menu pin being release after long press. + if (!activeLevel) { - _isLongPressed = false; // Keep track of long press state - _stopTime = now; // remember stopping time - if (_longPressStopFunc) - _longPressStopFunc(); - if (_paramLongPressStopFunc) - _paramLongPressStopFunc(_longPressStopFuncParam); - _state = 0; // restart. + _newState(OneButton::OCS_PRESSEND); + _startTime = now; + } else { - // button is being long pressed - _stopTime = now; // remember stopping time - _isLongPressed = true; // Keep track of long press state - if (_duringLongPressFunc) - _duringLongPressFunc(); - if (_paramDuringLongPressFunc) - _paramDuringLongPressFunc(_duringLongPressFuncParam); + // still the button is pressed + if (_duringLongPressFunc) _duringLongPressFunc(); + if (_paramDuringLongPressFunc) _paramDuringLongPressFunc(_duringLongPressFuncParam); } // if + break; + + case OneButton::OCS_PRESSEND: + // button was released. + if ((activeLevel) && (waitTime < _debounceTicks)) { + // button was released to quickly so I assume some bouncing. + _newState(_lastState); // go back + + } else if (waitTime >= _debounceTicks) { + if (_longPressStopFunc) _longPressStopFunc(); + if (_paramLongPressStopFunc) _paramLongPressStopFunc(_longPressStopFuncParam); + reset(); + } + break; } // if + } // OneButton.tick() diff --git a/src/OneButton.h b/src/OneButton.h index f566a38..b1e4a8a 100644 --- a/src/OneButton.h +++ b/src/OneButton.h @@ -18,6 +18,7 @@ // sources of input. // 26.09.2018 Initialization moved into class declaration. // 26.09.2018 Jay M Ericsson: compiler warnings removed. +// 29.01.2020 improvements from ShaggyDog18 // ----- #ifndef OneButton_h @@ -29,7 +30,7 @@ extern "C" { typedef void (*callbackFunction)(void); -typedef void (*parameterizedCallbackFunction)(void*); +typedef void (*parameterizedCallbackFunction)(void *); } @@ -45,71 +46,66 @@ class OneButton * @param activeLow Set to true when the input level is LOW when the button is pressed, Default is true. * @param pullupActive Activate the internal pullup when available. Default is true. */ - OneButton(int pin, boolean activeLow = true, bool pullupActive = true); + OneButton(const int pin, const boolean activeLow = true, const bool pullupActive = true); // ----- Set runtime parameters ----- /** * set # millisec after safe click is assumed. */ - void setDebounceTicks(int ticks); + void setDebounceTicks(const int ticks); /** * set # millisec after single click is assumed. */ - void setClickTicks(int ticks); + void setClickTicks(const int ticks); /** * set # millisec after press is assumed. */ - void setPressTicks(int ticks); + void setPressTicks(const int ticks); /** * Attach an event to be called when a single click is detected. - * @param newFunction + * @param newFunction This function will be called when the event has been detected. */ void attachClick(callbackFunction newFunction); - void attachClick(parameterizedCallbackFunction newFunction, void* parameter); + void attachClick(parameterizedCallbackFunction newFunction, void *parameter); /** * Attach an event to be called after a double click is detected. - * @param newFunction + * @param newFunction This function will be called when the event has been detected. */ void attachDoubleClick(callbackFunction newFunction); - void attachDoubleClick(parameterizedCallbackFunction newFunction, void* parameter); + void attachDoubleClick(parameterizedCallbackFunction newFunction, void *parameter); /** - * @deprecated Replaced by longPressStart, longPressStop, and duringLongPress. - * @param newFunction - */ - void attachPress(callbackFunction newFunction); - - /** - * Attach an event to fire as soon as the button is pressed down. - * @param newFunction + * Attach an event to be called after a multi click is detected. + * @param newFunction This function will be called when the event has been detected. */ - void attachPressStart(callbackFunction newFunction); + void attachMultiClick(callbackFunction newFunction); + void attachMultiClick(parameterizedCallbackFunction newFunction, void *parameter); /** * Attach an event to fire when the button is pressed and held down. * @param newFunction */ void attachLongPressStart(callbackFunction newFunction); - void attachLongPressStart(parameterizedCallbackFunction newFunction, void* parameter); + void attachLongPressStart(parameterizedCallbackFunction newFunction, void *parameter); /** * Attach an event to fire as soon as the button is released after a long press. * @param newFunction */ void attachLongPressStop(callbackFunction newFunction); - void attachLongPressStop(parameterizedCallbackFunction newFunction, void* parameter); + void attachLongPressStop(parameterizedCallbackFunction newFunction, void *parameter); /** * Attach an event to fire periodically while the button is held down. * @param newFunction */ void attachDuringLongPress(callbackFunction newFunction); - void attachDuringLongPress(parameterizedCallbackFunction newFunction, void* parameter); + void attachDuringLongPress(parameterizedCallbackFunction newFunction, void *parameter); // ----- State machine functions ----- @@ -119,6 +115,7 @@ class OneButton */ void tick(void); + /** * @brief Call this function every time the input level has changed. * Using this function no digital input pin is checked because the current @@ -126,65 +123,90 @@ class OneButton */ void tick(bool level); + /** - * Detect whether or not the button is currently inside a long press. - * @return + * Reset the button state machine. */ - bool isLongPressed(); + void reset(void); + + + /* + * return number of clicks in any case: single or multiple clicks + */ + int getNumberClicks(void); + /** - * Get the current number of ticks that the button has been held down for. - * @return + * @return true if we are currently handling button press flow + * (This allows power sensitive applications to know when it is safe to power down the main CPU) */ - int getPressedTicks(); + bool isIdle() const { return _state == OCS_INIT; } /** - * Reset the button state machine. + * @return true when a long press is detected */ - void reset(void); + bool isLongPressed() const { return _state == OCS_PRESS; }; + private: - int _pin; // hardware pin number. + int _pin; // hardware pin number. unsigned int _debounceTicks = 50; // number of ticks for debounce times. - unsigned int _clickTicks = 600; // number of ticks that have to pass by - // before a click is detected. - unsigned int _pressTicks = 1000; // number of ticks that have to pass by - // before a long button press is detected + unsigned int _clickTicks = 400; // number of msecs before a click is detected. + unsigned int _pressTicks = 800; // number of msecs before a long button press is detected int _buttonPressed; - bool _isLongPressed = false; - // These variables will hold functions acting as event source. callbackFunction _clickFunc = NULL; parameterizedCallbackFunction _paramClickFunc = NULL; - void* _clickFuncParam = NULL; + void *_clickFuncParam = NULL; callbackFunction _doubleClickFunc = NULL; parameterizedCallbackFunction _paramDoubleClickFunc = NULL; - void* _doubleClickFuncParam = NULL; + void *_doubleClickFuncParam = NULL; - callbackFunction _pressFunc = NULL; - callbackFunction _pressStartFunc = NULL; + callbackFunction _multiClickFunc = NULL; + parameterizedCallbackFunction _paramMultiClickFunc = NULL; + void *_multiClickFuncParam = NULL; callbackFunction _longPressStartFunc = NULL; parameterizedCallbackFunction _paramLongPressStartFunc = NULL; - void* _longPressStartFuncParam = NULL; + void *_longPressStartFuncParam = NULL; callbackFunction _longPressStopFunc = NULL; parameterizedCallbackFunction _paramLongPressStopFunc = NULL; - void* _longPressStopFuncParam; + void *_longPressStopFuncParam; callbackFunction _duringLongPressFunc = NULL; parameterizedCallbackFunction _paramDuringLongPressFunc = NULL; - void* _duringLongPressFuncParam = NULL; + void *_duringLongPressFuncParam = NULL; // These variables that hold information across the upcoming tick calls. // They are initialized once on program start and are updated every time the // tick function is called. - int _state = 0; - unsigned long _startTime; // will be set in state 1 - unsigned long _stopTime; // will be set in state 2 + + // define FiniteStateMachine + enum stateMachine_t : int { + OCS_INIT = 0, + OCS_DOWN = 1, + OCS_UP = 2, + OCS_COUNT = 3, + OCS_PRESS = 6, + OCS_PRESSEND = 7, + UNKNOWN = 99 + }; + + /** + * Advance to a new state and save the last one to come back in cas of bouncing detection. + */ + void _newState(stateMachine_t nextState); + + stateMachine_t _state = OCS_INIT; + stateMachine_t _lastState = OCS_INIT; // used for debouncing + + unsigned long _startTime; // start of current input change to checking debouncing + int _nClicks; // count the number of clicks with this variable + int _maxClicks = 1; // max number (1, 2, multi=3) of clicks of interest by registration of event functions. }; #endif