Merge pull request #343 from jquatier/button-management
Improved Button Management
This commit is contained in:
125
examples/companion_radio/Button.cpp
Normal file
125
examples/companion_radio/Button.cpp
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
#include "Button.h"
|
||||||
|
|
||||||
|
Button::Button(uint8_t pin, bool activeState)
|
||||||
|
: _pin(pin), _activeState(activeState), _isAnalog(false), _analogThreshold(20) {
|
||||||
|
_currentState = false; // Initialize as not pressed
|
||||||
|
_lastState = _currentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
Button::Button(uint8_t pin, bool activeState, bool isAnalog, uint16_t analogThreshold)
|
||||||
|
: _pin(pin), _activeState(activeState), _isAnalog(isAnalog), _analogThreshold(analogThreshold) {
|
||||||
|
_currentState = false; // Initialize as not pressed
|
||||||
|
_lastState = _currentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Button::begin() {
|
||||||
|
_currentState = readButton();
|
||||||
|
_lastState = _currentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Button::update() {
|
||||||
|
uint32_t now = millis();
|
||||||
|
|
||||||
|
// Read button at specified interval
|
||||||
|
if (now - _lastReadTime < BUTTON_READ_INTERVAL_MS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_lastReadTime = now;
|
||||||
|
|
||||||
|
bool newState = readButton();
|
||||||
|
|
||||||
|
// Check if state has changed
|
||||||
|
if (newState != _lastState) {
|
||||||
|
_stateChangeTime = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce check
|
||||||
|
if ((now - _stateChangeTime) > BUTTON_DEBOUNCE_TIME_MS) {
|
||||||
|
if (newState != _currentState) {
|
||||||
|
_currentState = newState;
|
||||||
|
handleStateChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastState = newState;
|
||||||
|
|
||||||
|
// Handle multi-click timeout
|
||||||
|
if (_state == WAITING_FOR_MULTI_CLICK && (now - _releaseTime) > BUTTON_CLICK_TIMEOUT_MS) {
|
||||||
|
// Timeout reached, process the clicks
|
||||||
|
if (_clickCount == 1) {
|
||||||
|
triggerEvent(SHORT_PRESS);
|
||||||
|
} else if (_clickCount == 2) {
|
||||||
|
triggerEvent(DOUBLE_PRESS);
|
||||||
|
} else if (_clickCount >= 3) {
|
||||||
|
triggerEvent(TRIPLE_PRESS);
|
||||||
|
}
|
||||||
|
_clickCount = 0;
|
||||||
|
_state = IDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle long press while button is held
|
||||||
|
if (_state == PRESSED && (now - _pressTime) > BUTTON_LONG_PRESS_TIME_MS) {
|
||||||
|
triggerEvent(LONG_PRESS);
|
||||||
|
_state = IDLE; // Prevent multiple press events
|
||||||
|
_clickCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Button::readButton() {
|
||||||
|
if (_isAnalog) {
|
||||||
|
return (analogRead(_pin) < _analogThreshold);
|
||||||
|
} else {
|
||||||
|
return (digitalRead(_pin) == _activeState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Button::handleStateChange() {
|
||||||
|
uint32_t now = millis();
|
||||||
|
|
||||||
|
if (_currentState) {
|
||||||
|
// Button pressed
|
||||||
|
_pressTime = now;
|
||||||
|
_state = PRESSED;
|
||||||
|
triggerEvent(ANY_PRESS);
|
||||||
|
} else {
|
||||||
|
// Button released
|
||||||
|
if (_state == PRESSED) {
|
||||||
|
uint32_t pressDuration = now - _pressTime;
|
||||||
|
|
||||||
|
if (pressDuration < BUTTON_LONG_PRESS_TIME_MS) {
|
||||||
|
// Short press detected
|
||||||
|
_clickCount++;
|
||||||
|
_releaseTime = now;
|
||||||
|
_state = WAITING_FOR_MULTI_CLICK;
|
||||||
|
} else {
|
||||||
|
// Long press already handled in update()
|
||||||
|
_state = IDLE;
|
||||||
|
_clickCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Button::triggerEvent(EventType event) {
|
||||||
|
_lastEvent = event;
|
||||||
|
|
||||||
|
switch (event) {
|
||||||
|
case ANY_PRESS:
|
||||||
|
if (_onAnyPress) _onAnyPress();
|
||||||
|
break;
|
||||||
|
case SHORT_PRESS:
|
||||||
|
if (_onShortPress) _onShortPress();
|
||||||
|
break;
|
||||||
|
case DOUBLE_PRESS:
|
||||||
|
if (_onDoublePress) _onDoublePress();
|
||||||
|
break;
|
||||||
|
case TRIPLE_PRESS:
|
||||||
|
if (_onTriplePress) _onTriplePress();
|
||||||
|
break;
|
||||||
|
case LONG_PRESS:
|
||||||
|
if (_onLongPress) _onLongPress();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
77
examples/companion_radio/Button.h
Normal file
77
examples/companion_radio/Button.h
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
// Button timing configuration
|
||||||
|
#define BUTTON_DEBOUNCE_TIME_MS 50 // Debounce time in ms
|
||||||
|
#define BUTTON_CLICK_TIMEOUT_MS 500 // Max time between clicks for multi-click
|
||||||
|
#define BUTTON_LONG_PRESS_TIME_MS 3000 // Time to trigger long press (3 seconds)
|
||||||
|
#define BUTTON_READ_INTERVAL_MS 10 // How often to read the button
|
||||||
|
|
||||||
|
class Button {
|
||||||
|
public:
|
||||||
|
enum EventType {
|
||||||
|
NONE,
|
||||||
|
SHORT_PRESS,
|
||||||
|
DOUBLE_PRESS,
|
||||||
|
TRIPLE_PRESS,
|
||||||
|
LONG_PRESS,
|
||||||
|
ANY_PRESS
|
||||||
|
};
|
||||||
|
|
||||||
|
using EventCallback = std::function<void()>;
|
||||||
|
|
||||||
|
Button(uint8_t pin, bool activeState = LOW);
|
||||||
|
Button(uint8_t pin, bool activeState, bool isAnalog, uint16_t analogThreshold = 20);
|
||||||
|
|
||||||
|
void begin();
|
||||||
|
void update();
|
||||||
|
|
||||||
|
// Set callbacks for different events
|
||||||
|
void onShortPress(EventCallback callback) { _onShortPress = callback; }
|
||||||
|
void onDoublePress(EventCallback callback) { _onDoublePress = callback; }
|
||||||
|
void onTriplePress(EventCallback callback) { _onTriplePress = callback; }
|
||||||
|
void onLongPress(EventCallback callback) { _onLongPress = callback; }
|
||||||
|
void onAnyPress(EventCallback callback) { _onAnyPress = callback; }
|
||||||
|
|
||||||
|
// State getters
|
||||||
|
bool isPressed() const { return _currentState; }
|
||||||
|
EventType getLastEvent() const { return _lastEvent; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum State {
|
||||||
|
IDLE,
|
||||||
|
PRESSED,
|
||||||
|
RELEASED,
|
||||||
|
WAITING_FOR_MULTI_CLICK
|
||||||
|
};
|
||||||
|
|
||||||
|
uint8_t _pin;
|
||||||
|
bool _activeState;
|
||||||
|
bool _isAnalog;
|
||||||
|
uint16_t _analogThreshold;
|
||||||
|
|
||||||
|
State _state = IDLE;
|
||||||
|
bool _currentState;
|
||||||
|
bool _lastState;
|
||||||
|
|
||||||
|
uint32_t _stateChangeTime = 0;
|
||||||
|
uint32_t _pressTime = 0;
|
||||||
|
uint32_t _releaseTime = 0;
|
||||||
|
uint32_t _lastReadTime = 0;
|
||||||
|
|
||||||
|
uint8_t _clickCount = 0;
|
||||||
|
EventType _lastEvent = NONE;
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
EventCallback _onShortPress = nullptr;
|
||||||
|
EventCallback _onDoublePress = nullptr;
|
||||||
|
EventCallback _onTriplePress = nullptr;
|
||||||
|
EventCallback _onLongPress = nullptr;
|
||||||
|
EventCallback _onAnyPress = nullptr;
|
||||||
|
|
||||||
|
bool readButton();
|
||||||
|
void handleStateChange();
|
||||||
|
void triggerEvent(EventType event);
|
||||||
|
};
|
||||||
@@ -57,6 +57,24 @@ void UITask::begin(DisplayDriver* display, NodePrefs* node_prefs, const char* bu
|
|||||||
#ifdef PIN_BUZZER
|
#ifdef PIN_BUZZER
|
||||||
buzzer.begin();
|
buzzer.begin();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Initialize button with appropriate configuration
|
||||||
|
#if defined(PIN_USER_BTN) || defined(PIN_USER_BTN_ANA)
|
||||||
|
#ifdef PIN_USER_BTN
|
||||||
|
_userButton = new Button(PIN_USER_BTN, USER_BTN_PRESSED);
|
||||||
|
#else
|
||||||
|
_userButton = new Button(PIN_USER_BTN_ANA, USER_BTN_PRESSED, true, 20);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
_userButton->begin();
|
||||||
|
|
||||||
|
// Set up button callbacks
|
||||||
|
_userButton->onShortPress([this]() { handleButtonShortPress(); });
|
||||||
|
_userButton->onDoublePress([this]() { handleButtonDoublePress(); });
|
||||||
|
_userButton->onTriplePress([this]() { handleButtonTriplePress(); });
|
||||||
|
_userButton->onLongPress([this]() { handleButtonLongPress(); });
|
||||||
|
_userButton->onAnyPress([this]() { handleButtonAnyPress(); });
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITask::soundBuzzer(UIEventType bet) {
|
void UITask::soundBuzzer(UIEventType bet) {
|
||||||
@@ -69,6 +87,9 @@ switch(bet){
|
|||||||
case UIEventType::channelMessage:
|
case UIEventType::channelMessage:
|
||||||
buzzer.play("kerplop:d=16,o=6,b=120:32g#,32c#");
|
buzzer.play("kerplop:d=16,o=6,b=120:32g#,32c#");
|
||||||
break;
|
break;
|
||||||
|
case UIEventType::ack:
|
||||||
|
buzzer.play("ack:d=32,o=8,b=120:c");
|
||||||
|
break;
|
||||||
case UIEventType::roomMessage:
|
case UIEventType::roomMessage:
|
||||||
case UIEventType::newContactMessage:
|
case UIEventType::newContactMessage:
|
||||||
case UIEventType::none:
|
case UIEventType::none:
|
||||||
@@ -233,53 +254,8 @@ void UITask::userLedHandler() {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITask::buttonHandler() {
|
/*
|
||||||
#if defined(PIN_USER_BTN) || defined(PIN_USER_BTN_ANA)
|
hardware-agnostic pre-shutdown activity should be done here
|
||||||
static int prev_btn_state = !USER_BTN_PRESSED;
|
|
||||||
static int prev_btn_state_ana = !USER_BTN_PRESSED;
|
|
||||||
static unsigned long btn_state_change_time = 0;
|
|
||||||
static unsigned long next_read = 0;
|
|
||||||
int cur_time = millis();
|
|
||||||
if (cur_time >= next_read) {
|
|
||||||
int btn_state = 0;
|
|
||||||
int btn_state_ana = 0;
|
|
||||||
#ifdef PIN_USER_BTN
|
|
||||||
btn_state = digitalRead(PIN_USER_BTN);
|
|
||||||
#endif
|
|
||||||
#ifdef PIN_USER_BTN_ANA
|
|
||||||
btn_state_ana = (analogRead(PIN_USER_BTN_ANA) < 20); // analogRead returns a value hopefully below 20 when button is pressed.
|
|
||||||
#endif
|
|
||||||
if (btn_state != prev_btn_state || btn_state_ana != prev_btn_state_ana) { // check for either digital or analogue button change of state
|
|
||||||
if (btn_state == USER_BTN_PRESSED || btn_state_ana == USER_BTN_PRESSED) { // pressed?
|
|
||||||
if (_display != NULL) {
|
|
||||||
if (_display->isOn()) {
|
|
||||||
clearMsgPreview();
|
|
||||||
} else {
|
|
||||||
_display->turnOn();
|
|
||||||
_need_refresh = true;
|
|
||||||
}
|
|
||||||
_auto_off = cur_time + AUTO_OFF_MILLIS; // extend auto-off timer
|
|
||||||
}
|
|
||||||
} else { // unpressed ? check pressed time ...
|
|
||||||
if ((cur_time - btn_state_change_time) > 5000) {
|
|
||||||
#ifdef PIN_STATUS_LED
|
|
||||||
digitalWrite(PIN_STATUS_LED, LOW);
|
|
||||||
delay(10);
|
|
||||||
#endif
|
|
||||||
shutdown(); // without restart
|
|
||||||
}
|
|
||||||
}
|
|
||||||
btn_state_change_time = millis();
|
|
||||||
prev_btn_state = btn_state;
|
|
||||||
prev_btn_state_ana = btn_state_ana;
|
|
||||||
}
|
|
||||||
next_read = millis() + 100; // 10 reads per second
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* hardware-agnostic pre-shutdown activity should be done here
|
|
||||||
*/
|
*/
|
||||||
void UITask::shutdown(bool restart){
|
void UITask::shutdown(bool restart){
|
||||||
|
|
||||||
@@ -303,7 +279,11 @@ void UITask::shutdown(bool restart){
|
|||||||
}
|
}
|
||||||
|
|
||||||
void UITask::loop() {
|
void UITask::loop() {
|
||||||
buttonHandler();
|
#if defined(PIN_USER_BTN) || defined(PIN_USER_BTN_ANA)
|
||||||
|
if (_userButton) {
|
||||||
|
_userButton->update();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
userLedHandler();
|
userLedHandler();
|
||||||
|
|
||||||
#ifdef PIN_BUZZER
|
#ifdef PIN_BUZZER
|
||||||
@@ -328,3 +308,57 @@ void UITask::loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UITask::handleButtonAnyPress() {
|
||||||
|
MESH_DEBUG_PRINTLN("UITask: any press triggered");
|
||||||
|
// called on any button press before other events, to wake up the display quickly
|
||||||
|
// do not refresh the display here, as it may block the button handler
|
||||||
|
if (_display != NULL) {
|
||||||
|
_displayWasOn = _display->isOn(); // Track display state before any action
|
||||||
|
if (!_displayWasOn) {
|
||||||
|
_display->turnOn();
|
||||||
|
}
|
||||||
|
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UITask::handleButtonShortPress() {
|
||||||
|
MESH_DEBUG_PRINTLN("UITask: short press triggered");
|
||||||
|
if (_display != NULL) {
|
||||||
|
// Only clear message preview if display was already on before button press
|
||||||
|
if (_displayWasOn) {
|
||||||
|
// If display was on and showing message preview, clear it
|
||||||
|
if (_origin[0] && _msg[0]) {
|
||||||
|
clearMsgPreview();
|
||||||
|
} else {
|
||||||
|
// Otherwise, refresh the display
|
||||||
|
_need_refresh = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Note: Display turn-on and auto-off timer extension are handled by handleButtonAnyPress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UITask::handleButtonDoublePress() {
|
||||||
|
MESH_DEBUG_PRINTLN("UITask: double press triggered");
|
||||||
|
// Not implemented. TODO: possibly send an advert here?
|
||||||
|
}
|
||||||
|
|
||||||
|
void UITask::handleButtonTriplePress() {
|
||||||
|
MESH_DEBUG_PRINTLN("UITask: triple press triggered");
|
||||||
|
// Toggle buzzer quiet mode
|
||||||
|
#ifdef PIN_BUZZER
|
||||||
|
if (buzzer.isQuiet()) {
|
||||||
|
buzzer.quiet(false);
|
||||||
|
soundBuzzer(UIEventType::ack);
|
||||||
|
} else {
|
||||||
|
soundBuzzer(UIEventType::ack);
|
||||||
|
buzzer.quiet(true);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void UITask::handleButtonLongPress() {
|
||||||
|
MESH_DEBUG_PRINTLN("UITask: long press triggered");
|
||||||
|
shutdown();
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "NodePrefs.h"
|
#include "NodePrefs.h"
|
||||||
|
#include "Button.h"
|
||||||
|
|
||||||
enum class UIEventType
|
enum class UIEventType
|
||||||
{
|
{
|
||||||
@@ -16,7 +17,8 @@
|
|||||||
contactMessage,
|
contactMessage,
|
||||||
channelMessage,
|
channelMessage,
|
||||||
roomMessage,
|
roomMessage,
|
||||||
newContactMessage
|
newContactMessage,
|
||||||
|
ack
|
||||||
};
|
};
|
||||||
|
|
||||||
class UITask {
|
class UITask {
|
||||||
@@ -34,11 +36,24 @@ class UITask {
|
|||||||
char _msg[80];
|
char _msg[80];
|
||||||
int _msgcount;
|
int _msgcount;
|
||||||
bool _need_refresh = true;
|
bool _need_refresh = true;
|
||||||
|
bool _displayWasOn = false; // Track display state before button press
|
||||||
|
|
||||||
|
// Button handlers
|
||||||
|
#if defined(PIN_USER_BTN) || defined(PIN_USER_BTN_ANA)
|
||||||
|
Button* _userButton = nullptr;
|
||||||
|
#endif
|
||||||
|
|
||||||
void renderCurrScreen();
|
void renderCurrScreen();
|
||||||
void buttonHandler();
|
|
||||||
void userLedHandler();
|
void userLedHandler();
|
||||||
void renderBatteryIndicator(uint16_t batteryMilliVolts);
|
void renderBatteryIndicator(uint16_t batteryMilliVolts);
|
||||||
|
|
||||||
|
// Button action handlers
|
||||||
|
void handleButtonAnyPress();
|
||||||
|
void handleButtonShortPress();
|
||||||
|
void handleButtonDoublePress();
|
||||||
|
void handleButtonTriplePress();
|
||||||
|
void handleButtonLongPress();
|
||||||
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
|
|||||||
@@ -79,8 +79,7 @@ build_flags =
|
|||||||
build_src_filter = ${Heltec_t114.build_src_filter}
|
build_src_filter = ${Heltec_t114.build_src_filter}
|
||||||
+<helpers/nrf52/T114Board.cpp>
|
+<helpers/nrf52/T114Board.cpp>
|
||||||
+<helpers/nrf52/SerialBLEInterface.cpp>
|
+<helpers/nrf52/SerialBLEInterface.cpp>
|
||||||
+<../examples/companion_radio/main.cpp>
|
+<../examples/companion_radio>
|
||||||
+<../examples/companion_radio/UITask.cpp>
|
|
||||||
+<helpers/ui/ST7789Display.cpp>
|
+<helpers/ui/ST7789Display.cpp>
|
||||||
+<helpers/ui/OLEDDisplay.cpp>
|
+<helpers/ui/OLEDDisplay.cpp>
|
||||||
+<helpers/ui/OLEDDisplayFonts.cpp>
|
+<helpers/ui/OLEDDisplayFonts.cpp>
|
||||||
|
|||||||
Reference in New Issue
Block a user