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 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-05-15 11:43:38 +02:00
co-authored by Claude Sonnet 4.6
parent 39731bb7fa
commit b3c6416874
6 changed files with 234 additions and 26 deletions
+24
View File
@@ -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)); file.read((uint8_t *)_prefs.dashboard_fields, sizeof(_prefs.dashboard_fields));
if (file.available()) { if (file.available()) {
file.read((uint8_t *)&_prefs.advert_auto_interval_sec, sizeof(_prefs.advert_auto_interval_sec)); 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.dm_notif, sizeof(_prefs.dm_notif));
file.write((uint8_t *)_prefs.dashboard_fields, sizeof(_prefs.dashboard_fields)); 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.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(); file.close();
} }
+14
View File
@@ -62,4 +62,18 @@ struct NodePrefs { // persisted to file
DmNotifEntry dm_notif[DM_NOTIF_TABLE_MAX]; // 16*5 = 80 bytes 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 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 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];
}; };
@@ -21,6 +21,7 @@ class RingtoneEditorScreen : public UIScreen {
uint8_t _notes[MAX_NOTES]; uint8_t _notes[MAX_NOTES];
uint8_t _len; uint8_t _len;
uint8_t _bpm_idx; uint8_t _bpm_idx;
int _slot; // 0=melody1, 1=melody2
int _cursor; int _cursor;
int _scroll; int _scroll;
bool _menu_open; bool _menu_open;
@@ -29,7 +30,7 @@ class RingtoneEditorScreen : public UIScreen {
char _play_buf[220]; // persistent RTTTL buffer — library holds a pointer into it char _play_buf[220]; // persistent RTTTL buffer — library holds a pointer into it
enum MenuOpt { 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 M_INSERT, M_DELETE, M_SAVE, M_DISCARD, M_COUNT
}; };
static const char* MENU_LABELS[M_COUNT]; static const char* MENU_LABELS[M_COUNT];
@@ -71,12 +72,16 @@ class RingtoneEditorScreen : public UIScreen {
} }
public: public:
RingtoneEditorScreen(UITask* task, NodePrefs* prefs) : _task(task), _prefs(prefs) {} RingtoneEditorScreen(UITask* task, NodePrefs* prefs) : _task(task), _prefs(prefs), _slot(0) {}
void enter() { void enter(int slot = 0) {
_bpm_idx = (_prefs && _prefs->ringtone_bpm_idx < 5) ? _prefs->ringtone_bpm_idx : 2; _slot = (slot == 1) ? 1 : 0;
_len = (_prefs && _prefs->ringtone_len <= (uint8_t)MAX_NOTES) ? _prefs->ringtone_len : 0; bool s2 = (_slot == 1);
if (_prefs) memcpy(_notes, _prefs->ringtone_notes, sizeof(_notes)); _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; _cursor = 0;
_scroll = 0; _scroll = 0;
_menu_open = false; _menu_open = false;
@@ -89,7 +94,7 @@ public:
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
char hdr[32]; 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.setCursor(0, 0);
display.print(hdr); display.print(hdr);
display.fillRect(0, 10, display.width(), 1); display.fillRect(0, 10, display.width(), 1);
@@ -171,6 +176,8 @@ public:
display.setCursor(mx + 4, iy); display.setCursor(mx + 4, iy);
if (item == M_PLAY_STOP) if (item == M_PLAY_STOP)
display.print(_task->isMelodyPlaying() ? "Stop" : "Play"); display.print(_task->isMelodyPlaying() ? "Stop" : "Play");
else if (item == M_SWITCH)
display.print(_slot == 0 ? "Edit Melody 2" : "Edit Melody 1");
else else
display.print(MENU_LABELS[item]); display.print(MENU_LABELS[item]);
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
@@ -215,6 +222,10 @@ public:
_task->playMelody(_play_buf); _task->playMelody(_play_buf);
} }
break; break;
case M_SWITCH:
_task->stopMelody();
this->enter(1 - _slot);
break;
case M_DURATION: case M_DURATION:
if (_cursor < _len) { if (_cursor < _len) {
uint8_t p = notePitch(_notes[_cursor]); uint8_t p = notePitch(_notes[_cursor]);
@@ -246,9 +257,15 @@ public:
break; break;
case M_SAVE: case M_SAVE:
if (_prefs) { if (_prefs) {
_prefs->ringtone_bpm_idx = _bpm_idx; if (_slot == 1) {
_prefs->ringtone_len = _len; _prefs->ringtone2_bpm_idx = _bpm_idx;
memcpy(_prefs->ringtone_notes, _notes, sizeof(_notes)); _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(); the_mesh.savePrefs();
} }
_task->stopMelody(); _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::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::PITCH_NAMES[8] = { 'p', 'c', 'd', 'e', 'f', 'g', 'a', 'b' };
const char* RingtoneEditorScreen::MENU_LABELS[RingtoneEditorScreen::M_COUNT] = { 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"
}; };
+161 -14
View File
@@ -115,6 +115,8 @@ class SettingsScreen : public UIScreen {
SECTION_SOUND, SECTION_SOUND,
BUZZER, BUZZER,
BUZZER_VOLUME, BUZZER_VOLUME,
DM_MELODY,
CH_MELODY,
// Home pages section // Home pages section
SECTION_HOME_PAGES, SECTION_HOME_PAGES,
HOME_CLOCK, HOME_RECENT, HOME_RADIO, HOME_BT, HOME_ADVERT, HOME_CLOCK, HOME_RECENT, HOME_RADIO, HOME_BT, HOME_ADVERT,
@@ -349,6 +351,18 @@ class SettingsScreen : public UIScreen {
display.setCursor(VAL_X, y); display.setCursor(VAL_X, y);
display.print("N/A"); display.print("N/A");
#endif #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)) { } else if (isHomePage(item)) {
display.print(homePageLabel(item)); display.print(homePageLabel(item));
display.setCursor(VAL_X, y); display.setCursor(VAL_X, y);
@@ -513,6 +527,14 @@ public:
#endif #endif
return right || left; 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)) { if (isHomePage(_selected) && p && (left || right || enter)) {
p->home_pages_mask ^= homePageBit(_selected); p->home_pages_mask ^= homePageBit(_selected);
_dirty = true; _dirty = true;
@@ -636,6 +658,7 @@ class QuickMsgScreen : public UIScreen {
PopupMenu _ctx_menu; PopupMenu _ctx_menu;
bool _ctx_dirty; bool _ctx_dirty;
char _ctx_notif_item[22]; char _ctx_notif_item[22];
char _ctx_melody_item[20];
struct ChHistEntry { uint8_t ch_idx; char text[140]; }; struct ChHistEntry { uint8_t ch_idx; char text[140]; };
ChHistEntry _hist[CH_HIST_MAX]; ChHistEntry _hist[CH_HIST_MAX];
@@ -884,6 +907,61 @@ class QuickMsgScreen : public UIScreen {
p->dm_notif[0].state = state; 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: public:
QuickMsgScreen(UITask* task) QuickMsgScreen(UITask* task)
: _task(task), _phase(MODE_SELECT), _mode_sel(0), : _task(task), _phase(MODE_SELECT), _mode_sel(0),
@@ -1355,10 +1433,14 @@ public:
if (the_mesh.getContactByIdx(_sorted[_contact_sel], ci)) { if (the_mesh.getContactByIdx(_sorted[_contact_sel], ci)) {
if (_ctx_menu.selectedIndex() == 0) { if (_ctx_menu.selectedIndex() == 0) {
_task->clearDMUnread(ci.id.pub_key); _task->clearDMUnread(ci.id.pub_key);
} else { } else if (_ctx_menu.selectedIndex() == 1) {
uint8_t nstate = dmNotifState(ci.id.pub_key); uint8_t nstate = dmNotifState(ci.id.pub_key);
setDmNotifState(ci.id.pub_key, (nstate + 1) % 3); setDmNotifState(ci.id.pub_key, (nstate + 1) % 3);
_ctx_dirty = true; _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); the_mesh.getContactByIdx(_sorted[_contact_sel], ci);
snprintf(_ctx_notif_item, sizeof(_ctx_notif_item), "Notif: %s", snprintf(_ctx_notif_item, sizeof(_ctx_notif_item), "Notif: %s",
NOTIF_LABELS[dmNotifState(ci.id.pub_key)]); 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("Mark as read");
_ctx_menu.addItem(_ctx_notif_item); _ctx_menu.addItem(_ctx_notif_item);
_ctx_menu.addItem(_ctx_melody_item);
_ctx_dirty = false; _ctx_dirty = false;
return true; return true;
} }
@@ -1409,10 +1495,14 @@ public:
uint8_t ch_idx = _channel_indices[_channel_sel]; uint8_t ch_idx = _channel_indices[_channel_sel];
if (_ctx_menu.selectedIndex() == 0) { if (_ctx_menu.selectedIndex() == 0) {
_ch_unread[ch_idx] = 0; _ch_unread[ch_idx] = 0;
} else { } else if (_ctx_menu.selectedIndex() == 1) {
uint8_t nstate = chNotifState(ch_idx); uint8_t nstate = chNotifState(ch_idx);
setChNotifState(ch_idx, (nstate + 1) % 3); setChNotifState(ch_idx, (nstate + 1) % 3);
_ctx_dirty = true; _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) { if (res != PopupMenu::NONE && _ctx_dirty) {
@@ -1447,9 +1537,13 @@ public:
static const char* NOTIF_LABELS[] = { "default", "OFF", "ON" }; static const char* NOTIF_LABELS[] = { "default", "OFF", "ON" };
snprintf(_ctx_notif_item, sizeof(_ctx_notif_item), "Notif: %s", snprintf(_ctx_notif_item, sizeof(_ctx_notif_item), "Notif: %s",
NOTIF_LABELS[chNotifState(ch_idx)]); 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("Mark all read");
_ctx_menu.addItem(_ctx_notif_item); _ctx_menu.addItem(_ctx_notif_item);
_ctx_menu.addItem(_ctx_melody_item);
_ctx_dirty = false; _ctx_dirty = false;
return true; return true;
} }
@@ -2290,8 +2384,8 @@ void UITask::gotoToolsScreen() {
setCurrScreen(tools_screen); setCurrScreen(tools_screen);
} }
void UITask::gotoRingtoneEditor() { void UITask::gotoRingtoneEditor(int slot) {
((RingtoneEditorScreen*)ringtone_edit)->enter(); ((RingtoneEditorScreen*)ringtone_edit)->enter(slot);
setCurrScreen(ringtone_edit); setCurrScreen(ringtone_edit);
} }
@@ -2365,6 +2459,27 @@ void UITask::showAlert(const char* text, int duration_millis) {
_alert_expiry = millis() + 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) { void UITask::notify(UIEventType t) {
#if defined(PIN_BUZZER) #if defined(PIN_BUZZER)
switch(t){ switch(t){
@@ -2387,8 +2502,25 @@ switch(t){
} }
_last_notif_dm_valid = false; _last_notif_dm_valid = false;
if (play) { if (play) {
if (force) buzzer.playForced("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7"); int slot = _node_prefs ? (int)_node_prefs->notif_melody_dm : 0;
else buzzer.play("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7"); 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; break;
} }
@@ -2398,19 +2530,34 @@ switch(t){
if (_last_notif_ch_idx >= 0 && _node_prefs) { if (_last_notif_ch_idx >= 0 && _node_prefs) {
uint64_t mask = 1ULL << _last_notif_ch_idx; uint64_t mask = 1ULL << _last_notif_ch_idx;
if (_node_prefs->ch_notif_override & mask) { if (_node_prefs->ch_notif_override & mask) {
if (!(_node_prefs->ch_notif_muted & mask)) { play = true; force = true; } // state 2: force-on if (!(_node_prefs->ch_notif_muted & mask)) { play = true; force = true; }
// state 1: muted — play stays false
} else { } else {
play = !buzzer.isQuiet(); // state 0: follow global play = !buzzer.isQuiet();
} }
} else { } else {
play = !buzzer.isQuiet(); play = !buzzer.isQuiet();
} }
_last_notif_ch_idx = -1;
if (play) { if (play) {
if (force) buzzer.playForced("kerplop:d=16,o=6,b=120:32g#,32c#"); int slot = _node_prefs ? (int)_node_prefs->notif_melody_ch : 0;
else buzzer.play("kerplop:d=16,o=6,b=120:32g#,32c#"); 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; break;
} }
case UIEventType::ack: case UIEventType::ack:
+2 -1
View File
@@ -34,6 +34,7 @@ class UITask : public AbstractUITask {
unsigned long _next_refresh, _auto_off; unsigned long _next_refresh, _auto_off;
NodePrefs* _node_prefs; NodePrefs* _node_prefs;
char _alert[80]; char _alert[80];
char _notif_mel_buf[220]; // persistent RTTTL buffer for custom notification melodies
unsigned long _alert_expiry; unsigned long _alert_expiry;
int _msgcount; int _msgcount;
int _room_unread; int _room_unread;
@@ -99,7 +100,7 @@ public:
void gotoSettingsScreen(); void gotoSettingsScreen();
void gotoQuickMsgScreen(); void gotoQuickMsgScreen();
void gotoToolsScreen(); void gotoToolsScreen();
void gotoRingtoneEditor(); void gotoRingtoneEditor(int slot = 0);
void gotoBotScreen(); void gotoBotScreen();
void gotoNearbyScreen(); void gotoNearbyScreen();
void gotoDashboardConfig(); void gotoDashboardConfig();
+5
View File
@@ -32,11 +32,16 @@ bool genericBuzzer::isPlaying() {
} }
void genericBuzzer::applyVolume() { void genericBuzzer::applyVolume() {
#if !defined(NRF52_PLATFORM)
// After tone() sets 50% duty, analogWrite overrides duty cycle on the same PWM channel. // 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. // Level 4 = 50% (leave tone as-is), lower levels reduce duty = quieter output.
static const uint8_t duty[] = { 6, 20, 50, 90, 128 }; static const uint8_t duty[] = { 6, 20, 50, 90, 128 };
uint8_t d = duty[_volume_level < 5 ? _volume_level : 4]; uint8_t d = duty[_volume_level < 5 ? _volume_level : 4];
if (d < 128) analogWrite(PIN_BUZZER, d); 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) { void genericBuzzer::setVolume(uint8_t level) {