mirror of
https://github.com/meshtastic/firmware.git
synced 2026-09-28 17:29:06 -04:00
* 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.
634 lines
24 KiB
C++
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
|