From b6032701e50e443ca650b4b3cd422b7e9ba09c96 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Sun, 25 Jan 2026 05:23:01 +0300 Subject: [PATCH 1/8] feat(proto): update generated config.pb.h for WiFi power management Add wifi_on_external_power_only and wifi_power_loss_timeout_secs fields to NetworkConfig structure. Updated init macros, field tags, and size. Note: Full regeneration with nanopb required before merge. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- src/mesh/generated/meshtastic/config.pb.h | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index d93f6fafa97..35770ab7436 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -469,6 +469,14 @@ typedef struct _meshtastic_Config_NetworkConfig { uint32_t enabled_protocols; /* Enable/Disable ipv6 support */ bool ipv6_enabled; + /* Only enable WiFi when connected to external power (USB). + WiFi will be automatically disabled when running on battery + after wifi_power_loss_timeout_secs delay. */ + bool wifi_on_external_power_only; + /* Delay in seconds before disabling WiFi after external power is lost. + This allows for brief power interruptions without toggling WiFi. + Default: 30 seconds. Set to 0 for immediate disable. */ + uint32_t wifi_power_loss_timeout_secs; } meshtastic_Config_NetworkConfig; /* Display Config */ @@ -730,7 +738,7 @@ extern "C" { #define meshtastic_Config_DeviceConfig_init_default {_meshtastic_Config_DeviceConfig_Role_MIN, 0, 0, 0, _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN, 0, 0, 0, 0, "", 0, _meshtastic_Config_DeviceConfig_BuzzerMode_MIN} #define meshtastic_Config_PositionConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, _meshtastic_Config_PositionConfig_GpsMode_MIN} #define meshtastic_Config_PowerConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0} -#define meshtastic_Config_NetworkConfig_init_default {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_default, "", 0, 0} +#define meshtastic_Config_NetworkConfig_init_default {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_default, "", 0, 0, 0, 0} #define meshtastic_Config_NetworkConfig_IpV4Config_init_default {0, 0, 0, 0} #define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0} #define meshtastic_Config_LoRaConfig_init_default {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0} @@ -741,7 +749,7 @@ extern "C" { #define meshtastic_Config_DeviceConfig_init_zero {_meshtastic_Config_DeviceConfig_Role_MIN, 0, 0, 0, _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN, 0, 0, 0, 0, "", 0, _meshtastic_Config_DeviceConfig_BuzzerMode_MIN} #define meshtastic_Config_PositionConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, _meshtastic_Config_PositionConfig_GpsMode_MIN} #define meshtastic_Config_PowerConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0} -#define meshtastic_Config_NetworkConfig_init_zero {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_zero, "", 0, 0} +#define meshtastic_Config_NetworkConfig_init_zero {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_zero, "", 0, 0, 0, 0} #define meshtastic_Config_NetworkConfig_IpV4Config_init_zero {0, 0, 0, 0} #define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0} #define meshtastic_Config_LoRaConfig_init_zero {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0} @@ -798,6 +806,8 @@ extern "C" { #define meshtastic_Config_NetworkConfig_rsyslog_server_tag 9 #define meshtastic_Config_NetworkConfig_enabled_protocols_tag 10 #define meshtastic_Config_NetworkConfig_ipv6_enabled_tag 11 +#define meshtastic_Config_NetworkConfig_wifi_on_external_power_only_tag 12 +#define meshtastic_Config_NetworkConfig_wifi_power_loss_timeout_secs_tag 13 #define meshtastic_Config_DisplayConfig_screen_on_secs_tag 1 #define meshtastic_Config_DisplayConfig_gps_format_tag 2 #define meshtastic_Config_DisplayConfig_auto_screen_carousel_secs_tag 3 @@ -1038,7 +1048,7 @@ extern const pb_msgdesc_t meshtastic_Config_SessionkeyConfig_msg; #define meshtastic_Config_DisplayConfig_size 34 #define meshtastic_Config_LoRaConfig_size 85 #define meshtastic_Config_NetworkConfig_IpV4Config_size 20 -#define meshtastic_Config_NetworkConfig_size 204 +#define meshtastic_Config_NetworkConfig_size 212 #define meshtastic_Config_PositionConfig_size 62 #define meshtastic_Config_PowerConfig_size 52 #define meshtastic_Config_SecurityConfig_size 178 From 41c84f924919e9b5c6d9862692f3820b257f537c Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Sun, 25 Jan 2026 05:23:08 +0300 Subject: [PATCH 2/8] feat(http): add deinitWebServer function for WiFi hot toggle Add deinitWebServer() to properly stop and clean up web servers (HTTPS and HTTP) when WiFi is disabled at runtime. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- src/mesh/http/WebServer.cpp | 20 ++++++++++++++++++++ src/mesh/http/WebServer.h | 1 + 2 files changed, 21 insertions(+) diff --git a/src/mesh/http/WebServer.cpp b/src/mesh/http/WebServer.cpp index 3a264fa5a0b..58121e4b1ca 100644 --- a/src/mesh/http/WebServer.cpp +++ b/src/mesh/http/WebServer.cpp @@ -247,4 +247,24 @@ void initWebServer() LOG_ERROR("Web Servers Failed! ;-( "); } } + +void deinitWebServer() +{ + LOG_DEBUG("Deinit Web Server"); + isWebServerReady = false; + + if (secureServer) { + secureServer->stop(); + delete secureServer; + secureServer = nullptr; + } + + if (insecureServer) { + insecureServer->stop(); + delete insecureServer; + insecureServer = nullptr; + } + + LOG_INFO("Web Servers Stopped"); +} #endif diff --git a/src/mesh/http/WebServer.h b/src/mesh/http/WebServer.h index e7a29a5a7df..7a2edc78d61 100644 --- a/src/mesh/http/WebServer.h +++ b/src/mesh/http/WebServer.h @@ -6,6 +6,7 @@ #include void initWebServer(); +void deinitWebServer(); void createSSLCert(); class WebServerThread : private concurrency::OSThread From 92435c4634ac97a159dedcf6e66962269f00807e Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Sun, 25 Jan 2026 05:23:14 +0300 Subject: [PATCH 3/8] feat(udp): add stop method to UdpMulticastHandler Add stop() method to properly close UDP multicast listener when WiFi is disabled at runtime. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- src/mesh/udp/UdpMulticastHandler.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/mesh/udp/UdpMulticastHandler.h b/src/mesh/udp/UdpMulticastHandler.h index 2df8686a315..48971607553 100644 --- a/src/mesh/udp/UdpMulticastHandler.h +++ b/src/mesh/udp/UdpMulticastHandler.h @@ -39,6 +39,12 @@ class UdpMulticastHandler final } } + void stop() + { + LOG_DEBUG("Stopping UDP Multicast"); + udp.close(); + } + void onReceive(AsyncUDPPacket packet) { size_t packetLength = packet.length(); From 615c61599ba97d950b6edd9d118de9cf921c9065 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Sun, 25 Jan 2026 05:23:22 +0300 Subject: [PATCH 4/8] feat(wifi): add deinitWifiServices for proper WiFi shutdown Add deinitWifiServices() to properly stop all WiFi-dependent services before disabling WiFi: - Syslog client - API server - Web server - mDNS - NTP client Reset APStartupComplete flag to allow services to reinitialize on reconnect. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- src/mesh/wifi/WiFiAPClient.cpp | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/mesh/wifi/WiFiAPClient.cpp b/src/mesh/wifi/WiFiAPClient.cpp index a95dfa58f62..35e52f6bc41 100644 --- a/src/mesh/wifi/WiFiAPClient.cpp +++ b/src/mesh/wifi/WiFiAPClient.cpp @@ -248,12 +248,47 @@ bool isWifiAvailable() } } +// Deinit all WiFi-dependent services (called before WiFi off) +static void deinitWifiServices() +{ + LOG_DEBUG("Deinit WiFi services"); + + // Disable syslog + syslog.disable(); + + // Stop API server + deInitApiServer(); + +#ifdef ARCH_ESP32 +#if !MESHTASTIC_EXCLUDE_WEBSERVER + // Stop web server + deinitWebServer(); +#endif + + // Stop mDNS + MDNS.end(); +#endif + +#ifndef DISABLE_NTP + // Stop NTP client + timeClient.end(); +#endif + + // Reset flag so services reinitialize on reconnect + APStartupComplete = false; + + LOG_INFO("WiFi services stopped"); +} + // Disable WiFi void deinitWifi() { LOG_INFO("WiFi deinit"); if (isWifiAvailable()) { + // First stop all services that depend on WiFi + deinitWifiServices(); + #ifdef ARCH_ESP32 WiFi.disconnect(true, false); #elif defined(ARCH_RP2040) From 51b2cb10df82505a5621ab73f2b1b74436ba32b6 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Sun, 25 Jan 2026 05:23:30 +0300 Subject: [PATCH 5/8] feat(power): implement WiFi power management based on power source Add handleWifiPowerManagement() to automatically manage WiFi based on external power state: - When external power is lost: start timer, disable WiFi after timeout - When external power is restored: cancel timer, re-enable WiFi - Timeout prevents accidental WiFi toggle on brief power interruptions Configuration: - wifi_on_external_power_only: enable feature (default: false) - wifi_power_loss_timeout_secs: delay before WiFi disable (default: 30s) Closes: https://github.com/meshtastic/firmware/issues/9427 Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- src/Power.cpp | 72 +++++++++++++++++++++++++++++++++++++++++++++ src/mesh/NodeDB.cpp | 4 +++ src/power.h | 3 ++ 3 files changed, 79 insertions(+) diff --git a/src/Power.cpp b/src/Power.cpp index b2a4ddaaf6e..128f5e1c53d 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -30,6 +30,10 @@ #include "input/LinuxInputImpl.h" #endif +#if HAS_WIFI +#include "mesh/wifi/WiFiAPClient.h" +#endif + // Working USB detection for powered/charging states on the RAK platform #ifdef NRF_APM #include "nrfx_power.h" @@ -49,6 +53,13 @@ #endif +// WiFi power management state +#if HAS_WIFI +static uint32_t wifiPowerLossTimerStart = 0; +static bool wifiPowerLossTimerActive = false; +static bool wifiWasDisabledByPowerLoss = false; +#endif + #ifndef DELAY_FOREVER #define DELAY_FOREVER portMAX_DELAY #endif @@ -956,10 +967,71 @@ void Power::readPowerStatus() } } +#if HAS_WIFI +/** + * Handle automatic WiFi enable/disable based on power source. + * When wifi_on_external_power_only is enabled: + * - WiFi is enabled when external power (USB) is connected + * - WiFi is disabled after timeout when running on battery + */ +void Power::handleWifiPowerManagement() +{ + // Feature disabled - nothing to do + if (!config.network.wifi_on_external_power_only) { + return; + } + + // WiFi not configured - nothing to do + if (!config.network.wifi_enabled || !config.network.wifi_ssid[0]) { + return; + } + + bool hasExternalPower = powerStatus && powerStatus->getHasUSB(); + uint32_t timeoutSecs = config.network.wifi_power_loss_timeout_secs; + if (timeoutSecs == 0) { + timeoutSecs = 30; // Default 30 seconds if not configured + } + + if (!hasExternalPower && isWifiAvailable()) { + // Running on battery with WiFi active - start/check timer + if (!wifiPowerLossTimerActive) { + LOG_INFO("External power lost, WiFi will disable in %u seconds", timeoutSecs); + wifiPowerLossTimerStart = millis(); + wifiPowerLossTimerActive = true; + } + + // Check if timeout expired + if (millis() - wifiPowerLossTimerStart >= timeoutSecs * 1000) { + LOG_INFO("Power loss timeout expired, disabling WiFi"); + deinitWifi(); + wifiPowerLossTimerActive = false; + wifiWasDisabledByPowerLoss = true; + } + } else if (hasExternalPower) { + // External power connected + if (wifiPowerLossTimerActive) { + LOG_INFO("External power restored, canceling WiFi disable timer"); + wifiPowerLossTimerActive = false; + } + + // Re-enable WiFi if it was disabled by power loss + if (wifiWasDisabledByPowerLoss && !isWifiAvailable()) { + LOG_INFO("External power restored, re-enabling WiFi"); + initWifi(); + wifiWasDisabledByPowerLoss = false; + } + } +} +#endif // HAS_WIFI + int32_t Power::runOnce() { readPowerStatus(); +#if HAS_WIFI + handleWifiPowerManagement(); +#endif + #ifdef HAS_PMU // WE no longer use the IRQ line to wake the CPU (due to false wakes from // sleep), but we do poll the IRQ status by reading the registers over I2C diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 2a32940867e..597a883364f 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -743,6 +743,10 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) config.network.ipv6_enabled = default_network_ipv6_enabled; #endif + // WiFi power management defaults + config.network.wifi_on_external_power_only = false; + config.network.wifi_power_loss_timeout_secs = 30; + #ifdef DISPLAY_FLIP_SCREEN config.display.flip_screen = true; #endif diff --git a/src/power.h b/src/power.h index 5f887c36b64..660cfcc70f4 100644 --- a/src/power.h +++ b/src/power.h @@ -103,6 +103,9 @@ class Power : private concurrency::OSThread void powerCommandsCheck(); void readPowerStatus(); +#if HAS_WIFI + void handleWifiPowerManagement(); +#endif virtual bool setup(); virtual int32_t runOnce() override; void setStatusHandler(meshtastic::PowerStatus *handler) { statusHandler = handler; } From 3a8a1e884faf4d0b6f55c516b58a506d8a4af536 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Sun, 25 Jan 2026 05:23:42 +0300 Subject: [PATCH 6/8] chore: update protobufs submodule for WiFi power management Points to feat/wifi-power-management branch with new NetworkConfig fields. Will be updated to upstream master after protobufs PR is merged. See: https://github.com/meshtastic/protobufs/pull/851 Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- protobufs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protobufs b/protobufs index 77c8329a59a..8ebab18ba63 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 77c8329a59a9c96a61c447b5d5f1a52ca583e4f2 +Subproject commit 8ebab18ba632a2421c0968d82564c4dbac645cba From f3dee0fad8b99a6bc7de55c44465fab148185934 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Sun, 25 Jan 2026 05:39:48 +0300 Subject: [PATCH 7/8] fix(udp): guard AsyncUDP close() for ESP32 only The close() method is only available in ESP32's AsyncUDP implementation. Other platforms (nrf52, portduino/native) don't have this method, causing build failures in native simulator tests. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- src/mesh/udp/UdpMulticastHandler.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mesh/udp/UdpMulticastHandler.h b/src/mesh/udp/UdpMulticastHandler.h index 48971607553..5f49388f434 100644 --- a/src/mesh/udp/UdpMulticastHandler.h +++ b/src/mesh/udp/UdpMulticastHandler.h @@ -42,7 +42,9 @@ class UdpMulticastHandler final void stop() { LOG_DEBUG("Stopping UDP Multicast"); +#ifdef ARCH_ESP32 udp.close(); +#endif } void onReceive(AsyncUDPPacket packet) From 51da3cab28ef2dbedaf7fd9f2b2fba0403ea2676 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Sun, 25 Jan 2026 05:58:09 +0300 Subject: [PATCH 8/8] fix(power): exclude portduino from WiFi power management Portduino has HAS_WIFI=1 but excludes mesh/wifi/ from build, causing undefined reference errors for isWifiAvailable(), initWifi(), deinitWifi(). Added !defined(ARCH_PORTDUINO) guard to all WiFi power management code. Co-Authored-By: Claude Signed-off-by: Aleksei Sviridkin --- src/Power.cpp | 10 +++++----- src/power.h | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Power.cpp b/src/Power.cpp index 128f5e1c53d..8fb4758ca4a 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -30,7 +30,7 @@ #include "input/LinuxInputImpl.h" #endif -#if HAS_WIFI +#if HAS_WIFI && !defined(ARCH_PORTDUINO) #include "mesh/wifi/WiFiAPClient.h" #endif @@ -54,7 +54,7 @@ #endif // WiFi power management state -#if HAS_WIFI +#if HAS_WIFI && !defined(ARCH_PORTDUINO) static uint32_t wifiPowerLossTimerStart = 0; static bool wifiPowerLossTimerActive = false; static bool wifiWasDisabledByPowerLoss = false; @@ -967,7 +967,7 @@ void Power::readPowerStatus() } } -#if HAS_WIFI +#if HAS_WIFI && !defined(ARCH_PORTDUINO) /** * Handle automatic WiFi enable/disable based on power source. * When wifi_on_external_power_only is enabled: @@ -1022,13 +1022,13 @@ void Power::handleWifiPowerManagement() } } } -#endif // HAS_WIFI +#endif // HAS_WIFI && !defined(ARCH_PORTDUINO) int32_t Power::runOnce() { readPowerStatus(); -#if HAS_WIFI +#if HAS_WIFI && !defined(ARCH_PORTDUINO) handleWifiPowerManagement(); #endif diff --git a/src/power.h b/src/power.h index 660cfcc70f4..2ccd29bc8ba 100644 --- a/src/power.h +++ b/src/power.h @@ -103,7 +103,7 @@ class Power : private concurrency::OSThread void powerCommandsCheck(); void readPowerStatus(); -#if HAS_WIFI +#if HAS_WIFI && !defined(ARCH_PORTDUINO) void handleWifiPowerManagement(); #endif virtual bool setup();