Max - Silent alarm option added

This commit is contained in:
pelgraine
2026-06-07 21:33:25 +10:00
parent 9a04bd1a67
commit 51ef01f3a6
2 changed files with 170 additions and 12 deletions
+137 -12
View File
@@ -45,6 +45,14 @@ void meck_audio_route_amp();
void meck_audio_codec_init();
#endif
#if defined(LilyGo_TDeck_Pro_Max)
// Silent (vibrate) alarm haptic shims, defined in target.cpp for the same
// reason as the audio shims above -- this UI header cannot see the board.
void meck_alarm_haptic_begin();
void meck_alarm_haptic_buzz();
void meck_alarm_haptic_stop();
#endif
// ============================================================================
// Configuration
// ============================================================================
@@ -57,6 +65,17 @@ void meck_audio_codec_init();
#define ALARM_CHECK_INTERVAL_MS 10000 // Check alarms every 10 seconds
#define ALARM_FIRE_COOLDOWN_S 90 // Don't re-fire same alarm within 90s
#if defined(LilyGo_TDeck_Pro_Max)
// Silent (vibrate) alarm. The chosen-sound slot stores this one-byte sentinel
// instead of a filename; 0x01 can never be the first byte of a real filename.
// Same value as NotifSounds' marker, kept local so this header need not pull in
// NotifSounds.h.
#define ALARM_VIBRATE_MARKER "\x01"
#define ALARM_VIBRATE_GROUP 3 // buzzes per group
#define ALARM_VIBRATE_BUZZ_MS 1200 // spacing between buzz starts (~1s effect + margin)
#define ALARM_VIBRATE_PAUSE_MS 4000 // pause after each group before repeating
#endif
// Config file magic + version for forward compatibility
#define ALARM_CFG_MAGIC 0x4D4B414C // "MKAL"
#define ALARM_CFG_VERSION 2 // v2: ALARM_SOUND_MAX increased to 128
@@ -145,6 +164,33 @@ private:
String _resolvedSoundPath; // Full path of currently playing alarm sound
int _restartAttempts; // Retry counter for audio restart loop
// Synthetic rows shown above the file list in the sound picker.
// On MAX this is the "Buzzer (vibrate)" row; elsewhere there are none.
#if defined(LilyGo_TDeck_Pro_Max)
static const int kVibrateRows = 1;
#else
static const int kVibrateRows = 0;
#endif
#if defined(LilyGo_TDeck_Pro_Max)
// Silent (vibrate) alarm cadence state
bool _vibrating;
int _vibBuzzCount; // Buzzes fired in the current group (0..GROUP)
unsigned long _vibNextMs; // millis() of the next buzz/pause transition
static bool slotIsVibrate(const AlarmSlot& s) {
return (uint8_t)s.sound[0] == (uint8_t)ALARM_VIBRATE_MARKER[0];
}
void startVibrate() {
meck_alarm_haptic_begin();
_vibrating = true;
meck_alarm_haptic_buzz();
_vibBuzzCount = 1;
_vibNextMs = millis() + ALARM_VIBRATE_BUZZ_MS;
}
#endif
// ---- Day-of-week helpers ----
static const char* dowShort(int dow) {
@@ -371,6 +417,13 @@ private:
_restartAttempts = 0;
Serial.println("ALARM: Audio stopped");
}
#if defined(LilyGo_TDeck_Pro_Max)
if (_vibrating) {
_vibrating = false;
_vibBuzzCount = 0;
meck_alarm_haptic_stop();
}
#endif
}
// ---- Standard footer (matching all Meck screens) ----
@@ -507,6 +560,12 @@ private:
snprintf(fields[FIELD_VOLUME].value, 32, "%d", _editCopy.volume);
fields[FIELD_VOLUME].label = "Volume";
#if defined(LilyGo_TDeck_Pro_Max)
if (slotIsVibrate(_editCopy)) {
strncpy(fields[FIELD_SOUND].value, "Buzzer (vibrate)", 31);
fields[FIELD_SOUND].value[31] = '\0';
} else
#endif
if (_editCopy.sound[0] != '\0') {
char sndDisplay[28];
strncpy(sndDisplay, _editCopy.sound, 27);
@@ -600,7 +659,7 @@ private:
display.setColor(DisplayDriver::LIGHT);
if (_soundFiles.empty()) {
if (_soundFiles.empty() && kVibrateRows == 0) {
display.setTextSize(0);
display.setCursor(0, 20);
display.print("No .mp3 files found.");
@@ -617,11 +676,12 @@ private:
int listTop = 13;
int listBottom = display.height() - 14;
int visibleItems = (listBottom - listTop) / itemHeight;
int totalItems = kVibrateRows + (int)_soundFiles.size();
if (_soundSelected < _soundScroll) _soundScroll = _soundSelected;
if (_soundSelected >= _soundScroll + visibleItems) _soundScroll = _soundSelected - visibleItems + 1;
for (int i = 0; i < visibleItems && (_soundScroll + i) < (int)_soundFiles.size(); i++) {
for (int i = 0; i < visibleItems && (_soundScroll + i) < totalItems; i++) {
int idx = _soundScroll + i;
int y = listTop + i * itemHeight;
@@ -633,10 +693,15 @@ private:
display.setColor(DisplayDriver::LIGHT);
}
// Display filename without extension
String displayName = _soundFiles[idx];
int dot = displayName.lastIndexOf('.');
if (dot > 0) displayName = displayName.substring(0, dot);
String displayName;
if (kVibrateRows && idx == 0) {
displayName = "Buzzer (vibrate)";
} else {
// Display filename without extension
displayName = _soundFiles[idx - kVibrateRows];
int dot = displayName.lastIndexOf('.');
if (dot > 0) displayName = displayName.substring(0, dot);
}
// Truncate if too long
if (displayName.length() > 34) displayName = displayName.substring(0, 34);
@@ -669,6 +734,12 @@ private:
display.drawTextCentered(display.width() / 2, 34, label);
// Sound name
#if defined(LilyGo_TDeck_Pro_Max)
if (slotIsVibrate(slot)) {
display.setTextSize(0);
display.drawTextCentered(display.width() / 2, 48, "Buzzer (vibrate)");
} else
#endif
if (slot.sound[0] != '\0') {
display.setTextSize(0);
char sndDisplay[24];
@@ -807,10 +878,15 @@ private:
scanSoundFiles();
_soundSelected = 0;
_soundScroll = 0;
#if defined(LilyGo_TDeck_Pro_Max)
if (slotIsVibrate(_editCopy)) {
_soundSelected = 0;
} else
#endif
if (_editCopy.sound[0] != '\0') {
for (int i = 0; i < (int)_soundFiles.size(); i++) {
if (_soundFiles[i] == String(_editCopy.sound)) {
_soundSelected = i;
_soundSelected = kVibrateRows + i;
break;
}
}
@@ -836,10 +912,15 @@ private:
scanSoundFiles();
_soundSelected = 0;
_soundScroll = 0;
#if defined(LilyGo_TDeck_Pro_Max)
if (slotIsVibrate(_editCopy)) {
_soundSelected = 0;
} else
#endif
if (_editCopy.sound[0] != '\0') {
for (int i = 0; i < (int)_soundFiles.size(); i++) {
if (_soundFiles[i] == String(_editCopy.sound)) {
_soundSelected = i;
_soundSelected = kVibrateRows + i;
break;
}
}
@@ -873,15 +954,25 @@ private:
return true;
}
if (c == 's' || c == 0xF1) {
if (_soundSelected < (int)_soundFiles.size() - 1) _soundSelected++;
if (_soundSelected < kVibrateRows + (int)_soundFiles.size() - 1) _soundSelected++;
return true;
}
// Enter - pick sound
if (c == '\r' || c == '\n') {
if (!_soundFiles.empty() && _soundSelected < (int)_soundFiles.size()) {
strncpy(_editCopy.sound, _soundFiles[_soundSelected].c_str(), ALARM_SOUND_MAX - 1);
#if defined(LilyGo_TDeck_Pro_Max)
if (_soundSelected == 0) {
// Synthetic "Buzzer (vibrate)" row
strncpy(_editCopy.sound, ALARM_VIBRATE_MARKER, ALARM_SOUND_MAX - 1);
_editCopy.sound[ALARM_SOUND_MAX - 1] = '\0';
} else
#endif
{
int fileIdx = _soundSelected - kVibrateRows;
if (fileIdx >= 0 && fileIdx < (int)_soundFiles.size()) {
strncpy(_editCopy.sound, _soundFiles[fileIdx].c_str(), ALARM_SOUND_MAX - 1);
_editCopy.sound[ALARM_SOUND_MAX - 1] = '\0';
}
}
_mode = EDIT_ALARM;
return true;
@@ -936,6 +1027,11 @@ public:
_alarmAudioActive = false;
_resolvedSoundPath = "";
_restartAttempts = 0;
#if defined(LilyGo_TDeck_Pro_Max)
_vibrating = false;
_vibBuzzCount = 0;
_vibNextMs = 0;
#endif
memset(_lastFiredEpoch, 0, sizeof(_lastFiredEpoch));
memset(&_editCopy, 0, sizeof(_editCopy));
loadConfig();
@@ -1023,7 +1119,14 @@ public:
_ringingStart = millis();
_lastFiredEpoch[slotIdx] = millis() / 1000; // Approximate — replaced by RTC below
_mode = RINGING;
startAlarmAudio(slotIdx);
#if defined(LilyGo_TDeck_Pro_Max)
if (slotIsVibrate(_config.slots[slotIdx])) {
startVibrate();
} else
#endif
{
startAlarmAudio(slotIdx);
}
Serial.printf("ALARM: Firing alarm %d (%02d:%02d)\n",
slotIdx + 1, _config.slots[slotIdx].hour, _config.slots[slotIdx].minute);
}
@@ -1038,6 +1141,28 @@ public:
// ---- Audio tick (called from main loop for alarm playback) ----
void alarmAudioTick() {
#if defined(LilyGo_TDeck_Pro_Max)
if (_vibrating) {
// Auto-timeout (mirrors the audio path's 5-minute auto-dismiss)
if (_ringing && (millis() - _ringingStart > ALARM_RINGING_TIMEOUT_MS)) {
dismiss();
return;
}
// ALARM_VIBRATE_GROUP buzzes spaced ALARM_VIBRATE_BUZZ_MS apart, then a
// ALARM_VIBRATE_PAUSE_MS pause, repeating until dismiss/snooze/timeout.
if ((long)(millis() - _vibNextMs) >= 0) {
meck_alarm_haptic_buzz();
_vibBuzzCount++;
if (_vibBuzzCount >= ALARM_VIBRATE_GROUP) {
_vibBuzzCount = 0;
_vibNextMs = millis() + ALARM_VIBRATE_PAUSE_MS;
} else {
_vibNextMs = millis() + ALARM_VIBRATE_BUZZ_MS;
}
}
return;
}
#endif
if (!_audio || !_alarmAudioActive) return;
_audio->loop();
+33
View File
@@ -6,6 +6,10 @@
#include "ES8311.h" // MAX: native ES8311 codec init (Arduino Wire)
#endif
#if defined(LilyGo_TDeck_Pro_Max)
#include "DRV2605Haptic.h" // MAX: haptic motor for the silent (vibrate) alarm
#endif
TDeckProMaxBoard board;
#if defined(P_LORA_SCLK)
@@ -109,4 +113,33 @@ void meck_audio_codec_init() {
static bool es8311_ready = false;
if (!es8311_ready) es8311_ready = es8311_init_44100_16bit();
}
#endif
#if defined(LilyGo_TDeck_Pro_Max)
// Haptic shims for the silent (vibrate) alarm. The alarm UI header cannot see
// the board object or the DRV2605 driver (same reason the audio shims above
// exist), so these free functions wrap them. The DRV2605 motor supply is gated
// by the XL9555 (motorEnable) and MUST be on before begin() or the device will
// not ACK. This haptic object is independent of the one UITask uses for the
// "Buzzer (vibrate)" notification channels.
static DRV2605Haptic s_alarm_haptic;
static bool s_alarm_haptic_ready = false;
// Power the motor rail and bring the DRV2605 up. Re-running is harmless: the
// XL9555 write latches and begin() re-applies the same register sequence.
void meck_alarm_haptic_begin() {
board.motorEnable();
delay(10); // let the motor rail settle before I2C
s_alarm_haptic_ready = s_alarm_haptic.begin();
}
// Fire one strong buzz (effect 14 ~1s, matching the notification path).
void meck_alarm_haptic_buzz() {
if (s_alarm_haptic_ready) s_alarm_haptic.buzz(14);
}
// Cut the motor rail when the alarm stops/snoozes/dismisses.
void meck_alarm_haptic_stop() {
board.motorDisable();
}
#endif