From db84bdf3b446b8230a45d9acd2cc12b36a064d5f Mon Sep 17 00:00:00 2001 From: Andrew Yong Date: Fri, 28 Aug 2026 19:58:23 +0000 Subject: [PATCH] Reduce ExternalNotificationModule flash usage (RTTTL + InputBroker) (#10989) * Generalize RTTTL exclusion into MESHTASTIC_EXCLUDE_RTTTL ExternalNotificationModule already stubbed out RTTTL playback for STM32WL/portduino/ESP32C6 via a raw ARCH/CONFIG_IDF check, but the ringtone config plumbing around it (protobuf message, encode/decode tables, /prefs/ringtone.proto persistence, admin get/set-ringtone handlers) still compiled in even though it can never do anything on those platforms. Introduce MESHTASTIC_EXCLUDE_RTTTL and gate the dead ringtone plumbing behind it too. The flag is set in each architecture's *_base build_flags (stm32_base, esp32c6_base, portduino_base) rather than in the module itself - this matches how every other MESHTASTIC_EXCLUDE_* flag in the tree is set (e.g. stm32_base already sets ten of them directly, and esp32c6_base already excludes PAXCOUNTER for an analogous platform-can't-support-this reason), rather than introducing a new per-architecture C header pattern. Behavior is unchanged on all three platforms; overridable via -D like every other MESHTASTIC_EXCLUDE_* flag. Also guard the two HAS_I2S ringtone-playback call sites with !MESHTASTIC_EXCLUDE_RTTTL alongside HAS_I2S, since rtttlConfig itself is now only declared when RTTTL is not excluded. No current platform defines both HAS_I2S and MESHTASTIC_EXCLUDE_RTTTL simultaneously, so this has no effect today, but prevents a future HAS_I2S platform that also excludes RTTTL from failing to compile. Saves 368 bytes flash / 236 bytes RAM on wio-e5 with no loss to the GPIO on/off notification toggle itself, which does not depend on RTTTL. Signed-off-by: Andrew Yong Assisted-by: Claude Sonnet 5 * Skip unused InputBroker observer in ExternalNotificationModule The inputObserver CallbackObserver member was declared unconditionally, even though its only use site was already gated behind MESHTASTIC_EXCLUDE_INPUTBROKER (set for all of stm32 in stm32.ini). Because it's a non-trivial member, the compiler still generated its constructor/destructor as part of ExternalNotificationModule's own lifecycle even when InputBroker is compiled out entirely. Gate the member and its only consumer, handleInputEvent(), behind the same flag as their use site, and match the codebase's dominant !MESHTASTIC_EXCLUDE_X style (used ~330 times) rather than !defined(MESHTASTIC_EXCLUDE_X) (used ~20 times) while touching this flag's other call site. Saves an additional 288 bytes flash on wio-e5, no RAM change, no functional impact since InputBroker was already unused on this platform. Signed-off-by: Andrew Yong Assisted-by: Claude Sonnet 5 * fix(native-wasm): exclude RTTTL to unbreak build The browser node builds its own build_flags from arduino_base rather than inheriting portduino_base, so it did not pick up the MESHTASTIC_EXCLUDE_RTTTL flag added to portduino_base. With the inline ARCH_PORTDUINO stub in ExternalNotificationModule.h now replaced by that flag, native-wasm tried to include the unavailable NonBlockingRtttl.h. Set MESHTASTIC_EXCLUDE_RTTTL=1 directly in the native-wasm env alongside its other exclusion flags. Assisted-by: Claude Sonnet 5 Signed-off-by: Andrew Yong --------- Signed-off-by: Andrew Yong Co-authored-by: Ben Meadors --- src/modules/ExternalNotificationModule.cpp | 25 ++++++++++++++++------ src/modules/ExternalNotificationModule.h | 10 +++++++-- variants/native/portduino.ini | 1 + variants/native/portduino/platformio.ini | 2 ++ variants/stm32/stm32.ini | 19 ++++++++-------- 5 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp index 425d0b33ee..0c97bf3575 100644 --- a/src/modules/ExternalNotificationModule.cpp +++ b/src/modules/ExternalNotificationModule.cpp @@ -62,7 +62,10 @@ bool ascending = true; #define ASCII_BELL 0x07 +#if !MESHTASTIC_EXCLUDE_RTTTL meshtastic_RTTTLConfig rtttlConfig; +static const char *rtttlConfigFile = "/prefs/ringtone.proto"; +#endif ExternalNotificationModule *externalNotificationModule; @@ -70,8 +73,6 @@ bool externalCurrentState[3] = {}; uint32_t externalTurnedOn[3] = {}; -static const char *rtttlConfigFile = "/prefs/ringtone.proto"; - int32_t ExternalNotificationModule::runOnce() { if (!moduleConfig.external_notification.enabled) { @@ -147,7 +148,7 @@ int32_t ExternalNotificationModule::runOnce() } // Play RTTTL over i2s audio interface if enabled as buzzer -#ifdef HAS_I2S +#if defined(HAS_I2S) && !MESHTASTIC_EXCLUDE_RTTTL if (moduleConfig.external_notification.use_i2s_as_buzzer) { if (audioThread->isPlaying()) { // Continue playing @@ -158,7 +159,7 @@ int32_t ExternalNotificationModule::runOnce() delay = EXT_NOTIFICATION_FAST_THREAD_MS; } #endif -#if defined(HAS_I2S_SPEAKER_NRF52) +#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()) { @@ -169,6 +170,7 @@ int32_t ExternalNotificationModule::runOnce() 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()) { @@ -180,6 +182,7 @@ int32_t ExternalNotificationModule::runOnce() // we need fast updates to play the RTTTL delay = EXT_NOTIFICATION_FAST_THREAD_MS; } +#endif return delay; } @@ -348,16 +351,18 @@ ExternalNotificationModule::ExternalNotificationModule() // moduleConfig.external_notification.alert_message_buzzer = true; if (moduleConfig.external_notification.enabled) { -#if !defined(MESHTASTIC_EXCLUDE_INPUTBROKER) +#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"); @@ -486,11 +491,13 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP void ExternalNotificationModule::triggerBuzzerOutput() { if (moduleConfig.external_notification.use_i2s_as_buzzer) { -#ifdef HAS_I2S +#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); } @@ -573,6 +580,7 @@ AdminMessageHandleResult ExternalNotificationModule::handleAdminMessageForModule 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); @@ -584,6 +592,7 @@ AdminMessageHandleResult ExternalNotificationModule::handleAdminMessageForModule this->handleSetRingtone(request->set_canned_message_module_messages); result = AdminMessageHandleResult::HANDLED; break; +#endif default: result = AdminMessageHandleResult::NOT_HANDLED; @@ -592,6 +601,7 @@ AdminMessageHandleResult ExternalNotificationModule::handleAdminMessageForModule return result; } +#if !MESHTASTIC_EXCLUDE_RTTTL void ExternalNotificationModule::handleGetRingtone(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response) { LOG_INFO("*** handleGetRingtone"); @@ -615,7 +625,9 @@ void ExternalNotificationModule::handleSetRingtone(const char *from_msg) 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) { @@ -624,3 +636,4 @@ int ExternalNotificationModule::handleInputEvent(const InputEvent *event) } return 0; } +#endif diff --git a/src/modules/ExternalNotificationModule.h b/src/modules/ExternalNotificationModule.h index 75f831d04c..969638583c 100644 --- a/src/modules/ExternalNotificationModule.h +++ b/src/modules/ExternalNotificationModule.h @@ -23,10 +23,10 @@ extern AmbientLightingThread *ambientLightingThread; #endif #endif -#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) +#if !MESHTASTIC_EXCLUDE_RTTTL #include #else -// Noop class for portduino. +// Noop class for portduino/STM32WL/ESP32C6 - none can drive PWM RTTTL playback. class rtttl { public: @@ -47,8 +47,10 @@ class rtttl */ class ExternalNotificationModule : public SinglePortModule, private concurrency::OSThread { +#if !MESHTASTIC_EXCLUDE_INPUTBROKER CallbackObserver inputObserver = CallbackObserver(this, &ExternalNotificationModule::handleInputEvent); +#endif uint32_t output = 0; #ifdef NEOPIXEL_STATUS_NOTIFICATION_PIN @@ -58,7 +60,9 @@ class ExternalNotificationModule : public SinglePortModule, private concurrency: public: ExternalNotificationModule(); +#if !MESHTASTIC_EXCLUDE_INPUTBROKER int handleInputEvent(const InputEvent *arg); +#endif uint32_t nagCycleCutoff = 1; @@ -76,8 +80,10 @@ class ExternalNotificationModule : public SinglePortModule, private concurrency: // Fire the configured message outputs for a non-message event such as a geofence crossing. void startNotification(); +#if !MESHTASTIC_EXCLUDE_RTTTL void handleGetRingtone(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response); void handleSetRingtone(const char *from_msg); +#endif protected: /** Called to handle a particular incoming message diff --git a/variants/native/portduino.ini b/variants/native/portduino.ini index 33e0a8b8bb..5068973e24 100644 --- a/variants/native/portduino.ini +++ b/variants/native/portduino.ini @@ -49,6 +49,7 @@ build_flags_common = -fPIC -Isrc/platform/portduino -DRADIOLIB_EEPROM_UNSUPPORTED + -DMESHTASTIC_EXCLUDE_RTTTL ; No PWM/RTTTL ringtone playback support on this platform. -lpthread -lyaml-cpp -ljsoncpp diff --git a/variants/native/portduino/platformio.ini b/variants/native/portduino/platformio.ini index 1eaff83434..0a9db5d99a 100644 --- a/variants/native/portduino/platformio.ini +++ b/variants/native/portduino/platformio.ini @@ -444,6 +444,8 @@ build_flags = ${arduino_base.build_flags} -DMESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION=1 -DMESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 -DMESHTASTIC_EXCLUDE_STOREFORWARD=1 -DMESHTASTIC_EXCLUDE_SERIAL=1 -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 -DMESHTASTIC_EXCLUDE_WAYPOINT=1 + ; No PWM/RTTTL ringtone playback support in the browser node. + -DMESHTASTIC_EXCLUDE_RTTTL=1 ; The firmware-specific emcc *link* settings (exported fns, runtime methods, the ; WebUSB Asyncify import seam, the ES-module factory name) can't ride in diff --git a/variants/stm32/stm32.ini b/variants/stm32/stm32.ini index 6f7477882f..fd6ca8babe 100644 --- a/variants/stm32/stm32.ini +++ b/variants/stm32/stm32.ini @@ -16,19 +16,20 @@ build_flags = ${arduino_base.build_flags} -flto -Isrc/platform/stm32wl -g - -DMESHTASTIC_EXCLUDE_AUDIO=1 -DMESHTASTIC_EXCLUDE_ATAK=1 ; ATAK is quite big, disable it for big flash savings. - -DMESHTASTIC_EXCLUDE_INPUTBROKER=1 - -DMESHTASTIC_EXCLUDE_POWERMON=1 - -DMESHTASTIC_EXCLUDE_SCREEN=1 - -DMESHTASTIC_EXCLUDE_MQTT=1 + -DMESHTASTIC_EXCLUDE_AUDIO=1 -DMESHTASTIC_EXCLUDE_BLUETOOTH=1 - -DMESHTASTIC_EXCLUDE_WIFI=1 - -DMESHTASTIC_EXCLUDE_TZ=1 ; Exclude TZ to save some flash space. - -DMESHTASTIC_EXCLUDE_XEDDSA=1 ; Individual STM32WL variants opt in after size validation. + -DMESHTASTIC_EXCLUDE_INPUTBROKER=1 + -DMESHTASTIC_EXCLUDE_MQTT=1 -DMESHTASTIC_EXCLUDE_PKT_HISTORY_HASH=1 - -DMESHTASTIC_EXCLUDE_WAYPOINT=1 + -DMESHTASTIC_EXCLUDE_POWERMON=1 -DMESHTASTIC_EXCLUDE_POWER_TELEMETRY=1 + -DMESHTASTIC_EXCLUDE_RTTTL=1 ; No PWM/RTTTL ringtone playback support on this platform. + -DMESHTASTIC_EXCLUDE_SCREEN=1 + -DMESHTASTIC_EXCLUDE_TZ=1 ; Exclude TZ to save some flash space. + -DMESHTASTIC_EXCLUDE_WAYPOINT=1 + -DMESHTASTIC_EXCLUDE_WIFI=1 + -DMESHTASTIC_EXCLUDE_XEDDSA=1 ; Individual STM32WL variants opt in after size validation. -DSERIAL_RX_BUFFER_SIZE=256 ; For GPS - the default of 64 is too small. -DHAS_SCREEN=0 ; Always disable screen for STM32, it is not supported. ;-DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF ; Enable this if enabling debugg logging. It is REQUIRED for at least traceroute debug prints - without it the length returned by printf ends up uninitialized.