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")
+80
View File
@@ -0,0 +1,80 @@
#pragma once
#include <Arduino.h>
#include <Wire.h>
// Minimal inline DRV2605 haptic driver for the T-Deck Pro MAX.
//
// The DRV2605 sits on the shared Wire bus at I2C 0x5A. Its motor supply is
// gated by the XL9555 expander (board.motorEnable()), which MUST be switched on
// before begin() or the device will not ACK.
//
// The register sequence mirrors Adafruit_DRV2605::begin() followed by
// selectLibrary(1) + setMode(INTTRIG) - i.e. exactly what the LilyGo basic.ino
// example does: an ERM motor, open-loop, internal-trigger, effect library 1.
class DRV2605Haptic {
public:
DRV2605Haptic(uint8_t addr = 0x5A, TwoWire* wire = &Wire)
: _addr(addr), _wire(wire) {}
// Returns false if the DRV2605 does not ACK (e.g. motor rail still off).
bool begin() {
_wire->beginTransmission(_addr);
if (_wire->endTransmission() != 0) return false;
writeReg(REG_MODE, 0x00); // exit standby, internal trigger
writeReg(REG_RTPIN, 0x00); // no real-time playback input
writeReg(REG_WAVESEQ1, 1); // default effect: strong click
writeReg(REG_WAVESEQ2, 0); // end of sequence
writeReg(REG_OVERDRIVE, 0);
writeReg(REG_SUSTAINPOS, 0);
writeReg(REG_SUSTAINNEG, 0);
writeReg(REG_BREAK, 0);
writeReg(REG_AUDIOMAX, 0x64);
writeReg(REG_FEEDBACK, readReg(REG_FEEDBACK) & 0x7F); // N_ERM_LRA = 0 -> ERM
writeReg(REG_CONTROL3, readReg(REG_CONTROL3) | 0x20); // ERM_OPEN_LOOP = 1
writeReg(REG_LIBRARY, 1); // ERM effect library 1
writeReg(REG_MODE, 0x00); // internal trigger
return true;
}
// Fire a single library effect (1 = strong click, 100%).
void buzz(uint8_t effect = 1) {
writeReg(REG_WAVESEQ1, effect);
writeReg(REG_WAVESEQ2, 0);
writeReg(REG_GO, 1);
}
private:
static const uint8_t REG_MODE = 0x01;
static const uint8_t REG_RTPIN = 0x02;
static const uint8_t REG_LIBRARY = 0x03;
static const uint8_t REG_WAVESEQ1 = 0x04;
static const uint8_t REG_WAVESEQ2 = 0x05;
static const uint8_t REG_GO = 0x0C;
static const uint8_t REG_OVERDRIVE = 0x0D;
static const uint8_t REG_SUSTAINPOS = 0x0E;
static const uint8_t REG_SUSTAINNEG = 0x0F;
static const uint8_t REG_BREAK = 0x10;
static const uint8_t REG_AUDIOMAX = 0x13;
static const uint8_t REG_FEEDBACK = 0x1A;
static const uint8_t REG_CONTROL3 = 0x1D;
uint8_t _addr;
TwoWire* _wire;
void writeReg(uint8_t reg, uint8_t val) {
_wire->beginTransmission(_addr);
_wire->write(reg);
_wire->write(val);
_wire->endTransmission();
}
uint8_t readReg(uint8_t reg) {
_wire->beginTransmission(_addr);
_wire->write(reg);
_wire->endTransmission(false);
_wire->requestFrom(_addr, (uint8_t)1);
return _wire->available() ? _wire->read() : 0;
}
};