Files
MeshCore/src/helpers/ui/buzzer.cpp
T
mikecarperandR0ck f57dffa2be Add saved Companion sound and vibration controls
Default fresh settings to sound off and vibration on, and keep existing saved choices. Apply changes immediately across all three UIs and make custom notification patterns respect the same mute controls.

Keep the Wio Mini Companion within its existing flash boundary by storing its LED switch in a reserved preference byte and shortening CLI help without removing commands. Add coverage for live output changes, persistence, and storage failures.

Adapted from https://github.com/meshcore-dev/MeshCore/pull/3603.

Co-authored-by: R0ck <develop@hectospark.co.uk>
2026-10-20 09:33:43 -07:00

259 lines
7.8 KiB
C++

#include "Arduino.h"
#ifdef PIN_BUZZER
#include "buzzer.h"
#if MESH_GPS_VOICE
#if defined(MESH_GPS_VOICE_G726_BITRATE)
// Board source filters already compile this player entry point. Keep the
// experimental decoder out of ordinary images and avoid changing every
// board's filter just for the hardware comparison.
#include "g726/g726_decoder.c"
#include "G726VoiceData.h"
#else
#include "ButtonVoiceData.h"
#include "GpsVoiceData.h"
#endif
#include "nrf52/GpsVoicePlayer.h"
static mesh::audio::GpsVoicePlayer gps_voice;
extern "C" void PWM3_IRQHandler() { gps_voice.interrupt(); }
static mesh::audio::VoiceClip buttonVoiceClip(ButtonVoicePrompt prompt) {
using namespace mesh::audio;
switch (prompt) {
case ButtonVoicePrompt::Ready: return readyClip;
case ButtonVoicePrompt::NotificationCleared: return notificationClearedClip;
case ButtonVoicePrompt::AdvertQueued: return advertQueuedClip;
case ButtonVoicePrompt::AdvertFailed: return advertFailedClip;
case ButtonVoicePrompt::SoundOn: return soundOnClip;
case ButtonVoicePrompt::SoundOff: return soundOffClip;
case ButtonVoicePrompt::GpsOn: return gpsOnClip;
case ButtonVoicePrompt::GpsOff: return gpsOffClip;
case ButtonVoicePrompt::ActionFailed: return actionFailedClip;
case ButtonVoicePrompt::UsbSetup: return usbSetupClip;
case ButtonVoicePrompt::ShuttingDown: return shuttingDownClip;
case ButtonVoicePrompt::Restarting: return restartingClip;
#ifdef HAS_DRV2605
case ButtonVoicePrompt::AlertsSoundVibration: return alertsSoundVibrationClip;
case ButtonVoicePrompt::AlertsSoundOnly: return alertsSoundOnlyClip;
case ButtonVoicePrompt::AlertsVibrationOnly: return alertsVibrationOnlyClip;
case ButtonVoicePrompt::AlertsSilent: return alertsSilentClip;
#else
case ButtonVoicePrompt::AlertsSoundVibration:
case ButtonVoicePrompt::AlertsSoundOnly:
case ButtonVoicePrompt::AlertsVibrationOnly:
case ButtonVoicePrompt::AlertsSilent: return {};
#endif
}
return {};
}
#endif
void genericBuzzer::begin() {
digitalWrite(PIN_BUZZER, LOW);
pinMode(PIN_BUZZER, OUTPUT);
#ifdef PIN_BUZZER_EN
digitalWrite(PIN_BUZZER_EN, LOW);
pinMode(PIN_BUZZER_EN, OUTPUT);
#endif
// Start muted until the UI applies the saved preference.
quiet(_is_quiet);
}
void genericBuzzer::play(const char *melody) {
#if MESH_GPS_VOICE
// A muted ordinary alert must not cancel the one courtesy confirmation
// that may still be queued or playing while the preference is quiet.
if (_is_quiet) return;
#endif
if (isPlaying()) // interrupt existing
{
stop();
}
if (_is_quiet) return;
rtttl::begin(PIN_BUZZER,melody);
// Serial.print("DBG: Playing melody - isQuiet: ");
// Serial.println(isQuiet());
}
void genericBuzzer::playNotification(const char* melody) {
// Muted background alerts must not interrupt an explicit button prompt.
if (_is_quiet) return;
stop();
#ifdef PIN_BUZZER_EN
digitalWrite(PIN_BUZZER_EN, HIGH);
#endif
rtttl::begin(PIN_BUZZER, melody);
}
void genericBuzzer::stopNotification() {
#if MESH_GPS_VOICE
// Notification timers own their tones, not user-requested confirmations.
// Preserve a pending direction tone, speech gap, or active voice clip.
if (_voice_pending || gps_voice.active()) return;
#endif
stop();
}
bool genericBuzzer::speakGps(bool enabled) {
return speakButton(enabled ? ButtonVoicePrompt::GpsOn : ButtonVoicePrompt::GpsOff);
}
bool genericBuzzer::speakButton(ButtonVoicePrompt prompt, bool allowQuiet) {
const char* melody = nullptr;
switch (prompt) {
case ButtonVoicePrompt::GpsOn:
melody = startup_song;
break;
case ButtonVoicePrompt::GpsOff:
case ButtonVoicePrompt::ShuttingDown:
case ButtonVoicePrompt::Restarting:
melody = shutdown_song;
break;
case ButtonVoicePrompt::Ready:
case ButtonVoicePrompt::NotificationCleared:
case ButtonVoicePrompt::AdvertQueued:
case ButtonVoicePrompt::AdvertFailed:
case ButtonVoicePrompt::SoundOn:
case ButtonVoicePrompt::SoundOff:
case ButtonVoicePrompt::ActionFailed:
case ButtonVoicePrompt::UsbSetup:
case ButtonVoicePrompt::AlertsSoundVibration:
case ButtonVoicePrompt::AlertsSoundOnly:
case ButtonVoicePrompt::AlertsVibrationOnly:
case ButtonVoicePrompt::AlertsSilent:
#if MESH_GPS_VOICE && !defined(HAS_DRV2605)
// Reject unavailable modes before cancelling any valid audio.
if (prompt >= ButtonVoicePrompt::AlertsSoundVibration) return false;
#endif
break;
default:
return false;
}
stop();
if (_is_quiet && !allowQuiet) return true;
#if !MESH_GPS_VOICE
// The UI retains its legacy acknowledgements on untested boards. New
// voice-only prompts must not introduce tones there (including Ready).
if (!melody) return true;
#endif
#ifdef PIN_BUZZER_EN
digitalWrite(PIN_BUZZER_EN, HIGH);
#endif
#if MESH_GPS_VOICE
_voice_prompt = prompt;
_voice_pending = true;
_voice_allow_quiet = allowQuiet;
_voice_gap_started = false;
#endif
// Preserve the recognisable rising/falling control tone even where voice
// playback is disabled. Optional tones release their PWM before speech.
if (melody) rtttl::begin(PIN_BUZZER, melody);
return true;
}
#ifdef MESH_BUTTON_AUDIO_HIL
void genericBuzzer::voiceStatus(char* reply, size_t size) {
#if MESH_GPS_VOICE
snprintf(reply, size, "voice playing=%u started=%lu completed=%lu cancelled=%lu samples=%lu quiet=%u gain=%u pending=%u tone=%u",
gps_voice.playing(), (unsigned long)gps_voice.started(),
(unsigned long)gps_voice.completed(), (unsigned long)gps_voice.cancelled(),
(unsigned long)gps_voice.played(), _is_quiet, gps_voice.gain(),
_voice_pending, rtttl::isPlaying());
#else
snprintf(reply, size, "voice unsupported");
#endif
}
bool genericBuzzer::voiceGain(uint8_t gain) {
#if MESH_GPS_VOICE
return !_voice_pending && gps_voice.setGain(gain);
#else
(void)gain;
return false;
#endif
}
#endif
void genericBuzzer::stop() {
rtttl::stop();
#if MESH_GPS_VOICE
_voice_pending = false;
_voice_allow_quiet = false;
_voice_gap_started = false;
gps_voice.stop();
#endif
#ifdef PIN_BUZZER_EN
if (_is_quiet) digitalWrite(PIN_BUZZER_EN, LOW);
#endif
}
bool genericBuzzer::isPlaying() {
#if MESH_GPS_VOICE
if (_voice_pending || gps_voice.active()) return true;
#endif
return rtttl::isPlaying();
}
void genericBuzzer::loop() {
#if MESH_GPS_VOICE
gps_voice.service();
if (gps_voice.playing()) return;
if (_voice_pending) {
if (!rtttl::done()) {
rtttl::play();
return;
}
if (!_voice_gap_started) {
rtttl::stop();
_voice_gap_ms = millis();
_voice_gap_started = true;
}
if (static_cast<uint32_t>(millis() - _voice_gap_ms) < 100) return;
_voice_pending = false;
_voice_gap_started = false;
if (!_is_quiet || _voice_allow_quiet)
gps_voice.start(buttonVoiceClip(_voice_prompt));
_voice_allow_quiet = false;
if (gps_voice.playing()) return;
}
#endif
if (!rtttl::done()) {
rtttl::play();
return;
}
// Courtesy mute/unmute speech can temporarily power the driver while the
// preference stays quiet. Return both pins to their low-power state.
if (_is_quiet) {
digitalWrite(PIN_BUZZER, LOW);
#ifdef PIN_BUZZER_EN
digitalWrite(PIN_BUZZER_EN, LOW);
#endif
}
}
void genericBuzzer::startup() {
play(startup_song);
}
void genericBuzzer::shutdown() {
play(shutdown_song);
}
void genericBuzzer::quiet(bool buzzer_state) {
if (buzzer_state) stop();
_is_quiet = buzzer_state;
#ifdef PIN_BUZZER_EN
if (_is_quiet) {
digitalWrite(PIN_BUZZER_EN, LOW);
} else {
digitalWrite(PIN_BUZZER_EN, HIGH);
}
#endif
}
bool genericBuzzer::isQuiet() {
return _is_quiet;
}
#endif // ifdef PIN_BUZZER