Files
MeshCore-Solo/src/helpers/ui/buzzer.h
MarekZegare4 1387cc9507 fix(eink): drive ringtone note advance from TIMER1 IRQ, not loop() poll
On nRF52 the buzzer advanced notes by polling millis() >= _note_end_ms in
loop(). A blocking display refresh (e-ink endFrame) starves loop(), so a
note boundary that falls inside a refresh is serviced late — the note
plays long, or the next is skipped. The keypress-time 300 ms render delay
only masked the first note.

Advance notes from a hardware TIMER1 compare interrupt instead, scheduled
for each note's exact duration, so timing is independent of render cadence.
TIMER0 is the SoftDevice's; TIMER1 is free (tone() uses PWM2, nrfx TIMER1
driver is disabled). loop() becomes a no-op on nRF52; the UITask keypress
render-delay workaround is removed.

_disarmNoteTimer() clears the latched NVIC pending IRQ (not just the event)
on stop()/_nrfBegin(), so a note-advance latched just before a stop can't
fire spuriously — which would skip a new melody's first note or blip after
an explicit stop. Event read-backs flush the write buffer per the nRF52
event anomaly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 11:37:48 +02:00

89 lines
3.4 KiB
C++

#pragma once
#include <Arduino.h>
// NRF52 uses a custom non-blocking RTTTL player (see buzzer.cpp); only the
// other platforms pull in the NonBlockingRtttl library here.
#if !defined(NRF52_PLATFORM)
#include <NonBlockingRtttl.h>
#endif
/* class abstracts underlying RTTTL library
Just a simple implementation to start. At the moment use same
melody for message and discovery
Suggest enum type for different sounds
- on message
- on discovery
TODO
- make message ring tone configurable
*/
class genericBuzzer
{
public:
void begin();
void play(const char *melody);
void playForced(const char *melody);
void loop();
void startup();
void shutdown();
bool isPlaying();
void quiet(bool buzzer_state);
bool isQuiet();
void setVolume(uint8_t level);
uint8_t getVolume() const { return _volume_level; }
void stop();
private:
uint8_t _volume_level = 4;
void applyVolume();
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";
bool _is_quiet = true;
#if defined(NRF52_PLATFORM)
// Own RTTTL player — bypasses tone() to allow volume control from note start.
// tone() pre-computes seq_refresh so the DMA repeats 50% duty for ~30ms before
// re-reading its buffer; we cannot beat that timing. By owning NRF_PWM2 directly
// and setting REFRESH=0, DMA re-reads _duty_buf every period so duty takes effect
// immediately at SEQSTART.
volatile uint16_t _duty_buf = 0; // DMA source — must stay in RAM
const char* _rtttl_pos = nullptr;
bool _rtttl_done = true;
bool _pwm_on = false;
uint8_t _def_dur = 4;
uint8_t _def_oct = 5;
uint16_t _def_bpm = 120;
void _nrfBegin(const char* melody);
void _nrfAdvance();
void _nrfStartPwm(uint16_t freq);
void _nrfStopPwm();
uint8_t _dutyPct() const;
static uint16_t _noteFreq(char letter, bool sharp, uint8_t octave);
static bool _parseNext(const char*& pos, uint8_t def_dur, uint8_t def_oct,
uint16_t bpm, uint16_t& freq_hz, uint32_t& dur_ms);
static void _parseHeader(const char* melody, uint8_t& def_dur, uint8_t& def_oct,
uint16_t& bpm, const char*& notes_start);
// TIMER1-driven note advance: a hardware compare interrupt calls
// _nrfAdvance() at the exact moment the current note's duration ends,
// so playback timing survives a blocking display refresh (e-ink)
// instead of waiting for the next loop() poll. Same reasoning as
// MomentaryButton's GPIO-IRQ edge capture, just timer- rather than
// pin-driven. TIMER0 is reserved by the SoftDevice; TIMER1 is free.
void _armNoteTimer(uint32_t dur_ms);
void _disarmNoteTimer();
static genericBuzzer* _isr_instance;
public:
// Must be public: the extern "C" TIMER1_IRQHandler (see buzzer.cpp) is
// a free function — the vector table requires that exact symbol — so
// it needs access from outside the class to dispatch into it.
static void _timer1ISR();
#endif
};