Max - New buzzer(vibrate) option in custom channel notification preferences - silent buzz notification.

This commit is contained in:
pelgraine
2026-06-04 23:54:07 +10:00
parent b6e3c7e0a5
commit fb4d8273a9
5 changed files with 184 additions and 15 deletions
+25 -1
View File
@@ -12,6 +12,10 @@
#include "ModemManager.h" // Serial CLI modem commands
#endif
#if defined(LilyGo_TDeck_Pro_Max)
#include "DRV2605Haptic.h" // TEMP: inline haptic driver for the 'buzz' CLI test command
#endif
#define CMD_APP_START 1
#define CMD_SEND_TXT_MSG 2
#define CMD_SEND_CHANNEL_TXT_MSG 3
@@ -3506,7 +3510,27 @@ void MyMesh::checkCLIRescueCmd() {
}
} else if (strcmp(cli_command, "reboot") == 0) {
}
#if defined(LilyGo_TDeck_Pro_Max)
else if (strcmp(cli_command, "buzz") == 0) {
// TEMP: fire the DRV2605 haptic motor once to confirm it works.
// Lazy-inits the driver (and motor power rail) on first invocation.
static DRV2605Haptic haptic;
static bool haptic_ready = false;
if (!haptic_ready) {
board.motorEnable();
delay(10); // let the motor rail settle before I2C
haptic_ready = haptic.begin();
}
if (haptic_ready) {
haptic.buzz(1);
Serial.println(" > buzz");
} else {
Serial.println(" > buzz: DRV2605 not found");
}
}
#endif
else if (strcmp(cli_command, "reboot") == 0) {
board.reboot(); // doesn't return
} else {
Serial.println(" Error: unknown command (try 'help')");
@@ -38,6 +38,10 @@
#endif
#define NOTIF_SOUND_NAME_MAX 32
// Reserved one-byte sentinel stored in a channel's name slot to mean
// "Buzzer (vibrate)" instead of a sound filename. 0x01 can never be the first
// byte of a real filename, so it is unambiguous and needs no config change.
#define NOTIF_VIBRATE_MARKER "\x01"
#define NOTIF_SOUND_SLOTS (MAX_GROUP_CHANNELS + 1) // +1 for DMs
#define NOTIF_SOUND_CONFIG_PATH "/meshcore/notif_sounds.cfg"
#define NOTIF_SOUND_MAGIC 0x4E534E44 // "NSND"
@@ -100,6 +104,18 @@ public:
setSoundForChannel(channel_idx, nullptr);
}
// --- Vibrate (haptic) selection ---
// "Buzzer (vibrate)" is stored as a reserved marker in the channel's name
// slot, so it persists through the existing config save/load unchanged.
bool isVibrateForChannel(uint8_t channel_idx) const {
const char* s = getSoundForChannel(channel_idx);
return s && (uint8_t)s[0] == (uint8_t)NOTIF_VIBRATE_MARKER[0];
}
void setVibrateForChannel(uint8_t channel_idx) {
setSoundForChannel(channel_idx, NOTIF_VIBRATE_MARKER);
}
// --- Sound file scanning ---
void scanSoundFiles() {
@@ -2345,9 +2345,14 @@ public:
int maxVisible = (listBot - listTop) / lineH;
if (maxVisible < 3) maxVisible = 3;
// Total items: 1 ("Default") + sound file count
// Total items: "Default" (+ "Buzzer (vibrate)" on MAX) + sound file count
const auto& files = notifSounds.getSoundFiles();
int totalItems = 1 + (int)files.size();
#if defined(LilyGo_TDeck_Pro_Max)
const int kFileBase = 2; // 0=Default, 1=Buzzer(vibrate), 2+=files
#else
const int kFileBase = 1; // 0=Default, 1+=files
#endif
int totalItems = kFileBase + (int)files.size();
// Centre scroll on selection
_notifSoundScroll = max(0, min(_notifSoundSelected - maxVisible / 2,
@@ -2370,9 +2375,13 @@ public:
display.setCursor(bx + 6, sy);
if (i == 0) {
display.print("Default (silent)");
#if defined(LilyGo_TDeck_Pro_Max)
} else if (i == 1) {
display.print("Buzzer (vibrate)");
#endif
} else {
// Show filename without extension
String displayName = files[i - 1];
String displayName = files[i - kFileBase];
int dot = displayName.lastIndexOf('.');
if (dot > 0) displayName = displayName.substring(0, dot);
if (displayName.length() > 28) displayName = displayName.substring(0, 28);
@@ -2954,7 +2963,12 @@ public:
#if defined(MECK_AUDIO_VARIANT) || defined(HAS_4G_MODEM)
if (_editMode == EDIT_NOTIF_SOUND) {
const auto& files = notifSounds.getSoundFiles();
int totalItems = 1 + (int)files.size(); // 0=Default, rest=files
#if defined(LilyGo_TDeck_Pro_Max)
const int kFileBase = 2; // 0=Default, 1=Buzzer(vibrate), 2+=files
#else
const int kFileBase = 1; // 0=Default, 1+=files
#endif
int totalItems = kFileBase + (int)files.size();
if (c == 'w' || c == 'W' || c == 0xF2 || c == KEY_UP) {
if (_notifSoundSelected > 0) _notifSoundSelected--;
@@ -2965,11 +2979,15 @@ public:
return true;
}
if (c == '\r' || c == 13) {
// Select: 0 = clear (default silent), 1+ = file
// Select: 0 = clear (default silent), [MAX: 1 = vibrate], rest = file
if (_notifSoundSelected == 0) {
notifSounds.clearSoundForChannel(_notifSoundChannel);
#if defined(LilyGo_TDeck_Pro_Max)
} else if (_notifSoundSelected == 1) {
notifSounds.setVibrateForChannel(_notifSoundChannel);
#endif
} else {
int fileIdx = _notifSoundSelected - 1;
int fileIdx = _notifSoundSelected - kFileBase;
if (fileIdx >= 0 && fileIdx < (int)files.size()) {
notifSounds.setSoundForChannel(_notifSoundChannel, files[fileIdx].c_str());
}
@@ -3904,14 +3922,26 @@ public:
notifSounds.scanSoundFiles();
_notifSoundSelected = 0; // 0 = "Default (silent)"
_notifSoundScroll = 0;
#if defined(LilyGo_TDeck_Pro_Max)
const int kFileBase = 2; // 0=Default, 1=Buzzer(vibrate), 2+=files
#else
const int kFileBase = 1; // 0=Default, 1+=files
#endif
// Pre-select current assignment
const char* current = notifSounds.getSoundForChannel(_notifSoundChannel);
if (current && current[0] != '\0') {
const auto& files = notifSounds.getSoundFiles();
for (int i = 0; i < (int)files.size(); i++) {
if (files[i] == String(current)) {
_notifSoundSelected = i + 1; // +1 because 0 is "Default"
break;
#if defined(LilyGo_TDeck_Pro_Max)
if (notifSounds.isVibrateForChannel(_notifSoundChannel)) {
_notifSoundSelected = 1; // 1 = "Buzzer (vibrate)"
} else
#endif
{
const char* current = notifSounds.getSoundForChannel(_notifSoundChannel);
if (current && current[0] != '\0') {
const auto& files = notifSounds.getSoundFiles();
for (int i = 0; i < (int)files.size(); i++) {
if (files[i] == String(current)) {
_notifSoundSelected = i + kFileBase;
break;
}
}
}
}
+20 -1
View File
@@ -19,6 +19,9 @@
#include "MapScreen.h"
#endif
#include "target.h"
#if defined(LilyGo_TDeck_Pro_Max)
#include "DRV2605Haptic.h" // haptic motor for "Buzzer (vibrate)" channels
#endif
#if defined(LilyGo_T5S3_EPaper_Pro) || defined(MECK_AUDIO_VARIANT)
#include "HomeIcons.h"
#endif
@@ -1645,6 +1648,21 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
// Set the flag for notify() which is called immediately after newMsg().
// If a custom notification tone is assigned and notifications are active,
// request MP3 playback and suppress the RTTTL buzzer so they don't overlap.
#if defined(LilyGo_TDeck_Pro_Max)
// Channel set to "Buzzer (vibrate)": pulse the motor instead of a tone.
if (!suppressNotif && notifSounds.isVibrateForChannel(channel_idx)) {
static DRV2605Haptic s_haptic;
static bool s_hapticReady = false;
if (!s_hapticReady) {
board.motorEnable();
delay(10); // let the motor rail settle before I2C
s_hapticReady = s_haptic.begin();
}
if (s_hapticReady) s_haptic.buzz(1);
s_lastMsgSuppressed = true; // suppress the default RTTTL buzzer
} else
#endif
{
#ifdef MECK_AUDIO_VARIANT
if (!suppressNotif) {
const char* customSound = notifSounds.getSoundForChannel(channel_idx);
@@ -1677,7 +1695,8 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
#else
s_lastMsgSuppressed = suppressNotif;
#endif
}
// Add to channel history screen with channel index, path data, and SNR
// For DMs (channel_idx == 0xFF):
// - Regular DMs: prefix text with sender name ("NodeName: hello")