mirror of
https://github.com/pelgraine/Meck.git
synced 2026-07-06 01:31:17 +02:00
276 lines
8.2 KiB
C++
276 lines
8.2 KiB
C++
#pragma once
|
|
|
|
// =============================================================================
|
|
// NotifSounds.h -- Per-channel notification sound configuration
|
|
//
|
|
// Stores a custom sound filename per channel for notification tones.
|
|
// Config persisted to /meshcore/notif_sounds.cfg on SD card.
|
|
//
|
|
// Audio variant: Sound files are MP3s in /alarms/ (shared with alarm clock).
|
|
// Playback is request-based: UITask calls requestPlay() when a message
|
|
// arrives on a channel with a custom tone. main.cpp loop() polls
|
|
// hasPendingPlay() and drives the Audio* object.
|
|
//
|
|
// 4G variant: Sound files are 8kHz mono WAVs embedded in firmware
|
|
// (ModemBundledSounds.h) and transferred to modem C:/ filesystem on boot.
|
|
// UITask calls modemManager.requestNotifTone() directly.
|
|
//
|
|
// Guard: MECK_AUDIO_VARIANT || HAS_4G_MODEM
|
|
// =============================================================================
|
|
|
|
#if defined(MECK_AUDIO_VARIANT) || defined(HAS_4G_MODEM)
|
|
|
|
#ifndef NOTIF_SOUNDS_H
|
|
#define NOTIF_SOUNDS_H
|
|
|
|
#include <Arduino.h>
|
|
#include <SD.h>
|
|
#include <vector>
|
|
#include <algorithm>
|
|
#include "variant.h"
|
|
|
|
#ifdef HAS_4G_MODEM
|
|
#include "ModemBundledSounds.h"
|
|
#endif
|
|
|
|
#ifndef MAX_GROUP_CHANNELS
|
|
#define MAX_GROUP_CHANNELS 20
|
|
#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"
|
|
#define NOTIF_SOUND_VERSION 1
|
|
#ifndef ALARMS_FOLDER
|
|
#define ALARMS_FOLDER "/alarms"
|
|
#endif
|
|
|
|
struct __attribute__((packed)) NotifSoundCfgHeader {
|
|
uint32_t magic;
|
|
uint8_t version;
|
|
uint8_t count; // Number of slots stored
|
|
uint8_t reserved[2];
|
|
// Followed by count * NOTIF_SOUND_NAME_MAX bytes
|
|
};
|
|
|
|
class NotifSounds {
|
|
public:
|
|
NotifSounds() {
|
|
memset(_sounds, 0, sizeof(_sounds));
|
|
#ifdef MECK_AUDIO_VARIANT
|
|
_pendingPlay = false;
|
|
_pendingFile[0] = '\0';
|
|
#endif
|
|
}
|
|
|
|
void begin() {
|
|
loadConfig();
|
|
Serial.println("NotifSounds: Config loaded");
|
|
}
|
|
|
|
// --- Config accessors ---
|
|
|
|
const char* getSoundForChannel(uint8_t channel_idx) const {
|
|
int slot = (channel_idx == 0xFF) ? MAX_GROUP_CHANNELS : (int)channel_idx;
|
|
if (slot < 0 || slot >= NOTIF_SOUND_SLOTS) return "";
|
|
return _sounds[slot];
|
|
}
|
|
|
|
bool hasSoundForChannel(uint8_t channel_idx) const {
|
|
const char* s = getSoundForChannel(channel_idx);
|
|
return s && s[0] != '\0';
|
|
}
|
|
|
|
void setSoundForChannel(uint8_t channel_idx, const char* filename) {
|
|
int slot = (channel_idx == 0xFF) ? MAX_GROUP_CHANNELS : (int)channel_idx;
|
|
if (slot < 0 || slot >= NOTIF_SOUND_SLOTS) return;
|
|
if (filename) {
|
|
strncpy(_sounds[slot], filename, NOTIF_SOUND_NAME_MAX - 1);
|
|
_sounds[slot][NOTIF_SOUND_NAME_MAX - 1] = '\0';
|
|
} else {
|
|
_sounds[slot][0] = '\0';
|
|
}
|
|
saveConfig();
|
|
Serial.printf("NotifSounds: Channel %d -> '%s'\n", (int)channel_idx,
|
|
_sounds[slot][0] ? _sounds[slot] : "(default)");
|
|
}
|
|
|
|
void clearSoundForChannel(uint8_t channel_idx) {
|
|
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() {
|
|
_soundFiles.clear();
|
|
|
|
#ifdef HAS_4G_MODEM
|
|
// 4G variant: available tones are embedded in firmware
|
|
for (int i = 0; i < MODEM_BUNDLED_TONE_COUNT; i++) {
|
|
_soundFiles.push_back(String(modemBundledTones[i].filename));
|
|
}
|
|
Serial.printf("NotifSounds: %d modem tones available\n", (int)_soundFiles.size());
|
|
|
|
#else // MECK_AUDIO_VARIANT
|
|
// Audio variant: scan SD card /alarms/ folder for MP3 files
|
|
if (!SD.exists(ALARMS_FOLDER)) {
|
|
SD.mkdir(ALARMS_FOLDER);
|
|
}
|
|
File root = SD.open(ALARMS_FOLDER);
|
|
if (!root || !root.isDirectory()) {
|
|
digitalWrite(SDCARD_CS, HIGH);
|
|
return;
|
|
}
|
|
File entry = root.openNextFile();
|
|
while (entry) {
|
|
if (!entry.isDirectory()) {
|
|
String name = entry.name();
|
|
if (name.length() > 0 && name.charAt(0) != '.') {
|
|
String lower = name;
|
|
lower.toLowerCase();
|
|
if (lower.endsWith(".mp3")) {
|
|
_soundFiles.push_back(name);
|
|
}
|
|
}
|
|
}
|
|
entry = root.openNextFile();
|
|
}
|
|
root.close();
|
|
digitalWrite(SDCARD_CS, HIGH);
|
|
std::sort(_soundFiles.begin(), _soundFiles.end());
|
|
Serial.printf("NotifSounds: Found %d sound files\n", (int)_soundFiles.size());
|
|
#endif
|
|
}
|
|
|
|
int getSoundFileCount() const { return (int)_soundFiles.size(); }
|
|
const String& getSoundFile(int idx) const { return _soundFiles[idx]; }
|
|
const std::vector<String>& getSoundFiles() const { return _soundFiles; }
|
|
|
|
#ifdef HAS_4G_MODEM
|
|
// Get the display label for a tone file (4G variant only).
|
|
// Returns the human-readable label from ModemBundledSounds.
|
|
const char* getToneLabel(int idx) const {
|
|
if (idx < 0 || idx >= MODEM_BUNDLED_TONE_COUNT) return "";
|
|
return modemBundledTones[idx].label;
|
|
}
|
|
#endif
|
|
|
|
// --- Pending playback request (audio variant only) ---
|
|
// The 4G variant calls modemManager.requestNotifTone() directly
|
|
// from UITask; no pending mechanism needed.
|
|
|
|
#ifdef MECK_AUDIO_VARIANT
|
|
void requestPlay(const char* fullPath) {
|
|
strncpy(_pendingFile, fullPath, sizeof(_pendingFile) - 1);
|
|
_pendingFile[sizeof(_pendingFile) - 1] = '\0';
|
|
_pendingPlay = true;
|
|
}
|
|
|
|
bool hasPendingPlay() const { return _pendingPlay; }
|
|
|
|
const char* getPendingFile() const { return _pendingFile; }
|
|
|
|
void clearPending() {
|
|
_pendingPlay = false;
|
|
_pendingFile[0] = '\0';
|
|
}
|
|
#endif
|
|
|
|
private:
|
|
char _sounds[NOTIF_SOUND_SLOTS][NOTIF_SOUND_NAME_MAX];
|
|
std::vector<String> _soundFiles;
|
|
|
|
#ifdef MECK_AUDIO_VARIANT
|
|
bool _pendingPlay;
|
|
char _pendingFile[48];
|
|
#endif
|
|
|
|
void loadConfig() {
|
|
memset(_sounds, 0, sizeof(_sounds));
|
|
|
|
if (!SD.exists(NOTIF_SOUND_CONFIG_PATH)) {
|
|
Serial.println("NotifSounds: No config file, using defaults");
|
|
digitalWrite(SDCARD_CS, HIGH);
|
|
return;
|
|
}
|
|
|
|
File f = SD.open(NOTIF_SOUND_CONFIG_PATH, "r");
|
|
if (!f) {
|
|
Serial.println("NotifSounds: Failed to open config");
|
|
return;
|
|
}
|
|
|
|
NotifSoundCfgHeader hdr;
|
|
if (f.read((uint8_t*)&hdr, sizeof(hdr)) != sizeof(hdr) ||
|
|
hdr.magic != NOTIF_SOUND_MAGIC || hdr.version != NOTIF_SOUND_VERSION) {
|
|
Serial.println("NotifSounds: Config invalid or wrong version");
|
|
f.close();
|
|
digitalWrite(SDCARD_CS, HIGH);
|
|
return;
|
|
}
|
|
|
|
int slotsToRead = hdr.count;
|
|
if (slotsToRead > NOTIF_SOUND_SLOTS) slotsToRead = NOTIF_SOUND_SLOTS;
|
|
|
|
for (int i = 0; i < slotsToRead; i++) {
|
|
if (f.read((uint8_t*)_sounds[i], NOTIF_SOUND_NAME_MAX) != NOTIF_SOUND_NAME_MAX) {
|
|
break;
|
|
}
|
|
_sounds[i][NOTIF_SOUND_NAME_MAX - 1] = '\0'; // Safety null-terminate
|
|
}
|
|
|
|
f.close();
|
|
digitalWrite(SDCARD_CS, HIGH);
|
|
Serial.printf("NotifSounds: Loaded %d slots from config\n", slotsToRead);
|
|
}
|
|
|
|
void saveConfig() {
|
|
if (!SD.exists("/meshcore")) {
|
|
SD.mkdir("/meshcore");
|
|
}
|
|
|
|
File f = SD.open(NOTIF_SOUND_CONFIG_PATH, FILE_WRITE);
|
|
if (!f) {
|
|
Serial.println("NotifSounds: Failed to save config");
|
|
return;
|
|
}
|
|
|
|
NotifSoundCfgHeader hdr;
|
|
hdr.magic = NOTIF_SOUND_MAGIC;
|
|
hdr.version = NOTIF_SOUND_VERSION;
|
|
hdr.count = NOTIF_SOUND_SLOTS;
|
|
hdr.reserved[0] = 0;
|
|
hdr.reserved[1] = 0;
|
|
f.write((uint8_t*)&hdr, sizeof(hdr));
|
|
|
|
for (int i = 0; i < NOTIF_SOUND_SLOTS; i++) {
|
|
f.write((uint8_t*)_sounds[i], NOTIF_SOUND_NAME_MAX);
|
|
}
|
|
|
|
f.close();
|
|
digitalWrite(SDCARD_CS, HIGH);
|
|
}
|
|
};
|
|
|
|
// Global singleton
|
|
extern NotifSounds notifSounds;
|
|
|
|
#endif // NOTIF_SOUNDS_H
|
|
#endif // MECK_AUDIO_VARIANT || HAS_4G_MODEM
|