From 8e23190140964144bbb14ba01a7bfca4bbf7ee5c Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 15 Jul 2025 19:41:32 +0200 Subject: [PATCH] Changes on SEN5X library - removing pm_env as well --- src/modules/Telemetry/AirQualityTelemetry.cpp | 31 +- .../Telemetry/Sensor/PMSA003ISensor.cpp | 12 +- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 481 +++++++++++++++--- src/modules/Telemetry/Sensor/SEN5XSensor.h | 86 +++- src/serialization/MeshPacketSerializer.cpp | 24 +- .../MeshPacketSerializer_nRF52.cpp | 18 +- 6 files changed, 527 insertions(+), 125 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 1a2ed302e..73f6af45e 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -93,10 +93,8 @@ int32_t AirQualityTelemetryModule::runOnce() return pmsa003iSensor.wakeUp(); #endif /* PMSA003I_ENABLE_PIN */ -#ifdef SEN5X_ENABLE_PIN if (sen5xSensor.hasSensor() && !sen5xSensor.isActive()) return sen5xSensor.wakeUp(); -#endif /* SEN5X_ENABLE_PIN */ if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( @@ -114,13 +112,13 @@ int32_t AirQualityTelemetryModule::runOnce() lastSentToPhone = millis(); } + // TODO - When running this continuously, we are turning on and off the sensors but not sending data to mesh or phone, which turns on the device unnecessarily for a while #ifdef PMSA003I_ENABLE_PIN pmsa003iSensor.sleep(); #endif /* PMSA003I_ENABLE_PIN */ -#ifdef SEN5X_ENABLE_PIN - sen5xSensor.sleep(); -#endif /* SEN5X_ENABLE_PIN */ + // TODO - Add logic here to send the sensor to idle ONLY if there is enough time to wake it up before the next reading cycle + sen5xSensor.idle(); } return min(sendToPhoneIntervalMs, result); @@ -166,8 +164,7 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta const auto &m = telemetry.variant.air_quality_metrics; // Check if any telemetry field has valid data - bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard || m.has_pm10_environmental || m.has_pm25_environmental || - m.has_pm100_environmental; + bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard; if (!hasAny) { display->drawString(x, currentY, "No Telemetry"); @@ -231,9 +228,10 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack t->variant.air_quality_metrics.pm10_standard, t->variant.air_quality_metrics.pm25_standard, t->variant.air_quality_metrics.pm100_standard); - LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i", - t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental, - t->variant.air_quality_metrics.pm100_environmental); + // TODO - Decide what to do with these + // LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i", + // t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental, + // t->variant.air_quality_metrics.pm100_environmental); #endif // release previous packet before occupying a new spot if (lastMeasurementPacket != nullptr) @@ -253,16 +251,14 @@ bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag; m->variant.air_quality_metrics = meshtastic_AirQualityMetrics_init_zero; + // TODO - This is currently problematic, as it assumes only one sensor connected + // We should implement some logic to avoid not getting data if one sensor disconnects if (pmsa003iSensor.hasSensor()) { - // TODO - Should we check for sensor state here? - // If a sensor is sleeping, we should know and check to wake it up valid = valid && pmsa003iSensor.getMetrics(m); hasSensor = true; } if (sen5xSensor.hasSensor()) { - // TODO - Should we check for sensor state here? - // If a sensor is sleeping, we should know and check to wake it up valid = valid && sen5xSensor.getMetrics(m); hasSensor = true; } @@ -303,11 +299,12 @@ 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=%f, pm25_standard=%f, pm100_standard=%f, pm10_environmental=%f, pm100_environmental=%f", + LOG_INFO("Send: pm10_standard=%.2f, pm25_standard=%.2f, pm100_standard=%.2f", m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard, - m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.pm10_environmental, - m.variant.air_quality_metrics.pm100_environmental); + m.variant.air_quality_metrics.pm100_standard); meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 2b165cd6d..cd123be42 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -84,12 +84,12 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) measurement->variant.air_quality_metrics.has_pm100_standard = true; measurement->variant.air_quality_metrics.pm100_standard = pmsa003iData.pm100_standard; - measurement->variant.air_quality_metrics.has_pm10_environmental = true; - measurement->variant.air_quality_metrics.pm10_environmental = pmsa003iData.pm10_env; - measurement->variant.air_quality_metrics.has_pm25_environmental = true; - measurement->variant.air_quality_metrics.pm25_environmental = pmsa003iData.pm25_env; - measurement->variant.air_quality_metrics.has_pm100_environmental = true; - measurement->variant.air_quality_metrics.pm100_environmental = pmsa003iData.pm100_env; + // measurement->variant.air_quality_metrics.has_pm10_environmental = true; + // measurement->variant.air_quality_metrics.pm10_environmental = pmsa003iData.pm10_env; + // measurement->variant.air_quality_metrics.has_pm25_environmental = true; + // measurement->variant.air_quality_metrics.pm25_environmental = pmsa003iData.pm25_env; + // measurement->variant.air_quality_metrics.has_pm100_environmental = true; + // measurement->variant.air_quality_metrics.pm100_environmental = pmsa003iData.pm100_env; return true; } diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index b37d5f963..36c306d67 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -5,9 +5,21 @@ #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "SEN5XSensor.h" #include "TelemetrySensor.h" +#include "FSCommon.h" +#include "SPILock.h" SEN5XSensor::SEN5XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SEN5X, "SEN5X") {} +bool SEN5XSensor::restoreClock(uint32_t currentClock){ +#ifdef SEN5X_I2C_CLOCK_SPEED + if (currentClock != SEN5X_I2C_CLOCK_SPEED){ + LOG_DEBUG("Restoring I2C clock to %uHz", currentClock); + return bus->setClock(currentClock); + } + return true; +#endif +} + bool SEN5XSensor::getVersion() { if (!sendCommand(SEN5X_GET_FIRMWARE_VERSION)){ @@ -27,9 +39,9 @@ bool SEN5XSensor::getVersion() hardwareVer = versionBuffer[3] + (versionBuffer[4] / 10); protocolVer = versionBuffer[5] + (versionBuffer[6] / 10); - LOG_INFO("SEN5X Firmware Version: %d", firmwareVer); - LOG_INFO("SEN5X Hardware Version: %d", hardwareVer); - LOG_INFO("SEN5X Protocol Version: %d", protocolVer); + LOG_INFO("SEN5X Firmware Version: %0.2f", firmwareVer); + LOG_INFO("SEN5X Hardware Version: %0.2f", hardwareVer); + LOG_INFO("SEN5X Protocol Version: %0.2f", protocolVer); return true; } @@ -95,17 +107,28 @@ bool SEN5XSensor::sendCommand(uint16_t command, uint8_t* buffer, uint8_t byteNum while (bi < byteNumber) { toSend[i++] = buffer[bi++]; toSend[i++] = buffer[bi++]; - uint8_t calcCRC = CRC(&buffer[bi - 2]); + uint8_t calcCRC = sen5xCRC(&buffer[bi - 2]); toSend[i++] = calcCRC; } } +#ifdef SEN5X_I2C_CLOCK_SPEED + uint32_t currentClock; + currentClock = bus->getClock(); + if (currentClock != SEN5X_I2C_CLOCK_SPEED){ + LOG_DEBUG("Changing I2C clock to %u", SEN5X_I2C_CLOCK_SPEED); + bus->setClock(SEN5X_I2C_CLOCK_SPEED); + } +#endif + // Transmit the data LOG_INFO("Beginning connection to SEN5X: 0x%x", address); bus->beginTransmission(address); size_t writtenBytes = bus->write(toSend, bufferSize); uint8_t i2c_error = bus->endTransmission(); + restoreClock(currentClock); + if (writtenBytes != bufferSize) { LOG_ERROR("SEN5X: Error writting on I2C bus"); return false; @@ -120,32 +143,40 @@ bool SEN5XSensor::sendCommand(uint16_t command, uint8_t* buffer, uint8_t byteNum uint8_t SEN5XSensor::readBuffer(uint8_t* buffer, uint8_t byteNumber) { - size_t readedBytes = bus->requestFrom(address, byteNumber); +#ifdef SEN5X_I2C_CLOCK_SPEED + uint32_t currentClock; + currentClock = bus->getClock(); + if (currentClock != SEN5X_I2C_CLOCK_SPEED){ + LOG_DEBUG("Changing I2C clock to %u", SEN5X_I2C_CLOCK_SPEED); + bus->setClock(SEN5X_I2C_CLOCK_SPEED); + } +#endif - if (readedBytes != byteNumber) { + size_t readBytes = bus->requestFrom(address, byteNumber); + if (readBytes != byteNumber) { LOG_ERROR("SEN5X: Error reading I2C bus"); return 0; } uint8_t i = 0; uint8_t receivedBytes = 0; - while (readedBytes > 0) { + while (readBytes > 0) { buffer[i++] = bus->read(); // Just as a reminder: i++ returns i and after that increments. buffer[i++] = bus->read(); uint8_t recvCRC = bus->read(); - uint8_t calcCRC = CRC(&buffer[i - 2]); + uint8_t calcCRC = sen5xCRC(&buffer[i - 2]); if (recvCRC != calcCRC) { LOG_ERROR("SEN5X: Checksum error while receiving msg"); return 0; } - readedBytes -=3; + readBytes -=3; receivedBytes += 2; } - + restoreClock(currentClock); return receivedBytes; } -uint8_t SEN5XSensor::CRC(uint8_t* buffer) +uint8_t SEN5XSensor::sen5xCRC(uint8_t* buffer) { // This code is based on Sensirion's own implementation https://github.com/Sensirion/arduino-core/blob/41fd02cacf307ec4945955c58ae495e56809b96c/src/SensirionCrc.cpp uint8_t crc = 0xff; @@ -174,6 +205,131 @@ bool SEN5XSensor::I2Cdetect(TwoWire *_Wire, uint8_t address) else return false; } +bool SEN5XSensor::idle() +{ + // In continous mode we don't sleep + if (continousMode || forcedContinousMode) { + LOG_ERROR("SEN5X: Not going to idle mode, we are in continous mode!!"); + return false; + } + // TODO - Get VOC state before going to idle mode + // vocStateFromSensor(); + + if (!sendCommand(SEN5X_STOP_MEASUREMENT)) { + LOG_ERROR("SEN5X: Error stoping measurement"); + return false; + } + delay(200); // From Sensirion Arduino library + + LOG_INFO("SEN5X: Stop measurement mode"); + + state = SEN5X_IDLE; + measureStarted = 0; + + return true; +} + +void SEN5XSensor::loadCleaningState() +{ +#ifdef FSCom + spiLock->lock(); + auto file = FSCom.open(sen5XCleaningFileName, FILE_O_READ); + if (file) { + file.read(); + file.close(); + LOG_INFO("Cleaning state %u read for %s read from %s", lastCleaning, sensorName, sen5XCleaningFileName); + } else { + LOG_INFO("No %s state found (File: %s)", sensorName, sen5XCleaningFileName); + } + spiLock->unlock(); +#else + LOG_ERROR("ERROR: Filesystem not implemented"); +#endif +} + +void SEN5XSensor::updateCleaningState() +{ +#ifdef FSCom + spiLock->lock(); + + if (FSCom.exists(sen5XCleaningFileName) && !FSCom.remove(sen5XCleaningFileName)) { + LOG_WARN("Can't remove old state file"); + } + auto file = FSCom.open(sen5XCleaningFileName, FILE_O_WRITE); + if (file) { + LOG_INFO("Save cleaning state %u for %s to %s", lastCleaning, sensorName, sen5XCleaningFileName); + file.write(lastCleaning); + file.flush(); + file.close(); + } else { + LOG_INFO("Can't write %s state (File: %s)", sensorName, sen5XCleaningFileName); + } + + spiLock->unlock(); +#else + LOG_ERROR("ERROR: Filesystem not implemented"); +#endif +} + +bool SEN5XSensor::isActive(){ + return state == SEN5X_MEASUREMENT || state == SEN5X_MEASUREMENT_2; +} + +uint32_t SEN5XSensor::wakeUp(){ + LOG_INFO("SEN5X: Attempting to wakeUp sensor"); + if (!sendCommand(SEN5X_START_MEASUREMENT)) { + LOG_INFO("SEN5X: Error starting measurement"); + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + delay(50); // From Sensirion Arduino library + + LOG_INFO("SEN5X: Setting measurement mode"); + uint32_t now; + now = getTime(); + measureStarted = now; + state = SEN5X_MEASUREMENT; + if (state == SEN5X_MEASUREMENT) + LOG_INFO("SEN5X: Started measurement mode"); + return SEN5X_WARMUP_MS_1; +} + +bool SEN5XSensor::startCleaning() +{ + state = SEN5X_CLEANING; + + // Note that this command can only be run when the sensor is in measurement mode + if (!sendCommand(SEN5X_START_MEASUREMENT)) { + LOG_ERROR("SEN5X: Error starting measurment mode"); + return false; + } + delay(50); // From Sensirion Arduino library + + if (!sendCommand(SEN5X_START_FAN_CLEANING)) { + LOG_ERROR("SEN5X: Error starting fan cleaning"); + return false; + } + delay(20); // From Sensirion Arduino library + + // This message will be always printed so the user knows the device it's not hung + LOG_INFO("SEN5X: Started fan cleaning it will take 10 seconds..."); + + uint16_t started = millis(); + while (millis() - started < 10500) { + // Serial.print("."); + delay(500); + } + LOG_INFO(" Cleaning done!!"); + + // Save timestamp in flash so we know when a week has passed + uint32_t now; + now = getTime(); + lastCleaning = now; + updateCleaningState(); + + idle(); + return true; +} + int32_t SEN5XSensor::runOnce() { state = SEN5X_NOT_DETECTED; @@ -183,7 +339,7 @@ int32_t SEN5XSensor::runOnce() } bus = nodeTelemetrySensorsMap[sensorType].second; - // sen5x.begin(*bus); + address = (uint8_t)nodeTelemetrySensorsMap[sensorType].first; delay(50); // without this there is an error on the deviceReset function @@ -248,84 +404,265 @@ int32_t SEN5XSensor::runOnce() status = 1; LOG_INFO("SEN5X Enabled"); - // uint16_t error; - // char errorMessage[256]; - // error = sen5x.deviceReset(); - // if (error) { - // LOG_INFO("Error trying to execute deviceReset(): "); - // errorToString(error, errorMessage, 256); - // LOG_INFO(errorMessage); - // return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - // } + // Check if it is time to do a cleaning + // TODO - this is not currently working as intended - always reading 0 from the file. We should probably make a unified approach for both the cleaning and the VOCstate + loadCleaningState(); + LOG_INFO("Last cleaning time: %u", lastCleaning); + if (lastCleaning) { + LOG_INFO("Last cleaning is valid"); - // error = sen5x.startMeasurement(); - // if (error) { - // LOG_INFO("Error trying to execute startMeasurement(): "); - // errorToString(error, errorMessage, 256); - // LOG_INFO(errorMessage); - // return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - // } else { - // status = 1; - // } + uint32_t now; + now = getTime(); + LOG_INFO("Current time %us", now); + uint32_t passed = now - lastCleaning; + LOG_INFO("Elapsed time since last cleaning: %us", passed); + if (passed > ONE_WEEK_IN_SECONDS && (now > 1514764800)) { // If current date greater than 01/01/2018 (validity check) + LOG_INFO("SEN5X: More than a week since las cleaning, cleaning..."); + startCleaning(); + } else { + LOG_INFO("Last cleaning date (in epoch): %u", lastCleaning); + } + } else { + LOG_INFO("Last cleaning is not valid"); + // We asume 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. + lastCleaning = getTime(); + updateCleaningState(); + LOG_INFO("SEN5X: No valid last cleaning date found, saving it now: %u", lastCleaning); + } + + // TODO - Should wakeUp happen here? return initI2CSensor(); } void SEN5XSensor::setup() { -#ifdef SEN5X_ENABLE_PIN - pinMode(SEN5X_ENABLE_PIN, OUTPUT); - digitalWrite(SEN5X_ENABLE_PIN, HIGH); - delay(25); -#endif /* SEN5X_ENABLE_PIN */ } -#ifdef SEN5X_ENABLE_PIN -// void SEN5XSensor::sleep() { -// digitalWrite(SEN5X_ENABLE_PIN, LOW); -// state = SSEN5XState::SEN5X_OFF; +bool SEN5XSensor::readValues() +{ + if (!sendCommand(SEN5X_READ_VALUES)){ + LOG_ERROR("SEN5X: Error sending read command"); + return false; + } + LOG_DEBUG("SEN5X: Reading PM Values"); + delay(20); // From Sensirion Arduino library + + uint8_t dataBuffer[24]; + size_t receivedNumber = readBuffer(&dataBuffer[0], 24); + if (receivedNumber == 0) { + LOG_ERROR("SEN5X: Error getting values"); + return false; + } + + // First get the integers + uint16_t uint_pM1p0 = static_cast((dataBuffer[0] << 8) | dataBuffer[1]); + uint16_t uint_pM2p5 = static_cast((dataBuffer[2] << 8) | dataBuffer[3]); + uint16_t uint_pM4p0 = static_cast((dataBuffer[4] << 8) | dataBuffer[5]); + uint16_t uint_pM10p0 = static_cast((dataBuffer[6] << 8) | dataBuffer[7]); + int16_t int_humidity = static_cast((dataBuffer[8] << 8) | dataBuffer[9]); + int16_t int_temperature = static_cast((dataBuffer[10] << 8) | dataBuffer[11]); + int16_t int_vocIndex = static_cast((dataBuffer[12] << 8) | dataBuffer[13]); + int16_t int_noxIndex = static_cast((dataBuffer[14] << 8) | dataBuffer[15]); + + // TODO we should check if values are NAN before converting them + // convert them based on Sensirion Arduino lib + sen5xmeasurement.pM1p0 = uint_pM1p0 / 10.0f; + sen5xmeasurement.pM2p5 = uint_pM2p5 / 10.0f; + sen5xmeasurement.pM4p0 = uint_pM4p0 / 10.0f; + sen5xmeasurement.pM10p0 = uint_pM10p0 / 10.0f; + sen5xmeasurement.humidity = int_humidity / 100.0f; + sen5xmeasurement.temperature = int_temperature / 200.0f; + sen5xmeasurement.vocIndex = int_vocIndex / 10.0f; + sen5xmeasurement.noxIndex = int_noxIndex / 10.0f; + + // TODO - this is currently returning crap + LOG_INFO("Got: pM1p0=%.2f, pM2p5=%.2f, pM4p0=%.2f, pM10p0=%.2f", + sen5xmeasurement.pM1p0, sen5xmeasurement.pM2p5, + sen5xmeasurement.pM4p0, sen5xmeasurement.pM10p0); + + return true; +} + +bool SEN5XSensor::readPnValues() +{ + if (!sendCommand(SEN5X_READ_PM_VALUES)){ + LOG_ERROR("SEN5X: Error sending read command"); + return false; + } + LOG_DEBUG("SEN5X: Reading PN Values"); + delay(20); // From Sensirion Arduino library + + uint8_t dataBuffer[30]; + size_t receivedNumber = readBuffer(&dataBuffer[0], 30); + if (receivedNumber == 0) { + LOG_ERROR("SEN5X: Error getting PM values"); + return false; + } + + // First get the integers + // uint16_t uint_pM1p0 = static_cast((dataBuffer[0] << 8) | dataBuffer[1]); + // uint16_t uint_pM2p5 = static_cast((dataBuffer[2] << 8) | dataBuffer[3]); + // uint16_t uint_pM4p0 = static_cast((dataBuffer[4] << 8) | dataBuffer[5]); + // uint16_t uint_pM10p0 = static_cast((dataBuffer[6] << 8) | dataBuffer[7]); + uint16_t uint_pN0p5 = static_cast((dataBuffer[8] << 8) | dataBuffer[9]); + uint16_t uint_pN1p0 = static_cast((dataBuffer[10] << 8) | dataBuffer[11]); + uint16_t uint_pN2p5 = static_cast((dataBuffer[12] << 8) | dataBuffer[13]); + uint16_t uint_pN4p0 = static_cast((dataBuffer[14] << 8) | dataBuffer[15]); + uint16_t uint_pN10p0 = static_cast((dataBuffer[16] << 8) | dataBuffer[17]); + uint16_t uint_tSize = static_cast((dataBuffer[18] << 8) | dataBuffer[19]); + + // Convert them based on Sensirion Arduino lib + // sen5xmeasurement.pM1p0 = uint_pM1p0 / 10.0f; + // sen5xmeasurement.pM2p5 = uint_pM2p5 / 10.0f; + // sen5xmeasurement.pM4p0 = uint_pM4p0 / 10.0f; + // sen5xmeasurement.pM10p0 = uint_pM10p0 / 10.0f; + sen5xmeasurement.pN0p5 = uint_pN0p5 / 10; + sen5xmeasurement.pN1p0 = uint_pN1p0 / 10; + sen5xmeasurement.pN2p5 = uint_pN2p5 / 10; + sen5xmeasurement.pN4p0 = uint_pN4p0 / 10; + sen5xmeasurement.pN10p0 = uint_pN10p0 / 10; + sen5xmeasurement.tSize = uint_tSize / 1000.0f; + + // Convert PN readings from #/cm3 to #/0.1l + sen5xmeasurement.pN0p5 *= 100; + sen5xmeasurement.pN1p0 *= 100; + sen5xmeasurement.pN2p5 *= 100; + sen5xmeasurement.pN4p0 *= 100; + sen5xmeasurement.pN10p0 *= 100; + sen5xmeasurement.tSize *= 100; + + // TODO - this is currently returning crap + LOG_INFO("Got: pN0p5=%u, pN1p0=%u, pN2p5=%u, pN4p0=%u, pN10p0=%u, tSize=%.2f", + sen5xmeasurement.pN0p5, sen5xmeasurement.pN1p0, + sen5xmeasurement.pN2p5, sen5xmeasurement.pN4p0, + sen5xmeasurement.pN10p0, sen5xmeasurement.tSize + ); + + return true; +} + +// TODO - Decide if we want to have this here or not +// bool SEN5XSensor::readRawValues() +// { +// if (!sendCommand(SEN5X_READ_RAW_VALUES)){ +// LOG_ERROR("SEN5X: Error sending read command"); +// return false; +// } +// delay(20); // From Sensirion Arduino library + +// uint8_t dataBuffer[12]; +// size_t receivedNumber = readBuffer(&dataBuffer[0], 12); +// if (receivedNumber == 0) { +// LOG_ERROR("SEN5X: Error getting Raw values"); +// return false; +// } + +// // Get values +// rawHumidity = static_cast((dataBuffer[0] << 8) | dataBuffer[1]); +// rawTemperature = static_cast((dataBuffer[2] << 8) | dataBuffer[3]); +// rawVoc = static_cast((dataBuffer[4] << 8) | dataBuffer[5]); +// rawNox = static_cast((dataBuffer[6] << 8) | dataBuffer[7]); + +// return true; // } -// uint32_t SEN5XSensor::wakeUp() { -// digitalWrite(SEN5X_ENABLE_PIN, HIGH); -// state = SEN5XState::SEN5X_IDLE; -// return SEN5X_WARMUP_MS; -// } -#endif /* SEN5X_ENABLE_PIN */ +uint8_t SEN5XSensor::getMeasurements() +{ + // Try to get new data + if (!sendCommand(SEN5X_READ_DATA_READY)){ + LOG_ERROR("SEN5X: Error sending command data ready flag"); + return 2; + } + delay(20); // From Sensirion Arduino library + + uint8_t dataReadyBuffer[3]; + size_t charNumber = readBuffer(&dataReadyBuffer[0], 3); + if (charNumber == 0) { + LOG_ERROR("SEN5X: Error getting device version value"); + return 2; + } + + bool data_ready = dataReadyBuffer[1]; + + if (!data_ready) { + LOG_INFO("SEN5X: Data is not ready"); + return 1; + } + + if(!readValues()) { + LOG_ERROR("SEN5X: Error getting readings"); + return 2; + } + + if(!readPnValues()) { + LOG_ERROR("SEN5X: Error getting PM readings"); + return 2; + } + + // if(!readRawValues()) { + // LOG_ERROR("SEN5X: Error getting Raw readings"); + // return 2; + // } + + return 0; +} bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement) { - uint16_t error; - char errorMessage[256]; + LOG_INFO("SEN5X: Attempting to get metrics"); + if (!isActive()){ + LOG_INFO("SEN5X: not in measurement mode"); + return false; + } - // Read Measurement - float massConcentrationPm1p0; - float massConcentrationPm2p5; - float massConcentrationPm4p0; - float massConcentrationPm10p0; - float ambientHumidity; - float ambientTemperature; - float vocIndex; - float noxIndex; + uint8_t response; + response = getMeasurements(); - // error = sen5x.readMeasuredValues( - // massConcentrationPm1p0, massConcentrationPm2p5, massConcentrationPm4p0, - // massConcentrationPm10p0, ambientHumidity, ambientTemperature, vocIndex, - // noxIndex); + if (response == 0) { + measurement->variant.air_quality_metrics.has_pm10_standard = true; + measurement->variant.air_quality_metrics.pm10_standard = sen5xmeasurement.pM1p0; + measurement->variant.air_quality_metrics.has_pm25_standard = true; + measurement->variant.air_quality_metrics.pm25_standard = sen5xmeasurement.pM2p5; + measurement->variant.air_quality_metrics.has_pm40_standard = true; + measurement->variant.air_quality_metrics.pm40_standard = sen5xmeasurement.pM4p0; + measurement->variant.air_quality_metrics.has_pm100_standard = true; + measurement->variant.air_quality_metrics.pm100_standard = sen5xmeasurement.pM10p0; - // if (error) { - // LOG_INFO("Error trying to execute readMeasuredValues(): "); - // errorToString(error, errorMessage, 256); - // LOG_INFO(errorMessage); - // return false; - // } + measurement->variant.air_quality_metrics.has_particles_05um = true; + measurement->variant.air_quality_metrics.particles_05um = sen5xmeasurement.pN0p5; + measurement->variant.air_quality_metrics.has_particles_10um = true; + measurement->variant.air_quality_metrics.particles_10um = sen5xmeasurement.pN1p0; + measurement->variant.air_quality_metrics.has_particles_25um = true; + measurement->variant.air_quality_metrics.particles_25um = sen5xmeasurement.pN2p5; + measurement->variant.air_quality_metrics.has_particles_40um = true; + measurement->variant.air_quality_metrics.particles_40um = sen5xmeasurement.pN4p0; + measurement->variant.air_quality_metrics.has_particles_100um = true; + measurement->variant.air_quality_metrics.particles_100um = sen5xmeasurement.pN10p0; - // measurement->variant.air_quality_metrics.has_pm10_standard = true; - // measurement->variant.air_quality_metrics.pm10_standard = massConcentrationPm1p0; - // measurement->variant.air_quality_metrics.has_pm25_standard = true; - // measurement->variant.air_quality_metrics.pm25_standard = massConcentrationPm2p5; - // measurement->variant.air_quality_metrics.has_pm100_standard = true; - // measurement->variant.air_quality_metrics.pm100_standard = massConcentrationPm10p0; + if (model == SEN54 || model == SEN55) { + measurement->variant.air_quality_metrics.has_pm_humidity = true; + measurement->variant.air_quality_metrics.pm_humidity = sen5xmeasurement.humidity; + measurement->variant.air_quality_metrics.has_pm_temperature = true; + measurement->variant.air_quality_metrics.pm_temperature = sen5xmeasurement.temperature; + measurement->variant.air_quality_metrics.has_pm_nox_idx = true; + measurement->variant.air_quality_metrics.pm_nox_idx = sen5xmeasurement.noxIndex; + } + + if (model == SEN55) { + measurement->variant.air_quality_metrics.has_pm_voc_idx = true; + measurement->variant.air_quality_metrics.pm_voc_idx = sen5xmeasurement.vocIndex; + } + return true; + } else if (response == 1) { + // TODO return because data was not ready yet + // Should this return false? + return false; + } else if (response == 2) { + // Return with error for non-existing data + return false; + } return true; } diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.h b/src/modules/Telemetry/Sensor/SEN5XSensor.h index 9b80e3224..b8e0a0ac9 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.h +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h @@ -5,13 +5,45 @@ #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include "Wire.h" -// #include +#include "RTC.h" -#ifndef SEN5X_WARMUP_MS +#ifndef SEN5X_WARMUP_MS_1 // from the SEN5X datasheet -#define SEN5X_WARMUP_MS 30000 +// #define SEN5X_WARMUP_MS_1 15000 - Change to this +#define SEN5X_WARMUP_MS_1 30000 #endif +// TODO - For now, we ignore this threshold, and we only use the MS_1 (to 30000) +#ifndef SEN5X_WARMUP_MS_2 +// from the SEN5X datasheet +#define SEN5X_WARMUP_MS_2 30000 +#endif + +#ifndef SEN5X_I2C_CLOCK_SPEED +#define SEN5X_I2C_CLOCK_SPEED 100000 +#endif + +#define ONE_WEEK_IN_SECONDS 604800 + +// TODO - These are currently ints in the protobuf +// Decide on final type for this values and change accordingly +struct _SEN5XMeasurements { + float pM1p0; + float pM2p5; + float pM4p0; + float pM10p0; + uint32_t pN0p5; + uint32_t pN1p0; + uint32_t pN2p5; + uint32_t pN4p0; + uint32_t pN10p0; + float tSize; + float humidity; + float temperature; + float vocIndex; + float noxIndex; +}; + class SEN5XSensor : public TelemetrySensor { private: @@ -45,26 +77,62 @@ class SEN5XSensor : public TelemetrySensor enum SEN5XState { SEN5X_OFF, SEN5X_IDLE, SEN5X_MEASUREMENT, SEN5X_MEASUREMENT_2, SEN5X_CLEANING, SEN5X_NOT_DETECTED }; SEN5XState state = SEN5X_OFF; + bool continousMode = false; + bool forcedContinousMode = false; + + // TODO + // Sensirion recommends taking a reading after 16 seconds, if the Perticle number reading is over 100#/cm3 the reading is OK, but if it is lower wait until 30 seconds and take it again. + // https://sensirion.com/resource/application_note/low_power_mode/sen5x + // TODO Implement logic for this concentrationThreshold + // This can reduce battery consumption by a lot + // uint16_t concentrationThreshold = 100; + bool sendCommand(uint16_t wichCommand); bool sendCommand(uint16_t wichCommand, uint8_t* buffer, uint8_t byteNumber=0); uint8_t readBuffer(uint8_t* buffer, uint8_t byteNumber); // Return number of bytes received - uint8_t CRC(uint8_t* buffer); + uint8_t sen5xCRC(uint8_t* buffer); bool I2Cdetect(TwoWire *_Wire, uint8_t address); + bool restoreClock(uint32_t); + bool startCleaning(); + uint8_t getMeasurements(); + bool readRawValues(); + bool readPnValues(); + bool readValues(); + + uint32_t measureStarted = 0; + _SEN5XMeasurements sen5xmeasurement; protected: + // Store status of the sensor in this file + const char *sen5XCleaningFileName = "/prefs/sen5XCleaning.dat"; + const char *sen5XVOCFileName = "/prefs/sen5XVOC.dat"; + + // Cleaning State + #define SEN5X_MAX_CLEANING_SIZE 32 + // Last cleaning status - if > 0 - valid, otherwise 0 + uint32_t lastCleaning = 0; + void loadCleaningState(); + void updateCleaningState(); + + // TODO - VOC State + // # define SEN5X_VOC_STATE_BUFFER_SIZE 12 + // uint8_t VOCstate[SEN5X_VOC_STATE_BUFFER_SIZE]; + // struct VOCstateStruct { uint8_t state[SEN5X_VOC_STATE_BUFFER_SIZE]; uint32_t time; bool valid=true; }; + // void loadVOCState(); + // void updateVOCState(); + virtual void setup() override; public: -// #ifdef SEN5X_ENABLE_PIN - // void sleep(); - // uint32_t wakeUp(); -// #endif - SEN5XSensor(); bool isActive(); + uint32_t wakeUp(); + bool idle(); virtual int32_t runOnce() override; virtual bool getMetrics(meshtastic_Telemetry *measurement) override; }; + + #endif \ No newline at end of file diff --git a/src/serialization/MeshPacketSerializer.cpp b/src/serialization/MeshPacketSerializer.cpp index 29a9b6840..43509edcd 100644 --- a/src/serialization/MeshPacketSerializer.cpp +++ b/src/serialization/MeshPacketSerializer.cpp @@ -125,18 +125,18 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, if (decoded->variant.air_quality_metrics.has_pm100_standard) { msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard); } - if (decoded->variant.air_quality_metrics.has_pm10_environmental) { - msgPayload["pm10_e"] = - new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental); - } - if (decoded->variant.air_quality_metrics.has_pm25_environmental) { - msgPayload["pm25_e"] = - new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental); - } - if (decoded->variant.air_quality_metrics.has_pm100_environmental) { - msgPayload["pm100_e"] = - new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental); - } + // if (decoded->variant.air_quality_metrics.has_pm10_environmental) { + // msgPayload["pm10_e"] = + // new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental); + // } + // if (decoded->variant.air_quality_metrics.has_pm25_environmental) { + // msgPayload["pm25_e"] = + // new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental); + // } + // if (decoded->variant.air_quality_metrics.has_pm100_environmental) { + // msgPayload["pm100_e"] = + // new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental); + // } } else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { if (decoded->variant.power_metrics.has_ch1_voltage) { msgPayload["voltage_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_voltage); diff --git a/src/serialization/MeshPacketSerializer_nRF52.cpp b/src/serialization/MeshPacketSerializer_nRF52.cpp index e0daa1a88..065c28827 100644 --- a/src/serialization/MeshPacketSerializer_nRF52.cpp +++ b/src/serialization/MeshPacketSerializer_nRF52.cpp @@ -120,15 +120,15 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, if (decoded->variant.air_quality_metrics.has_pm100_standard) { jsonObj["payload"]["pm100"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_standard; } - if (decoded->variant.air_quality_metrics.has_pm10_environmental) { - jsonObj["payload"]["pm10_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_environmental; - } - if (decoded->variant.air_quality_metrics.has_pm25_environmental) { - jsonObj["payload"]["pm25_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_environmental; - } - if (decoded->variant.air_quality_metrics.has_pm100_environmental) { - jsonObj["payload"]["pm100_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_environmental; - } + // if (decoded->variant.air_quality_metrics.has_pm10_environmental) { + // jsonObj["payload"]["pm10_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_environmental; + // } + // if (decoded->variant.air_quality_metrics.has_pm25_environmental) { + // jsonObj["payload"]["pm25_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_environmental; + // } + // if (decoded->variant.air_quality_metrics.has_pm100_environmental) { + // jsonObj["payload"]["pm100_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_environmental; + // } } else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { if (decoded->variant.power_metrics.has_ch1_voltage) { jsonObj["payload"]["voltage_ch1"] = decoded->variant.power_metrics.ch1_voltage;