1212#include " Router.h"
1313#include " TransmitHistory.h"
1414#include " UnitConversions.h"
15+ #include " UptimeClock.h"
1516#include " detect/ScanI2CTwoWire.h"
17+ #ifdef AIR_QUALITY_TELEMETRY_HISTORY_PATH
18+ #include " FileTelemetryStore.h"
19+ #endif
1620#include " gps/RTC.h"
1721#include " graphics/ScreenFonts.h"
1822#include " graphics/SharedUIDisplay.h"
@@ -175,8 +179,7 @@ int32_t AirQualityTelemetryModule::runOnce()
175179 return disable ();
176180 }
177181
178- // The local device-to-phone loop runs at its own near-realtime cadence; air_quality_interval
179- // paces only what goes on air, and keeps the online-node scaling it has always had.
182+ // air_quality_interval paces only what goes on air; the local loop has its own cadence.
180183 const uint32_t meshIntervalMs = Default::getConfiguredOrDefaultMsScaled (
181184 moduleConfig.telemetry .air_quality_interval , default_telemetry_broadcast_interval_secs, numOnlineNodes);
182185
@@ -186,9 +189,9 @@ int32_t AirQualityTelemetryModule::runOnce()
186189 const bool meshAllowed =
187190 airTime->isTxAllowedChannelUtil (config.device .role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
188191 airTime->isTxAllowedAirUtil ();
189- const bool meshDue = (lastMeshTelemetry == 0 ) || ! Throttle::isWithinTimespanMs (lastMeshTelemetry, meshIntervalMs);
192+ const bool meshDue = (lastMeshTelemetry == 0 ) || Throttle::hasElapsed (lastMeshTelemetry, meshIntervalMs);
190193
191- if (shouldReadSensors (everRead, millis (), lastReadMs, localLoopIntervalMs, service->isToPhoneQueueEmpty (),
194+ if (shouldReadSensors (everRead, Time::getMillis (), lastReadMs, localLoopIntervalMs, service->isToPhoneQueueEmpty (),
192195 meshDue && meshAllowed)) {
193196 const int32_t warmingUpMs = warmUpSensors ();
194197 if (warmingUpMs > 0 )
@@ -200,8 +203,8 @@ int32_t AirQualityTelemetryModule::runOnce()
200203 // Send to sleep sensors that can be to save power
201204 for (TelemetrySensor *sensor : sensors) {
202205 if (sensor->isActive () && sensor->canSleep ()) {
203- // Measured against the local loop, not the on-air interval: a sensor that cannot warm
204- // back up within one loop would never produce a reading if it were slept
206+ // Against the local loop, not the on-air interval: a sensor that cannot warm back up
207+ // within one loop would never produce a reading if it were slept
205208 if (sensor->wakeUpTimeMs () < (int32_t )localLoopIntervalMs) {
206209 LOG_DEBUG (" Disabling %s until next period" , sensor->sensorName );
207210 sensor->sleep ();
@@ -211,13 +214,12 @@ int32_t AirQualityTelemetryModule::runOnce()
211214 }
212215 }
213216
214- if (shouldSendToMesh (history. hasUnpublishedNewest (TELEMETRY_PUBLISHED_MESH ), meshDue, meshAllowed,
217+ if (shouldSendToMesh (history-> hasUnpublishedNewest (TELEMETRY_PUBLISHED_MESH ), meshDue, meshAllowed,
215218 isPowerSavingSensor ())) {
216- // Called even with nothing latched: sendTelemetry() also arms the pre-sleep sequence a
217- // power-saving SENSOR needs to get back to deep sleep
219+ // Called even with nothing to send: sendTelemetry() arms the power-saving SENSOR's sleep
218220 if (sendTelemetry (NODENUM_BROADCAST , false ) && transmitHistory)
219221 transmitHistory->setLastSentToMesh (TX_HISTORY_KEY_AIR_QUALITY_TELEMETRY );
220- } else if (history. hasUnpublishedNewest (TELEMETRY_PUBLISHED_PHONE ) && service->isToPhoneQueueEmpty ()) {
222+ } else if (history-> hasUnpublishedNewest (TELEMETRY_PUBLISHED_PHONE ) && service->isToPhoneQueueEmpty ()) {
221223 // Mesh transmission isn't due yet, but we can still update the phone
222224 sendTelemetry (NODENUM_BROADCAST , true );
223225 }
@@ -229,9 +231,8 @@ int32_t AirQualityTelemetryModule::runOnce()
229231 return FIVE_SECONDS_MS ;
230232 }
231233
232- // Cadence is driven by lastReadMs, measured from the read itself, so a sensor warm-up sits
233- // between two reads rather than being subtracted from the interval. Costs a few seconds of drift
234- // per cycle and saves tracking it.
234+ // Cadence runs from the read itself, so a warm-up sits between two reads. Costs a few seconds
235+ // of drift per cycle and saves tracking it.
235236 return min (localLoopIntervalMs, result);
236237}
237238
@@ -240,6 +241,7 @@ bool AirQualityTelemetryModule::shouldReadSensors(bool everRead, uint32_t nowMs,
240241{
241242 if (!everRead)
242243 return true ;
244+ // Subtract-first, as Throttle::hasElapsed() does, so the 32-bit wrap cancels
243245 return (phoneQueueEmpty || meshPublishDue) && (nowMs - lastReadMs) >= localIntervalMs;
244246}
245247
@@ -254,8 +256,7 @@ int32_t AirQualityTelemetryModule::warmUpSensors()
254256{
255257 int32_t pendingMs = 0 ;
256258
257- // Every sensor is woken in the same pass: one with a long warm-up used to return early and keep
258- // the others from ever starting theirs
259+ // All in one pass: a long warm-up used to return early and starve the others
259260 for (TelemetrySensor *sensor : sensors) {
260261 if (!sensor->canSleep ()) {
261262 LOG_DEBUG (" %s: no sleep support, skip" , sensor->sensorName );
@@ -276,19 +277,36 @@ int32_t AirQualityTelemetryModule::warmUpSensors()
276277 return pendingMs;
277278}
278279
280+ void AirQualityTelemetryModule::openHistory ()
281+ {
282+ #ifdef AIR_QUALITY_TELEMETRY_HISTORY_PATH
283+ auto *persistent = makeFileTelemetryStore<meshtastic_AirQualityMetrics>(
284+ AIR_QUALITY_TELEMETRY_HISTORY_PATH , AIR_QUALITY_TELEMETRY_HISTORY_SIZE , AIR_QUALITY_TELEMETRY_HISTORY_FS );
285+ if (persistent->isUsable ()) {
286+ history = persistent;
287+ return ;
288+ }
289+
290+ // No card, full filesystem, whatever: measuring into RAM beats not measuring
291+ delete persistent;
292+ LOG_WARN (" AQ history: %s unusable, keeping readings in RAM" , AIR_QUALITY_TELEMETRY_HISTORY_PATH );
293+ #endif
294+
295+ history = new RamTelemetryStore<meshtastic_AirQualityMetrics>(AIR_QUALITY_TELEMETRY_HISTORY_SIZE );
296+ }
297+
279298void AirQualityTelemetryModule::captureReading ()
280299{
281300 meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
282301
283302 if (getAirQualityTelemetry (&m)) {
284- history. push (m.variant .air_quality_metrics , m.time );
303+ history-> push (m.variant .air_quality_metrics , m.time );
285304 } else {
286305 LOG_WARN (" AQ read failed, keep the previous reading" );
287306 }
288307
289- // Stamped either way, so a sensor that keeps failing retries on the read interval instead of on
290- // every poll
291- lastReadMs = millis ();
308+ // Stamped either way, so a failing sensor retries on the interval rather than every poll
309+ lastReadMs = Time::getMillis ();
292310 everRead = true ;
293311}
294312
@@ -434,8 +452,7 @@ bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m)
434452 // There, if any sensor fails to read - valid = false.
435453 bool valid = false ;
436454 bool hasSensor = false ;
437- // getTime() falls back to uptime-as-epoch when no clock has been set, which would stamp the
438- // reading with a bogus 1970 date. 0 lets the receiver substitute its own receive time.
455+ // getTime() falls back to uptime-as-epoch when unset; 0 lets the receiver use its rx time
439456 m->time = getValidTime (RTCQualityDevice);
440457 m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag;
441458 m->variant .air_quality_metrics = meshtastic_AirQualityMetrics_init_zero;
@@ -472,17 +489,18 @@ meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply()
472489 }
473490 // Check for a request for air quality metrics
474491 if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) {
475- // Answer from what the local loop already measured. Reading the sensors here instead
476- // would block on I2C and return nothing useful while they are asleep or warming up.
477- if (history.isEmpty ()) {
492+ // From the loop's last reading: reading here would block on I2C and return nothing
493+ // useful while the sensors are asleep or warming up.
494+ TelemetryReading<meshtastic_AirQualityMetrics> reading;
495+ if (!history->newest (reading)) {
478496 LOG_INFO (" No air quality reading yet, no reply to request" );
479497 return NULL ;
480498 }
481499
482500 meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
483501 m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag;
484- m.time = history. newest () .time ;
485- m.variant .air_quality_metrics = history. newest () .metrics ;
502+ m.time = reading .time ;
503+ m.variant .air_quality_metrics = reading .metrics ;
486504 LOG_INFO (" Air quality telemetry reply to request" );
487505 return allocDataProtobuf (m);
488506 }
@@ -514,13 +532,14 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
514532{
515533 bool sent = false ;
516534 const TelemetryPublishChannel channel = phoneOnly ? TELEMETRY_PUBLISHED_PHONE : TELEMETRY_PUBLISHED_MESH ;
535+ TelemetryReading<meshtastic_AirQualityMetrics> reading;
517536
518- // Never send the same reading twice to the same destination
519- if (history. hasUnpublishedNewest ( channel)) {
537+ // One fetch then check the mask: on a file-backed store each read costs an open
538+ if (history-> newest (reading) && !(reading. publishedMask & channel)) {
520539 meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
521540 m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag;
522- m.time = history. newest () .time ;
523- m.variant .air_quality_metrics = history. newest () .metrics ;
541+ m.time = reading .time ;
542+ m.variant .air_quality_metrics = reading .metrics ;
524543
525544 logTelemetry (m);
526545
@@ -543,13 +562,13 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
543562 if (phoneOnly) {
544563 LOG_INFO (" Sending packet to phone" );
545564 service->sendToPhone (p);
546- history. markNewestPublished (TELEMETRY_PUBLISHED_PHONE );
565+ history-> markNewestPublished (TELEMETRY_PUBLISHED_PHONE );
547566 } else {
548567 LOG_INFO (" Sending packet to mesh" );
549568 service->sendToMesh (p, RX_SRC_LOCAL , true );
550- history. markNewestPublished (TELEMETRY_PUBLISHED_MESH );
569+ history-> markNewestPublished (TELEMETRY_PUBLISHED_MESH );
551570 // ccToPhone above hands the phone this very reading, no separate phone send needed
552- history. markNewestPublished (TELEMETRY_PUBLISHED_PHONE );
571+ history-> markNewestPublished (TELEMETRY_PUBLISHED_PHONE );
553572
554573 if (isPowerSavingSensor ()) {
555574 meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed ();
0 commit comments