From b3c6416874b40e30fb53016e58ff4ba751c3992b Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Fri, 15 May 2026 11:43:38 +0200 Subject: [PATCH] feat: melody assignment per channel/DM, dual ringtone slots, volume cleanup Add second ringtone slot (ringtone2_*) to NodePrefs and DataStore. Add per-channel and per-DM melody assignment (built-in / melody 1 / 2) stored in NodePrefs bitmasks and DmMelodyEntry table. Settings screen exposes DM/CH sound items; RingtoneEditorScreen supports switching between slots. notify() uses buildMelodyFromPrefs() to select the correct RTTTL string per contact. Remove broken nRF52 volume control from buzzer.cpp: tone() uses NRF_PWM2 with DECODER.MODE=Auto so TASKS_NEXTSTEP has no effect and duty cannot be changed before the first DMA read without patching the Arduino core. applyVolume() is now a no-op on nRF52. Co-Authored-By: Claude Sonnet 4.6 --- examples/companion_radio/DataStore.cpp | 24 +++ examples/companion_radio/NodePrefs.h | 14 ++ .../ui-new/RingtoneEditorScreen.h | 39 ++-- examples/companion_radio/ui-new/UITask.cpp | 175 ++++++++++++++++-- examples/companion_radio/ui-new/UITask.h | 3 +- src/helpers/ui/buzzer.cpp | 5 + 6 files changed, 234 insertions(+), 26 deletions(-) diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 027b77a0..7e430003 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -270,6 +270,22 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no file.read((uint8_t *)_prefs.dashboard_fields, sizeof(_prefs.dashboard_fields)); if (file.available()) { file.read((uint8_t *)&_prefs.advert_auto_interval_sec, sizeof(_prefs.advert_auto_interval_sec)); + if (file.available()) { + file.read((uint8_t *)&_prefs.ringtone2_bpm_idx, sizeof(_prefs.ringtone2_bpm_idx)); + file.read((uint8_t *)&_prefs.ringtone2_len, sizeof(_prefs.ringtone2_len)); + file.read((uint8_t *)_prefs.ringtone2_notes, sizeof(_prefs.ringtone2_notes)); + if (file.available()) { + file.read((uint8_t *)&_prefs.notif_melody_dm, sizeof(_prefs.notif_melody_dm)); + file.read((uint8_t *)&_prefs.notif_melody_ch, sizeof(_prefs.notif_melody_ch)); + if (file.available()) { + file.read((uint8_t *)&_prefs.ch_notif_melody_set, sizeof(_prefs.ch_notif_melody_set)); + file.read((uint8_t *)&_prefs.ch_notif_melody_2, sizeof(_prefs.ch_notif_melody_2)); + if (file.available()) { + file.read((uint8_t *)_prefs.dm_melody, sizeof(_prefs.dm_melody)); + } + } + } + } } } } @@ -346,6 +362,14 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)_prefs.dm_notif, sizeof(_prefs.dm_notif)); file.write((uint8_t *)_prefs.dashboard_fields, sizeof(_prefs.dashboard_fields)); file.write((uint8_t *)&_prefs.advert_auto_interval_sec, sizeof(_prefs.advert_auto_interval_sec)); + file.write((uint8_t *)&_prefs.ringtone2_bpm_idx, sizeof(_prefs.ringtone2_bpm_idx)); + file.write((uint8_t *)&_prefs.ringtone2_len, sizeof(_prefs.ringtone2_len)); + file.write((uint8_t *)_prefs.ringtone2_notes, sizeof(_prefs.ringtone2_notes)); + file.write((uint8_t *)&_prefs.notif_melody_dm, sizeof(_prefs.notif_melody_dm)); + file.write((uint8_t *)&_prefs.notif_melody_ch, sizeof(_prefs.notif_melody_ch)); + file.write((uint8_t *)&_prefs.ch_notif_melody_set, sizeof(_prefs.ch_notif_melody_set)); + file.write((uint8_t *)&_prefs.ch_notif_melody_2, sizeof(_prefs.ch_notif_melody_2)); + file.write((uint8_t *)_prefs.dm_melody, sizeof(_prefs.dm_melody)); file.close(); } diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 94e593c3..3f70e34e 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -62,4 +62,18 @@ struct NodePrefs { // persisted to file DmNotifEntry dm_notif[DM_NOTIF_TABLE_MAX]; // 16*5 = 80 bytes uint8_t dashboard_fields[3]; // 0=None,1=Batt,2=Temp,3=Hum,4=Pres,5=GPS,6=Alt,7=Lux,8=CO2,9=Nodes uint32_t advert_auto_interval_sec; // periodic 0-hop advert with GPS: 0=off, else seconds + // Second melody slot (same packing as ringtone_*) + uint8_t ringtone2_bpm_idx; + uint8_t ringtone2_len; + uint8_t ringtone2_notes[32]; + // Global melody for notifications: 0=built-in, 1=melody1, 2=melody2 + uint8_t notif_melody_dm; + uint8_t notif_melody_ch; + // Per-channel melody override (2 bitmasks, 1 bit per channel) + uint64_t ch_notif_melody_set; // bit i = channel i has explicit melody + uint64_t ch_notif_melody_2; // bit i = use melody 2 (else melody 1, when set bit is set) + // Per-DM melody table + struct DmMelodyEntry { uint8_t prefix[4]; uint8_t slot; }; // slot: 0=global,1=melody1,2=melody2 + static const int DM_MELODY_TABLE_MAX = 16; + DmMelodyEntry dm_melody[DM_MELODY_TABLE_MAX]; }; \ No newline at end of file diff --git a/examples/companion_radio/ui-new/RingtoneEditorScreen.h b/examples/companion_radio/ui-new/RingtoneEditorScreen.h index ab2b9d79..84cb09f4 100644 --- a/examples/companion_radio/ui-new/RingtoneEditorScreen.h +++ b/examples/companion_radio/ui-new/RingtoneEditorScreen.h @@ -21,6 +21,7 @@ class RingtoneEditorScreen : public UIScreen { uint8_t _notes[MAX_NOTES]; uint8_t _len; uint8_t _bpm_idx; + int _slot; // 0=melody1, 1=melody2 int _cursor; int _scroll; bool _menu_open; @@ -29,7 +30,7 @@ class RingtoneEditorScreen : public UIScreen { char _play_buf[220]; // persistent RTTTL buffer — library holds a pointer into it enum MenuOpt { - M_PLAY_STOP, M_DURATION, M_BPM_UP, M_BPM_DOWN, + M_PLAY_STOP, M_SWITCH, M_DURATION, M_BPM_UP, M_BPM_DOWN, M_INSERT, M_DELETE, M_SAVE, M_DISCARD, M_COUNT }; static const char* MENU_LABELS[M_COUNT]; @@ -71,12 +72,16 @@ class RingtoneEditorScreen : public UIScreen { } public: - RingtoneEditorScreen(UITask* task, NodePrefs* prefs) : _task(task), _prefs(prefs) {} + RingtoneEditorScreen(UITask* task, NodePrefs* prefs) : _task(task), _prefs(prefs), _slot(0) {} - void enter() { - _bpm_idx = (_prefs && _prefs->ringtone_bpm_idx < 5) ? _prefs->ringtone_bpm_idx : 2; - _len = (_prefs && _prefs->ringtone_len <= (uint8_t)MAX_NOTES) ? _prefs->ringtone_len : 0; - if (_prefs) memcpy(_notes, _prefs->ringtone_notes, sizeof(_notes)); + void enter(int slot = 0) { + _slot = (slot == 1) ? 1 : 0; + bool s2 = (_slot == 1); + _bpm_idx = (_prefs && (s2 ? _prefs->ringtone2_bpm_idx : _prefs->ringtone_bpm_idx) < 5) + ? (s2 ? _prefs->ringtone2_bpm_idx : _prefs->ringtone_bpm_idx) : 2; + uint8_t rlen = _prefs ? (s2 ? _prefs->ringtone2_len : _prefs->ringtone_len) : 0; + _len = (rlen <= (uint8_t)MAX_NOTES) ? rlen : 0; + if (_prefs) memcpy(_notes, s2 ? _prefs->ringtone2_notes : _prefs->ringtone_notes, sizeof(_notes)); _cursor = 0; _scroll = 0; _menu_open = false; @@ -89,7 +94,7 @@ public: display.setColor(DisplayDriver::LIGHT); char hdr[32]; - snprintf(hdr, sizeof(hdr), "BPM:%u %d/%d", BPM_OPTS[_bpm_idx], _len, MAX_NOTES); + snprintf(hdr, sizeof(hdr), "M%d BPM:%u %d/%d", _slot + 1, BPM_OPTS[_bpm_idx], _len, MAX_NOTES); display.setCursor(0, 0); display.print(hdr); display.fillRect(0, 10, display.width(), 1); @@ -171,6 +176,8 @@ public: display.setCursor(mx + 4, iy); if (item == M_PLAY_STOP) display.print(_task->isMelodyPlaying() ? "Stop" : "Play"); + else if (item == M_SWITCH) + display.print(_slot == 0 ? "Edit Melody 2" : "Edit Melody 1"); else display.print(MENU_LABELS[item]); display.setColor(DisplayDriver::LIGHT); @@ -215,6 +222,10 @@ public: _task->playMelody(_play_buf); } break; + case M_SWITCH: + _task->stopMelody(); + this->enter(1 - _slot); + break; case M_DURATION: if (_cursor < _len) { uint8_t p = notePitch(_notes[_cursor]); @@ -246,9 +257,15 @@ public: break; case M_SAVE: if (_prefs) { - _prefs->ringtone_bpm_idx = _bpm_idx; - _prefs->ringtone_len = _len; - memcpy(_prefs->ringtone_notes, _notes, sizeof(_notes)); + if (_slot == 1) { + _prefs->ringtone2_bpm_idx = _bpm_idx; + _prefs->ringtone2_len = _len; + memcpy(_prefs->ringtone2_notes, _notes, sizeof(_notes)); + } else { + _prefs->ringtone_bpm_idx = _bpm_idx; + _prefs->ringtone_len = _len; + memcpy(_prefs->ringtone_notes, _notes, sizeof(_notes)); + } the_mesh.savePrefs(); } _task->stopMelody(); @@ -313,5 +330,5 @@ const uint8_t RingtoneEditorScreen::DUR_VALS[4] = { 4, 8, 16, 32 }; const char* RingtoneEditorScreen::DUR_LABELS[4] = { "1/4", "1/8", "1/16", "1/32" }; const char RingtoneEditorScreen::PITCH_NAMES[8] = { 'p', 'c', 'd', 'e', 'f', 'g', 'a', 'b' }; const char* RingtoneEditorScreen::MENU_LABELS[RingtoneEditorScreen::M_COUNT] = { - "Play/Stop", "Duration", "BPM+", "BPM-", "Insert", "Delete", "Save & Exit", "Discard" + "Play/Stop", "Switch Melody", "Duration", "BPM+", "BPM-", "Insert", "Delete", "Save & Exit", "Discard" }; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 372c20fd..525a1bda 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -115,6 +115,8 @@ class SettingsScreen : public UIScreen { SECTION_SOUND, BUZZER, BUZZER_VOLUME, + DM_MELODY, + CH_MELODY, // Home pages section SECTION_HOME_PAGES, HOME_CLOCK, HOME_RECENT, HOME_RADIO, HOME_BT, HOME_ADVERT, @@ -349,6 +351,18 @@ class SettingsScreen : public UIScreen { display.setCursor(VAL_X, y); display.print("N/A"); #endif + } else if (item == DM_MELODY) { + display.print("DM sound"); + display.setCursor(VAL_X, y); + { static const char* L[] = { "built-in", "M1", "M2" }; + uint8_t v = p ? p->notif_melody_dm : 0; + display.print(L[v < 3 ? v : 0]); } + } else if (item == CH_MELODY) { + display.print("Ch sound"); + display.setCursor(VAL_X, y); + { static const char* L[] = { "built-in", "M1", "M2" }; + uint8_t v = p ? p->notif_melody_ch : 0; + display.print(L[v < 3 ? v : 0]); } } else if (isHomePage(item)) { display.print(homePageLabel(item)); display.setCursor(VAL_X, y); @@ -513,6 +527,14 @@ public: #endif return right || left; } + if (_selected == DM_MELODY && p && (left || right || enter)) { + p->notif_melody_dm = (p->notif_melody_dm + (left ? 2 : 1)) % 3; + _dirty = true; return true; + } + if (_selected == CH_MELODY && p && (left || right || enter)) { + p->notif_melody_ch = (p->notif_melody_ch + (left ? 2 : 1)) % 3; + _dirty = true; return true; + } if (isHomePage(_selected) && p && (left || right || enter)) { p->home_pages_mask ^= homePageBit(_selected); _dirty = true; @@ -636,6 +658,7 @@ class QuickMsgScreen : public UIScreen { PopupMenu _ctx_menu; bool _ctx_dirty; char _ctx_notif_item[22]; + char _ctx_melody_item[20]; struct ChHistEntry { uint8_t ch_idx; char text[140]; }; ChHistEntry _hist[CH_HIST_MAX]; @@ -884,6 +907,61 @@ class QuickMsgScreen : public UIScreen { p->dm_notif[0].state = state; } + uint8_t chNotifMelody(uint8_t ch_idx) const { + NodePrefs* p = _task->getNodePrefs(); + if (!p) return 0; + uint64_t mask = 1ULL << ch_idx; + if (!(p->ch_notif_melody_set & mask)) return 0; + return (p->ch_notif_melody_2 & mask) ? 2 : 1; + } + + void setChNotifMelody(uint8_t ch_idx, uint8_t slot) { + NodePrefs* p = _task->getNodePrefs(); + if (!p) return; + uint64_t mask = 1ULL << ch_idx; + if (slot == 0) { + p->ch_notif_melody_set &= ~mask; + p->ch_notif_melody_2 &= ~mask; + } else if (slot == 1) { + p->ch_notif_melody_set |= mask; + p->ch_notif_melody_2 &= ~mask; + } else { + p->ch_notif_melody_set |= mask; + p->ch_notif_melody_2 |= mask; + } + } + + uint8_t dmMelodySlot(const uint8_t* pub_key) const { + NodePrefs* p = _task->getNodePrefs(); + if (!p) return 0; + for (int i = 0; i < NodePrefs::DM_MELODY_TABLE_MAX; i++) + if (p->dm_melody[i].slot && memcmp(p->dm_melody[i].prefix, pub_key, 4) == 0) + return p->dm_melody[i].slot; + return 0; + } + + void setDmMelody(const uint8_t* pub_key, uint8_t slot) { + NodePrefs* p = _task->getNodePrefs(); + if (!p) return; + for (int i = 0; i < NodePrefs::DM_MELODY_TABLE_MAX; i++) { + if (p->dm_melody[i].slot && memcmp(p->dm_melody[i].prefix, pub_key, 4) == 0) { + if (slot == 0) memset(&p->dm_melody[i], 0, sizeof(p->dm_melody[i])); + else p->dm_melody[i].slot = slot; + return; + } + } + if (slot == 0) return; + for (int i = 0; i < NodePrefs::DM_MELODY_TABLE_MAX; i++) { + if (p->dm_melody[i].slot == 0) { + memcpy(p->dm_melody[i].prefix, pub_key, 4); + p->dm_melody[i].slot = slot; + return; + } + } + memcpy(p->dm_melody[0].prefix, pub_key, 4); + p->dm_melody[0].slot = slot; + } + public: QuickMsgScreen(UITask* task) : _task(task), _phase(MODE_SELECT), _mode_sel(0), @@ -1355,10 +1433,14 @@ public: if (the_mesh.getContactByIdx(_sorted[_contact_sel], ci)) { if (_ctx_menu.selectedIndex() == 0) { _task->clearDMUnread(ci.id.pub_key); - } else { + } else if (_ctx_menu.selectedIndex() == 1) { uint8_t nstate = dmNotifState(ci.id.pub_key); setDmNotifState(ci.id.pub_key, (nstate + 1) % 3); _ctx_dirty = true; + } else { + uint8_t slot = dmMelodySlot(ci.id.pub_key); + setDmMelody(ci.id.pub_key, (slot + 1) % 3); + _ctx_dirty = true; } } } @@ -1394,9 +1476,13 @@ public: the_mesh.getContactByIdx(_sorted[_contact_sel], ci); snprintf(_ctx_notif_item, sizeof(_ctx_notif_item), "Notif: %s", NOTIF_LABELS[dmNotifState(ci.id.pub_key)]); - _ctx_menu.begin("Contact options", 2); + { static const char* ML[] = { "global", "M1", "M2" }; + snprintf(_ctx_melody_item, sizeof(_ctx_melody_item), "Melody: %s", + ML[dmMelodySlot(ci.id.pub_key)]); } + _ctx_menu.begin("Contact options", 3); _ctx_menu.addItem("Mark as read"); _ctx_menu.addItem(_ctx_notif_item); + _ctx_menu.addItem(_ctx_melody_item); _ctx_dirty = false; return true; } @@ -1409,10 +1495,14 @@ public: uint8_t ch_idx = _channel_indices[_channel_sel]; if (_ctx_menu.selectedIndex() == 0) { _ch_unread[ch_idx] = 0; - } else { + } else if (_ctx_menu.selectedIndex() == 1) { uint8_t nstate = chNotifState(ch_idx); setChNotifState(ch_idx, (nstate + 1) % 3); _ctx_dirty = true; + } else { + uint8_t slot = chNotifMelody(ch_idx); + setChNotifMelody(ch_idx, (slot + 1) % 3); + _ctx_dirty = true; } } if (res != PopupMenu::NONE && _ctx_dirty) { @@ -1447,9 +1537,13 @@ public: static const char* NOTIF_LABELS[] = { "default", "OFF", "ON" }; snprintf(_ctx_notif_item, sizeof(_ctx_notif_item), "Notif: %s", NOTIF_LABELS[chNotifState(ch_idx)]); - _ctx_menu.begin("Channel options", 2); + { static const char* ML[] = { "global", "M1", "M2" }; + snprintf(_ctx_melody_item, sizeof(_ctx_melody_item), "Melody: %s", + ML[chNotifMelody(ch_idx)]); } + _ctx_menu.begin("Channel options", 3); _ctx_menu.addItem("Mark all read"); _ctx_menu.addItem(_ctx_notif_item); + _ctx_menu.addItem(_ctx_melody_item); _ctx_dirty = false; return true; } @@ -2290,8 +2384,8 @@ void UITask::gotoToolsScreen() { setCurrScreen(tools_screen); } -void UITask::gotoRingtoneEditor() { - ((RingtoneEditorScreen*)ringtone_edit)->enter(); +void UITask::gotoRingtoneEditor(int slot) { + ((RingtoneEditorScreen*)ringtone_edit)->enter(slot); setCurrScreen(ringtone_edit); } @@ -2365,6 +2459,27 @@ void UITask::showAlert(const char* text, int duration_millis) { _alert_expiry = millis() + duration_millis; } +static void buildMelodyFromPrefs(const NodePrefs* p, int slot, char* buf, int size) { + static const uint16_t BPM_OPTS[] = { 60, 90, 120, 150, 180 }; + static const uint8_t DUR_VALS[] = { 4, 8, 16, 32 }; + static const char PITCHES[] = { 'p', 'c', 'd', 'e', 'f', 'g', 'a', 'b' }; + const uint8_t* notes = (slot == 2) ? p->ringtone2_notes : p->ringtone_notes; + uint8_t len = (slot == 2) ? p->ringtone2_len : p->ringtone_len; + uint8_t bpm_i = (slot == 2) ? p->ringtone2_bpm_idx : p->ringtone_bpm_idx; + if (len == 0) { buf[0] = '\0'; return; } + uint16_t bpm = BPM_OPTS[bpm_i < 5 ? bpm_i : 2]; + int pos = snprintf(buf, size, "Ring:d=8,o=5,b=%u:", bpm); + for (int i = 0; i < len && pos < size - 8; i++) { + if (i > 0 && pos < size - 1) buf[pos++] = ','; + uint8_t pitch = notes[i] & 0x07; + uint8_t octave = ((notes[i] >> 3) & 0x03) + 4; + uint8_t dur_val = DUR_VALS[(notes[i] >> 5) & 0x03]; + if (pitch == 0) pos += snprintf(buf + pos, size - pos, "%dp", dur_val); + else pos += snprintf(buf + pos, size - pos, "%d%c%d", dur_val, PITCHES[pitch], octave); + } + if (pos < size) buf[pos] = '\0'; +} + void UITask::notify(UIEventType t) { #if defined(PIN_BUZZER) switch(t){ @@ -2387,8 +2502,25 @@ switch(t){ } _last_notif_dm_valid = false; if (play) { - if (force) buzzer.playForced("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7"); - else buzzer.play("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7"); + int slot = _node_prefs ? (int)_node_prefs->notif_melody_dm : 0; + if (_node_prefs) { + for (int i = 0; i < NodePrefs::DM_MELODY_TABLE_MAX; i++) + if (_node_prefs->dm_melody[i].slot && + memcmp(_node_prefs->dm_melody[i].prefix, _last_notif_dm_prefix, 4) == 0) + { slot = _node_prefs->dm_melody[i].slot; break; } + } + bool custom_played = false; + if (slot > 0 && _node_prefs) { + buildMelodyFromPrefs(_node_prefs, slot, _notif_mel_buf, sizeof(_notif_mel_buf)); + if (_notif_mel_buf[0]) { + if (force) buzzer.playForced(_notif_mel_buf); else buzzer.play(_notif_mel_buf); + custom_played = true; + } + } + if (!custom_played) { + if (force) buzzer.playForced("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7"); + else buzzer.play("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7"); + } } break; } @@ -2398,19 +2530,34 @@ switch(t){ if (_last_notif_ch_idx >= 0 && _node_prefs) { uint64_t mask = 1ULL << _last_notif_ch_idx; if (_node_prefs->ch_notif_override & mask) { - if (!(_node_prefs->ch_notif_muted & mask)) { play = true; force = true; } // state 2: force-on - // state 1: muted — play stays false + if (!(_node_prefs->ch_notif_muted & mask)) { play = true; force = true; } } else { - play = !buzzer.isQuiet(); // state 0: follow global + play = !buzzer.isQuiet(); } } else { play = !buzzer.isQuiet(); } - _last_notif_ch_idx = -1; if (play) { - if (force) buzzer.playForced("kerplop:d=16,o=6,b=120:32g#,32c#"); - else buzzer.play("kerplop:d=16,o=6,b=120:32g#,32c#"); + int slot = _node_prefs ? (int)_node_prefs->notif_melody_ch : 0; + if (_last_notif_ch_idx >= 0 && _node_prefs) { + uint64_t mask = 1ULL << _last_notif_ch_idx; + if (_node_prefs->ch_notif_melody_set & mask) + slot = (_node_prefs->ch_notif_melody_2 & mask) ? 2 : 1; + } + bool custom_played = false; + if (slot > 0 && _node_prefs) { + buildMelodyFromPrefs(_node_prefs, slot, _notif_mel_buf, sizeof(_notif_mel_buf)); + if (_notif_mel_buf[0]) { + if (force) buzzer.playForced(_notif_mel_buf); else buzzer.play(_notif_mel_buf); + custom_played = true; + } + } + if (!custom_played) { + if (force) buzzer.playForced("kerplop:d=16,o=6,b=120:32g#,32c#"); + else buzzer.play("kerplop:d=16,o=6,b=120:32g#,32c#"); + } } + _last_notif_ch_idx = -1; break; } case UIEventType::ack: diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index c96711dc..af8f87ad 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -34,6 +34,7 @@ class UITask : public AbstractUITask { unsigned long _next_refresh, _auto_off; NodePrefs* _node_prefs; char _alert[80]; + char _notif_mel_buf[220]; // persistent RTTTL buffer for custom notification melodies unsigned long _alert_expiry; int _msgcount; int _room_unread; @@ -99,7 +100,7 @@ public: void gotoSettingsScreen(); void gotoQuickMsgScreen(); void gotoToolsScreen(); - void gotoRingtoneEditor(); + void gotoRingtoneEditor(int slot = 0); void gotoBotScreen(); void gotoNearbyScreen(); void gotoDashboardConfig(); diff --git a/src/helpers/ui/buzzer.cpp b/src/helpers/ui/buzzer.cpp index 4411a407..bca45ec9 100644 --- a/src/helpers/ui/buzzer.cpp +++ b/src/helpers/ui/buzzer.cpp @@ -32,11 +32,16 @@ bool genericBuzzer::isPlaying() { } void genericBuzzer::applyVolume() { +#if !defined(NRF52_PLATFORM) // After tone() sets 50% duty, analogWrite overrides duty cycle on the same PWM channel. // Level 4 = 50% (leave tone as-is), lower levels reduce duty = quieter output. static const uint8_t duty[] = { 6, 20, 50, 90, 128 }; uint8_t d = duty[_volume_level < 5 ? _volume_level : 4]; if (d < 128) analogWrite(PIN_BUZZER, d); +#endif + // On nRF52, tone() uses NRF_PWM2 with DECODER.MODE=Auto. The DMA reads duty at SEQSTART + // before software can write, and TASKS_NEXTSTEP only works in NextStep mode — so duty + // cannot be changed mid-note without patching the Arduino core. Volume has no effect. } void genericBuzzer::setVolume(uint8_t level) {