Fix SENSOR power saving deep sleep truncating TX and skipping sleep on failed reads (#10939)

* Fix SENSOR power saving deep sleep behavior

Deep sleep could be entered while a telemetry packet was still queued
or on air, truncating the transmission. canSleep() gained a deepSleep
parameter and now vetoes in that case; light sleep is unchanged.
Telemetry modules defer a pending deep sleep (bounded to 30s) until the
radio is idle and no longer let the sensor polling interval override
the 5s pre-sleep grace period. A failed sensor read still arms deep
sleep instead of leaving the node awake for a full telemetry interval.

Fixes #10890
Fixes #10932

* Deduplicate telemetry deep sleep deferral logic

Move the radio-busy deferral and its counter into BaseTelemetryModule
and add an isPowerSavingSensor() helper. Removes the telemetry-specific
counter from OSThread. The sleep arming block stays per module because
it needs protected OSThread members not visible to the base class.
This commit is contained in:
Thomas Göttgens
2026-07-08 07:30:46 -05:00
committed by GitHub
parent ba473bf529
commit 38074f584f
11 changed files with 162 additions and 48 deletions
+24 -7
View File
@@ -119,6 +119,8 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
int32_t AirQualityTelemetryModule::runOnce()
{
if (sleepOnNextExecution == true) {
if (shouldDeferDeepSleep())
return PREFLIGHT_SLEEP_RETRY_MS;
sleepOnNextExecution = false;
uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval,
default_telemetry_broadcast_interval_secs);
@@ -223,6 +225,12 @@ int32_t AirQualityTelemetryModule::runOnce()
}
}
}
if (sleepOnNextExecution) {
// Honor the pre-sleep grace period armed in sendTelemetry(): OSThread reschedules with
// this return value, which would otherwise override setIntervalFromNow() and delay or
// mistime the pending deep sleep
return FIVE_SECONDS_MS;
}
return min(sendToPhoneIntervalMs, result);
}
@@ -422,7 +430,8 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag;
m.time = getTime();
if (getAirQualityTelemetry(&m)) {
bool validTelemetry = getAirQualityTelemetry(&m);
if (validTelemetry) {
bool hasAnyPM =
m.variant.air_quality_metrics.has_pm10_standard || m.variant.air_quality_metrics.has_pm25_standard ||
@@ -474,7 +483,7 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
LOG_INFO("Sending packet to mesh");
service->sendToMesh(p, RX_SRC_LOCAL, true);
if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) {
if (isPowerSavingSensor()) {
meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed();
notification->level = meshtastic_LogRecord_Level_INFO;
notification->time = getValidTime(RTCQualityFromNet);
@@ -483,14 +492,22 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
default_telemetry_broadcast_interval_secs) /
1000U);
service->sendClientNotification(notification);
sleepOnNextExecution = true;
LOG_DEBUG("Start next execution in 5s, then sleep");
setIntervalFromNow(FIVE_SECONDS_MS);
}
}
return true;
}
return false;
// Arm the pre-sleep sequence even when no valid reading was available this cycle: a
// power-saving SENSOR node must still return to deep sleep, otherwise it stays awake
// until the next telemetry interval and drains its battery
if (!phoneOnly && isPowerSavingSensor()) {
if (!validTelemetry)
LOG_WARN("Air quality telemetry unavailable this cycle, sleep without sending");
sleepOnNextExecution = true;
preflightSleepDeferrals = 0;
LOG_DEBUG("Start next execution in 5s, then sleep");
setIntervalFromNow(FIVE_SECONDS_MS);
}
return validTelemetry;
}
AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp,