From 10387652199b741a3db561f35333795014c1f188 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sat, 30 Aug 2025 13:00:22 +0200 Subject: [PATCH] Working implementation on VOCState * Adds initial timer for SEN55 to not sleep if VOCstate is not stable (1h) * Adds conditions for stability and sensor state --- src/modules/Telemetry/AirQualityTelemetry.cpp | 13 +- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 229 +++++++++++------- src/modules/Telemetry/Sensor/SEN5XSensor.h | 19 +- 3 files changed, 172 insertions(+), 89 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 33d6399c5..8ecd72563 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -79,13 +79,16 @@ int32_t AirQualityTelemetryModule::runOnce() } // Wake up the sensors that need it, before we need to take telemetry data - if ((lastSentToMesh == 0) || + // TODO - Do it for SENSOR ROLE too? + if (((lastSentToMesh == 0) || (sen5xSensor.hasSensor() && !Throttle::isWithinTimespanMs(lastSentToMesh - SEN5X_WARMUP_MS_1, Default::getConfiguredOrDefaultMsScaled( moduleConfig.telemetry.air_quality_interval, default_telemetry_broadcast_interval_secs, numOnlineNodes))) || (pmsa003iSensor.hasSensor() && !Throttle::isWithinTimespanMs(lastSentToMesh - PMSA003I_WARMUP_MS, Default::getConfiguredOrDefaultMsScaled( moduleConfig.telemetry.air_quality_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes)))) { + default_telemetry_broadcast_interval_secs, numOnlineNodes)))) && + airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && + airTime->isTxAllowedAirUtil()) { if (sen5xSensor.hasSensor() && !sen5xSensor.isActive()) return sen5xSensor.wakeUp(); @@ -100,10 +103,11 @@ int32_t AirQualityTelemetryModule::runOnce() if (sen5xSensor.hasSensor() && sen5xSensor.isActive()) { sen5xPendingForReady = sen5xSensor.pendingForReady(); LOG_DEBUG("SEN5X: Pending for ready %ums", sen5xPendingForReady); - if (sen5xPendingForReady) { + if (sen5xPendingForReady > 0) { return sen5xPendingForReady; } } + LOG_DEBUG("Checking if sending telemetry"); if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( @@ -122,6 +126,7 @@ int32_t AirQualityTelemetryModule::runOnce() } // Send the sensor to idle ONLY if there is enough time to wake it up before the next reading cycle + // TODO - include conditions here for module timing #ifdef PMSA003I_ENABLE_PIN if (pmsa003iSensor.hasSensor() && pmsa003iSensor.isActive()) { if (PMSA003I_WARMUP_MS < Default::getConfiguredOrDefaultMsScaled( @@ -322,8 +327,10 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag; m.time = getTime(); + // TODO - if one sensor fails here, we will stop taking measurements from everything // Can we do this in a smarter way, for instance checking the nodeTelemetrySensor map and making it dynamic? + if (getAirQualityTelemetry(&m)) { LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u", m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard, diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index 2c31d3f1d..6e1ed89c9 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -218,25 +218,36 @@ bool SEN5XSensor::I2Cdetect(TwoWire *_Wire, uint8_t address) bool SEN5XSensor::idle() { + // From the datasheet: + // By default, the VOC algorithm resets its state to initial + // values each time a measurement is started, + // even if the measurement was stopped only for a short + // time. So, the VOC index output value needs a long time + // until it is stable again. This can be avoided by + // restoring the previously memorized algorithm state before + // starting the measure mode - - // Get VOC state before going to idle mode - if (vocStateFromSensor()) { - // TODO Should this be saved with saveState()? - // It so, we can consider not saving it when rebooting as - // we would have likely saved it recently - - // Check if we have time, and store it - uint32_t now; // If time is RTCQualityNone, it will return zero - now = getValidTime(RTCQuality::RTCQualityDevice); - - if (now) { - vocTime = now; - vocValid = true; - // saveState(); - } - } else { + // If the stabilisation period is not passed for SEN55, don't go to idle + if (model == SEN55) { + // Get VOC state before going to idle mode vocValid = false; + if (vocStateFromSensor()) { + vocValid = vocStateValid(); + // Check if we have time, and store it + uint32_t now; // If time is RTCQualityNone, it will return zero + now = getValidTime(RTCQuality::RTCQualityDevice); + if (now) { + // Check if state is valid (non-zero) + vocTime = now; + } + } + + if (vocStateStable() && vocValid) { + saveState(); + } else { + LOG_INFO("SEN5X: Not stopping measurement, vocState is not stable yet!"); + return true; + } } if (!oneShotMode) { @@ -248,8 +259,8 @@ bool SEN5XSensor::idle() LOG_ERROR("SEN5X: Error stoping measurement"); return false; } - delay(200); // From Sensirion Datasheet + delay(200); // From Sensirion Datasheet LOG_INFO("SEN5X: Stop measurement mode"); state = SEN5X_IDLE; @@ -257,12 +268,40 @@ bool SEN5XSensor::idle() return true; } +bool SEN5XSensor::vocStateRecent(uint32_t now){ + if (now) { + uint32_t passed = now - vocTime; //in seconds + + // Check if state is recent, less than 10 minutes (600 seconds) + if (passed < SEN5X_VOC_VALID_TIME && (now > SEN5X_VOC_VALID_DATE)) { + return true; + } + } + return false; +} + +bool SEN5XSensor::vocStateValid() { + if (!vocState[0] && !vocState[1] && !vocState[2] && !vocState[3] && + !vocState[4] && !vocState[5] && !vocState[6] && !vocState[7]) { + LOG_DEBUG("SEN5X: VOC state is all 0, invalid"); + return false; + } else { + LOG_DEBUG("SEN5X: VOC state is valid"); + return true; + } +} + bool SEN5XSensor::vocStateToSensor() { if (model != SEN55){ return true; } + if (!vocStateValid()) { + LOG_INFO("SEN5X: VOC state is invalid, not sending"); + return true; + } + if (!sendCommand(SEN5X_STOP_MEASUREMENT)) { LOG_ERROR("SEN5X: Error stoping measurement"); return false; @@ -320,8 +359,7 @@ bool SEN5XSensor::vocStateFromSensor() vocState[7] = vocBuffer[10]; // Print the state (if debug is on) - LOG_DEBUG("SEN5X: VOC state retrieved from sensor"); - LOG_DEBUG("[%u, %u, %u, %u, %u, %u, %u, %u]", + LOG_DEBUG("SEN5X: VOC state retrieved from sensor: [%u, %u, %u, %u, %u, %u, %u, %u]", vocState[0],vocState[1], vocState[2], vocState[3], vocState[4],vocState[5], vocState[6], vocState[7]); @@ -337,24 +375,36 @@ bool SEN5XSensor::loadState() if (file) { LOG_INFO("%s state read from %s", sensorName, sen5XStateFileName); pb_istream_t stream = {&readcb, &file, meshtastic_SEN5XState_size}; + if (!pb_decode(&stream, &meshtastic_SEN5XState_msg, &sen5xstate)) { LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream)); } else { lastCleaning = sen5xstate.last_cleaning_time; lastCleaningValid = sen5xstate.last_cleaning_valid; oneShotMode = sen5xstate.one_shot_mode; - // Unpack state - vocState[7] = (uint8_t)(sen5xstate.voc_state >> 56); - vocState[6] = (uint8_t)(sen5xstate.voc_state >> 48); - vocState[5] = (uint8_t)(sen5xstate.voc_state >> 40); - vocState[4] = (uint8_t)(sen5xstate.voc_state >> 32); - vocState[3] = (uint8_t)(sen5xstate.voc_state >> 24); - vocState[2] = (uint8_t)(sen5xstate.voc_state >> 16); - vocState[1] = (uint8_t)(sen5xstate.voc_state >> 8); - vocState[0] = (uint8_t)sen5xstate.voc_state; - vocTime = sen5xstate.voc_time; - vocValid = sen5xstate.voc_valid; + if (model == SEN55) { + vocTime = sen5xstate.voc_state_time; + vocValid = sen5xstate.voc_state_valid; + // Unpack state + vocState[7] = (uint8_t)(sen5xstate.voc_state_array >> 56); + vocState[6] = (uint8_t)(sen5xstate.voc_state_array >> 48); + vocState[5] = (uint8_t)(sen5xstate.voc_state_array >> 40); + vocState[4] = (uint8_t)(sen5xstate.voc_state_array >> 32); + vocState[3] = (uint8_t)(sen5xstate.voc_state_array >> 24); + vocState[2] = (uint8_t)(sen5xstate.voc_state_array >> 16); + vocState[1] = (uint8_t)(sen5xstate.voc_state_array >> 8); + vocState[0] = (uint8_t) sen5xstate.voc_state_array; + } + + // LOG_DEBUG("Loaded lastCleaning %u", lastCleaning); + // LOG_DEBUG("Loaded lastCleaningValid %u", lastCleaningValid); + // LOG_DEBUG("Loaded oneShotMode %s", oneShotMode ? "true" : "false"); + // LOG_DEBUG("Loaded vocTime %u", vocTime); + // LOG_DEBUG("Loaded [%u, %u, %u, %u, %u, %u, %u, %u]", + // vocState[7], vocState[6], vocState[5], vocState[4], vocState[3], vocState[2], vocState[1], vocState[0]); + // LOG_DEBUG("Loaded %svalid VOC state", vocValid ? "" : "in"); + okay = true; } file.close(); @@ -370,7 +420,7 @@ bool SEN5XSensor::loadState() bool SEN5XSensor::saveState() { - // TODO - This should be called before a reboot + // TODO - This should be called before a reboot for VOC index storage // is there a way to get notified? #ifdef FSCom auto file = SafeFile(sen5XStateFileName); @@ -379,20 +429,23 @@ bool SEN5XSensor::saveState() sen5xstate.last_cleaning_valid = lastCleaningValid; sen5xstate.one_shot_mode = oneShotMode; - // Unpack state (12 bytes in two parts) - sen5xstate.voc_state = ((uint64_t) vocState[7] << 56) | - ((uint64_t) vocState[6] << 48) | - ((uint64_t) vocState[5] << 40) | - ((uint64_t) vocState[4] << 32) | - ((uint32_t) vocState[3] << 24) | - ((uint32_t) vocState[2] << 16) | - ((uint32_t) vocState[1] << 8) | - vocState[0]; + if (model == SEN55) { + sen5xstate.has_voc_state_time = true; + sen5xstate.has_voc_state_valid = true; + sen5xstate.has_voc_state_array = true; - LOG_INFO("sen5xstate.voc_state %i", sen5xstate.voc_state); - - sen5xstate.voc_time = vocTime; - sen5xstate.voc_valid = vocValid; + sen5xstate.voc_state_time = vocTime; + sen5xstate.voc_state_valid = vocValid; + // Unpack state (8 bytes) + sen5xstate.voc_state_array = (((uint64_t) vocState[7]) << 56) | + ((uint64_t) vocState[6] << 48) | + ((uint64_t) vocState[5] << 40) | + ((uint64_t) vocState[4] << 32) | + ((uint64_t) vocState[3] << 24) | + ((uint64_t) vocState[2] << 16) | + ((uint64_t) vocState[1] << 8) | + ((uint64_t) vocState[0]); + } bool okay = false; @@ -421,42 +474,26 @@ bool SEN5XSensor::isActive(){ } uint32_t SEN5XSensor::wakeUp(){ - // LOG_INFO("SEN5X: Attempting to wakeUp sensor"); + uint32_t now; + now = getValidTime(RTCQuality::RTCQualityDevice); + LOG_DEBUG("SEN5X: Waking up sensor"); - // From the datasheet - // By default, the VOC algorithm resets its state to initial - // values each time a measurement is started, - // even if the measurement was stopped only for a short - // time. So, the VOC index output value needs a long time - // until it is stable again. This can be avoided by - // restoring the previously memorized algorithm state before - // starting the measure mode - - // TODO - This needs to be tested - // In SC, the sensor is operated in contionuous mode if - // VOCs are present, increasing battery consumption - // A different approach should be possible as stated on the - // datasheet (see above) - // uint32_t now, passed; - // now = getValidTime(RTCQuality::RTCQualityDevice); - // passed = now - vocTime; //in seconds - // // Check if state is recent, less than 10 minutes (600 seconds) - // if ((passed < SEN5X_VOC_VALID_TIME) && (now > SEN5X_VOC_VALID_DATE) && vocValid) { - // if (!vocStateToSensor()){ - // LOG_ERROR("SEN5X: Sending VOC state to sensor failed"); - // } - // } else { - // LOG_DEBUG("SEN5X: No valid VOC state found. Ignoring"); - // } + // Check if state is recent, less than 10 minutes (600 seconds) + if (vocStateRecent(now) && vocStateValid()) { + if (!vocStateToSensor()){ + LOG_ERROR("SEN5X: Sending VOC state to sensor failed"); + } + } else { + LOG_DEBUG("SEN5X: No valid VOC state found. Ignoring"); + } if (!sendCommand(SEN5X_START_MEASUREMENT)) { LOG_ERROR("SEN5X: Error starting measurement"); - // TODO - what should this return?? + // TODO - what should this return?? Something actually on the default interval return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } delay(50); // From Sensirion Datasheet - // LOG_INFO("SEN5X: Setting measurement mode"); // TODO - This is currently "problematic" // If time is updated in between reads, there is no way to // keep track of how long it has passed @@ -467,6 +504,15 @@ uint32_t SEN5XSensor::wakeUp(){ return SEN5X_WARMUP_MS_1; } +bool SEN5XSensor::vocStateStable() +{ + uint32_t now; + now = getTime(); + uint32_t sinceFirstMeasureStarted = (now - firstMeasureStarted); + LOG_DEBUG("sinceFirstMeasureStarted: %us", sinceFirstMeasureStarted); + return sinceFirstMeasureStarted > SEN55_VOC_STATE_WARMUP_S; +} + bool SEN5XSensor::startCleaning() { // Note: we only should enter here if we have a valid RTC with at least @@ -532,7 +578,7 @@ int32_t SEN5XSensor::runOnce() return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } - // Check if firmware version allows The direct switch between Measurement and RHT/Gas-Only Measurement mode + // Check the firmware version if (!getVersion()) return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; if (firmwareVer < 2) { LOG_ERROR("SEN5X: error firmware is too old and will not work with this implementation"); @@ -551,8 +597,8 @@ int32_t SEN5XSensor::runOnce() uint32_t now; int32_t passed; now = getValidTime(RTCQuality::RTCQualityDevice); - // If time is not RTCQualityNone, it will return non-zero + // If time is not RTCQualityNone, it will return non-zero if (now) { if (lastCleaningValid) { @@ -566,36 +612,35 @@ int32_t SEN5XSensor::runOnce() LOG_INFO("SEN5X: Cleaning not needed (%ds passed). Last cleaning date (in epoch): %us", passed, lastCleaning); } } else { - // We assume the device has just been updated or it is new, so no need to trigger a cleaning. + // We assume the device has just been updated or it is new, + // so no need to trigger a cleaning. // Just save the timestamp to do a cleaning one week from now. - // TODO - could we trigger this after getting time? // Otherwise, we will never trigger cleaning in some cases lastCleaning = now; lastCleaningValid = true; LOG_INFO("SEN5X: No valid last cleaning date found, saving it now: %us", lastCleaning); saveState(); } + if (model == SEN55) { if (!vocValid) { LOG_INFO("SEN5X: No valid VOC's state found"); } else { - passed = now - vocTime; //in seconds - - // Check if state is recent, less than 10 minutes (600 seconds) - if (passed < SEN5X_VOC_VALID_TIME && (now > SEN5X_VOC_VALID_DATE)) { + // Check if state is recent + if (vocStateRecent(now)) { // If current date greater than 01/01/2018 (validity check) // Send it to the sensor LOG_INFO("SEN5X: VOC state is valid and recent"); vocStateToSensor(); } else { - LOG_INFO("SEN5X VOC state is to old or date is invalid"); + LOG_INFO("SEN5X: VOC state is too old or date is invalid"); + LOG_DEBUG("SEN5X: vocTime %u, Passed %u, and now %u", vocTime, passed, now); } } } - } else { // TODO - Should this actually ignore? We could end up never cleaning... - LOG_INFO("SEN5X: Not enough RTCQuality, ignoring saved state"); + LOG_INFO("SEN5X: Not enough RTCQuality, ignoring saved state. Trying again later"); } return initI2CSensor(); @@ -645,6 +690,17 @@ bool SEN5XSensor::readValues() sen5xmeasurement.pM1p0, sen5xmeasurement.pM2p5, sen5xmeasurement.pM4p0, sen5xmeasurement.pM10p0); + if (model == SEN54 || model == SEN55) { + LOG_DEBUG("Got: humidity=%.2f, temperature=%.2f, noxIndex=%.2f", + sen5xmeasurement.humidity, sen5xmeasurement.temperature, + sen5xmeasurement.noxIndex); + } + + if (model == SEN55) { + LOG_DEBUG("Got: vocIndex=%.2f", + sen5xmeasurement.vocIndex); + } + return true; } @@ -783,6 +839,10 @@ int32_t SEN5XSensor::pendingForReady(){ return SEN5X_WARMUP_MS_1 - sinceMeasureStarted; } + if (!firstMeasureStarted) { + firstMeasureStarted = now; + } + // Get PN values to check if we are above or below threshold readPnValues(true); @@ -883,6 +943,7 @@ bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement) } } + return true; } else if (response == 1) { // TODO return because data was not ready yet diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.h b/src/modules/Telemetry/Sensor/SEN5XSensor.h index 1f4694a35..d94bfe76c 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.h +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h @@ -20,6 +20,16 @@ #define SEN5X_I2C_CLOCK_SPEED 100000 #endif +/* +Time after which the sensor can go to sleep, as the warmup period has passed +and the VOCs sensor will is allowed to stop (although needs to recover the state +each time) +*/ +#ifndef SEN55_VOC_STATE_WARMUP_S +// TODO for Testing 5' - Sensirion recommends 1h. We can try to test a smaller value +#define SEN55_VOC_STATE_WARMUP_S 3600 +#endif + #define ONE_WEEK_IN_SECONDS 604800 struct _SEN5XMeasurements { @@ -77,6 +87,7 @@ class SEN5XSensor : public TelemetrySensor // Flag to work on one-shot (read and sleep), or continuous mode bool oneShotMode = true; void setMode(bool setOneShot); + bool vocStateValid(); bool sendCommand(uint16_t command); bool sendCommand(uint16_t command, uint8_t* buffer, uint8_t byteNumber=0); @@ -91,6 +102,7 @@ class SEN5XSensor : public TelemetrySensor bool readValues(); uint32_t measureStarted = 0; + uint32_t firstMeasureStarted = 0; _SEN5XMeasurements sen5xmeasurement; protected: @@ -108,10 +120,13 @@ class SEN5XSensor : public TelemetrySensor // VOC State #define SEN5X_VOC_STATE_BUFFER_SIZE 8 uint8_t vocState[SEN5X_VOC_STATE_BUFFER_SIZE]; - uint32_t vocTime; - bool vocValid = true; + uint32_t vocTime = 0; + bool vocValid = false; + bool vocStateFromSensor(); bool vocStateToSensor(); + bool vocStateStable(); + bool vocStateRecent(uint32_t now); virtual void setup() override;