From efa2b4b1b73e02f4da010fb47eff948d6870d19a Mon Sep 17 00:00:00 2001 From: seagull9000 Date: Fri, 23 May 2025 17:58:13 +1200 Subject: [PATCH] RTTTL-tone-for-Channel-Message I was a bit remiss in removing the tone for channel message event - this puts one in. So: DM event - plays a tone (per current) Channel Message - new shorter tone All others aren't defined at present. Need muting function before we get too carried away. --- examples/companion_radio/UITask.cpp | 2 + src/helpers/ui/button.cpp | 292 ++++++++++++++++++++++++++++ src/helpers/ui/button.h | 79 ++++++++ 3 files changed, 373 insertions(+) create mode 100644 src/helpers/ui/button.cpp create mode 100644 src/helpers/ui/button.h diff --git a/examples/companion_radio/UITask.cpp b/examples/companion_radio/UITask.cpp index f97b47f4..58ffc4d0 100644 --- a/examples/companion_radio/UITask.cpp +++ b/examples/companion_radio/UITask.cpp @@ -67,6 +67,8 @@ switch(bet){ buzzer.play("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7"); break; case UIEventType::channelMessage: + buzzer.play("kerplop:d=16,o=6,b=120:32g#,32c#"); + break; case UIEventType::roomMessage: case UIEventType::newContactMessage: case UIEventType::none: diff --git a/src/helpers/ui/button.cpp b/src/helpers/ui/button.cpp new file mode 100644 index 00000000..7e715b44 --- /dev/null +++ b/src/helpers/ui/button.cpp @@ -0,0 +1,292 @@ +#include "Button.h" + +Button::Button() : + _pin(0), + _inputMode(INPUT_PULLUP), + _buttonPressedState(HIGH), // Assuming INPUT_PULLUP, so HIGH is unpressed + _lastButtonState(HIGH), + _buttonState(false), // Debounced state: false is unpressed + _lastDebouncedState(false), + _lastDebounceTime(0), + _pressStartTime(0), + _releaseTime(0), + _currentSequenceCount(0), + _lastPressEndTime(0), + _callbackCount(0) +{ + for (uint8_t i = 0; i < MAX_PRESSES_IN_SEQUENCE; ++i) { + _currentSequence[i] = ButtonPressType::NONE; + } + for (uint8_t i = 0; i < MAX_CALLBACKS; ++i) { + _registeredCallbacks[i].count = 0; + _registeredCallbacks[i].callback = nullptr; + _registeredCallbacks[i].triggeredInSequence = false; + for (uint8_t j = 0; j < MAX_PRESSES_IN_SEQUENCE; ++j) { + _registeredCallbacks[i].sequence[j] = ButtonPressType::NONE; + } + } +} + +void Button::attach(uint8_t pin, uint8_t inputMode) { + _pin = pin; + _inputMode = inputMode; + pinMode(_pin, _inputMode); + if (_inputMode == INPUT_PULLUP) { + _buttonPressedState = LOW; // Pressed is LOW for PULLUP + _lastButtonState = HIGH; // Initial unpressed state + } else { + _buttonPressedState = HIGH; // Pressed is HIGH for PULLDOWN or regular INPUT + _lastButtonState = LOW; // Initial unpressed state + } + _lastDebouncedState = false; // Not pressed + _buttonState = false; // Not pressed +} + +void Button::update() { + if (_pin == 0) return; // Not attached + + bool reading = digitalRead(_pin); + unsigned long currentTime = millis(); + + // Debounce logic + if (reading != _lastButtonState) { + _lastDebounceTime = currentTime; + } + _lastButtonState = reading; + + if ((currentTime - _lastDebounceTime) > DEBOUNCE_DELAY) { + bool currentDebouncedState = (reading == _buttonPressedState); + + // Edge detection + if (currentDebouncedState != _lastDebouncedState) { + _lastDebouncedState = currentDebouncedState; + + if (currentDebouncedState) { // Button pressed + _pressStartTime = currentTime; + // If a new press starts too long after the previous one, reset sequence + if (_currentSequenceCount > 0 && (currentTime - _lastPressEndTime) > SHORT_PRESS_TIMEOUT) { + _resetSequence(); + } + } else { // Button released + _releaseTime = currentTime; + unsigned long pressDuration = _releaseTime - _pressStartTime; + ButtonPressType pressType = _determinePressType(pressDuration); + + if (pressType != ButtonPressType::NONE) { + _addPressToSequence(pressType); + _lastPressEndTime = currentTime; // Mark end time for next press timeout + _evaluateSequences(); + } + } + } + } + + // Check for sequence timeout if a sequence is in progress + if (_currentSequenceCount > 0 && (currentTime - _lastPressEndTime) > SHORT_PRESS_TIMEOUT) { + // If we timed out, and haven't triggered anything specific from the partial sequence, + // it's time to reset. Callbacks for partial matches should have already fired in _evaluateSequences. + // This handles the case where a sequence like S1 is defined, and the user presses S, then waits. + // We need to ensure S1 callback fires if it exists, even if S2 or S3 are also defined. + // _evaluateSequences called on release handles this. This timeout mainly resets for the next input. + _resetSequence(); + } +} + + +ButtonPressType Button::_determinePressType(unsigned long pressDuration) { + if (pressDuration >= LONG_LONG_PRESS_MIN_DURATION) { + return ButtonPressType::LL; + } else if (pressDuration >= LONGISH_PRESS_MIN_DURATION && pressDuration < LONGISH_PRESS_MAX_DURATION) { + return ButtonPressType::LS; + } else if (pressDuration > DEBOUNCE_DELAY && pressDuration <= SHORT_PRESS_MAX_DURATION) { // Ensure it's more than debounce + return ButtonPressType::S; + } + return ButtonPressType::NONE; +} + +void Button::_addPressToSequence(ButtonPressType pressType) { + if (_currentSequenceCount < MAX_PRESSES_IN_SEQUENCE) { + // Specific handling for LL: it can only be LL1 + if (pressType == ButtonPressType::LL) { + if (_currentSequenceCount == 0) { // LL only valid as the first press in its "sequence" + _currentSequence[_currentSequenceCount++] = pressType; + } else { + // An LL press occurred after other presses, which is not a defined LL1 sequence. + // Treat as an invalid sequence progression, so reset. + _resetSequence(); + // Potentially start a new sequence with this LL if it was intended as LL1. + // For simplicity now, we just reset. More complex logic could try to salvage. + } + } else if (pressType == ButtonPressType::LS) { + // LS can form sequences up to LS3 + // Check if previous was also LS or if sequence is empty + if (_currentSequenceCount == 0 || _currentSequence[_currentSequenceCount -1] == ButtonPressType::LS) { + _currentSequence[_currentSequenceCount++] = pressType; + } else { + // Mixing LS with S in a sequence is not explicitly defined as LS1/2/3 or S1/2/3 + // So, reset the sequence and start new with this LS. + _resetSequence(); + _currentSequence[_currentSequenceCount++] = pressType; + } + } else if (pressType == ButtonPressType::S) { + // S can form sequences up to S3 + // Check if previous was also S or if sequence is empty + if (_currentSequenceCount == 0 || _currentSequence[_currentSequenceCount -1] == ButtonPressType::S) { + _currentSequence[_currentSequenceCount++] = pressType; + } else { + // Mixing S with LS in a sequence is not S1/2/3 or LS1/2/3 + // So, reset the sequence and start new with this S. + _resetSequence(); + _currentSequence[_currentSequenceCount++] = pressType; + } + } + } else { + // Sequence array is full, this shouldn't normally happen if MAX_PRESSES_IN_SEQUENCE is large enough. + // Reset to be safe. + _resetSequence(); + // And add the current press as the start of a new sequence. + if (pressType != ButtonPressType::NONE) { // ensure it's a valid press + _currentSequence[_currentSequenceCount++] = pressType; + } + } +} + +void Button::_evaluateSequences() { + if (_currentSequenceCount == 0) return; + + bool exactMatchFound = false; + + for (uint8_t i = 0; i < _callbackCount; ++i) { + ButtonSequence& registered = _registeredCallbacks[i]; + if (registered.callback == nullptr) continue; + + if (registered.count == _currentSequenceCount) { + bool match = true; + for (uint8_t j = 0; j < _currentSequenceCount; ++j) { + if (_currentSequence[j] != registered.sequence[j]) { + match = false; + break; + } + } + if (match) { + // Exact match found + if (!registered.triggeredInSequence) { // Check if already triggered for this evaluation pass + registered.callback(); + registered.triggeredInSequence = true; // Mark as triggered for this pass + } + exactMatchFound = true; + // For an exact match, we generally reset the current sequence + // as it has been fully consumed by a callback. + } + } + } + + // After checking all callbacks, reset their triggeredInSequence flags for the next evaluation pass + for (uint8_t i = 0; i < _callbackCount; ++i) { + _registeredCallbacks[i].triggeredInSequence = false; + } + + // If an exact match was found, the sequence is "consumed" and should be reset. + // This allows, for example, S1 to trigger, then S2 to trigger, then S3. + // If we don't reset, S1 would trigger, then S1+S2 would require an (S,S) callback. + // The current logic means S triggers S1 callback, then if another S comes, (S,S) triggers S2 callback. + if (exactMatchFound) { + _resetSequence(); + } else { + // If no exact match, but the sequence is full (e.g., S, S, S and no S3 callback) + // or if it's an LL press (which is always a sequence of 1) + // then we should reset to allow new sequences. + if (_currentSequenceCount >= MAX_PRESSES_IN_SEQUENCE || + (_currentSequenceCount > 0 && _currentSequence[_currentSequenceCount -1] == ButtonPressType::LL)) { + _resetSequence(); + } + // Also, if the current sequence is S,S and there's no S2 callback, + // but there IS an S1 callback, S1 should have triggered. + // The current _evaluateSequences iterates all callbacks. + // The problem is if a user defines S and S,S. + // Press S: S callback fires. Current sequence is {S}. + // Press S again: Current sequence becomes {S,S}. S,S callback fires. Sequence resets. This is good. + + // What if user defines S, S,S, S,S,S. + // Press 1: S fires. Seq={S}. + // Press 2: S,S fires. Seq={S,S}. + // Press 3: S,S,S fires. Seq={S,S,S}. + // The reset on exact match handles this. + + // What if only S,S,S is defined? + // Press 1: Seq={S}. No exact match. No reset. + // Press 2: Seq={S,S}. No exact match. No reset. + // Press 3: Seq={S,S,S}. S,S,S fires. Seq resets. Good. + + // What if only S is defined? + // Press 1: Seq={S}. S fires. Seq resets. Good. + // User presses S, S, S. + // S -> S fires, seq resets. + // S -> S fires, seq resets. + // S -> S fires, seq resets. + // This seems correct. A sequence ends once it matches *a* callback. + + // Exception: LL is always a single event. + if (_currentSequenceCount == 1 && _currentSequence[0] == ButtonPressType::LL) { + // LL callback would have fired if registered. Sequence should reset. + // This is handled by `exactMatchFound` leading to reset, + // or by the `_currentSequence[_currentSequenceCount -1] == ButtonPressType::LL` condition above. + } + } +} + + +void Button::_resetSequence() { + _currentSequenceCount = 0; + for (uint8_t i = 0; i < MAX_PRESSES_IN_SEQUENCE; ++i) { + _currentSequence[i] = ButtonPressType::NONE; + } + // _lastPressEndTime is not reset here, because it's used to see if a *new* press + // is starting a new sequence or continuing one (within SHORT_PRESS_TIMEOUT). + // When a sequence fully resets (e.g. due to timeout or completion), + // the _pressStartTime of the *next* press will be compared to the _lastPressEndTime + // of the *previous* press that completed/timed-out the sequence. +} + +bool Button::onPress(ButtonPressType pressType, ButtonCallback callback) { + if (pressType == ButtonPressType::NONE) return false; + // This is a shorthand for a sequence of one. + return onPressSequence(&pressType, 1, callback); +} + +bool Button::onPressSequence(const ButtonPressType types[], uint8_t count, ButtonCallback callback) { + if (_callbackCount >= MAX_CALLBACKS || count == 0 || count > MAX_PRESSES_IN_SEQUENCE) { + return false; // No space or invalid sequence + } + + // Restriction: LL can only be a sequence of 1. + if (count > 1) { + for(uint8_t i=0; i < count; ++i) { + if (types[i] == ButtonPressType::LL) return false; // LL cannot be part of a multi-press sequence definition. + } + } + // Restriction: Sequences must be homogenous (all S or all LS), or a single LL. + if (count > 1) { + ButtonPressType firstType = types[0]; + if (firstType == ButtonPressType::LL) return false; // Should have been caught above. + for (uint8_t i = 1; i < count; ++i) { + if (types[i] != firstType || types[i] == ButtonPressType::LL) { + // Heterogeneous sequence (e.g. S, LS) or LL in multi-press is not allowed by problem def. + return false; + } + } + } + + + _registeredCallbacks[_callbackCount].count = count; + for (uint8_t i = 0; i < count; ++i) { + _registeredCallbacks[_callbackCount].sequence[i] = types[i]; + } + for (uint8_t i = count; i < MAX_PRESSES_IN_SEQUENCE; ++i) { // Zero out rest + _registeredCallbacks[_callbackCount].sequence[i] = ButtonPressType::NONE; + } + _registeredCallbacks[_callbackCount].callback = callback; + _registeredCallbacks[_callbackCount].triggeredInSequence = false; + _callbackCount++; + return true; +} \ No newline at end of file diff --git a/src/helpers/ui/button.h b/src/helpers/ui/button.h new file mode 100644 index 00000000..0f0e3b92 --- /dev/null +++ b/src/helpers/ui/button.h @@ -0,0 +1,79 @@ +#ifndef BUTTON_H +#define BUTTON_H + +#include + +// Maximum number of presses in a sequence (e.g., S3, LS3) +#define MAX_PRESSES_IN_SEQUENCE 3 +// Maximum number of sequences that can be registered +#define MAX_CALLBACKS 10 +// Debounce delay in milliseconds +#define DEBOUNCE_DELAY 50 +// Time to distinguish between short presses in a sequence (ms) +#define SHORT_PRESS_TIMEOUT 500 +// Short press max duration (ms) +#define SHORT_PRESS_MAX_DURATION (SHORT_PRESS_TIMEOUT - 1) // Must be less than timeout +// Longish press min duration (ms) +#define LONGISH_PRESS_MIN_DURATION 1000 +// Longish press max duration (ms) +#define LONGISH_PRESS_MAX_DURATION 2500 +// Long-Long press min duration (ms) +#define LONG_LONG_PRESS_MIN_DURATION 4000 + + +// Enum to represent different press types +enum class ButtonPressType { + NONE, + S, // Short Press + LS, // Longish Press + LL // Long-Long Press +}; + +// Type alias for the callback function +typedef void (*ButtonCallback)(); + +struct ButtonSequence { + ButtonPressType sequence[MAX_PRESSES_IN_SEQUENCE]; + uint8_t count; // Number of presses in this specific sequence + ButtonCallback callback; + bool triggeredInSequence; // To prevent multiple triggers during a sequence evaluation +}; + +class Button { +public: + Button(); + void attach(uint8_t pin, uint8_t inputMode = INPUT_PULLUP); + void update(); + + // Methods to add callbacks + // For single press types (e.g., one long-long press) + bool onPress(ButtonPressType pressType, ButtonCallback callback); + // For sequences of presses (e.g., S, S, S or LS, LS) + bool onPressSequence(const ButtonPressType types[], uint8_t count, ButtonCallback callback); + +private: + uint8_t _pin; + uint8_t _inputMode; + bool _buttonPressedState; // Physical state of the button (HIGH/LOW) + bool _lastButtonState; // Previous physical state for change detection + bool _buttonState; // Debounced button state (true for pressed) + bool _lastDebouncedState; // Last debounced state + + unsigned long _lastDebounceTime; + unsigned long _pressStartTime; + unsigned long _releaseTime; + + ButtonPressType _currentSequence[MAX_PRESSES_IN_SEQUENCE]; + uint8_t _currentSequenceCount; + unsigned long _lastPressEndTime; // Time when the last press in a potential sequence ended + + ButtonSequence _registeredCallbacks[MAX_CALLBACKS]; + uint8_t _callbackCount; + + void _resetSequence(); + void _addPressToSequence(ButtonPressType pressType); + void _evaluateSequences(); + ButtonPressType _determinePressType(unsigned long pressDuration); +}; + +#endif // BUTTON_H \ No newline at end of file