diff --git a/examples/companion_radio/Button.cpp b/examples/companion_radio/Button.cpp new file mode 100644 index 00000000..ec1f0f69 --- /dev/null +++ b/examples/companion_radio/Button.cpp @@ -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; + } +} \ No newline at end of file diff --git a/examples/companion_radio/Button.h b/examples/companion_radio/Button.h new file mode 100644 index 00000000..47c792bd --- /dev/null +++ b/examples/companion_radio/Button.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include + +// 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; + + 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); +}; \ No newline at end of file diff --git a/examples/companion_radio/UITask.cpp b/examples/companion_radio/UITask.cpp index 675972dc..5ff5f140 100644 --- a/examples/companion_radio/UITask.cpp +++ b/examples/companion_radio/UITask.cpp @@ -57,6 +57,24 @@ void UITask::begin(DisplayDriver* display, NodePrefs* node_prefs, const char* bu #ifdef PIN_BUZZER buzzer.begin(); #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) { @@ -69,6 +87,9 @@ switch(bet){ case UIEventType::channelMessage: buzzer.play("kerplop:d=16,o=6,b=120:32g#,32c#"); break; + case UIEventType::ack: + buzzer.play("ack:d=32,o=8,b=120:c"); + break; case UIEventType::roomMessage: case UIEventType::newContactMessage: case UIEventType::none: @@ -233,53 +254,8 @@ void UITask::userLedHandler() { #endif } -void UITask::buttonHandler() { - #if defined(PIN_USER_BTN) || defined(PIN_USER_BTN_ANA) - 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 +/* + hardware-agnostic pre-shutdown activity should be done here */ void UITask::shutdown(bool restart){ @@ -303,7 +279,11 @@ void UITask::shutdown(bool restart){ } void UITask::loop() { - buttonHandler(); + #if defined(PIN_USER_BTN) || defined(PIN_USER_BTN_ANA) + if (_userButton) { + _userButton->update(); + } + #endif userLedHandler(); #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(); +} \ No newline at end of file diff --git a/examples/companion_radio/UITask.h b/examples/companion_radio/UITask.h index d774e54c..acf5237e 100644 --- a/examples/companion_radio/UITask.h +++ b/examples/companion_radio/UITask.h @@ -9,6 +9,7 @@ #endif #include "NodePrefs.h" +#include "Button.h" enum class UIEventType { @@ -16,7 +17,8 @@ contactMessage, channelMessage, roomMessage, - newContactMessage + newContactMessage, + ack }; class UITask { @@ -34,11 +36,24 @@ class UITask { char _msg[80]; int _msgcount; 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 buttonHandler(); void userLedHandler(); void renderBatteryIndicator(uint16_t batteryMilliVolts); + + // Button action handlers + void handleButtonAnyPress(); + void handleButtonShortPress(); + void handleButtonDoublePress(); + void handleButtonTriplePress(); + void handleButtonLongPress(); + public: diff --git a/variants/t114/platformio.ini b/variants/t114/platformio.ini index 9e72bbd6..37e31e6f 100644 --- a/variants/t114/platformio.ini +++ b/variants/t114/platformio.ini @@ -79,8 +79,7 @@ build_flags = build_src_filter = ${Heltec_t114.build_src_filter} + + - +<../examples/companion_radio/main.cpp> - +<../examples/companion_radio/UITask.cpp> + +<../examples/companion_radio> + + +