From f81f5a849a2f12dcd26fc4df252ae4f57b94c7fe Mon Sep 17 00:00:00 2001 From: Mark Jocas Date: Mon, 1 Jun 2026 13:48:58 +0200 Subject: [PATCH] feat(companion_radio): distinct buzzer notifications (#2626) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Give advert and toggleable settings (buzzer, GPS) their own short, easily distinguishable buzzer tones so users of buzzer-only devices (e.g. T1000E) can tell events apart by ear. - genericBuzzer::playToggle(count, enabled): generic, opt-in helper for the count/direction convention (N notes ascending if enabled, descending if disabled). Lives next to play()/startup()/shutdown(); any caller can use it, nothing forces it. - AbstractUITask: virtual notifyToggle(count, enabled) with an empty default — subclasses that don't care don't override. UIEventType only gains one event-shaped entry (advertSent), so it doesn't grow one entry per toggleable setting. - ui-orig / ui-new / ui-tiny: notify() handles fixed events; notifyToggle() is a one-line delegate to buzzer.playToggle(). Button presses, home-page actions, toggleBuzzer (3 notes) and toggleGPS (4 notes) route through notifyToggle(). - Preserve existing silent-when-connected behavior for incoming contact messages: when the companion app is connected it handles user notification, so the device stays quiet. - Fix mute-confirmation bug: hold mute until the off-tone finishes so the user actually hears it when disabling the buzzer. --- examples/companion_radio/AbstractUITask.h | 4 +++- examples/companion_radio/ui-new/UITask.cpp | 20 ++++++++++++++++---- examples/companion_radio/ui-new/UITask.h | 1 + examples/companion_radio/ui-orig/UITask.cpp | 20 ++++++++++++++++---- examples/companion_radio/ui-orig/UITask.h | 1 + examples/companion_radio/ui-tiny/UITask.cpp | 20 ++++++++++++++++---- examples/companion_radio/ui-tiny/UITask.h | 1 + src/helpers/ui/buzzer.cpp | 14 ++++++++++++++ src/helpers/ui/buzzer.h | 1 + 9 files changed, 69 insertions(+), 13 deletions(-) diff --git a/examples/companion_radio/AbstractUITask.h b/examples/companion_radio/AbstractUITask.h index 0eee45ae..75b234e4 100644 --- a/examples/companion_radio/AbstractUITask.h +++ b/examples/companion_radio/AbstractUITask.h @@ -19,7 +19,8 @@ enum class UIEventType { channelMessage, roomMessage, newContactMessage, - ack + ack, + advertSent }; class AbstractUITask { @@ -42,5 +43,6 @@ public: virtual void msgRead(int msgcount) = 0; virtual void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) = 0; virtual void notify(UIEventType t = UIEventType::none) = 0; + virtual void notifyToggle(int count, bool enabled) {} virtual void loop() = 0; }; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index ee12ca74..98423b23 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -434,7 +434,7 @@ public: return true; } if (c == KEY_ENTER && _page == HomePage::ADVERT) { - _task->notify(UIEventType::ack); + _task->notify(UIEventType::advertSent); if (the_mesh.advert()) { _task->showAlert("Advert sent!", 1000); } else { @@ -609,6 +609,9 @@ switch(t){ case UIEventType::ack: buzzer.play("ack:d=32,o=8,b=120:c"); break; + case UIEventType::advertSent: + buzzer.play("Advert:d=16,o=6,b=240:c,e,g,c7"); + break; case UIEventType::roomMessage: case UIEventType::newContactMessage: case UIEventType::none: @@ -625,6 +628,12 @@ switch(t){ #endif } +void UITask::notifyToggle(int count, bool enabled) { +#if defined(PIN_BUZZER) + buzzer.playToggle(count, enabled); +#endif +} + void UITask::msgRead(int msgcount) { _msgcount = msgcount; @@ -909,11 +918,11 @@ void UITask::toggleGPS() { if (strcmp(_sensors->getSettingValue(i), "1") == 0) { _sensors->setSettingValue("gps", "0"); _node_prefs->gps_enabled = 0; - notify(UIEventType::ack); + notifyToggle(4, false); } else { _sensors->setSettingValue("gps", "1"); _node_prefs->gps_enabled = 1; - notify(UIEventType::ack); + notifyToggle(4, true); } the_mesh.savePrefs(); showAlert(_node_prefs->gps_enabled ? "GPS: Enabled" : "GPS: Disabled", 800); @@ -929,8 +938,11 @@ void UITask::toggleBuzzer() { #ifdef PIN_BUZZER if (buzzer.isQuiet()) { buzzer.quiet(false); - notify(UIEventType::ack); + notifyToggle(3, true); } else { + // play the off-tone before muting so the user hears the confirmation + notifyToggle(3, false); + while (buzzer.isPlaying()) buzzer.loop(); buzzer.quiet(true); } _node_prefs->buzzer_quiet = buzzer.isQuiet(); diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index a77ad6e7..0959043d 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -95,6 +95,7 @@ public: void msgRead(int msgcount) override; void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override; void notify(UIEventType t = UIEventType::none) override; + void notifyToggle(int count, bool enabled) override; void loop() override; void shutdown(bool restart = false); diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index 55290467..5733fefb 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -103,6 +103,9 @@ switch(t){ case UIEventType::ack: buzzer.play("ack:d=32,o=8,b=120:c"); break; + case UIEventType::advertSent: + buzzer.play("Advert:d=16,o=6,b=240:c,e,g,c7"); + break; case UIEventType::roomMessage: case UIEventType::newContactMessage: case UIEventType::none: @@ -114,6 +117,12 @@ switch(t){ // Serial.println((int) t); } +void UITask::notifyToggle(int count, bool enabled) { +#if defined(PIN_BUZZER) + buzzer.playToggle(count, enabled); +#endif +} + void UITask::msgRead(int msgcount) { _msgcount = msgcount; if (msgcount == 0) { @@ -393,7 +402,7 @@ void UITask::handleButtonDoublePress() { MESH_DEBUG_PRINTLN("UITask: double press triggered, sending advert"); // ADVERT #ifdef PIN_BUZZER - notify(UIEventType::ack); + notify(UIEventType::advertSent); #endif if (the_mesh.advert()) { MESH_DEBUG_PRINTLN("Advert sent!"); @@ -411,9 +420,12 @@ void UITask::handleButtonTriplePress() { #ifdef PIN_BUZZER if (buzzer.isQuiet()) { buzzer.quiet(false); - notify(UIEventType::ack); + notifyToggle(3, true); sprintf(_alert, "Buzzer: ON"); } else { + // play the off-tone before muting so the user hears the confirmation + notifyToggle(3, false); + while (buzzer.isPlaying()) buzzer.loop(); buzzer.quiet(true); sprintf(_alert, "Buzzer: OFF"); } @@ -432,11 +444,11 @@ void UITask::handleButtonQuadruplePress() { if (strcmp(_sensors->getSettingName(i), "gps") == 0) { if (strcmp(_sensors->getSettingValue(i), "1") == 0) { _sensors->setSettingValue("gps", "0"); - notify(UIEventType::ack); + notifyToggle(4, false); sprintf(_alert, "GPS: Disabled"); } else { _sensors->setSettingValue("gps", "1"); - notify(UIEventType::ack); + notifyToggle(4, true); sprintf(_alert, "GPS: Enabled"); } break; diff --git a/examples/companion_radio/ui-orig/UITask.h b/examples/companion_radio/ui-orig/UITask.h index 60cd0d04..3fb9fccb 100644 --- a/examples/companion_radio/ui-orig/UITask.h +++ b/examples/companion_radio/ui-orig/UITask.h @@ -67,6 +67,7 @@ public: void msgRead(int msgcount) override; void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override; void notify(UIEventType t = UIEventType::none) override; + void notifyToggle(int count, bool enabled) override; void loop() override; void shutdown(bool restart = false); diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index 45a07a02..3274baa4 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -394,7 +394,7 @@ public: return true; } if (c == KEY_ENTER && _page == HomePage::ADVERT) { - _task->notify(UIEventType::ack); + _task->notify(UIEventType::advertSent); if (the_mesh.advert()) { _task->showAlert("Advert sent!", 1000); } else { @@ -481,6 +481,9 @@ switch(t){ case UIEventType::ack: buzzer.play("ack:d=32,o=8,b=120:c"); break; + case UIEventType::advertSent: + buzzer.play("Advert:d=16,o=6,b=240:c,e,g,c7"); + break; case UIEventType::roomMessage: case UIEventType::newContactMessage: case UIEventType::none: @@ -497,6 +500,12 @@ switch(t){ #endif } +void UITask::notifyToggle(int count, bool enabled) { +#if defined(PIN_BUZZER) + buzzer.playToggle(count, enabled); +#endif +} + void UITask::msgRead(int msgcount) { _msgcount = msgcount; @@ -796,11 +805,11 @@ void UITask::toggleGPS() { if (strcmp(_sensors->getSettingValue(i), "1") == 0) { _sensors->setSettingValue("gps", "0"); _node_prefs->gps_enabled = 0; - notify(UIEventType::ack); + notifyToggle(4, false); } else { _sensors->setSettingValue("gps", "1"); _node_prefs->gps_enabled = 1; - notify(UIEventType::ack); + notifyToggle(4, true); } the_mesh.savePrefs(); showAlert(_node_prefs->gps_enabled ? "GPS: Enabled" : "GPS: Disabled", 800); @@ -816,8 +825,11 @@ void UITask::toggleBuzzer() { #ifdef PIN_BUZZER if (buzzer.isQuiet()) { buzzer.quiet(false); - notify(UIEventType::ack); + notifyToggle(3, true); } else { + // play the off-tone before muting so the user hears the confirmation + notifyToggle(3, false); + while (buzzer.isPlaying()) buzzer.loop(); buzzer.quiet(true); } _node_prefs->buzzer_quiet = buzzer.isQuiet(); diff --git a/examples/companion_radio/ui-tiny/UITask.h b/examples/companion_radio/ui-tiny/UITask.h index 344e48b9..eb1e80bb 100644 --- a/examples/companion_radio/ui-tiny/UITask.h +++ b/examples/companion_radio/ui-tiny/UITask.h @@ -103,6 +103,7 @@ public: void msgRead(int msgcount) override; void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override; void notify(UIEventType t = UIEventType::none) override; + void notifyToggle(int count, bool enabled) override; void loop() override; void shutdown(bool restart = false); diff --git a/src/helpers/ui/buzzer.cpp b/src/helpers/ui/buzzer.cpp index dde59f5d..1410cf7f 100644 --- a/src/helpers/ui/buzzer.cpp +++ b/src/helpers/ui/buzzer.cpp @@ -44,6 +44,20 @@ void genericBuzzer::shutdown() { play(shutdown_song); } +void genericBuzzer::playToggle(int count, bool enabled) { + static const char *notes[] = {"c", "e", "g", "c7", "e7", "g7"}; + const int max_notes = (int)(sizeof(notes) / sizeof(notes[0])); + if (count < 1) count = 1; + if (count > max_notes) count = max_notes; + char melody[64]; + int n = snprintf(melody, sizeof(melody), "Tg:d=16,o=6,b=200:"); + for (int i = 0; i < count && n < (int)sizeof(melody); i++) { + int idx = enabled ? i : (count - 1 - i); + n += snprintf(melody + n, sizeof(melody) - n, "%s%s", i ? "," : "", notes[idx]); + } + play(melody); +} + void genericBuzzer::quiet(bool buzzer_state) { _is_quiet = buzzer_state; #ifdef PIN_BUZZER_EN diff --git a/src/helpers/ui/buzzer.h b/src/helpers/ui/buzzer.h index 0a500552..54343cc4 100644 --- a/src/helpers/ui/buzzer.h +++ b/src/helpers/ui/buzzer.h @@ -21,6 +21,7 @@ class genericBuzzer public: void begin(); // set up buzzer port void play(const char *melody); // Generic play function + void playToggle(int count, bool enabled); // play toggle tone void loop(); // loop driven-nonblocking void startup(); // play startup sound void shutdown(); // play shutdown sound