feat(companion): quiet hours -- muted on a schedule, the alarm still rings

Settings > Sound > Quiet hours (L2: its own group; L1: Quiet hrs / from /
until): from quiet_from to quiet_to (local hours, wrapping past midnight)
the buzzer is muted and a message doesn't wake the screen; the clock alarm
still rings. A manual mute or unmute inside the window stands until the
window ends. Off with the clock unset.

NodePrefs gains quiet_hours / quiet_from / quiet_to (appended to the file,
sentinel 0xC0DE0030; sizeof 2832 on nRF52 and ESP32), off by default,
22:00-07:00. hourInWindow() / localHour() are shared with the bot's quiet
hours. soundctl::tick() replaces autoTick() (Auto and quiet hours).

Also fixes the L2 side-button mute: it went by the mode, not by whether
the buzzer was muted, so with Auto + an app connected (and now quiet hours)
the first press muted instead of unmuting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-28 21:59:40 +02:00
co-authored by Claude Opus 5.5
parent 2e9654fd1c
commit 1def29335c
10 changed files with 121 additions and 32 deletions
+6 -2
View File
@@ -52,12 +52,17 @@ for a second radio). They are L2-specific and block the loop for ~90 s.
## D. Quiet hours in the core ## D. Quiet hours in the core
- [ ] A start and end time when the device is silent; in ui-core, so L1, L2 - [x] A start and end time when the device is silent; in ui-core, so L1, L2
and other boards get it. New NodePrefs fields go at the end of the and other boards get it. New NodePrefs fields go at the end of the
stored layout, so an L1 keeps its settings after the update. stored layout, so an L1 keeps its settings after the update.
To decide: the Clock alarm rings anyway (proposed yes); whether a To decide: the Clock alarm rings anyway (proposed yes); whether a
message wakes the screen during quiet hours; with the clock not set, message wakes the screen during quiet hours; with the clock not set,
quiet hours are off (proposed). quiet hours are off (proposed).
Done 2026-09-28: NodePrefs quiet_hours / quiet_from / quiet_to
(sentinel 0x30, sizeof 2832), off by default, 22-7; the alarm rings, a
message doesn't wake the screen, a manual mute / unmute stands until
the window ends. hourInWindow() / localHour() shared with the bot's
quiet hours. L2 tested; L1 built, not flashed yet.
## E. OTA end-to-end test ## E. OTA end-to-end test
@@ -107,5 +112,4 @@ it; implement the tiers, docs and website after the merge.
## Open decisions ## Open decisions
- D: alarm during quiet hours; messages waking the screen.
- I: tier spec before stage F, or after the merge. - I: tier spec before stage F, or after the merge.
+15
View File
@@ -627,6 +627,15 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
rd(&_prefs.display_brightness_pct, sizeof(_prefs.display_brightness_pct)); rd(&_prefs.display_brightness_pct, sizeof(_prefs.display_brightness_pct));
if (_prefs.display_brightness_pct > 100) _prefs.display_brightness_pct = 0; if (_prefs.display_brightness_pct > 100) _prefs.display_brightness_pct = 0;
// quiet_hours / quiet_from / quiet_to. Out of range -> off, 22-7; an older
// file's sentinel bytes landing here are reset below on the mismatch.
rd(&_prefs.quiet_hours, sizeof(_prefs.quiet_hours));
rd(&_prefs.quiet_from, sizeof(_prefs.quiet_from));
rd(&_prefs.quiet_to, sizeof(_prefs.quiet_to));
if (_prefs.quiet_hours > 1) _prefs.quiet_hours = 0;
if (_prefs.quiet_from > 23) _prefs.quiet_from = 22;
if (_prefs.quiet_to > 23) _prefs.quiet_to = 7;
// Schema sentinel: bumped on layout changes. Mismatch means an older file // Schema sentinel: bumped on layout changes. Mismatch means an older file
// (or a different schema); rd() and the clamps above already keep every // (or a different schema); rd() and the clamps above already keep every
// field within its valid range regardless, so we just log it here — // field within its valid range regardless, so we just log it here —
@@ -636,6 +645,9 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
if (sentinel != NodePrefs::SCHEMA_SENTINEL) { if (sentinel != NodePrefs::SCHEMA_SENTINEL) {
MESH_DEBUG_PRINTLN("prefs schema sentinel mismatch: got 0x%08X, expected 0x%08X — re-saving on next change", MESH_DEBUG_PRINTLN("prefs schema sentinel mismatch: got 0x%08X, expected 0x%08X — re-saving on next change",
(unsigned)sentinel, (unsigned)NodePrefs::SCHEMA_SENTINEL); (unsigned)sentinel, (unsigned)NodePrefs::SCHEMA_SENTINEL);
// 0xC0DE002F → 0xC0DE0030: quiet hours appended; from an older file they
// hold its sentinel's bytes, so start off, 22:00-07:00.
if (sentinel < 0xC0DE0030) { _prefs.quiet_hours = 0; _prefs.quiet_from = 22; _prefs.quiet_to = 7; }
// 0xC0DE002E → 0xC0DE002F: display_brightness_pct appended; from an older // 0xC0DE002E → 0xC0DE002F: display_brightness_pct appended; from an older
// file it holds a stray sentinel byte (0x2E = "46 %"), so start from the level. // file it holds a stray sentinel byte (0x2E = "46 %"), so start from the level.
_prefs.display_brightness_pct = 0; _prefs.display_brightness_pct = 0;
@@ -828,6 +840,9 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.loc_share_scope, sizeof(_prefs.loc_share_scope)); file.write((uint8_t *)&_prefs.loc_share_scope, sizeof(_prefs.loc_share_scope));
file.write((uint8_t *)&_prefs.loc_share_duration_idx, sizeof(_prefs.loc_share_duration_idx)); file.write((uint8_t *)&_prefs.loc_share_duration_idx, sizeof(_prefs.loc_share_duration_idx));
file.write((uint8_t *)&_prefs.display_brightness_pct, sizeof(_prefs.display_brightness_pct)); file.write((uint8_t *)&_prefs.display_brightness_pct, sizeof(_prefs.display_brightness_pct));
file.write((uint8_t *)&_prefs.quiet_hours, sizeof(_prefs.quiet_hours));
file.write((uint8_t *)&_prefs.quiet_from, sizeof(_prefs.quiet_from));
file.write((uint8_t *)&_prefs.quiet_to, sizeof(_prefs.quiet_to));
// Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL. Its write is // Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL. Its write is
// the one we check: once the flash fills, writes return 0, so a good // the one we check: once the flash fills, writes return 0, so a good
+2
View File
@@ -1846,6 +1846,8 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_prefs.radio_fem_txgain = 0; _prefs.radio_fem_txgain = 0;
_prefs.display_brightness = 2; // medium brightness by default _prefs.display_brightness = 2; // medium brightness by default
_prefs.buzzer_volume = 4; // max volume by default _prefs.buzzer_volume = 4; // max volume by default
_prefs.quiet_from = 22; // quiet hours (off by default) 22:00-07:00
_prefs.quiet_to = 7;
_prefs.ringtone_bpm_idx = 2; // 120 bpm default _prefs.ringtone_bpm_idx = 2; // 120 bpm default
_prefs.ringtone_len = 0; // no custom ringtone by default _prefs.ringtone_len = 0; // no custom ringtone by default
_prefs.ringtone2_bpm_idx = 2; // 120 bpm default _prefs.ringtone2_bpm_idx = 2; // 120 bpm default
+3 -8
View File
@@ -123,14 +123,9 @@ void MyMesh::botDmRecord(const uint8_t* pubkey) {
// width window (start == end) disables the feature; a window where start > end // width window (start == end) disables the feature; a window where start > end
// wraps past midnight. // wraps past midnight.
bool MyMesh::botInQuietHours() const { bool MyMesh::botInQuietHours() const {
if (_prefs.bot_quiet_start == _prefs.bot_quiet_end) return false; // disabled int h;
uint32_t utc = getRTCClock()->getCurrentTime(); if (!localHour(getRTCClock()->getCurrentTime(), _prefs.tz_offset_hours, h)) return false; // clock not set -- don't suppress
if (utc < 1000000000UL) return false; // clock not set — don't suppress return hourInWindow(h, _prefs.bot_quiet_start, _prefs.bot_quiet_end);
uint32_t local = utc + (int32_t)_prefs.tz_offset_hours * 3600;
int h = (int)((local / 3600) % 24);
int s = _prefs.bot_quiet_start, e = _prefs.bot_quiet_end;
return (s < e) ? (h >= s && h < e) // same-day window
: (h >= s || h < e); // overnight window
} }
void MyMesh::tryBotReplyDM(const ContactInfo& from, const char* text, uint8_t hops) { void MyMesh::tryBotReplyDM(const ContactInfo& from, const char* text, uint8_t hops) {
+31 -2
View File
@@ -317,6 +317,13 @@ struct NodePrefs { // persisted to file
uint8_t buzzer_quiet; uint8_t buzzer_quiet;
uint8_t buzzer_volume; // 0=min..4=max, default 4 uint8_t buzzer_volume; // 0=min..4=max, default 4
uint8_t buzzer_auto; // 0=manual (default), 1=auto-mute when BT connected uint8_t buzzer_auto; // 0=manual (default), 1=auto-mute when BT connected
// Quiet hours: muted from quiet_from to quiet_to (local hours 0-23, wrapping
// past midnight) while quiet_hours is on; the clock alarm still rings and a
// message doesn't wake the screen (inQuietHours() below). A manual mute /
// unmute during the window stands until it ends (soundctl::tick()).
uint8_t quiet_hours; // 0=off (default), 1=on
uint8_t quiet_from; // default 22
uint8_t quiet_to; // default 7
// Settings > Display > "Msg wake". Stored inverted (same reason as // Settings > Display > "Msg wake". Stored inverted (same reason as
// fav_sort_off below) so both a fresh memset and an older prefs file (no // fav_sort_off below) so both a fresh memset and an older prefs file (no
// bytes here at all) mean "on" -- today's behaviour, where an incoming // bytes here at all) mean "on" -- today's behaviour, where an incoming
@@ -627,7 +634,7 @@ struct NodePrefs { // persisted to file
// repeat_* fields) instead of at the tail, which shifted every field after // repeat_* fields) instead of at the tail, which shifted every field after
// them by 25 bytes when loading an older file. Never released, but a dev // them by 25 bytes when loading an older file. Never released, but a dev
// build wrote it, so the number must not be reused for anything else. // build wrote it, so the number must not be reused for anything else.
static const uint32_t SCHEMA_SENTINEL = 0xC0DE002F; static const uint32_t SCHEMA_SENTINEL = 0xC0DE0030;
// Bit-index for each home page. Used by page_order (entries store bit+1) and // Bit-index for each home page. Used by page_order (entries store bit+1) and
// by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage // by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage
@@ -704,6 +711,25 @@ struct NodePrefs { // persisted to file
} }
}; };
// Local hour `h` inside [start, end): a window with start > end wraps past
// midnight; start == end is no window. Shared by the bot's quiet hours and the
// sound's.
static inline bool hourInWindow(int h, int start, int end) {
if (start == end) return false;
return start < end ? (h >= start && h < end) : (h >= start || h < end);
}
// The local hour from a UTC time; false while the clock isn't set.
static inline bool localHour(uint32_t utc, int8_t tz_hours, int& h) {
if (utc < 1000000000UL) return false;
h = (int)(((int64_t)utc + (int64_t)tz_hours * 3600) / 3600 % 24);
return true;
}
// Settings > Sound > Quiet hours in force at `utc` (never with the clock unset).
static inline bool inQuietHours(const NodePrefs& p, uint32_t utc) {
int h;
return p.quiet_hours && localHour(utc, p.tz_offset_hours, h) && hourInWindow(h, p.quiet_from, p.quiet_to);
}
// ── Serialization tripwire ─────────────────────────────────────────────────── // ── Serialization tripwire ───────────────────────────────────────────────────
// NodePrefs is written/read field-by-field, in order, by DataStore::savePrefs() // NodePrefs is written/read field-by-field, in order, by DataStore::savePrefs()
// and loadPrefsInt(); the on-disk format IS the struct's field layout. There is // and loadPrefsInt(); the on-disk format IS the struct's field layout. There is
@@ -782,7 +808,10 @@ struct NodePrefs { // persisted to file
// (ESP32) builds, sizeof unchanged at 2824. loc_share_duration_idx (0xC0DE002E) // (ESP32) builds, sizeof unchanged at 2824. loc_share_duration_idx (0xC0DE002E)
// likewise (sim build; see the check below). display_brightness_pct (0xC0DE002F) // likewise (sim build; see the check below). display_brightness_pct (0xC0DE002F)
// likewise (L2 ESP32 + L1 nRF52 builds). // likewise (L2 ESP32 + L1 nRF52 builds).
static_assert(sizeof(NodePrefs) == 2824, // quiet_hours / quiet_from / quiet_to (0xC0DE0030) added 8 bytes, not 3 (next
// to buzzer_auto, rounded up to the alignment) -- sizeof 2832, confirmed via real
// WioTrackerL1_companion_solo_dual (nRF52/ARM) and L2 (ESP32) builds.
static_assert(sizeof(NodePrefs) == 2832,
"NodePrefs layout changed — sync DataStore save/load + clamp, bump " "NodePrefs layout changed — sync DataStore save/load + clamp, bump "
"SCHEMA_SENTINEL, then update this size (see steps above)."); "SCHEMA_SENTINEL, then update this size (see steps above).");
@@ -25,7 +25,7 @@ enum Page : uint8_t { PG_NAV, PG_ADVERT, PG_DEVICE, PG_MESSAGES, PG_SOUND, PG_CO
enum Section : uint8_t { SEC_TRAIL, SEC_LIVE_SHARE, SEC_LOCATOR, SEC_ADVERT, enum Section : uint8_t { SEC_TRAIL, SEC_LIVE_SHARE, SEC_LOCATOR, SEC_ADVERT,
SEC_DISPLAY, SEC_POWER, SEC_TIME, SEC_UNITS, SEC_DISPLAY, SEC_POWER, SEC_TIME, SEC_UNITS,
SEC_MESSAGES, SEC_CONTACTS, SEC_MESSAGES, SEC_CONTACTS,
SEC_SOUND, SEC_SOUND_FOR, SEC_COUNT }; SEC_SOUND, SEC_SOUND_FOR, SEC_QUIET, SEC_COUNT };
static const char* pageTitle(uint8_t p) { static const char* pageTitle(uint8_t p) {
static const char* T[PG_COUNT] = { "Map options", "Advert", "Display & power", "Messages & contacts", "Sound" }; static const char* T[PG_COUNT] = { "Map options", "Advert", "Display & power", "Messages & contacts", "Sound" };
@@ -35,14 +35,14 @@ static const char* sectionTitle(uint8_t s) {
static const char* T[SEC_COUNT] = { "TRAIL", "LIVE SHARE", "ARRIVAL ALERT", "AUTOMATIC", static const char* T[SEC_COUNT] = { "TRAIL", "LIVE SHARE", "ARRIVAL ALERT", "AUTOMATIC",
"DISPLAY", "POWER", "TIME", "UNITS", "DISPLAY", "POWER", "TIME", "UNITS",
"MESSAGES", "CONTACTS", "MESSAGES", "CONTACTS",
"SOUND", "PLAYS FOR" }; "SOUND", "PLAYS FOR", "QUIET HOURS" };
return s < SEC_COUNT ? T[s] : ""; return s < SEC_COUNT ? T[s] : "";
} }
static uint8_t sectionPage(uint8_t s) { static uint8_t sectionPage(uint8_t s) {
static const uint8_t P[SEC_COUNT] = { PG_NAV, PG_NAV, PG_NAV, PG_ADVERT, static const uint8_t P[SEC_COUNT] = { PG_NAV, PG_NAV, PG_NAV, PG_ADVERT,
PG_DEVICE, PG_DEVICE, PG_DEVICE, PG_DEVICE, PG_DEVICE, PG_DEVICE, PG_DEVICE, PG_DEVICE,
PG_MESSAGES, PG_MESSAGES, PG_MESSAGES, PG_MESSAGES,
PG_SOUND, PG_SOUND }; PG_SOUND, PG_SOUND, PG_SOUND };
return s < SEC_COUNT ? P[s] : PG_DEVICE; return s < SEC_COUNT ? P[s] : PG_DEVICE;
} }
@@ -136,6 +136,10 @@ static void optVolume(uint8_t v, char* b, int n, const NodePrefs&) {
static const char* L[5] = { "Quietest", "Quiet", "Medium", "Loud", "Loudest" }; static const char* L[5] = { "Quietest", "Quiet", "Medium", "Loud", "Loudest" };
snprintf(b, n, "%s", L[v < 5 ? v : 4]); snprintf(b, n, "%s", L[v < 5 ? v : 4]);
} }
static void optHour(uint8_t v, char* b, int n, const NodePrefs& p) { // "22:00", or "10 PM" with the 12-hour clock
if (!p.clock_12h) snprintf(b, n, "%02u:00", (unsigned)v);
else snprintf(b, n, "%u %s", (unsigned)(v % 12 ? v % 12 : 12), v < 12 ? "AM" : "PM");
}
static void optSound(uint8_t v, char* b, int n, const NodePrefs&) { snprintf(b, n, "%s", soundctl::soundLabel(v)); } static void optSound(uint8_t v, char* b, int n, const NodePrefs&) { snprintf(b, n, "%s", soundctl::soundLabel(v)); }
static void optAdvertScope(uint8_t v, char* b, int n, const NodePrefs&) { static void optAdvertScope(uint8_t v, char* b, int n, const NodePrefs&) {
snprintf(b, n, "%s", v == ADVERT_SOUND_SCOPE_ZERO_HOP ? "Direct only" : "All"); snprintf(b, n, "%s", v == ADVERT_SOUND_SCOPE_ZERO_HOP ? "Direct only" : "All");
@@ -209,6 +213,9 @@ static const Setting ALL[] = {
// On / Off / Auto spans two fields (soundctl::setMode): the frontend's own row. // On / Off / Auto spans two fields (soundctl::setMode): the frontend's own row.
IDX("Volume", nullptr, SEC_SOUND, buzzer_volume, 5, optVolume, applySound), IDX("Volume", nullptr, SEC_SOUND, buzzer_volume, 5, optVolume, applySound),
SW("Quiet hours", "Muted, screen dark; alarms ring", SEC_QUIET, quiet_hours, nullptr),
IDX("From", nullptr, SEC_QUIET, quiet_from, 24, optHour, nullptr),
IDX("Until", nullptr, SEC_QUIET, quiet_to, 24, optHour, nullptr),
IDX("Direct messages", nullptr, SEC_SOUND_FOR, notif_melody_dm, soundctl::SOUND_COUNT, optSound, nullptr), IDX("Direct messages", nullptr, SEC_SOUND_FOR, notif_melody_dm, soundctl::SOUND_COUNT, optSound, nullptr),
IDX("Channels", nullptr, SEC_SOUND_FOR, notif_melody_ch, soundctl::SOUND_COUNT, optSound, nullptr), IDX("Channels", nullptr, SEC_SOUND_FOR, notif_melody_ch, soundctl::SOUND_COUNT, optSound, nullptr),
+26 -10
View File
@@ -3,7 +3,7 @@
// Ringtone) and ui-lvgl (Settings > Sound, Melodies): the On / Off / Auto mode // Ringtone) and ui-lvgl (Settings > Sound, Melodies): the On / Off / Auto mode
// (buzzer_quiet + buzzer_auto), the volume, the built-in sounds, the labels of // (buzzer_quiet + buzzer_auto), the volume, the built-in sounds, the labels of
// the notification sound choices, and the two user melodies (ringtone_* / // the notification sound choices, and the two user melodies (ringtone_* /
// ringtone2_*) as an editable note list. Which sound a message plays is // ringtone2_*) as an editable note list. Quiet hours mute on a schedule. Which sound a message plays is
// SoundNotifier.h. // SoundNotifier.h.
#include "../NodePrefs.h" #include "../NodePrefs.h"
@@ -44,10 +44,21 @@ static const char* soundLabel(uint8_t v) {
static const uint8_t OVERRIDE_COUNT = 3; static const uint8_t OVERRIDE_COUNT = 3;
#ifdef PIN_BUZZER #ifdef PIN_BUZZER
// The mute that goes with the mode; `connected`: an app is connected. // Quiet hours (NodePrefs::inQuietHours) mute like the mode does. A mute or
static void applyMode(const NodePrefs* p, genericBuzzer& b, bool connected) { // unmute by hand inside the window stands until the window ends.
static bool s_quiet_now = false; // inside the window, as of the last tick
static bool s_quiet_override = false; // set by hand since the window began
// The mute that goes with the mode and quiet hours; `connected`: an app is
// connected.
static bool wantQuiet(const NodePrefs* p, bool connected) {
uint8_t m = mode(p); uint8_t m = mode(p);
b.quiet(m == MODE_OFF || (m == MODE_AUTO && connected)); if (m == MODE_OFF || (m == MODE_AUTO && connected)) return true;
return s_quiet_now && !s_quiet_override;
}
static void applyMode(const NodePrefs* p, genericBuzzer& b, bool connected, uint32_t utc) {
s_quiet_now = p && inQuietHours(*p, utc);
b.quiet(wantQuiet(p, connected));
} }
// Auto keeps buzzer_quiet (the manual choice it goes back to). The caller // Auto keeps buzzer_quiet (the manual choice it goes back to). The caller
@@ -56,16 +67,21 @@ static void setMode(NodePrefs* p, genericBuzzer& b, uint8_t m, bool connected) {
if (!p || m >= MODE_COUNT) return; if (!p || m >= MODE_COUNT) return;
p->buzzer_auto = m == MODE_AUTO; p->buzzer_auto = m == MODE_AUTO;
if (m != MODE_AUTO) p->buzzer_quiet = m == MODE_OFF; if (m != MODE_AUTO) p->buzzer_quiet = m == MODE_OFF;
applyMode(p, b, connected); s_quiet_override = s_quiet_now; // a choice made during quiet hours wins until they end
b.quiet(wantQuiet(p, connected));
} }
// From the loop: Auto follows the app connection. True when the mute changed. // From the loop: Auto follows the app connection, quiet hours the clock.
static bool autoTick(const NodePrefs* p, genericBuzzer& b, bool connected) { // True when the mute changed.
if (!p || !p->buzzer_auto || b.isQuiet() == connected) return false; static bool tick(const NodePrefs* p, genericBuzzer& b, bool connected, uint32_t utc) {
b.quiet(connected); if (!p) return false;
bool q = inQuietHours(*p, utc);
if (q != s_quiet_now) { s_quiet_now = q; s_quiet_override = false; }
bool want = wantQuiet(p, connected);
if (b.isQuiet() == want) return false;
b.quiet(want);
return true; return true;
} }
// Level 0..4, with a preview beep (through mute: it answers the user's own // Level 0..4, with a preview beep (through mute: it answers the user's own
// change). The caller saves the prefs. // change). The caller saves the prefs.
static void setVolume(NodePrefs* p, genericBuzzer& b, uint8_t level) { static void setVolume(NodePrefs* p, genericBuzzer& b, uint8_t level) {
+4 -4
View File
@@ -823,7 +823,7 @@ void UITask::begin(DisplayDriver* display_drv, SensorManager* sensors, NodePrefs
user_btn.begin(); user_btn.begin();
#endif #endif
#ifdef PIN_BUZZER #ifdef PIN_BUZZER
soundctl::applyMode(_prefs, _buzzer, isClientConnected()); soundctl::applyMode(_prefs, _buzzer, isClientConnected(), rtc_clock.getCurrentTime());
_buzzer.setVolume(_prefs ? _prefs->buzzer_volume : 4); _buzzer.setVolume(_prefs ? _prefs->buzzer_volume : 4);
_buzzer.begin(); // plays the startup sound unless muted _buzzer.begin(); // plays the startup sound unless muted
#endif #endif
@@ -852,7 +852,7 @@ MyMesh::Listener* UITask::meshListener() { return _core; }
void UITask::toggleMute() { void UITask::toggleMute() {
#ifdef PIN_BUZZER #ifdef PIN_BUZZER
if (!_prefs) return; if (!_prefs) return;
bool on = soundctl::mode(_prefs) != soundctl::MODE_ON; bool on = _buzzer.isQuiet(); // muted now, however (mode, Auto, quiet hours): sound on
soundctl::setMode(_prefs, _buzzer, on ? soundctl::MODE_ON : soundctl::MODE_OFF, isClientConnected()); soundctl::setMode(_prefs, _buzzer, on ? soundctl::MODE_ON : soundctl::MODE_OFF, isClientConnected());
prefsSave(); prefsSave();
if (on) _buzzer.playForced(soundctl::MEL_VOLUME); if (on) _buzzer.playForced(soundctl::MEL_VOLUME);
@@ -988,7 +988,7 @@ void UITask::loop() {
#endif #endif
#ifdef PIN_BUZZER #ifdef PIN_BUZZER
_buzzer.loop(); _buzzer.loop();
if (soundctl::autoTick(_prefs, _buzzer, isClientConnected()) && !_asleep) refreshStatusBar(); if (soundctl::tick(_prefs, _buzzer, isClientConnected(), rtc_clock.getCurrentTime()) && !_asleep) refreshStatusBar();
// The alarm melody repeats until dismissed or the ring window ends. // The alarm melody repeats until dismissed or the ring window ends.
if (_core->clock.isRinging() && !_buzzer.isPlaying()) playMelody(soundctl::MEL_ALARM); if (_core->clock.isRinging() && !_buzzer.isPlaying()) playMelody(soundctl::MEL_ALARM);
#endif #endif
@@ -1289,7 +1289,7 @@ void UITask::onMessageArrived(const UiEvent& ev) {
} }
// Wake for the message unless an app is already showing it, or the user // Wake for the message unless an app is already showing it, or the user
// turned message-wake off. // turned message-wake off.
bool wake_disabled = _prefs && _prefs->msg_wake_screen_off; bool wake_disabled = _prefs && (_prefs->msg_wake_screen_off || inQuietHours(*_prefs, rtc_clock.getCurrentTime())); // quiet hours: dark too
if (_asleep && !wake_disabled && !isClientConnected()) wake(); if (_asleep && !wake_disabled && !isClientConnected()) wake();
else if (!_asleep) lv_display_trigger_activity(NULL); else if (!_asleep) lv_display_trigger_activity(NULL);
bool open_here = _screen == SCR_THREAD && bool open_here = _screen == SCR_THREAD &&
@@ -41,6 +41,7 @@ class SettingsScreen : public UIScreen {
SECTION_SOUND, SECTION_SOUND,
BUZZER, BUZZER,
BUZZER_VOLUME, BUZZER_VOLUME,
QUIET_HOURS, QUIET_FROM, QUIET_TO,
DM_MELODY, DM_MELODY,
CH_MELODY, CH_MELODY,
AD_SOUND, AD_SOUND,
@@ -480,6 +481,17 @@ class SettingsScreen : public UIScreen {
display.setCursor(valCol(display), y); display.setCursor(valCol(display), y);
display.print("N/A"); display.print("N/A");
#endif #endif
} else if (item == QUIET_HOURS) {
display.print("Quiet hrs");
display.setCursor(valCol(display), y);
display.print((p && p->quiet_hours) ? "ON" : "OFF");
} else if (item == QUIET_FROM || item == QUIET_TO) {
display.print(item == QUIET_FROM ? " from" : " until");
display.setCursor(valCol(display), y);
{ char hb[8]; uint8_t h = p ? (item == QUIET_FROM ? p->quiet_from : p->quiet_to) : 0;
if (p && p->clock_12h) snprintf(hb, sizeof(hb), "%u%s", (unsigned)(h % 12 ? h % 12 : 12), h < 12 ? "AM" : "PM");
else snprintf(hb, sizeof(hb), "%02u:00", (unsigned)h);
display.print(hb); }
} else if (item == DM_MELODY) { } else if (item == DM_MELODY) {
display.print("DM sound"); display.print("DM sound");
display.setCursor(valCol(display), y); display.setCursor(valCol(display), y);
@@ -1027,6 +1039,15 @@ public:
#endif #endif
return right || left; return right || left;
} }
if (_selected == QUIET_HOURS && p && (left || right || enter)) {
p->quiet_hours ^= 1;
_dirty = true; return true;
}
if ((_selected == QUIET_FROM || _selected == QUIET_TO) && p && (left || right || enter)) {
uint8_t& h = _selected == QUIET_FROM ? p->quiet_from : p->quiet_to;
h = (h + (left ? 23 : 1)) % 24;
_dirty = true; return true;
}
if (_selected == DM_MELODY && p && (left || right || enter)) { if (_selected == DM_MELODY && p && (left || right || enter)) {
p->notif_melody_dm = (p->notif_melody_dm + (left ? SOUND_COUNT - 1 : 1)) % SOUND_COUNT; p->notif_melody_dm = (p->notif_melody_dm + (left ? SOUND_COUNT - 1 : 1)) % SOUND_COUNT;
_dirty = true; return true; _dirty = true; return true;
+3 -3
View File
@@ -1593,7 +1593,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
} }
#ifdef PIN_BUZZER #ifdef PIN_BUZZER
soundctl::applyMode(_node_prefs, buzzer, false); soundctl::applyMode(_node_prefs, buzzer, false, rtc_clock.getCurrentTime());
buzzer.setVolume(_node_prefs->buzzer_volume); buzzer.setVolume(_node_prefs->buzzer_volume);
buzzer.begin(); buzzer.begin();
#endif #endif
@@ -1952,7 +1952,7 @@ void UITask::onMessageArrived(const UiEvent& ev) {
showAlert(alert_buf, 3000); showAlert(alert_buf, 3000);
if (_display != NULL && !_locked) { if (_display != NULL && !_locked) {
bool wake_disabled = _node_prefs && _node_prefs->msg_wake_screen_off; bool wake_disabled = _node_prefs && (_node_prefs->msg_wake_screen_off || inQuietHours(*_node_prefs, rtc_clock.getCurrentTime()));
if (!wake_disabled && !_display->isOn() && !isClientConnected()) { // wake for the msg unless an app (BLE/USB) is already showing it, or the user disabled msg-wake if (!wake_disabled && !_display->isOn() && !isClientConnected()) { // wake for the msg unless an app (BLE/USB) is already showing it, or the user disabled msg-wake
_display->turnOn(); _display->turnOn();
} }
@@ -2686,7 +2686,7 @@ void UITask::loop() {
userLedHandler(); userLedHandler();
#ifdef PIN_BUZZER #ifdef PIN_BUZZER
if (soundctl::autoTick(_node_prefs, buzzer, isClientConnected())) // BLE bonded or an open USB port if (soundctl::tick(_node_prefs, buzzer, isClientConnected(), rtc_clock.getCurrentTime())) // BLE bonded or an open USB port
_next_refresh = 0; _next_refresh = 0;
if (buzzer.isPlaying()) buzzer.loop(); if (buzzer.isPlaying()) buzzer.loop();
#endif #endif