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.
This commit is contained in:
seagull9000
2025-05-23 17:58:13 +12:00
parent 16a283ac5b
commit efa2b4b1b7
3 changed files with 373 additions and 0 deletions

View File

@@ -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:

292
src/helpers/ui/button.cpp Normal file
View File

@@ -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;
}

79
src/helpers/ui/button.h Normal file
View File

@@ -0,0 +1,79 @@
#ifndef BUTTON_H
#define BUTTON_H
#include <Arduino.h>
// 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