mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-18 19:42:09 +00:00
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:
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user