diff --git a/platformio.ini b/platformio.ini index c0eb6fedb..98362f684 100644 --- a/platformio.ini +++ b/platformio.ini @@ -201,4 +201,4 @@ lib_deps = # renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core sensirion/Sensirion Core@0.7.1 # renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x - sensirion/Sensirion I2C SCD4x@1.1.0 + sensirion/Sensirion I2C SCD4x@1.1.0 \ No newline at end of file diff --git a/src/configuration.h b/src/configuration.h index cddc7ba7a..97138d475 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -195,6 +195,7 @@ along with this program. If not, see . #define LTR390UV_ADDR 0x53 #define XPOWERS_AXP192_AXP2101_ADDRESS 0x34 // same adress as TCA8418 #define PCT2075_ADDR 0x37 +#define SEN5X_ADDR 0x69 // ----------------------------------------------------------------------------- // ACCELEROMETER diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index dd290db98..e25d7ce55 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -75,6 +75,7 @@ class ScanI2C TCA8418KB, PCT2075, BMM150, + SEN5X } DeviceType; // typedef uint8_t DeviceAddress; diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 9e9441123..00bfda2a7 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -8,6 +8,7 @@ #endif #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) #include "meshUtils.h" // vformat + #endif bool in_array(uint8_t *array, int size, uint8_t lookfor) @@ -109,6 +110,75 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation return value; } +bool ScanI2CTwoWire::setClockSpeed(I2CPort port, uint32_t speed) { + + DeviceAddress addr(port, 0x00); + TwoWire *i2cBus; + +#if WIRE_INTERFACES_COUNT == 2 + if (port == I2CPort::WIRE1) { + i2cBus = &Wire1; + } else { +#endif + i2cBus = &Wire; +#if WIRE_INTERFACES_COUNT == 2 + } +#endif + + return i2cBus->setClock(speed); +} + +uint32_t ScanI2CTwoWire::getClockSpeed(I2CPort port) { + + DeviceAddress addr(port, 0x00); + TwoWire *i2cBus; + +#if WIRE_INTERFACES_COUNT == 2 + if (port == I2CPort::WIRE1) { + i2cBus = &Wire1; + } else { +#endif + i2cBus = &Wire; +#if WIRE_INTERFACES_COUNT == 2 + } +#endif + + return i2cBus->getClock(); +} + +/// for SEN5X detection +String readSEN5xProductName(TwoWire* i2cBus, uint8_t address) { + uint8_t cmd[] = { 0xD0, 0x14 }; + uint8_t response[48] = {0}; + + i2cBus->beginTransmission(address); + i2cBus->write(cmd, 2); + if (i2cBus->endTransmission() != 0) return ""; + + delay(20); + if (i2cBus->requestFrom(address, (uint8_t)48) != 48) return ""; + + for (int i = 0; i < 48 && i2cBus->available(); ++i) { + response[i] = i2cBus->read(); + } + + char productName[33] = {0}; + int j = 0; + for (int i = 0; i < 48 && j < 32; i += 3) { + if (response[i] >= 32 && response[i] <= 126) + productName[j++] = response[i]; + else + break; + + if (response[i + 1] >= 32 && response[i + 1] <= 126) + productName[j++] = response[i + 1]; + else + break; + } + + return String(productName); +} + #define SCAN_SIMPLE_CASE(ADDR, T, ...) \ case ADDR: \ logFoundDevice(__VA_ARGS__); \ @@ -464,21 +534,39 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) } break; - case ICM20948_ADDR: // same as BMX160_ADDR + case ICM20948_ADDR: // same as BMX160_ADDR and SEN5X_ADDR case ICM20948_ADDR_ALT: // same as MPU6050_ADDR + // ICM20948 Register check registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); if (registerValue == 0xEA) { type = ICM20948; logFoundDevice("ICM20948", (uint8_t)addr.address); break; - } else if (addr.address == BMX160_ADDR) { - type = BMX160; - logFoundDevice("BMX160", (uint8_t)addr.address); - break; } else { - type = MPU6050; - logFoundDevice("MPU6050", (uint8_t)addr.address); - break; + String prod = ""; + prod = readSEN5xProductName(i2cBus, addr.address); + if (prod.startsWith("SEN55")) { + type = SEN5X; + logFoundDevice("Sensirion SEN55", addr.address); + break; + } else if (prod.startsWith("SEN54")) { + type = SEN5X; + logFoundDevice("Sensirion SEN54", addr.address); + break; + } else if (prod.startsWith("SEN50")) { + type = SEN5X; + logFoundDevice("Sensirion SEN50", addr.address); + break; + } + if (addr.address == BMX160_ADDR) { + type = BMX160; + logFoundDevice("BMX160", (uint8_t)addr.address); + break; + } else { + type = MPU6050; + logFoundDevice("MPU6050", (uint8_t)addr.address); + break; + } } break; diff --git a/src/detect/ScanI2CTwoWire.h b/src/detect/ScanI2CTwoWire.h index 6988091ad..28b073a17 100644 --- a/src/detect/ScanI2CTwoWire.h +++ b/src/detect/ScanI2CTwoWire.h @@ -29,6 +29,9 @@ class ScanI2CTwoWire : public ScanI2C size_t countDevices() const override; + bool setClockSpeed(ScanI2C::I2CPort, uint32_t); + uint32_t getClockSpeed(ScanI2C::I2CPort); + protected: FoundDevice firstOfOrNONE(size_t, DeviceType[]) const override; diff --git a/src/main.cpp b/src/main.cpp index 2e2adfd46..5b5c1c520 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -472,6 +472,7 @@ void setup() Wire.setSCL(I2C_SCL); Wire.begin(); #elif defined(I2C_SDA) && !defined(ARCH_RP2040) + LOG_INFO("Starting Bus with (SDA) %d and (SCL) %d: ", I2C_SDA, I2C_SCL); Wire.begin(I2C_SDA, I2C_SCL); #elif defined(ARCH_PORTDUINO) if (settingsStrings[i2cdev] != "") { @@ -530,6 +531,26 @@ void setup() i2cScanner->scanPort(ScanI2C::I2CPort::WIRE); #endif +#ifdef I2C_CLOCK_SPEED + uint32_t currentClock; + currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE); + LOG_INFO("Clock speed: %uHz on WIRE", currentClock); + LOG_DEBUG("Setting Wire with defined clock speed, %uHz...", I2C_CLOCK_SPEED); + if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE, I2C_CLOCK_SPEED)) { + LOG_ERROR("Unable to set clock speed on WIRE"); + } else { + + currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE); + LOG_INFO("Set clock speed: %uHz on WIRE", currentClock); + } + // LOG_DEBUG("Starting Wire with defined clock speed, %d...", I2C_CLOCK_SPEED); + // if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE1, I2C_CLOCK_SPEED)) { + // LOG_ERROR("Unable to set clock speed on WIRE1"); + // } else { + // LOG_INFO("Set clock speed: %d on WIRE1", I2C_CLOCK_SPEED); + // } +#endif + auto i2cCount = i2cScanner->countDevices(); if (i2cCount == 0) { LOG_INFO("No I2C devices found"); @@ -682,7 +703,7 @@ void setup() scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::RAK12035, meshtastic_TelemetrySensorType_RAK12035); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::PCT2075, meshtastic_TelemetrySensorType_PCT2075); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X); - + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SEN5X, meshtastic_TelemetrySensorType_SEN5X); i2cScanner.reset(); #endif diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 3528f57f5..2ff5a345a 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -221,11 +221,7 @@ void setupModules() // TODO: How to improve this? #if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR new EnvironmentTelemetryModule(); -#if __has_include("Adafruit_PM25AQI.h") - if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) { - new AirQualityTelemetryModule(); - } -#endif + new AirQualityTelemetryModule(); #if !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX30102].first > 0 || nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MLX90614].first > 0) { diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 2472b95b1..91ceece77 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -1,36 +1,66 @@ #include "configuration.h" -#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h") +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "AirQualityTelemetry.h" #include "Default.h" +#include "AirQualityTelemetry.h" #include "MeshService.h" #include "NodeDB.h" #include "PowerFSM.h" #include "RTC.h" #include "Router.h" -#include "detect/ScanI2CTwoWire.h" +#include "UnitConversions.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/images.h" #include "main.h" +#include "sleep.h" #include -#ifndef PMSA003I_WARMUP_MS -// from the PMSA003I datasheet: -// "Stable data should be got at least 30 seconds after the sensor wakeup -// from the sleep mode because of the fan’s performance." -#define PMSA003I_WARMUP_MS 30000 +#if __has_include() +#include "Sensor/PMSA003ISensor.h" +PMSA003ISensor pmsa003iSensor; +#else +NullSensor pmsa003iSensor; +#endif + +// Small hack +#ifndef INCLUDE_SEN5X +#define INCLUDE_SEN5X 1 +#endif + +#ifdef INCLUDE_SEN5X +#include "Sensor/SEN5XSensor.h" +SEN5XSensor sen5xSensor; +#else +NullSensor sen5xSensor; #endif int32_t AirQualityTelemetryModule::runOnce() { + if (sleepOnNextExecution == true) { + sleepOnNextExecution = false; + uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs); + LOG_DEBUG("Sleeping for %ims, then awaking to send metrics again.", nightyNightMs); + doDeepSleep(nightyNightMs, true, false); + } + + uint32_t result = UINT32_MAX; + uint32_t sen5xPendingForReady; + /* Uncomment the preferences below if you want to use the module without having to configure it from the PythonAPI or WebUI. */ // moduleConfig.telemetry.air_quality_enabled = 1; + // TODO there is no config in module_config.proto for air_quality_screen_enabled. Reusing environment one, although it should have its own + // moduleConfig.telemetry.environment_screen_enabled = 1; + // moduleConfig.telemetry.air_quality_interval = 15; - if (!(moduleConfig.telemetry.air_quality_enabled)) { + if (!(moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.environment_screen_enabled || + AIR_QUALITY_TELEMETRY_MODULE_ENABLE)) { // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it return disable(); } @@ -42,79 +72,172 @@ int32_t AirQualityTelemetryModule::runOnce() if (moduleConfig.telemetry.air_quality_enabled) { LOG_INFO("Air quality Telemetry: init"); -#ifdef PMSA003I_ENABLE_PIN - // put the sensor to sleep on startup - pinMode(PMSA003I_ENABLE_PIN, OUTPUT); - digitalWrite(PMSA003I_ENABLE_PIN, LOW); -#endif /* PMSA003I_ENABLE_PIN */ + if (pmsa003iSensor.hasSensor()) + result = pmsa003iSensor.runOnce(); - if (!aqi.begin_I2C()) { -#ifndef I2C_NO_RESCAN - LOG_WARN("Could not establish i2c connection to AQI sensor. Rescan"); - // rescan for late arriving sensors. AQI Module starts about 10 seconds into the boot so this is plenty. - uint8_t i2caddr_scan[] = {PMSA0031_ADDR}; - uint8_t i2caddr_asize = 1; - auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); -#if defined(I2C_SDA1) - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1, i2caddr_scan, i2caddr_asize); -#endif - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE, i2caddr_scan, i2caddr_asize); - auto found = i2cScanner->find(ScanI2C::DeviceType::PMSA0031); - if (found.type != ScanI2C::DeviceType::NONE) { - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first = found.address.address; - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].second = - i2cScanner->fetchI2CBus(found.address); - return setStartDelay(); - } -#endif - return disable(); - } - return setStartDelay(); + if (sen5xSensor.hasSensor()) + result = sen5xSensor.runOnce(); } - return disable(); + + // it's possible to have this module enabled, only for displaying values on the screen. + // therefore, we should only enable the sensor loop if measurement is also enabled + return result == UINT32_MAX ? disable() : setStartDelay(); } else { // if we somehow got to a second run of this module with measurement disabled, then just wait forever - if (!moduleConfig.telemetry.air_quality_enabled) - return disable(); - - switch (state) { -#ifdef PMSA003I_ENABLE_PIN - case State::IDLE: - // sensor is in standby; fire it up and sleep - LOG_DEBUG("runOnce(): state = idle"); - digitalWrite(PMSA003I_ENABLE_PIN, HIGH); - state = State::ACTIVE; - - return PMSA003I_WARMUP_MS; -#endif /* PMSA003I_ENABLE_PIN */ - case State::ACTIVE: - // sensor is already warmed up; grab telemetry and send it - LOG_DEBUG("runOnce(): state = active"); - - if (((lastSentToMesh == 0) || - !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( - moduleConfig.telemetry.air_quality_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) && - airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && - airTime->isTxAllowedAirUtil()) { - sendTelemetry(); - lastSentToMesh = millis(); - } else if (service->isToPhoneQueueEmpty()) { - // Just send to phone when it's not our time to send to mesh yet - // Only send while queue is empty (phone assumed connected) - sendTelemetry(NODENUM_BROADCAST, true); - } - -#ifdef PMSA003I_ENABLE_PIN - // put sensor back to sleep - digitalWrite(PMSA003I_ENABLE_PIN, LOW); - state = State::IDLE; -#endif /* PMSA003I_ENABLE_PIN */ - - return sendToPhoneIntervalMs; - default: + if (!moduleConfig.telemetry.air_quality_enabled && !AIR_QUALITY_TELEMETRY_MODULE_ENABLE) { return disable(); } + + // Wake up the sensors that need it +#ifdef PMSA003I_ENABLE_PIN + if (pmsa003iSensor.hasSensor() && !pmsa003iSensor.isActive()) + return pmsa003iSensor.wakeUp(); +#endif /* PMSA003I_ENABLE_PIN */ + + // Wake up the sensors that need it, before we need to take telemetry data + if ((lastSentToMesh == 0) || + !Throttle::isWithinTimespanMs(lastSentToMesh - SEN5X_WARMUP_MS_1, Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) { + if (sen5xSensor.hasSensor() && !sen5xSensor.isActive()) + return sen5xSensor.wakeUp(); + } + + // Check if sen5x is ready to return data, or if it needs more time because of the low concentration threshold + if (sen5xSensor.hasSensor() && sen5xSensor.isActive()) { + sen5xPendingForReady = sen5xSensor.pendingForReady(); + LOG_DEBUG("SEN5X: Pending for ready %ums", sen5xPendingForReady); + if (sen5xPendingForReady) { + return sen5xPendingForReady; + } + } + + if (((lastSentToMesh == 0) || + !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && + airTime->isTxAllowedAirUtil()) { + sendTelemetry(); + lastSentToMesh = millis(); + } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && + (service->isToPhoneQueueEmpty())) { + // Just send to phone when it's not our time to send to mesh yet + // Only send while queue is empty (phone assumed connected) + sendTelemetry(NODENUM_BROADCAST, true); + lastSentToPhone = millis(); + } + + // 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 +#ifdef PMSA003I_ENABLE_PIN + pmsa003iSensor.sleep(); +#endif /* PMSA003I_ENABLE_PIN */ + + if (sen5xSensor.hasSensor() && sen5xSensor.isActive()) { + if (SEN5X_WARMUP_MS_2 < Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes)) { + LOG_DEBUG("SEN5X: Disabling sensor until next period"); + sen5xSensor.idle(); + } else { + LOG_DEBUG("SEN5X: Sensor stays enabled due to warm up period"); + } + } + } + return min(sendToPhoneIntervalMs, result); +} + +bool AirQualityTelemetryModule::wantUIFrame() +{ + return moduleConfig.telemetry.environment_screen_enabled; +} + +void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // === Setup display === + display->clear(); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + int line = 1; + + // === Set Title + const char *titleStr = (graphics::isHighResolution) ? "Environment" : "Env."; + + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); + + // === Row spacing setup === + const int rowHeight = FONT_HEIGHT_SMALL - 4; + int currentY = graphics::getTextPositions(display)[line++]; + + // === Show "No Telemetry" if no data available === + if (!lastMeasurementPacket) { + display->drawString(x, currentY, "No Telemetry"); + return; + } + + // Decode the telemetry message from the latest received packet + const meshtastic_Data &p = lastMeasurementPacket->decoded; + meshtastic_Telemetry telemetry; + if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &telemetry)) { + display->drawString(x, currentY, "No Telemetry"); + return; + } + + 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; + + if (!hasAny) { + display->drawString(x, currentY, "No Telemetry"); + return; + } + + // === First line: Show sender name + time since received (left), and first metric (right) === + const char *sender = getSenderShortName(*lastMeasurementPacket); + uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); + String agoStr = (agoSecs > 864000) ? "?" + : (agoSecs > 3600) ? String(agoSecs / 3600) + "h" + : (agoSecs > 60) ? String(agoSecs / 60) + "m" + : String(agoSecs) + "s"; + + String leftStr = String(sender) + " (" + agoStr + ")"; + display->drawString(x, currentY, leftStr); // Left side: who and when + + // === Collect sensor readings as label strings (no icons) === + std::vector entries; + + if (m.has_pm10_standard) + entries.push_back("PM1.0: " + String(m.pm10_standard) + "ug/m3"); + if (m.has_pm25_standard) + entries.push_back("PM2.5: " + String(m.pm25_standard) + "ug/m3"); + if (m.has_pm100_standard) + entries.push_back("PM10.0: " + String(m.pm100_standard) + "ug/m3"); + + // === Show first available metric on top-right of first line === + if (!entries.empty()) { + String valueStr = entries.front(); + int rightX = SCREEN_WIDTH - display->getStringWidth(valueStr); + display->drawString(rightX, currentY, valueStr); + entries.erase(entries.begin()); // Remove from queue + } + + // === Advance to next line for remaining telemetry entries === + currentY += rowHeight; + + // === Draw remaining entries in 2-column format (left and right) === + for (size_t i = 0; i < entries.size(); i += 2) { + // Left column + display->drawString(x, currentY, entries[i]); + + // Right column if it exists + if (i + 1 < entries.size()) { + int rightX = SCREEN_WIDTH / 2; + display->drawString(rightX, currentY, entries[i + 1]); + } + + currentY += rowHeight; } } @@ -128,9 +251,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) @@ -144,35 +268,25 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) { - if (!aqi.read(&data)) { - LOG_WARN("Skip send measurements. Could not read AQIn"); - return false; - } - + bool valid = true; + bool hasSensor = false; m->time = getTime(); m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag; - m->variant.air_quality_metrics.has_pm10_standard = true; - m->variant.air_quality_metrics.pm10_standard = data.pm10_standard; - m->variant.air_quality_metrics.has_pm25_standard = true; - m->variant.air_quality_metrics.pm25_standard = data.pm25_standard; - m->variant.air_quality_metrics.has_pm100_standard = true; - m->variant.air_quality_metrics.pm100_standard = data.pm100_standard; + m->variant.air_quality_metrics = meshtastic_AirQualityMetrics_init_zero; - m->variant.air_quality_metrics.has_pm10_environmental = true; - m->variant.air_quality_metrics.pm10_environmental = data.pm10_env; - m->variant.air_quality_metrics.has_pm25_environmental = true; - m->variant.air_quality_metrics.pm25_environmental = data.pm25_env; - m->variant.air_quality_metrics.has_pm100_environmental = true; - m->variant.air_quality_metrics.pm100_environmental = data.pm100_env; + // 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()) { + valid = valid && pmsa003iSensor.getMetrics(m); + hasSensor = true; + } - LOG_INFO("Send: PM1.0(Standard)=%i, PM2.5(Standard)=%i, PM10.0(Standard)=%i", m->variant.air_quality_metrics.pm10_standard, - m->variant.air_quality_metrics.pm25_standard, m->variant.air_quality_metrics.pm100_standard); + if (sen5xSensor.hasSensor()) { + valid = valid && sen5xSensor.getMetrics(m); + hasSensor = true; + } - LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i", - m->variant.air_quality_metrics.pm10_environmental, m->variant.air_quality_metrics.pm25_environmental, - m->variant.air_quality_metrics.pm100_environmental); - - return true; + return valid && hasSensor; } meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply() @@ -206,7 +320,15 @@ meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply() 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=%.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); + meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; p->decoded.want_response = false; @@ -221,16 +343,51 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) lastMeasurementPacket = packetPool.allocCopy(*p); if (phoneOnly) { - LOG_INFO("Send packet to phone"); + LOG_INFO("Sending packet to phone"); service->sendToPhone(p); } else { - LOG_INFO("Send packet to mesh"); + 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) { + meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed(); + notification->level = meshtastic_LogRecord_Level_INFO; + notification->time = getValidTime(RTCQualityFromNet); + sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment", + Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval, + 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; } +AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) +{ + AdminMessageHandleResult result = AdminMessageHandleResult::NOT_HANDLED; +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL + if (pmsa003iSensor.hasSensor()) { + result = pmsa003iSensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + + if (sen5xSensor.hasSensor()) { + result = sen5xSensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + +#endif + return result; +} + #endif \ No newline at end of file diff --git a/src/modules/Telemetry/AirQualityTelemetry.h b/src/modules/Telemetry/AirQualityTelemetry.h index 0142ee686..8314c54bc 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.h +++ b/src/modules/Telemetry/AirQualityTelemetry.h @@ -1,12 +1,18 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h") +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #pragma once + +#ifndef AIR_QUALITY_TELEMETRY_MODULE_ENABLE +#define AIR_QUALITY_TELEMETRY_MODULE_ENABLE 0 +#endif + #include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "Adafruit_PM25AQI.h" #include "NodeDB.h" #include "ProtobufModule.h" +#include +#include class AirQualityTelemetryModule : private concurrency::OSThread, public ProtobufModule { @@ -20,18 +26,15 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public Protobuf ProtobufModule("AirQualityTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { lastMeasurementPacket = nullptr; - setIntervalFromNow(10 * 1000); - aqi = Adafruit_PM25AQI(); nodeStatusObserver.observe(&nodeStatus->onNewStatus); - -#ifdef PMSA003I_ENABLE_PIN - // the PMSA003I sensor uses about 300mW on its own; support powering it off when it's not actively taking - // a reading - state = State::IDLE; -#else - state = State::ACTIVE; -#endif + setIntervalFromNow(10 * 1000); } + virtual bool wantUIFrame() override; +#if !HAS_SCREEN + void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +#else + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; +#endif protected: /** Called to handle a particular incoming message @@ -49,19 +52,15 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public Protobuf */ bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); + virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; private: - enum State { - IDLE = 0, - ACTIVE = 1, - }; - - State state; - Adafruit_PM25AQI aqi; - PM25_AQI_Data data = {0}; bool firstTime = true; meshtastic_MeshPacket *lastMeasurementPacket; uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute uint32_t lastSentToMesh = 0; + uint32_t lastSentToPhone = 0; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index d1b10fa82..2d6a8a0cb 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -743,8 +743,6 @@ bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) LOG_INFO("Send: soil_temperature=%f, soil_moisture=%u", m.variant.environment_metrics.soil_temperature, m.variant.environment_metrics.soil_moisture); - sensor_read_error_count = 0; - meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; p->decoded.want_response = false; diff --git a/src/modules/Telemetry/EnvironmentTelemetry.h b/src/modules/Telemetry/EnvironmentTelemetry.h index d70c063fc..ffbb229f0 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.h +++ b/src/modules/Telemetry/EnvironmentTelemetry.h @@ -62,7 +62,6 @@ class EnvironmentTelemetryModule : private concurrency::OSThread, public Protobu uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute uint32_t lastSentToMesh = 0; uint32_t lastSentToPhone = 0; - uint32_t sensor_read_error_count = 0; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp new file mode 100644 index 000000000..cd123be42 --- /dev/null +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -0,0 +1,97 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "PMSA003ISensor.h" +#include "TelemetrySensor.h" +#include "detect/ScanI2CTwoWire.h" +#include + +PMSA003ISensor::PMSA003ISensor() : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I") {} + +int32_t PMSA003ISensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + +#ifdef PMSA003I_ENABLE_PIN +// TODO not sure why this was like this + sleep(); +#endif /* PMSA003I_ENABLE_PIN */ + + if (!pmsa003i.begin_I2C()){ +#ifndef I2C_NO_RESCAN + LOG_WARN("Could not establish i2c connection to AQI sensor. Rescan"); + // rescan for late arriving sensors. AQI Module starts about 10 seconds into the boot so this is plenty. + uint8_t i2caddr_scan[] = {PMSA0031_ADDR}; + uint8_t i2caddr_asize = 1; + auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); +#if defined(I2C_SDA1) + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1, i2caddr_scan, i2caddr_asize); +#endif + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE, i2caddr_scan, i2caddr_asize); + auto found = i2cScanner->find(ScanI2C::DeviceType::PMSA0031); + if (found.type != ScanI2C::DeviceType::NONE) { + nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first = found.address.address; + nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].second = + i2cScanner->fetchI2CBus(found.address); + return initI2CSensor(); + } +#endif + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + return initI2CSensor(); +} + +void PMSA003ISensor::setup() +{ +#ifdef PMSA003I_ENABLE_PIN + pinMode(PMSA003I_ENABLE_PIN, OUTPUT); +#endif /* PMSA003I_ENABLE_PIN */ +} + +#ifdef PMSA003I_ENABLE_PIN +void PMSA003ISensor::sleep() { + digitalWrite(PMSA003I_ENABLE_PIN, LOW); + state = State::IDLE; +} + +uint32_t PMSA003ISensor::wakeUp() { + digitalWrite(PMSA003I_ENABLE_PIN, HIGH); + state = State::ACTIVE; + return PMSA003I_WARMUP_MS; +} +#endif /* PMSA003I_ENABLE_PIN */ + +bool PMSA003ISensor::isActive() { + return state == State::ACTIVE; +} + +bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) +{ + if (!pmsa003i.read(&pmsa003iData)) { + LOG_WARN("Skip send measurements. Could not read AQI"); + return false; + } + + measurement->variant.air_quality_metrics.has_pm10_standard = true; + measurement->variant.air_quality_metrics.pm10_standard = pmsa003iData.pm10_standard; + measurement->variant.air_quality_metrics.has_pm25_standard = true; + measurement->variant.air_quality_metrics.pm25_standard = pmsa003iData.pm25_standard; + 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; + + return true; +} + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h new file mode 100644 index 000000000..7e460ce33 --- /dev/null +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -0,0 +1,49 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include "detect/ScanI2CTwoWire.h" +#include + +#ifndef PMSA003I_WARMUP_MS +// from the PMSA003I datasheet: +// "Stable data should be got at least 30 seconds after the sensor wakeup +// from the sleep mode because of the fan’s performance." +#define PMSA003I_WARMUP_MS 30000 +#endif + +class PMSA003ISensor : public TelemetrySensor +{ + private: + Adafruit_PM25AQI pmsa003i = Adafruit_PM25AQI(); + PM25_AQI_Data pmsa003iData = {0}; + + protected: + virtual void setup() override; + + public: + enum State { + IDLE = 0, + ACTIVE = 1, + }; + +#ifdef PMSA003I_ENABLE_PIN + void sleep(); + uint32_t wakeUp(); + // the PMSA003I sensor uses about 300mW on its own; support powering it off when it's not actively taking + // a reading + // put the sensor to sleep on startup + State state = State::IDLE; +#else + State state = State::ACTIVE; +#endif + + PMSA003ISensor(); + bool isActive(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; +}; + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp new file mode 100644 index 000000000..dc505fec8 --- /dev/null +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -0,0 +1,671 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#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)){ + LOG_ERROR("SEN5X: Error sending version command"); + return false; + } + delay(20); // From Sensirion Arduino library + + uint8_t versionBuffer[12]; + size_t charNumber = readBuffer(&versionBuffer[0], 3); + if (charNumber == 0) { + LOG_ERROR("SEN5X: Error getting data ready flag value"); + return false; + } + + firmwareVer = versionBuffer[0] + (versionBuffer[1] / 10); + hardwareVer = versionBuffer[3] + (versionBuffer[4] / 10); + protocolVer = versionBuffer[5] + (versionBuffer[6] / 10); + + 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; +} + +bool SEN5XSensor::findModel() +{ + if (!sendCommand(SEN5X_GET_PRODUCT_NAME)) { + LOG_ERROR("SEN5X: Error asking for product name"); + return false; + } + delay(50); // From Sensirion Arduino library + + const uint8_t nameSize = 48; + uint8_t name[nameSize]; + size_t charNumber = readBuffer(&name[0], nameSize); + if (charNumber == 0) { + LOG_ERROR("SEN5X: Error getting device name"); + return false; + } + + // We only check the last character that defines the model SEN5X + switch(name[4]) + { + case 48: + model = SEN50; + LOG_INFO("SEN5X: found sensor model SEN50"); + break; + case 52: + model = SEN54; + LOG_INFO("SEN5X: found sensor model SEN54"); + break; + case 53: + model = SEN55; + LOG_INFO("SEN5X: found sensor model SEN55"); + break; + } + + return true; +} + +bool SEN5XSensor::sendCommand(uint16_t command) +{ + uint8_t nothing; + return sendCommand(command, ¬hing, 0); +} + +bool SEN5XSensor::sendCommand(uint16_t command, uint8_t* buffer, uint8_t byteNumber) +{ + // At least we need two bytes for the command + uint8_t bufferSize = 2; + + // Add space for CRC bytes (one every two bytes) + if (byteNumber > 0) bufferSize += byteNumber + (byteNumber / 2); + + uint8_t toSend[bufferSize]; + uint8_t i = 0; + toSend[i++] = static_cast((command & 0xFF00) >> 8); + toSend[i++] = static_cast((command & 0x00FF) >> 0); + + // Prepare buffer with CRC every third byte + uint8_t bi = 0; + if (byteNumber > 0) { + while (bi < byteNumber) { + toSend[i++] = buffer[bi++]; + toSend[i++] = buffer[bi++]; + 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; + } + + if (i2c_error != 0) { + LOG_ERROR("SEN5X: Error on I2C communication: %x", i2c_error); + return false; + } + return true; +} + +uint8_t SEN5XSensor::readBuffer(uint8_t* buffer, uint8_t 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 + + 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 (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 = sen5xCRC(&buffer[i - 2]); + if (recvCRC != calcCRC) { + LOG_ERROR("SEN5X: Checksum error while receiving msg"); + return 0; + } + readBytes -=3; + receivedBytes += 2; + } + restoreClock(currentClock); + return receivedBytes; +} + +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; + + for (uint8_t i=0; i<2; i++){ + + crc ^= buffer[i]; + + for (uint8_t bit=8; bit>0; bit--) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x31; + else + crc = (crc << 1); + } + } + + return crc; +} + +bool SEN5XSensor::I2Cdetect(TwoWire *_Wire, uint8_t address) +{ + _Wire->beginTransmission(address); + byte error = _Wire->endTransmission(); + + if (error == 0) return true; + 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("SEN5X: Cleaning state %u read for %s read from %s", lastCleaning, sensorName, sen5XCleaningFileName); + } else { + LOG_INFO("SEN5X: No %s state found (File: %s)", sensorName, sen5XCleaningFileName); + } + spiLock->unlock(); +#else + LOG_ERROR("SEN5X: ERROR - Filesystem not implemented"); +#endif +} + +void SEN5XSensor::updateCleaningState() +{ +#ifdef FSCom + spiLock->lock(); + + if (FSCom.exists(sen5XCleaningFileName) && !FSCom.remove(sen5XCleaningFileName)) { + LOG_WARN("SEN5X: Can't remove old state file"); + } + auto file = FSCom.open(sen5XCleaningFileName, FILE_O_WRITE); + if (file) { + LOG_INFO("SEN5X: Save cleaning state %u for %s to %s", lastCleaning, sensorName, sen5XCleaningFileName); + file.write(lastCleaning); + file.flush(); + file.close(); + } else { + LOG_INFO("SEN5X: Can't write %s state (File: %s)", sensorName, sen5XCleaningFileName); + } + + spiLock->unlock(); +#else + LOG_ERROR("SEN5X: 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("SEN5X: 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; + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + + bus = nodeTelemetrySensorsMap[sensorType].second; + address = (uint8_t)nodeTelemetrySensorsMap[sensorType].first; + + delay(50); // without this there is an error on the deviceReset function + + if (!sendCommand(SEN5X_RESET)) { + LOG_ERROR("SEN5X: Error reseting device"); + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + delay(200); // From Sensirion Arduino library + + if (!findModel()) { + LOG_ERROR("SEN5X: error finding sensor model"); + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + + // Check if firmware version allows The direct switch between Measurement and RHT/Gas-Only Measurement mode + 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"); + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + delay(200); // From Sensirion Arduino library + + // Detection succeeded + state = SEN5X_IDLE; + status = 1; + LOG_INFO("SEN5X Enabled"); + + // 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("SEN5X: Last cleaning time: %u", lastCleaning); + if (lastCleaning) { + LOG_INFO("SEN5X: Last cleaning is valid"); + + uint32_t now; + now = getTime(); + LOG_INFO("SEN5X: Current time %us", now); + uint32_t passed = now - lastCleaning; + LOG_INFO("SEN5X: 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("SEN5X: Last cleaning date (in epoch): %u", lastCleaning); + } + } else { + LOG_INFO("SEN5X: 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); + } + + return initI2CSensor(); +} + +void SEN5XSensor::setup() +{ +} + +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 + // TODO - Change based on the type of final values + 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 - change depending on the final values + LOG_DEBUG("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 PN 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.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 + // TODO Remove accumuluative values: + // https://github.com/fablabbcn/smartcitizen-kit-2x/issues/85 + sen5xmeasurement.pN0p5 *= 100; + sen5xmeasurement.pN1p0 *= 100; + sen5xmeasurement.pN2p5 *= 100; + sen5xmeasurement.pN4p0 *= 100; + sen5xmeasurement.pN10p0 *= 100; + sen5xmeasurement.tSize *= 100; + + // TODO - Change depending on the final values + LOG_DEBUG("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; +// } + +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; +} + +int32_t SEN5XSensor::pendingForReady(){ + uint32_t now; + now = getTime(); + uint32_t sinceMeasureStarted = (now - measureStarted)*1000; + LOG_DEBUG("SEN5X: Since measure started: %ums", sinceMeasureStarted); + switch (state) { + case SEN5X_MEASUREMENT: { + + if (sinceMeasureStarted < SEN5X_WARMUP_MS_1) { + LOG_INFO("SEN5X: not enough time passed since starting measurement"); + return SEN5X_WARMUP_MS_1 - sinceMeasureStarted; + } + + // Get PN values to check if we are above or below threshold + readPnValues(); + + // If the reading is low (the tyhreshold is in #/cm3) and second warmUp hasn't passed we return to come back later + if ((sen5xmeasurement.pN4p0 / 100) < SEN5X_PN4P0_CONC_THD && sinceMeasureStarted < SEN5X_WARMUP_MS_2) { + LOG_INFO("SEN5X: Concentration is low, we will ask again in the second warm up period"); + state = SEN5X_MEASUREMENT_2; + // Report how many seconds are pending to cover the first warm up period + return SEN5X_WARMUP_MS_2 - sinceMeasureStarted; + } + return 0; + } + case SEN5X_MEASUREMENT_2: { + if (sinceMeasureStarted < SEN5X_WARMUP_MS_2) { + // Report how many seconds are pending to cover the first warm up period + return SEN5X_WARMUP_MS_2 - sinceMeasureStarted; + } + return 0; + } + default: { + return -1; + } + } +} + +bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement) +{ + LOG_INFO("SEN5X: Attempting to get metrics"); + if (!isActive()){ + LOG_INFO("SEN5X: not in measurement mode"); + return false; + } + + uint8_t response; + response = getMeasurements(); + + 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; + + 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; + + 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? + idle(); + return false; + } else if (response == 2) { + // Return with error for non-existing data + idle(); + return false; + } + + return true; +} + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.h b/src/modules/Telemetry/Sensor/SEN5XSensor.h new file mode 100644 index 000000000..71b32a6f6 --- /dev/null +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h @@ -0,0 +1,134 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include "Wire.h" +#include "RTC.h" + +// Warm up times for SEN5X from the datasheet +#ifndef SEN5X_WARMUP_MS_1 +#define SEN5X_WARMUP_MS_1 15000 +#endif + +#ifndef SEN5X_WARMUP_MS_2 +#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: + TwoWire * bus; + uint8_t address; + + bool getVersion(); + float firmwareVer = -1; + float hardwareVer = -1; + float protocolVer = -1; + bool findModel(); + + // Commands + #define SEN5X_RESET 0xD304 + #define SEN5X_GET_PRODUCT_NAME 0xD014 + #define SEN5X_GET_FIRMWARE_VERSION 0xD100 + #define SEN5X_START_MEASUREMENT 0x0021 + #define SEN5X_START_MEASUREMENT_RHT_GAS 0x0037 + #define SEN5X_STOP_MEASUREMENT 0x0104 + #define SEN5X_READ_DATA_READY 0x0202 + #define SEN5X_START_FAN_CLEANING 0x5607 + #define SEN5X_RW_VOCS_STATE 0x6181 + + #define SEN5X_READ_VALUES 0x03C4 + #define SEN5X_READ_RAW_VALUES 0x03D2 + #define SEN5X_READ_PM_VALUES 0x0413 + + enum SEN5Xmodel { SEN5X_UNKNOWN = 0, SEN50 = 0b001, SEN54 = 0b010, SEN55 = 0b100 }; + SEN5Xmodel model = SEN5X_UNKNOWN; + + enum SEN5XState { SEN5X_OFF, SEN5X_IDLE, SEN5X_MEASUREMENT, SEN5X_MEASUREMENT_2, SEN5X_CLEANING, SEN5X_NOT_DETECTED }; + SEN5XState state = SEN5X_OFF; + // TODO - Remove + bool continousMode = false; + bool forcedContinousMode = false; + + 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 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: + + SEN5XSensor(); + bool isActive(); + uint32_t wakeUp(); + bool idle(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + + // Sensirion recommends taking a reading after 15 seconds, if the Particle 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 + #define SEN5X_PN4P0_CONC_THD 100 + // This value represents the time needed for pending data + int32_t pendingForReady(); +}; + + + +#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;