From 25fbf5844493260d210acd515b329b46b2c0b093 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 3 Jun 2025 07:08:31 -0500 Subject: [PATCH] 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 --- src/ButtonThread.cpp | 117 ++++++++++++++++++ src/ButtonThread.h | 24 ++++ src/buzz/buzz.cpp | 33 +++++ src/buzz/buzz.h | 5 +- src/graphics/niche/InkHUD/Events.cpp | 13 ++ src/modules/CannedMessageModule.cpp | 2 + variants/ELECROW-ThinkNode-M1/nicheGraphics.h | 13 +- .../heltec_vision_master_e213/nicheGraphics.h | 8 +- .../heltec_vision_master_e290/nicheGraphics.h | 8 +- 9 files changed, 218 insertions(+), 5 deletions(-) diff --git a/src/ButtonThread.cpp b/src/ButtonThread.cpp index d898d4839..a423b4f36 100644 --- a/src/ButtonThread.cpp +++ b/src/ButtonThread.cpp @@ -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; } diff --git a/src/ButtonThread.h b/src/ButtonThread.h index 22ead4156..05fa46892 100644 --- a/src/ButtonThread.h +++ b/src/ButtonThread.h @@ -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(); diff --git a/src/buzz/buzz.cpp b/src/buzz/buzz.cpp index 6ba2f4140..8c7d4ec2b 100644 --- a/src/buzz/buzz.cpp +++ b/src/buzz/buzz.cpp @@ -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)); +} diff --git a/src/buzz/buzz.h b/src/buzz/buzz.h index adeaca73d..75afe6d90 100644 --- a/src/buzz/buzz.h +++ b/src/buzz/buzz.h @@ -5,4 +5,7 @@ void playLongBeep(); void playStartMelody(); void playShutdownMelody(); void playGPSEnableBeep(); -void playGPSDisableBeep(); \ No newline at end of file +void playGPSDisableBeep(); +void playComboTune(); +void playBoop(); +void playLongPressLeadUp(); \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Events.cpp b/src/graphics/niche/InkHUD/Events.cpp index ee6c04938..73d27fe56 100644 --- a/src/graphics/niche/InkHUD/Events.cpp +++ b/src/graphics/niche/InkHUD/Events.cpp @@ -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 } diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index af7acc736..0d48a7376 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -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() { diff --git a/variants/ELECROW-ThinkNode-M1/nicheGraphics.h b/variants/ELECROW-ThinkNode-M1/nicheGraphics.h index c2c351925..f3b709261 100644 --- a/variants/ELECROW-ThinkNode-M1/nicheGraphics.h +++ b/variants/ELECROW-ThinkNode-M1/nicheGraphics.h @@ -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(); diff --git a/variants/heltec_vision_master_e213/nicheGraphics.h b/variants/heltec_vision_master_e213/nicheGraphics.h index 7eccb2955..c0063ba3f 100644 --- a/variants/heltec_vision_master_e213/nicheGraphics.h +++ b/variants/heltec_vision_master_e213/nicheGraphics.h @@ -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(); diff --git a/variants/heltec_vision_master_e290/nicheGraphics.h b/variants/heltec_vision_master_e290/nicheGraphics.h index af78df746..e5b487158 100644 --- a/variants/heltec_vision_master_e290/nicheGraphics.h +++ b/variants/heltec_vision_master_e290/nicheGraphics.h @@ -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();