Settings: add buzzer volume control (5 levels, like brightness)

- NodePrefs: add buzzer_volume field (0=min..4=max, default 4)
- DataStore: persist buzzer_volume at end of prefs file (backward-compat)
- buzzer: setVolume()/getVolume(); applyVolume() sets PWM duty cycle via
  analogWrite() after each rtttl::play() call to control output amplitude
- Settings UI: new BzrVol bar item in Sound section (left/right to adjust)
- UITask: apply saved volume on startup alongside brightness

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-05-12 09:17:37 +02:00
parent 861a65d4b1
commit ffa568938f
7 changed files with 57 additions and 1 deletions

View File

@@ -244,6 +244,9 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
file.read((uint8_t *)&_prefs.ch_notif_muted, sizeof(_prefs.ch_notif_muted)); // 1552
file.read((uint8_t *)&_prefs.dm_show_all, sizeof(_prefs.dm_show_all)); // 1560
file.read((uint8_t *)&_prefs.room_fav_only, sizeof(_prefs.room_fav_only)); // 1561
if (file.available()) {
file.read((uint8_t *)&_prefs.buzzer_volume, sizeof(_prefs.buzzer_volume)); // 1562
}
}
file.close();
@@ -295,6 +298,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.ch_notif_muted, sizeof(_prefs.ch_notif_muted)); // 1552
file.write((uint8_t *)&_prefs.dm_show_all, sizeof(_prefs.dm_show_all)); // 1560
file.write((uint8_t *)&_prefs.room_fav_only, sizeof(_prefs.room_fav_only)); // 1561
file.write((uint8_t *)&_prefs.buzzer_volume, sizeof(_prefs.buzzer_volume)); // 1562
file.close();
}

View File

@@ -864,6 +864,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_prefs.gps_enabled = 0; // GPS disabled by default
_prefs.gps_interval = 0; // No automatic GPS updates by default
_prefs.display_brightness = 2; // medium brightness by default
_prefs.buzzer_volume = 4; // max volume by default
_prefs.auto_off_secs = 15; // 15 seconds auto-off by default
_prefs.tz_offset_hours = 0; // UTC by default
_prefs.low_batt_mv = 3400; // auto-shutdown at 3.4V by default

View File

@@ -25,6 +25,7 @@ struct NodePrefs { // persisted to file
uint32_t ble_pin;
uint8_t advert_loc_policy;
uint8_t buzzer_quiet;
uint8_t buzzer_volume; // 0=min..4=max, default 4
uint8_t gps_enabled; // GPS enabled flag (0=disabled, 1=enabled)
uint32_t gps_interval; // GPS read interval in seconds
uint8_t autoadd_config; // bitmask for auto-add contacts config

View File

@@ -116,6 +116,7 @@ class SettingsScreen : public UIScreen {
// Sound section
SECTION_SOUND,
BUZZER,
BUZZER_VOLUME,
// Radio section
SECTION_RADIO,
TX_POWER,
@@ -273,6 +274,14 @@ class SettingsScreen : public UIScreen {
display.print(_task->isBuzzerQuiet() ? "OFF" : "ON");
#else
display.print("N/A");
#endif
} else if (item == BUZZER_VOLUME) {
display.print("BzrVol");
#ifdef PIN_BUZZER
renderBar(display, VAL_X, y, _task->getBuzzerVolume() + 1, 5);
#else
display.setCursor(VAL_X, y);
display.print("N/A");
#endif
} else if (item == TX_POWER) {
display.print("TX Pwr");
@@ -577,6 +586,14 @@ public:
_task->toggleBuzzer(); // saves immediately internally
return true;
}
if (_selected == BUZZER_VOLUME) {
#ifdef PIN_BUZZER
uint8_t lvl = _task->getBuzzerVolume();
if (right && lvl < 4) { _task->setBuzzerVolumeLevel(lvl + 1); _dirty = true; return true; }
if (left && lvl > 0) { _task->setBuzzerVolumeLevel(lvl - 1); _dirty = true; return true; }
#endif
return right || left;
}
if (_selected == TX_POWER && p) {
if (right && p->tx_power_dbm < 22) { p->tx_power_dbm++; _task->applyTxPower(); _dirty = true; return true; }
if (left && p->tx_power_dbm > 2) { p->tx_power_dbm--; _task->applyTxPower(); _dirty = true; return true; }
@@ -2157,6 +2174,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
#ifdef PIN_BUZZER
buzzer.quiet(_node_prefs->buzzer_quiet);
buzzer.setVolume(_node_prefs->buzzer_volume);
buzzer.begin();
#endif
@@ -2589,6 +2607,16 @@ void UITask::setBrightnessLevel(uint8_t level) {
_next_refresh = 0;
}
void UITask::setBuzzerVolumeLevel(uint8_t level) {
#ifdef PIN_BUZZER
if (_node_prefs == NULL) return;
if (level > 4) level = 4;
_node_prefs->buzzer_volume = level;
buzzer.setVolume(level);
_next_refresh = 0;
#endif
}
void UITask::toggleBuzzer() {
// Toggle buzzer quiet mode
#ifdef PIN_BUZZER

View File

@@ -107,6 +107,8 @@ public:
void applyBrightness();
void setBrightnessLevel(uint8_t level);
uint8_t getBrightnessLevel() const { return _node_prefs ? _node_prefs->display_brightness : 2; }
void setBuzzerVolumeLevel(uint8_t level);
uint8_t getBuzzerVolume() const { return _node_prefs ? _node_prefs->buzzer_volume : 4; }
void applyTxPower();
void applyGPSInterval();
uint32_t autoOffMillis() const {

View File

@@ -31,8 +31,24 @@ bool genericBuzzer::isPlaying() {
return rtttl::isPlaying();
}
void genericBuzzer::applyVolume() {
// 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);
}
void genericBuzzer::setVolume(uint8_t level) {
_volume_level = level < 5 ? level : 4;
if (isPlaying()) applyVolume();
}
void genericBuzzer::loop() {
if (!rtttl::done()) rtttl::play();
if (!rtttl::done()) {
rtttl::play();
if (_volume_level < 4) applyVolume();
}
}
void genericBuzzer::startup() {

View File

@@ -28,8 +28,12 @@ class genericBuzzer
bool isPlaying(); // returns true if a sound is still playing else false
void quiet(bool buzzer_state); // enables or disables the buzzer
bool isQuiet(); // get buzzer state on/off
void setVolume(uint8_t level); // 0=min..4=max
uint8_t getVolume() const { return _volume_level; }
private:
uint8_t _volume_level = 4;
void applyVolume();
// gemini's picks:
const char *startup_song = "Startup:d=4,o=5,b=160:16c6,16e6,8g6";
const char *shutdown_song = "Shutdown:d=4,o=5,b=100:8g5,16e5,16c5";