From f81f5a849a2f12dcd26fc4df252ae4f57b94c7fe Mon Sep 17 00:00:00 2001 From: Mark Jocas Date: Mon, 1 Jun 2026 13:48:58 +0200 Subject: [PATCH 1/5] 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 0eee45aef3..75b234e4d4 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 @@ class AbstractUITask { 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 ee12ca740d..98423b2302 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -434,7 +434,7 @@ class HomeScreen : public UIScreen { 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 a77ad6e7ec..0959043d54 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -95,6 +95,7 @@ class UITask : public AbstractUITask { 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 5529046775..5733fefb22 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 60cd0d042c..3fb9fccbd4 100644 --- a/examples/companion_radio/ui-orig/UITask.h +++ b/examples/companion_radio/ui-orig/UITask.h @@ -67,6 +67,7 @@ class UITask : public AbstractUITask { 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 45a07a02ef..3274baa4c7 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -394,7 +394,7 @@ class HomeScreen : public UIScreen { 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 344e48b98f..eb1e80bb32 100644 --- a/examples/companion_radio/ui-tiny/UITask.h +++ b/examples/companion_radio/ui-tiny/UITask.h @@ -103,6 +103,7 @@ class UITask : public AbstractUITask { 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 dde59f5d48..1410cf7f1a 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 0a50055282..54343cc449 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 From 24b9bab4f1cd5cde25365f096328e11dfdf25340 Mon Sep 17 00:00:00 2001 From: Mark Jocas Date: Mon, 1 Jun 2026 16:21:47 +0200 Subject: [PATCH 2/5] Enhances buzzer audibility on small piezos Adjusts the duration and tempo of several buzzer melodies to make them more perceptible. Specifically, the advert sent and generic toggle sounds are modified with longer note durations and slower tempos, improving their clarity on typical small piezo speakers. --- examples/companion_radio/ui-new/UITask.cpp | 2 +- examples/companion_radio/ui-orig/UITask.cpp | 2 +- examples/companion_radio/ui-tiny/UITask.cpp | 2 +- src/helpers/ui/buzzer.cpp | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 98423b2302..9c92231505 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -610,7 +610,7 @@ switch(t){ 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"); + buzzer.play("Advert:d=8,o=6,b=180:c,e,g,c7"); break; case UIEventType::roomMessage: case UIEventType::newContactMessage: diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index 5733fefb22..83aa4c3cc1 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -104,7 +104,7 @@ switch(t){ 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"); + buzzer.play("Advert:d=8,o=6,b=180:c,e,g,c7"); break; case UIEventType::roomMessage: case UIEventType::newContactMessage: diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index 3274baa4c7..03d84e4cff 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -482,7 +482,7 @@ switch(t){ 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"); + buzzer.play("Advert:d=8,o=6,b=180:c,e,g,c7"); break; case UIEventType::roomMessage: case UIEventType::newContactMessage: diff --git a/src/helpers/ui/buzzer.cpp b/src/helpers/ui/buzzer.cpp index 1410cf7f1a..4ac7e74c7b 100644 --- a/src/helpers/ui/buzzer.cpp +++ b/src/helpers/ui/buzzer.cpp @@ -50,7 +50,8 @@ void genericBuzzer::playToggle(int count, bool enabled) { 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:"); + // d=8 (eighths) at b=180 -> ~166 ms per note, audible on small piezos. + int n = snprintf(melody, sizeof(melody), "Tg:d=8,o=6,b=180:"); 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]); From 913ed609a3c3c73f39db8a015fa42514a133f5a0 Mon Sep 17 00:00:00 2001 From: Mark Jocas Date: Mon, 1 Jun 2026 17:05:33 +0200 Subject: [PATCH 3/5] Makes buzzer melody buffer static Ensures the RTTTL player has a persistent buffer for melody data. The player stores a pointer to the melody, which requires the buffer to outlive the function call. A static buffer guarantees this, preventing issues from accessing deallocated memory. This is safe as concurrent playback is not supported. --- src/helpers/ui/buzzer.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/helpers/ui/buzzer.cpp b/src/helpers/ui/buzzer.cpp index 4ac7e74c7b..ee5fc4ce8d 100644 --- a/src/helpers/ui/buzzer.cpp +++ b/src/helpers/ui/buzzer.cpp @@ -49,8 +49,10 @@ void genericBuzzer::playToggle(int count, bool enabled) { 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]; - // d=8 (eighths) at b=180 -> ~166 ms per note, audible on small piezos. + // NonBlockingRtttl stores only a pointer to the melody, so the backing + // buffer must outlive the call. A static buffer is fine here because the + // library can't play two melodies at once anyway. + static char melody[64]; int n = snprintf(melody, sizeof(melody), "Tg:d=8,o=6,b=180:"); for (int i = 0; i < count && n < (int)sizeof(melody); i++) { int idx = enabled ? i : (count - 1 - i); From 256bce0482b75e935f95907867c7b7309f4de8f8 Mon Sep 17 00:00:00 2001 From: Mark Jocas Date: Tue, 2 Jun 2026 08:45:45 +0200 Subject: [PATCH 4/5] Refines buzzer melody buffer comment Simplifies the explanation for using a static buffer for buzzer melodies. --- src/helpers/ui/buzzer.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/helpers/ui/buzzer.cpp b/src/helpers/ui/buzzer.cpp index ee5fc4ce8d..a6c003f021 100644 --- a/src/helpers/ui/buzzer.cpp +++ b/src/helpers/ui/buzzer.cpp @@ -49,9 +49,7 @@ void genericBuzzer::playToggle(int count, bool enabled) { const int max_notes = (int)(sizeof(notes) / sizeof(notes[0])); if (count < 1) count = 1; if (count > max_notes) count = max_notes; - // NonBlockingRtttl stores only a pointer to the melody, so the backing - // buffer must outlive the call. A static buffer is fine here because the - // library can't play two melodies at once anyway. + A static buffer as the library can't play two melodies at once anyway. static char melody[64]; int n = snprintf(melody, sizeof(melody), "Tg:d=8,o=6,b=180:"); for (int i = 0; i < count && n < (int)sizeof(melody); i++) { From 369a4fe4f59bc9557aebc5068bba486709b74d83 Mon Sep 17 00:00:00 2001 From: Mark Jocas Date: Tue, 2 Jun 2026 09:12:52 +0200 Subject: [PATCH 5/5] Adds FAQ for companion radio buzzer tones Explains the distinct tones used on companion-radio devices for actions like toggle confirmations. Improves user understanding of audio feedback, especially on button-only devices. --- docs/faq.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index c33172462a..f51e8cafe8 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -65,6 +65,7 @@ A list of frequently-asked questions and answers for MeshCore - [5.14. Q: Are there projects built around MeshCore?](#514-q-are-there-projects-built-around-meshcore) - [5.15. Q: Are there client applications for Windows or Mac?](#515-q-are-there-client-applications-for-windows-or-mac) - [5.16. Q: Are there any resources that compare MeshCore to other LoRa systems?](#516-q-are-there-any-resources-that-compare-meshcore-to-other-lora-systems) + - [5.17. Q: What do the buzzer tones on my companion radio mean?](#517-q-what-do-the-buzzer-tones-on-my-companion-radio-mean) - [6. Troubleshooting](#6-troubleshooting) - [6.1. Q: My client says another client or a repeater or a room server was last seen many, many days ago.](#61-q-my-client-says-another-client-or-a-repeater-or-a-room-server-was-last-seen-many-many-days-ago) - [6.2. Q: A repeater or a client or a room server I expect to see on my discover list (on T-Deck) or contact list (on a smart device client) are not listed.](#62-q-a-repeater-or-a-client-or-a-room-server-i-expect-to-see-on-my-discover-list-on-t-deck-or-contact-list-on-a-smart-device-client-are-not-listed) @@ -681,6 +682,14 @@ https://github.com/mikecarper/meshfirmware/blob/main/MeshCoreAdvantages.md Meshcore vs Meshtastic by austinmesh.org https://www.austinmesh.org/learn/meshcore-vs-meshtastic/ +### 5.17. Q: What do the buzzer tones on my companion radio mean? + +**A:** On companion-radio devices the buzzer plays distinct tones so you can tell actions apart by ear, which is especially useful on button-only devices like the T1000-E. + +Toggle confirmations follow a simple convention: **ascending pitch = enabled**, **descending pitch = disabled**, and the **number of notes matches the number of button presses** that triggered the action. So a triple-press to toggle the buzzer plays 3 notes (ascending on, descending off), a quadruple-press to toggle GPS plays 4 notes, and so on. + +Other events (incoming direct message, channel message, ack, advert sent) keep their own short fixed signatures. + ---