Files
firmware/src/modules/ExternalNotificationModule.cpp
T
Thomas Göttgens 14eaa5587d Honor mute when waking the screen for a received message (#11688)
* fix(ui): honor mute when waking the screen for a received message

TextMessageModule fired powerFSM.trigger(EVENT_RECEIVED_MSG) for every text
packet, gated only by shouldWakeOnReceivedMessage(), which checks external
notification, device role and battery level but never the mute flags. A muted
channel therefore suppressed the banner and still lit the screen.

MessageRenderer::handleNewMessage() only computed mute for MessageType::BROADCAST,
so a DM from a muted node produced a banner and a wake.

Add isMutedForPacket() in Channels: a DM addressed to us reads the sender's
NodeInfoLite mute bit, every other packet reads the mute bit of the channel it
arrived on. This is the predicate ExternalNotificationModule already applied to
the buzzer, vibra and LED outputs, hoisted so all three call sites share it.

Bell and alert messages still break through mute on both paths, unchanged.

No protobuf or config change: ChannelSettings.module_settings.is_muted and the
NodeInfoLite mute bit already exist and are already settable from the device menu
and via AdminMessage.toggle_muted_node.

Closes #11674

* fix(ui): let an alert break through mute on the screen wake path

In COLOR display mode TextMessageModule skips handleNewMessage(), so
powerFSM.trigger(EVENT_RECEIVED_MSG) is the only wake an alert gets. Gating it
on mute alone dropped that wake for a bell on a muted channel.

Add MeshService::isAlertPayload(): an ASCII BEL in the payload while at least one
alert_bell_* output is enabled. The wake gate is now "not muted, or an alert".
MessageRenderer uses the same predicate instead of its own inline bell scan,
which also lifts that scan's arbitrary 100 byte cap.

Rename three test cases. Their names carried exactly 35 characters after the
test_ prefix, which matches the Lob API key format and tripped trufflehog in the
trunk check gate.
2026-09-01 11:52:40 +00:00

634 lines
24 KiB
C++

/**
* @file ExternalNotificationModule.cpp
* @brief Implementation of the ExternalNotificationModule class.
*
* This file contains the implementation of the ExternalNotificationModule class, which is responsible for handling external
* notifications such as vibration, buzzer, and LED lights. The class provides methods to turn on and off the external
* notification outputs and to play ringtones using PWM buzzer. It also includes default configurations and a runOnce() method to
* handle the module's behavior.
*
* Documentation:
* https://meshtastic.org/docs/configuration/module/external-notification
*
* @author Jm Casler & Meshtastic Team
* @date [Insert Date]
*/
#include "ExternalNotificationModule.h"
#include "Channels.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "Router.h"
#include "buzz/buzz.h"
#include "configuration.h"
#include "gps/RTC.h"
#include "main.h"
#include "mesh/Throttle.h"
#include "mesh/generated/meshtastic/rtttl.pb.h"
#include <Arduino.h>
#if defined(HAS_RGB_LED)
#include "AmbientLightingThread.h"
uint8_t red = 0;
uint8_t green = 0;
uint8_t blue = 0;
uint8_t white = 0;
uint8_t colorState = 1;
uint8_t brightnessIndex = 0;
uint8_t brightnessValues[] = {0, 10, 20, 30, 50, 90, 160, 170}; // blue gets multiplied by 1.5
bool ascending = true;
#endif
#ifndef PIN_BUZZER
#define PIN_BUZZER false
#endif
#if defined(HAS_I2S_SPEAKER_NRF52)
#include "platform/nrf52/NRF52RtttlPlayer.h"
#endif
/*
Documentation:
https://meshtastic.org/docs/configuration/module/external-notification
*/
// Default configurations
#ifdef EXT_NOTIFY_OUT
#define EXT_NOTIFICATION_MODULE_OUTPUT EXT_NOTIFY_OUT
#else
#define EXT_NOTIFICATION_MODULE_OUTPUT 0
#endif
#define EXT_NOTIFICATION_MODULE_OUTPUT_MS 1000
#define EXT_NOTIFICATION_FAST_THREAD_MS 25
#define ASCII_BELL 0x07
#if !MESHTASTIC_EXCLUDE_RTTTL
meshtastic_RTTTLConfig rtttlConfig;
static const char *rtttlConfigFile = "/prefs/ringtone.proto";
#endif
ExternalNotificationModule *externalNotificationModule;
bool externalCurrentState[3] = {};
uint32_t externalTurnedOn[3] = {};
int32_t ExternalNotificationModule::runOnce()
{
if (!moduleConfig.external_notification.enabled) {
return INT32_MAX; // we don't need this thread here...
} else {
uint32_t delay = EXT_NOTIFICATION_MODULE_OUTPUT_MS;
bool isRtttlPlaying = rtttl::isPlaying();
#ifdef HAS_I2S
// audioThread->isPlaying() also handles actually playing the RTTTL, needs to be called in loop
isRtttlPlaying = isRtttlPlaying || audioThread->isPlaying();
#endif
#if defined(HAS_I2S_SPEAKER_NRF52)
isRtttlPlaying = isRtttlPlaying || nrf52RtttlPlayer.isPlaying();
#endif
// isNagging is the armed flag; nagCycleCutoff holds a real deadline only while it is set
// (UINT32_MAX once stopped, 1 at boot), so short-circuit before the comparison.
const bool nagWindowExpired = !isNagging || Throttle::deadlinePassed(nagCycleCutoff);
if (nagWindowExpired && !isRtttlPlaying) {
// Turn off external notification immediately when timeout is reached, regardless of song state
nagCycleCutoff = UINT32_MAX;
ExternalNotificationModule::stopNow();
isNagging = false;
return INT32_MAX; // save cycles till we're needed again
}
// If the output is turned on, turn it back off after the given period of time.
if (isNagging) {
delay = (moduleConfig.external_notification.output_ms ? moduleConfig.external_notification.output_ms
: EXT_NOTIFICATION_MODULE_OUTPUT_MS);
// externalTurnedOn[] is when each output was last toggled, so these are intervals.
if (Throttle::hasElapsed(externalTurnedOn[0], delay)) {
setExternalState(0, !getExternal(0));
}
if (Throttle::hasElapsed(externalTurnedOn[1], delay)) {
setExternalState(1, !getExternal(1));
}
// Only toggle buzzer output if not using PWM mode (to avoid conflict with RTTTL)
if (!moduleConfig.external_notification.use_pwm && Throttle::hasElapsed(externalTurnedOn[2], delay)) {
LOG_DEBUG("EXTERNAL 2 %d compared to %d", externalTurnedOn[2] + moduleConfig.external_notification.output_ms,
millis());
setExternalState(2, !getExternal(2));
}
#if defined(HAS_RGB_LED)
red = (colorState & 4) ? brightnessValues[brightnessIndex] : 0; // Red enabled on colorState = 4,5,6,7
green = (colorState & 2) ? brightnessValues[brightnessIndex] : 0; // Green enabled on colorState = 2,3,6,7
blue = (colorState & 1) ? (brightnessValues[brightnessIndex] * 1.5) : 0; // Blue enabled on colorState = 1,3,5,7
white = (colorState & 12) ? brightnessValues[brightnessIndex] : 0;
if (ascending) { // fade in
brightnessIndex++;
if (brightnessIndex == (sizeof(brightnessValues) - 1)) {
ascending = false;
}
} else {
brightnessIndex--; // fade out
}
if (brightnessIndex == 0) {
ascending = true;
colorState++; // next color
if (colorState > 7) {
colorState = 1;
}
}
// we need fast updates for the color change
delay = EXT_NOTIFICATION_FAST_THREAD_MS;
#endif
#ifdef HAS_DRV2605
// Only trigger DRV2605 if vibration alerts are enabled
if (moduleConfig.external_notification.alert_message_vibra || moduleConfig.external_notification.alert_bell_vibra) {
drv.go();
}
#endif
}
// Play RTTTL over i2s audio interface if enabled as buzzer
#if defined(HAS_I2S) && !MESHTASTIC_EXCLUDE_RTTTL
if (moduleConfig.external_notification.use_i2s_as_buzzer) {
if (audioThread->isPlaying()) {
// Continue playing
} else if (isNagging && !Throttle::deadlinePassed(nagCycleCutoff)) {
audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone));
}
// we need fast updates to play the RTTTL
delay = EXT_NOTIFICATION_FAST_THREAD_MS;
}
#endif
#if defined(HAS_I2S_SPEAKER_NRF52) && !MESHTASTIC_EXCLUDE_RTTTL
// Play RTTTL over the I2S speaker (no piezo on this board).
if (canBuzz() && buzzerShouldAlert) {
if (nrf52RtttlPlayer.isPlaying()) {
nrf52RtttlPlayer.play();
} else if (isNagging && !Throttle::deadlinePassed(nagCycleCutoff)) {
nrf52RtttlPlayer.begin(rtttlConfig.ringtone);
}
delay = EXT_NOTIFICATION_FAST_THREAD_MS;
}
#endif
#if !MESHTASTIC_EXCLUDE_RTTTL
// now let the PWM buzzer play
if (moduleConfig.external_notification.use_pwm && config.device.buzzer_gpio && canBuzz() && buzzerShouldAlert) {
if (rtttl::isPlaying()) {
rtttl::play();
} else if (isNagging && !Throttle::deadlinePassed(nagCycleCutoff)) {
// start the song again if we have time left
rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone);
}
// we need fast updates to play the RTTTL
delay = EXT_NOTIFICATION_FAST_THREAD_MS;
}
#endif
return delay;
}
}
/**
* Based on buzzer mode, return true if we can buzz.
*/
bool ExternalNotificationModule::canBuzz()
{
if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED &&
config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY) {
return true;
}
return false;
}
bool ExternalNotificationModule::wantPacket(const meshtastic_MeshPacket *p)
{
return MeshService::isTextPayload(p);
}
/**
* Sets the external notification for the specified index.
*
* @param index The index of the external notification to change state.
* @param on Whether we are turning things on (true) or off (false).
*/
void ExternalNotificationModule::setExternalState(uint8_t index, bool on)
{
externalCurrentState[index] = on;
externalTurnedOn[index] = millis();
switch (index) {
case 1:
#ifdef UNPHONE
unphone.vibe(on); // the unPhone's vibration motor is on a i2c GPIO expander
#endif
if (moduleConfig.external_notification.output_vibra)
digitalWrite(moduleConfig.external_notification.output_vibra, on);
break;
case 2:
// Only control buzzer pin digitally if not using PWM mode
if (moduleConfig.external_notification.output_buzzer && !moduleConfig.external_notification.use_pwm)
digitalWrite(moduleConfig.external_notification.output_buzzer, on);
break;
default:
if (output > 0)
digitalWrite(output, (moduleConfig.external_notification.active ? on : !on));
#ifdef PCA_LED_NOTIFICATION
io.digitalWrite(PCA_LED_NOTIFICATION, on);
#endif
#ifdef NEOPIXEL_STATUS_NOTIFICATION_PIN
notificationPixel.setPixelColor(0, on ? NEOPIXEL_STATUS_NOTIFICATION_COLOR : 0);
notificationPixel.show();
#endif
break;
}
#if defined(HAS_RGB_LED)
if (!on) {
red = 0;
green = 0;
blue = 0;
white = 0;
}
if (ambientLightingThread) {
ambientLightingThread->setLighting(moduleConfig.ambient_lighting.current, red, green, blue);
}
#endif
#ifdef HAS_DRV2605
// Only trigger DRV2605 when setting vibration motor
bool shouldTriggerDRV = false;
if (on) {
if (index == 1 &&
(moduleConfig.external_notification.alert_message_vibra || moduleConfig.external_notification.alert_bell_vibra)) {
shouldTriggerDRV = true;
}
}
if (shouldTriggerDRV) {
drv.go();
} else if (!on && index == 1) {
drv.stop();
}
#endif
}
bool ExternalNotificationModule::getExternal(uint8_t index)
{
return externalCurrentState[index];
}
// Allow other firmware components to determine whether a notification is ongoing
bool ExternalNotificationModule::nagging()
{
return isNagging;
}
void ExternalNotificationModule::stopNow()
{
LOG_INFO("Turning off external notification: ");
LOG_INFO("Stop RTTTL playback");
rtttl::stop();
#ifdef HAS_I2S
LOG_INFO("Stop audioThread playback");
audioThread->stop();
#endif
#if defined(HAS_I2S_SPEAKER_NRF52)
nrf52RtttlPlayer.stop();
#endif
// Turn off all outputs
LOG_INFO("Turning off setExternalStates");
for (int i = 0; i < 3; i++) {
setExternalState(i, false);
externalTurnedOn[i] = 0;
}
setIntervalFromNow(0);
#ifdef HAS_DRV2605
drv.stop();
#endif
// Prevent the state machine from immediately re-triggering outputs after a manual stop.
isNagging = false;
buzzerShouldAlert = false;
nagCycleCutoff = UINT32_MAX;
#ifdef HAS_I2S
// GPIO0 is used as mclk for I2S audio and set to OUTPUT by the sound library
// T-Deck uses GPIO0 as trackball button, so restore the mode
#if defined(T_DECK) || (defined(BUTTON_PIN) && BUTTON_PIN == 0)
pinMode(0, INPUT);
#endif
#endif
}
ExternalNotificationModule::ExternalNotificationModule()
: SinglePortModule("ExternalNotificationModule", meshtastic_PortNum_TEXT_MESSAGE_APP),
concurrency::OSThread("ExternalNotification")
{
/*
Uncomment the preferences below if you want to use the module
without having to configure it from the PythonAPI or WebUI.
*/
// moduleConfig.external_notification.alert_message = true;
// moduleConfig.external_notification.alert_message_buzzer = true;
// moduleConfig.external_notification.alert_message_vibra = true;
// moduleConfig.external_notification.use_i2s_as_buzzer = true;
// moduleConfig.external_notification.active = true;
// moduleConfig.external_notification.alert_bell = 1;
// moduleConfig.external_notification.output_ms = 1000;
// moduleConfig.external_notification.output = 4; // RAK4631 IO4
// moduleConfig.external_notification.output_buzzer = 10; // RAK4631 IO6
// moduleConfig.external_notification.output_vibra = 28; // RAK4631 IO7
// moduleConfig.external_notification.nag_timeout = 300;
// T-Watch / T-Deck i2s audio as buzzer:
// moduleConfig.external_notification.enabled = true;
// moduleConfig.external_notification.nag_timeout = 300;
// moduleConfig.external_notification.output_ms = 1000;
// moduleConfig.external_notification.use_i2s_as_buzzer = true;
// moduleConfig.external_notification.alert_message_buzzer = true;
if (moduleConfig.external_notification.enabled) {
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
if (inputBroker) // put our callback in the inputObserver list
inputObserver.observe(inputBroker);
#endif
#if !MESHTASTIC_EXCLUDE_RTTTL
if (nodeDB->loadProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, sizeof(meshtastic_RTTTLConfig),
&meshtastic_RTTTLConfig_msg, &rtttlConfig) != LoadFileResult::LOAD_SUCCESS) {
memset(rtttlConfig.ringtone, 0, sizeof(rtttlConfig.ringtone));
// The default ringtone is always loaded from userPrefs.jsonc
strncpy(rtttlConfig.ringtone, USERPREFS_RINGTONE_RTTTL, sizeof(rtttlConfig.ringtone));
}
#endif
LOG_INFO("Init External Notification Module");
output = moduleConfig.external_notification.output ? moduleConfig.external_notification.output
: EXT_NOTIFICATION_MODULE_OUTPUT;
// Set the direction of a pin
if (output > 0) {
LOG_INFO("Use Pin %i in digital mode", output);
pinMode(output, OUTPUT);
}
#ifdef NEOPIXEL_STATUS_NOTIFICATION_PIN
LOG_INFO("Use WS2812 on GPIO %d as notification LED", NEOPIXEL_STATUS_NOTIFICATION_PIN);
notificationPixel.begin();
notificationPixel.clear();
notificationPixel.show();
#endif
setExternalState(0, false);
externalTurnedOn[0] = 0;
if (moduleConfig.external_notification.output_vibra) {
LOG_INFO("Use Pin %i for vibra motor", moduleConfig.external_notification.output_vibra);
pinMode(moduleConfig.external_notification.output_vibra, OUTPUT);
setExternalState(1, false);
externalTurnedOn[1] = 0;
}
if (moduleConfig.external_notification.output_buzzer && canBuzz()) {
if (!moduleConfig.external_notification.use_pwm) {
LOG_INFO("Use Pin %i for buzzer", moduleConfig.external_notification.output_buzzer);
pinMode(moduleConfig.external_notification.output_buzzer, OUTPUT);
setExternalState(2, false);
externalTurnedOn[2] = 0;
} else {
config.device.buzzer_gpio = config.device.buzzer_gpio ? config.device.buzzer_gpio : PIN_BUZZER;
// in PWM Mode we force the buzzer pin if it is set
LOG_INFO("Use Pin %i in PWM mode", config.device.buzzer_gpio);
}
}
} else {
LOG_INFO("External Notification Module Disabled");
disable();
}
}
ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshPacket &mp)
{
// Trigger external notification if enabled and not muted; isSilenced is from temporary mute toggles
if (moduleConfig.external_notification.enabled && !isSilenced) {
if (!isFromUs(&mp)) {
// Check if the message contains a bell character. Don't do this loop for every pin, just once.
auto &p = mp.decoded;
bool containsBell = false;
for (size_t i = 0; i < p.payload.size; i++) {
if (p.payload.bytes[i] == ASCII_BELL) {
containsBell = true;
break;
}
}
const bool isDmToUs = !isBroadcast(mp.to) && isToUs(&mp);
const bool is_muted = isMutedForPacket(mp);
const bool buzzerModeIsDirectOnly =
(config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY);
// Each output evaluates its own alert condition independently:
// alert_bell_* fires only when a bell character is present.
// alert_message_* fires on any non-muted message.
// Alert when receiving a bell = alertBell: true
// Alert when receiving a message = alertMessage: true
const bool genericShouldAlert = (moduleConfig.external_notification.alert_bell && containsBell) ||
(moduleConfig.external_notification.alert_message && !is_muted);
// Alert GPIO Vibra when receiving a bell = alertBellVibra: true
// Alert GPIO Vibra when receiving a message = alertMessageVibra: true
const bool vibraShouldAlert = (moduleConfig.external_notification.alert_bell_vibra && containsBell) ||
(moduleConfig.external_notification.alert_message_vibra && !is_muted);
// Alert GPIO Buzzer when receiving a bell = alertBellBuzzer: true
// Alert GPIO Buzzer when receiving a message = alertMessageBuzzer: true
// If you are already buzzing, keep going
buzzerShouldAlert =
buzzerShouldAlert || (canBuzz() && ((moduleConfig.external_notification.alert_bell_buzzer && containsBell) ||
(moduleConfig.external_notification.alert_message_buzzer && !is_muted)));
if (genericShouldAlert || vibraShouldAlert || buzzerShouldAlert) {
armNagCycle();
}
if (genericShouldAlert) {
LOG_INFO("externalNotificationModule - Generic alert");
setExternalState(0, true);
}
if (vibraShouldAlert) {
LOG_INFO("externalNotificationModule - Vibra alert");
triggerVibraOutput();
}
if (buzzerShouldAlert) {
LOG_INFO("externalNotificationModule - Buzzer alert");
if (buzzerModeIsDirectOnly && !isDmToUs && !containsBell) {
LOG_INFO("Buzzer suppressed: mode DIRECT_MSG_ONLY");
} else {
// Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us
triggerBuzzerOutput();
}
}
setIntervalFromNow(0); // run once so we know if we should do something
}
} else {
LOG_INFO("External Notification Module Disabled or muted");
}
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
}
void ExternalNotificationModule::triggerBuzzerOutput()
{
if (moduleConfig.external_notification.use_i2s_as_buzzer) {
#if defined(HAS_I2S) && !MESHTASTIC_EXCLUDE_RTTTL
audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone));
#endif
} else if (moduleConfig.external_notification.use_pwm) {
#if !MESHTASTIC_EXCLUDE_RTTTL
rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone);
#endif
} else {
setExternalState(2, true);
}
}
void ExternalNotificationModule::triggerVibraOutput()
{
#ifdef HAS_DRV2605
drv.setWaveform(0, 16);
drv.setWaveform(1, 0);
drv.setWaveform(2, 16);
drv.setWaveform(3, 0);
drv.setWaveform(4, 16);
drv.setWaveform(5, 0);
drv.setWaveform(6, 16);
drv.setWaveform(7, 0);
drv.go();
#endif
setExternalState(1, true);
}
void ExternalNotificationModule::armNagCycle()
{
const uint32_t durationMs = moduleConfig.external_notification.nag_timeout
? moduleConfig.external_notification.nag_timeout * 1000UL
: moduleConfig.external_notification.output_ms;
nagCycleCutoff = millis() + durationMs;
LOG_INFO("Toggling nagCycleCutoff to %lu", nagCycleCutoff);
isNagging = true;
}
void ExternalNotificationModule::startNotification()
{
if (!moduleConfig.external_notification.enabled || isSilenced)
return;
// Waypoint and geofence events are neither direct messages nor bells.
const bool buzzerModeIsDirectOnly = (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY);
const bool generic = moduleConfig.external_notification.alert_message;
const bool vibra = moduleConfig.external_notification.alert_message_vibra;
const bool buzzer = canBuzz() && moduleConfig.external_notification.alert_message_buzzer && !buzzerModeIsDirectOnly;
if (canBuzz() && moduleConfig.external_notification.alert_message_buzzer && buzzerModeIsDirectOnly)
LOG_INFO("Non-message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY");
if (!generic && !vibra && !buzzer)
return;
buzzerShouldAlert |= buzzer;
armNagCycle();
if (generic) {
LOG_INFO("externalNotificationModule - Generic alert");
setExternalState(0, true);
}
if (vibra) {
LOG_INFO("externalNotificationModule - Vibra alert");
triggerVibraOutput();
}
if (buzzer) {
LOG_INFO("externalNotificationModule - Buzzer alert");
triggerBuzzerOutput();
}
setIntervalFromNow(0); // run once so the nag/stop lifecycle in runOnce() takes over
}
/**
* @brief An admin message arrived to AdminModule. We are asked whether we want to handle that.
*
* @param mp The mesh packet arrived.
* @param request The AdminMessage request extracted from the packet.
* @param response The prepared response
* @return AdminMessageHandleResult HANDLED if message was handled
* HANDLED_WITH_RESULT if a result is also prepared.
*/
AdminMessageHandleResult ExternalNotificationModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp,
meshtastic_AdminMessage *request,
meshtastic_AdminMessage *response)
{
AdminMessageHandleResult result;
switch (request->which_payload_variant) {
#if !MESHTASTIC_EXCLUDE_RTTTL
case meshtastic_AdminMessage_get_ringtone_request_tag:
LOG_INFO("Client getting ringtone");
this->handleGetRingtone(mp, response);
result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE;
break;
case meshtastic_AdminMessage_set_ringtone_message_tag:
LOG_INFO("Client setting ringtone");
this->handleSetRingtone(request->set_canned_message_module_messages);
result = AdminMessageHandleResult::HANDLED;
break;
#endif
default:
result = AdminMessageHandleResult::NOT_HANDLED;
}
return result;
}
#if !MESHTASTIC_EXCLUDE_RTTTL
void ExternalNotificationModule::handleGetRingtone(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response)
{
LOG_INFO("*** handleGetRingtone");
if (req.decoded.want_response) {
response->which_payload_variant = meshtastic_AdminMessage_get_ringtone_response_tag;
strncpy(response->get_ringtone_response, rtttlConfig.ringtone, sizeof(response->get_ringtone_response));
} // Don't send anything if not instructed to. Better than asserting.
}
void ExternalNotificationModule::handleSetRingtone(const char *from_msg)
{
int changed = 0;
if (*from_msg) {
changed |= strcmp(rtttlConfig.ringtone, from_msg);
strncpy(rtttlConfig.ringtone, from_msg, sizeof(rtttlConfig.ringtone));
LOG_INFO("*** from_msg.text:%s", from_msg);
}
if (changed) {
nodeDB->saveProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, &meshtastic_RTTTLConfig_msg, &rtttlConfig);
}
}
#endif
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
int ExternalNotificationModule::handleInputEvent(const InputEvent *event)
{
if (nagCycleCutoff != UINT32_MAX) {
stopNow();
return 1;
}
return 0;
}
#endif