mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 23:08:11 +00:00
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:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
@@ -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"
|
||||
};
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user