Improved beeping booping and other buzzer based feedback (#6947)

* Improved beeping booping and other buzzer based feedback

* audible button feedback (#6949)

* Refactor

---------

Co-authored-by: todd-herbert <herbert.todd@gmail.com>
This commit is contained in:
Ben Meadors 2025-06-03 07:08:31 -05:00 committed by GitHub
parent 2cc2002675
commit 25fbf58444
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 218 additions and 5 deletions

View File

@ -172,6 +172,7 @@ void ButtonThread::sendAdHocPosition()
screen->print("Sent ad-hoc nodeinfo\n");
screen->forceDisplay(true); // Force a new UI frame, then force an EInk update
}
playComboTune();
}
int32_t ButtonThread::runOnce()
@ -197,10 +198,62 @@ int32_t ButtonThread::runOnce()
canSleep &= userButtonTouch.isIdle();
#endif
// Check for combination timeout
if (waitingForLongPress && (millis() - shortPressTime) > BUTTON_COMBO_TIMEOUT_MS) {
waitingForLongPress = false;
}
// Check if we should play lead-up sound during long press
// Play lead-up when button has been held for BUTTON_LEADUP_MS but before long press triggers
#if defined(BUTTON_PIN) || defined(USERPREFS_BUTTON_PIN) || defined(ARCH_PORTDUINO)
bool buttonCurrentlyPressed = false;
#if defined(BUTTON_PIN) || defined(USERPREFS_BUTTON_PIN)
// Read the actual physical state of the button pin
#if !defined(USERPREFS_BUTTON_PIN)
int buttonPin = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN;
#else
int buttonPin = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN;
#endif
buttonCurrentlyPressed = isButtonPressed(buttonPin);
#elif defined(ARCH_PORTDUINO)
if (settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC) {
// For portduino, assume active low
buttonCurrentlyPressed = isButtonPressed(settingsMap[user]);
}
#endif
static uint32_t buttonPressStartTime = 0;
static bool buttonWasPressed = false;
// Detect start of button press
if (buttonCurrentlyPressed && !buttonWasPressed) {
buttonPressStartTime = millis();
leadUpPlayed = false;
}
// Check if we should play lead-up sound
if (buttonCurrentlyPressed && !leadUpPlayed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS &&
(millis() - buttonPressStartTime) < BUTTON_LONGPRESS_MS) {
playLongPressLeadUp();
leadUpPlayed = true;
}
// Reset when button is released
if (!buttonCurrentlyPressed && buttonWasPressed) {
leadUpPlayed = false;
}
buttonWasPressed = buttonCurrentlyPressed;
#endif
if (btnEvent != BUTTON_EVENT_NONE) {
switch (btnEvent) {
case BUTTON_EVENT_PRESSED: {
LOG_BUTTON("press!");
// Play boop sound for every button press
playBoop();
// If a nag notification is running, stop it and prevent other actions
if (moduleConfig.external_notification.enabled && (externalNotificationModule->nagCycleCutoff != UINT32_MAX)) {
externalNotificationModule->stopNow();
@ -210,12 +263,24 @@ int32_t ButtonThread::runOnce()
sendAdHocPosition();
break;
#endif
// Start tracking for potential combination
waitingForLongPress = true;
shortPressTime = millis();
switchPage();
break;
}
case BUTTON_EVENT_PRESSED_SCREEN: {
LOG_BUTTON("AltPress!");
// Play boop sound for every button press
playBoop();
// Reset combination tracking
waitingForLongPress = false;
#ifdef ELECROW_ThinkNode_M1
// If a nag notification is running, stop it and prevent other actions
if (moduleConfig.external_notification.enabled && (externalNotificationModule->nagCycleCutoff != UINT32_MAX)) {
@ -235,6 +300,12 @@ int32_t ButtonThread::runOnce()
case BUTTON_EVENT_DOUBLE_PRESSED: {
LOG_BUTTON("Double press!");
// Play boop sound for every button press
playBoop();
// Reset combination tracking
waitingForLongPress = false;
#ifdef ELECROW_ThinkNode_M1
digitalWrite(PIN_EINK_EN, digitalRead(PIN_EINK_EN) == LOW);
break;
@ -250,6 +321,13 @@ int32_t ButtonThread::runOnce()
case BUTTON_EVENT_MULTI_PRESSED: {
LOG_BUTTON("Mulitipress! %hux", multipressClickCount);
// Play boop sound for every button press
playBoop();
// Reset combination tracking
waitingForLongPress = false;
switch (multipressClickCount) {
#if HAS_GPS && !defined(ELECROW_ThinkNode_M1)
// 3 clicks: toggle GPS
@ -307,6 +385,18 @@ int32_t ButtonThread::runOnce()
case BUTTON_EVENT_LONG_PRESSED: {
LOG_BUTTON("Long press!");
// Check if this is part of a short-press + long-press combination
if (waitingForLongPress && (millis() - shortPressTime) <= BUTTON_COMBO_TIMEOUT_MS) {
LOG_BUTTON("Combo detected: short-press + long-press!");
btnEvent = BUTTON_EVENT_COMBO_SHORT_LONG;
waitingForLongPress = false;
break;
}
// Reset combination tracking
waitingForLongPress = false;
powerFSM.trigger(EVENT_PRESS);
if (screen) {
@ -314,6 +404,8 @@ int32_t ButtonThread::runOnce()
screen->showOverlayBanner("Shutting Down..."); // Display for 3 seconds
}
// Lead-up sound already played during button hold
// Just a simple beep to confirm long press threshold reached
playBeep();
break;
}
@ -322,6 +414,10 @@ int32_t ButtonThread::runOnce()
// may wake the board immediatedly.
case BUTTON_EVENT_LONG_RELEASED: {
LOG_INFO("Shutdown from long press");
// Reset combination tracking
waitingForLongPress = false;
playShutdownMelody();
delay(3000);
power->shutdown();
@ -332,6 +428,13 @@ int32_t ButtonThread::runOnce()
#ifdef BUTTON_PIN_TOUCH
case BUTTON_EVENT_TOUCH_LONG_PRESSED: {
LOG_BUTTON("Touch press!");
// Play boop sound for every button press
playBoop();
// Reset combination tracking
waitingForLongPress = false;
// Ignore if: no screen
if (!screen)
break;
@ -353,6 +456,20 @@ int32_t ButtonThread::runOnce()
}
#endif // BUTTON_PIN_TOUCH
case BUTTON_EVENT_COMBO_SHORT_LONG: {
// Placeholder for short-press + long-press combination
LOG_BUTTON("Short-press + Long-press combination detected!");
// Play the combination tune
playComboTune();
// Optionally show a message on screen
if (screen) {
screen->showOverlayBanner("Combo Tune Played", 2000);
}
break;
}
default:
break;
}

View File

@ -16,6 +16,14 @@
#define BUTTON_TOUCH_MS 400
#endif
#ifndef BUTTON_COMBO_TIMEOUT_MS
#define BUTTON_COMBO_TIMEOUT_MS 2000 // 2 seconds to complete the combination
#endif
#ifndef BUTTON_LEADUP_MS
#define BUTTON_LEADUP_MS 2500 // Play lead-up sound after 2.5 seconds of holding
#endif
class ButtonThread : public concurrency::OSThread
{
public:
@ -30,6 +38,7 @@ class ButtonThread : public concurrency::OSThread
BUTTON_EVENT_LONG_PRESSED,
BUTTON_EVENT_LONG_RELEASED,
BUTTON_EVENT_TOUCH_LONG_PRESSED,
BUTTON_EVENT_COMBO_SHORT_LONG,
};
ButtonThread();
@ -41,6 +50,14 @@ class ButtonThread : public concurrency::OSThread
void setScreenFlag(bool flag) { screen_flag = flag; }
bool getScreenFlag() { return screen_flag; }
bool isInterceptingAndFocused();
bool isButtonPressed(int buttonPin)
{
#ifdef BUTTON_ACTIVE_LOW
return !digitalRead(buttonPin); // Active low: pressed = LOW
#else
return digitalRead(buttonPin); // Most buttons are active low by default
#endif
}
// Disconnect and reconnect interrupts for light sleep
#ifdef ARCH_ESP32
@ -74,6 +91,13 @@ class ButtonThread : public concurrency::OSThread
// Store click count during callback, for later use
volatile int multipressClickCount = 0;
// Combination tracking state
bool waitingForLongPress = false;
uint32_t shortPressTime = 0;
// Long press lead-up tracking
bool leadUpPlayed = false;
static void wakeOnIrq(int irq, int mode);
static void sendAdHocPosition();

View File

@ -87,3 +87,36 @@ void playShutdownMelody()
ToneDuration melody[] = {{NOTE_CS4, DURATION_1_8}, {NOTE_AS3, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}};
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
}
void playBoop()
{
// A short, friendly "boop" sound for button presses
ToneDuration melody[] = {{NOTE_A3, 50}}; // Very short A3 note
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
}
void playLongPressLeadUp()
{
// An ascending lead-up sequence for long press - builds anticipation
ToneDuration melody[] = {
{NOTE_C3, 100}, // Start low
{NOTE_E3, 100}, // Step up
{NOTE_G3, 100}, // Keep climbing
{NOTE_B3, 150} // Peak with longer note for emphasis
};
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
}
void playComboTune()
{
// Quick high-pitched notes with trills
ToneDuration melody[] = {
{NOTE_G3, 80}, // Quick chirp
{NOTE_B3, 60}, // Higher chirp
{NOTE_CS4, 80}, // Even higher
{NOTE_G3, 60}, // Quick trill down
{NOTE_CS4, 60}, // Quick trill up
{NOTE_B3, 120} // Ending chirp
};
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
}

View File

@ -6,3 +6,6 @@ void playStartMelody();
void playShutdownMelody();
void playGPSEnableBeep();
void playGPSDisableBeep();
void playComboTune();
void playBoop();
void playLongPressLeadUp();

View File

@ -3,6 +3,7 @@
#include "./Events.h"
#include "RTC.h"
#include "buzz.h"
#include "modules/AdminModule.h"
#include "modules/TextMessageModule.h"
#include "sleep.h"
@ -37,6 +38,10 @@ void InkHUD::Events::begin()
void InkHUD::Events::onButtonShort()
{
// Audio feedback (via buzzer)
// Short low tone
playBoop();
// Check which system applet wants to handle the button press (if any)
SystemApplet *consumer = nullptr;
for (SystemApplet *sa : inkhud->systemApplets) {
@ -55,6 +60,10 @@ void InkHUD::Events::onButtonShort()
void InkHUD::Events::onButtonLong()
{
// Audio feedback (via buzzer)
// Low tone, longer than playBoop
playBeep();
// Check which system applet wants to handle the button press (if any)
SystemApplet *consumer = nullptr;
for (SystemApplet *sa : inkhud->systemApplets) {
@ -102,6 +111,10 @@ int InkHUD::Events::beforeDeepSleep(void *unused)
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, false);
delay(1000); // Cooldown, before potentially yanking display power
// InkHUD shutdown complete
// Firmware shutdown continues for several seconds more; flash write still pending
playShutdownMelody();
return 0; // We agree: deep sleep now
}

View File

@ -10,6 +10,7 @@
#include "NodeDB.h"
#include "PowerFSM.h" // needed for button bypass
#include "SPILock.h"
#include "buzz.h"
#include "detect/ScanI2C.h"
#include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h"
@ -874,6 +875,7 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha
simulatedPacket.from = 0; // Local device
screen->handleTextMessage(&simulatedPacket);
}
playComboTune();
}
int32_t CannedMessageModule::runOnce()
{

View File

@ -22,6 +22,9 @@
#include "graphics/niche/Drivers/EInk/GDEY0154D67.h"
#include "graphics/niche/Inputs/TwoButton.h"
// Button feedback
#include "buzz.h"
void setupNicheGraphics()
{
using namespace NicheGraphics;
@ -98,8 +101,14 @@ void setupNicheGraphics()
buttons->setWiring(1, PIN_BUTTON1);
buttons->setTiming(1, 50, 500); // 500ms before latch
buttons->setHandlerDown(1, [backlight]() { backlight->peek(); });
buttons->setHandlerLongPress(1, [backlight]() { backlight->latch(); });
buttons->setHandlerShortPress(1, [backlight]() { backlight->off(); });
buttons->setHandlerLongPress(1, [backlight]() {
backlight->latch();
playBeep();
});
buttons->setHandlerShortPress(1, [backlight]() {
backlight->off();
playBoop();
});
// Begin handling button events
buttons->start();

View File

@ -21,6 +21,9 @@
#include "graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h"
#include "graphics/niche/Inputs/TwoButton.h"
// Button feedback
#include "buzz.h"
void setupNicheGraphics()
{
using namespace NicheGraphics;
@ -85,7 +88,10 @@ void setupNicheGraphics()
// #1: Aux Button
buttons->setWiring(1, BUTTON_PIN_SECONDARY);
buttons->setHandlerShortPress(1, [inkhud]() { inkhud->nextTile(); });
buttons->setHandlerShortPress(1, [inkhud]() {
inkhud->nextTile();
playBoop();
});
// Begin handling button events
buttons->start();

View File

@ -34,6 +34,9 @@ Different NicheGraphics UIs and different hardware variants will each have their
#include "graphics/niche/Drivers/EInk/DEPG0290BNS800.h"
#include "graphics/niche/Inputs/TwoButton.h"
// Button feedback
#include "buzz.h"
void setupNicheGraphics()
{
using namespace NicheGraphics;
@ -98,7 +101,10 @@ void setupNicheGraphics()
// #1: Aux Button
buttons->setWiring(1, BUTTON_PIN_SECONDARY);
buttons->setHandlerShortPress(1, [inkhud]() { inkhud->nextTile(); });
buttons->setHandlerShortPress(1, [inkhud]() {
inkhud->nextTile();
playBoop();
});
// Begin handling button events
buttons->start();