From 7f042a011af7d4cbb9e66451fb46d20d239ab0c3 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 1 Jul 2025 17:23:52 +0200 Subject: [PATCH 01/38] Move PMSA003I to separate class and update AQ telemetry --- src/modules/Telemetry/AirQualityTelemetry.cpp | 313 ++++++++++++------ src/modules/Telemetry/AirQualityTelemetry.h | 39 ++- .../Telemetry/EnvironmentTelemetry.cpp | 2 - src/modules/Telemetry/EnvironmentTelemetry.h | 1 - .../Telemetry/Sensor/PMSA003ISensor.cpp | 89 +++++ src/modules/Telemetry/Sensor/PMSA003ISensor.h | 52 +++ 6 files changed, 368 insertions(+), 128 deletions(-) create mode 100644 src/modules/Telemetry/Sensor/PMSA003ISensor.cpp create mode 100644 src/modules/Telemetry/Sensor/PMSA003ISensor.h diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 2472b95b1..8b7ab1b24 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -1,36 +1,54 @@ #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 int32_t AirQualityTelemetryModule::runOnce() { + if (sleepOnNextExecution == true) { + sleepOnNextExecution = false; + // uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.environment_update_interval, + 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; + /* 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 +60,141 @@ 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 (!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 (pmsa003iSensor.hasSensor()) + result = pmsa003iSensor.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.state == pmsa003iSensor::State::IDLE) + return pmsa003iSensor.wakeUp(); +#endif /* PMSA003I_ENABLE_PIN */ + + 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(); + } + +#ifdef PMSA003I_ENABLE_PIN + pmsa003iSensor.sleep(); +#endif /* PMSA003I_ENABLE_PIN */ + + } + 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 || m.has_pm10_environmental || m.has_pm25_environmental || + m.has_pm100_environmental; + + 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, 0) + "ug/m3"); + if (m.has_pm25_standard) + entries.push_back("PM2.5: " + String(m.pm25_standard, 0) + "ug/m3"); + if (m.has_pm100_standard) + entries.push_back("PM10.0: " + String(m.pm100_standard, 0) + "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; } } @@ -142,37 +222,23 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack return false; // Let others look at this message also if they want } +// CHECKED 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; + 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; + } - 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); - - 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 +272,14 @@ 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(); if (getAirQualityTelemetry(&m)) { + LOG_INFO("Send: pm10_standard=%f, pm25_standard=%f, pm100_standard=%f, pm10_environmental=%f, pm100_environmental=%f", + 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); + meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; p->decoded.want_response = false; @@ -221,16 +294,46 @@ 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; + } + + +#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..dacdf5ff4 --- /dev/null +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -0,0 +1,89 @@ +#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 +void sleep() { + digitalWrite(PMSA003I_ENABLE_PIN, LOW); + state = State::IDLE; +} + +uint32_t wakeUp() { + digitalWrite(PMSA003I_ENABLE_PIN, HIGH); + state = State::ACTIVE; +} +#endif /* PMSA003I_ENABLE_PIN */ + +bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) +{ + if (!pmsa003i.read(&pmsa003iData)) { + LOG_WARN("Skip send measurements. Could not read AQIn"); + 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..01b04368e --- /dev/null +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -0,0 +1,52 @@ +#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}; + +#ifdef PMSA003I_ENABLE_PIN + void sleep(); + uint32_t wakeUp(); +#endif + + protected: + virtual void setup() override; + + public: + enum State { + IDLE = 0, + ACTIVE = 1, + }; + +#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 + // put the sensor to sleep on startup + pinMode(PMSA003I_ENABLE_PIN, OUTPUT); + State state = State::IDLE; +#else + State state = State::ACTIVE; +#endif + + PMSA003ISensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; +}; + +#endif \ No newline at end of file From 835adb2eac8de9de76a03f875439e2490a6f6625 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 1 Jul 2025 22:17:38 +0200 Subject: [PATCH 02/38] AirQualityTelemetry module not depend on PM sensor presence --- src/modules/Modules.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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) { From 3b470b7f3b38bdd4f25fa911f9f8daefc3ef066e Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 2 Jul 2025 11:29:02 +0200 Subject: [PATCH 03/38] Remove commented line --- src/modules/Telemetry/AirQualityTelemetry.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 8b7ab1b24..75058f849 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -28,7 +28,6 @@ int32_t AirQualityTelemetryModule::runOnce() { if (sleepOnNextExecution == true) { sleepOnNextExecution = false; - // uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.environment_update_interval, 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); @@ -222,7 +221,6 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack return false; // Let others look at this message also if they want } -// CHECKED bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) { bool valid = true; From 2f68458a83076f84909fc03a20dd279c0f913745 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 2 Jul 2025 13:12:07 +0200 Subject: [PATCH 04/38] Fixes on PMS class --- src/modules/Telemetry/AirQualityTelemetry.cpp | 2 +- src/modules/Telemetry/Sensor/PMSA003ISensor.cpp | 13 ++++++++++--- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 75058f849..7689802ea 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -74,7 +74,7 @@ int32_t AirQualityTelemetryModule::runOnce() // Wake up the sensors that need it #ifdef PMSA003I_ENABLE_PIN - if (pmsa003iSensor.hasSensor() && pmsa003iSensor.state == pmsa003iSensor::State::IDLE) + if (pmsa003iSensor.hasSensor() && !pmsa003iSensor.isActive()) return pmsa003iSensor.wakeUp(); #endif /* PMSA003I_ENABLE_PIN */ diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index dacdf5ff4..8567d7e70 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -48,24 +48,31 @@ int32_t PMSA003ISensor::runOnce() void PMSA003ISensor::setup() { +#ifdef PMSA003I_ENABLE_PIN + pinMode(PMSA003I_ENABLE_PIN, OUTPUT); +#endif /* PMSA003I_ENABLE_PIN */ } #ifdef PMSA003I_ENABLE_PIN -void sleep() { +void PMSA003ISensor::sleep() { digitalWrite(PMSA003I_ENABLE_PIN, LOW); state = State::IDLE; } -uint32_t wakeUp() { +uint32_t PMSA003ISensor::wakeUp() { digitalWrite(PMSA003I_ENABLE_PIN, HIGH); state = State::ACTIVE; } #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 AQIn"); + LOG_WARN("Skip send measurements. Could not read AQI"); return false; } diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index 01b04368e..db7c9aaa9 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -38,13 +38,13 @@ class PMSA003ISensor : public TelemetrySensor // 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 - pinMode(PMSA003I_ENABLE_PIN, OUTPUT); 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; }; From e4903eb43070a554caa866c7937ba0380a3208fc Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 2 Jul 2025 14:41:44 +0200 Subject: [PATCH 05/38] Add missing warmup period to wakeUp function --- src/modules/Telemetry/Sensor/PMSA003ISensor.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 8567d7e70..67f00574e 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -62,6 +62,7 @@ void PMSA003ISensor::sleep() { uint32_t PMSA003ISensor::wakeUp() { digitalWrite(PMSA003I_ENABLE_PIN, HIGH); state = State::ACTIVE; + return PMSA003I_WARMUP_MS } #endif /* PMSA003I_ENABLE_PIN */ From 9111f88f02489bd9446615f39c4b2f86bc91d0c5 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sun, 6 Jul 2025 08:31:57 +0200 Subject: [PATCH 06/38] Fixes on compilation for different variants --- src/modules/Telemetry/Sensor/PMSA003ISensor.cpp | 2 +- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 67f00574e..2b165cd6d 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -62,7 +62,7 @@ void PMSA003ISensor::sleep() { uint32_t PMSA003ISensor::wakeUp() { digitalWrite(PMSA003I_ENABLE_PIN, HIGH); state = State::ACTIVE; - return PMSA003I_WARMUP_MS + return PMSA003I_WARMUP_MS; } #endif /* PMSA003I_ENABLE_PIN */ diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index db7c9aaa9..7e460ce33 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -20,11 +20,6 @@ class PMSA003ISensor : public TelemetrySensor Adafruit_PM25AQI pmsa003i = Adafruit_PM25AQI(); PM25_AQI_Data pmsa003iData = {0}; -#ifdef PMSA003I_ENABLE_PIN - void sleep(); - uint32_t wakeUp(); -#endif - protected: virtual void setup() override; @@ -35,6 +30,8 @@ class PMSA003ISensor : public TelemetrySensor }; #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 From 40af7b82c4bc63a2e63eda9355a5677e39212611 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Fri, 11 Jul 2025 14:39:26 +0200 Subject: [PATCH 07/38] Add functions to check for I2C bus speed and set it --- src/detect/ScanI2CTwoWire.cpp | 69 +++++++++++++++++++++++++++++++++++ src/detect/ScanI2CTwoWire.h | 3 ++ src/main.cpp | 21 +++++++++++ 3 files changed, 93 insertions(+) diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 652d50d51..215efc1d0 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -109,6 +109,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__); \ 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 1868d98c7..132dab9e2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -480,6 +480,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] != "") { @@ -538,6 +539,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"); From ff8691dc136555c997db5b6c23c69b1e99ea1d90 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sat, 12 Jul 2025 22:12:31 +0200 Subject: [PATCH 08/38] Add ScreenFonts.h Co-authored-by: Hannes Fuchs --- src/modules/Telemetry/AirQualityTelemetry.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 7689802ea..1c8e95a82 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -23,6 +23,7 @@ PMSA003ISensor pmsa003iSensor; #else NullSensor pmsa003iSensor; #endif +#include "graphics/ScreenFonts.h" int32_t AirQualityTelemetryModule::runOnce() { From 0dda175d97e8ea004c7d26c9af27abaf2e90ea83 Mon Sep 17 00:00:00 2001 From: Nashui-Yan Date: Tue, 22 Jul 2025 16:55:09 +0100 Subject: [PATCH 09/38] PMSA003I 1st round test --- src/modules/Telemetry/AirQualityTelemetry.cpp | 8 +- .../Telemetry/Sensor/PMSA003ISensor.cpp | 200 ++++++++++++------ src/modules/Telemetry/Sensor/PMSA003ISensor.h | 57 ++--- 3 files changed, 166 insertions(+), 99 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 1c8e95a82..9bc41bfa5 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -166,11 +166,11 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta std::vector entries; if (m.has_pm10_standard) - entries.push_back("PM1.0: " + String(m.pm10_standard, 0) + "ug/m3"); + 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, 0) + "ug/m3"); + 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, 0) + "ug/m3"); + entries.push_back("PM10.0: " + String(m.pm100_standard) + "ug/m3"); // === Show first available metric on top-right of first line === if (!entries.empty()) { @@ -274,7 +274,7 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag; m.time = getTime(); 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=%u, pm25_standard=%u, pm100_standard=%u, pm10_environmental=%u, pm100_environmental=%u", 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); diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 2b165cd6d..39f8269a2 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -1,97 +1,175 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #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") {} +#include -int32_t PMSA003ISensor::runOnce() +PMSA003ISensor::PMSA003ISensor() + : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I") { - 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 */ +#endif } -#ifdef PMSA003I_ENABLE_PIN -void PMSA003ISensor::sleep() { - digitalWrite(PMSA003I_ENABLE_PIN, LOW); - state = State::IDLE; +bool PMSA003ISensor::restoreClock(uint32_t currentClock){ +#ifdef PMSA003I_I2C_CLOCK_SPEED + if (currentClock != PMSA003I_I2C_CLOCK_SPEED){ + // LOG_DEBUG("Restoring I2C clock to %uHz", currentClock); + return bus->setClock(currentClock); + } + return true; +#endif } -uint32_t PMSA003ISensor::wakeUp() { - digitalWrite(PMSA003I_ENABLE_PIN, HIGH); - state = State::ACTIVE; - return PMSA003I_WARMUP_MS; -} -#endif /* PMSA003I_ENABLE_PIN */ +int32_t PMSA003ISensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); -bool PMSA003ISensor::isActive() { - return state == State::ACTIVE; + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + + bus = nodeTelemetrySensorsMap[sensorType].second; + address = (uint8_t)nodeTelemetrySensorsMap[sensorType].first; + +#ifdef PMSA003I_I2C_CLOCK_SPEED + uint32_t currentClock; + currentClock = bus->getClock(); + if (currentClock != PMSA003I_I2C_CLOCK_SPEED){ + // LOG_DEBUG("Changing I2C clock to %u", PMSA003I_I2C_CLOCK_SPEED); + bus->setClock(PMSA003I_I2C_CLOCK_SPEED); + } +#endif + + bus->beginTransmission(address); + if (bus->endTransmission() != 0) { + LOG_WARN("PMSA003I not found on I2C at 0x12"); + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + + restoreClock(currentClock); + + status = 1; + LOG_INFO("PMSA003I Enabled"); + + return initI2CSensor(); } bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) { - if (!pmsa003i.read(&pmsa003iData)) { - LOG_WARN("Skip send measurements. Could not read AQI"); + if(!isActive()){ + LOG_WARN("PMSA003I is not active"); + return false; + } + +#ifdef PMSA003I_I2C_CLOCK_SPEED + uint32_t currentClock; + currentClock = bus->getClock(); + if (currentClock != PMSA003I_I2C_CLOCK_SPEED){ + // LOG_DEBUG("Changing I2C clock to %u", PMSA003I_I2C_CLOCK_SPEED); + bus->setClock(PMSA003I_I2C_CLOCK_SPEED); + } +#endif + + bus->requestFrom(address, PMSA003I_FRAME_LENGTH); + if (bus->available() < PMSA003I_FRAME_LENGTH) { + LOG_WARN("PMSA003I read failed: incomplete data (%d bytes)", bus->available()); + return false; + } + + restoreClock(currentClock); + + for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) { + buffer[i] = bus->read(); + } + + if (buffer[0] != 0x42 || buffer[1] != 0x4D) { + LOG_WARN("PMSA003I frame header invalid: 0x%02X 0x%02X", buffer[0], buffer[1]); + return false; + } + + auto read16 = [](uint8_t *data, uint8_t idx) -> uint16_t { + return (data[idx] << 8) | data[idx + 1]; + }; + + computedChecksum = 0; + + for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH - 2; i++) { + computedChecksum += buffer[i]; + } + receivedChecksum = read16(buffer, PMSA003I_FRAME_LENGTH - 2); + + if (computedChecksum != receivedChecksum) { + LOG_WARN("PMSA003I checksum failed: computed 0x%04X, received 0x%04X", computedChecksum, receivedChecksum); 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.pm10_standard = read16(buffer, 4); + measurement->variant.air_quality_metrics.has_pm25_standard = true; - measurement->variant.air_quality_metrics.pm25_standard = pmsa003iData.pm25_standard; + measurement->variant.air_quality_metrics.pm25_standard = read16(buffer, 6); + measurement->variant.air_quality_metrics.has_pm100_standard = true; - measurement->variant.air_quality_metrics.pm100_standard = pmsa003iData.pm100_standard; + measurement->variant.air_quality_metrics.pm100_standard = read16(buffer, 8); 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.pm10_environmental = read16(buffer, 10); + measurement->variant.air_quality_metrics.has_pm25_environmental = true; + measurement->variant.air_quality_metrics.pm25_environmental = read16(buffer, 12); + + measurement->variant.air_quality_metrics.has_pm100_environmental = true; + measurement->variant.air_quality_metrics.pm100_environmental = read16(buffer, 14); + + measurement->variant.air_quality_metrics.has_particles_03um = true; + measurement->variant.air_quality_metrics.particles_03um = read16(buffer, 16); + + measurement->variant.air_quality_metrics.has_particles_05um = true; + measurement->variant.air_quality_metrics.particles_05um = read16(buffer, 18); + + measurement->variant.air_quality_metrics.has_particles_10um = true; + measurement->variant.air_quality_metrics.particles_10um = read16(buffer, 20); + + measurement->variant.air_quality_metrics.has_particles_25um = true; + measurement->variant.air_quality_metrics.particles_25um = read16(buffer, 22); + + measurement->variant.air_quality_metrics.has_particles_50um = true; + measurement->variant.air_quality_metrics.particles_50um = read16(buffer, 24); + + measurement->variant.air_quality_metrics.has_particles_100um = true; + measurement->variant.air_quality_metrics.particles_100um = read16(buffer, 26); + return true; } -#endif \ No newline at end of file +bool PMSA003ISensor::isActive() +{ + return state == State::ACTIVE; +} + +#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 + +#endif diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index 7e460ce33..d6f12dfbb 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -1,49 +1,38 @@ -#include "configuration.h" +#pragma once -#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 +#ifndef PMSA003I_I2C_CLOCK_SPEED +#define PMSA003I_I2C_CLOCK_SPEED 100000 +#endif + +#ifndef PMSA003I_ENABLE_PIN +#define PMSA003I_FRAME_LENGTH 32 #endif class PMSA003ISensor : public TelemetrySensor { - private: - Adafruit_PM25AQI pmsa003i = Adafruit_PM25AQI(); - PM25_AQI_Data pmsa003iData = {0}; - - protected: +public: + PMSA003ISensor(); virtual void setup() override; - - public: - enum State { - IDLE = 0, - ACTIVE = 1, - }; + virtual int32_t runOnce() override; + virtual bool restoreClock(uint32_t currentClock); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool isActive(); #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; -}; +private: + enum class State { IDLE, ACTIVE }; + State state = State::ACTIVE; + TwoWire * bus; + uint8_t address; -#endif \ No newline at end of file + uint16_t computedChecksum = 0; + uint16_t receivedChecksum = 0; + + uint8_t buffer[PMSA003I_FRAME_LENGTH]; +}; From 14eaa3e097e33fa11c6739470249045fbfd0ff2a Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 23 Jul 2025 13:42:34 +0200 Subject: [PATCH 10/38] Fix I2C scan speed --- src/main.cpp | 53 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 132dab9e2..deda7f107 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -524,6 +524,39 @@ void setup() LOG_INFO("Scan for i2c devices"); #endif +// Scan I2C port at desired speed +#ifdef SCAN_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...", SCAN_I2C_CLOCK_SPEED); + + if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE, SCAN_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); + } + + // TODO Check if necessary + // LOG_DEBUG("Starting Wire with defined clock speed, %d...", SCAN_I2C_CLOCK_SPEED); + // if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE1, SCAN_I2C_CLOCK_SPEED)) { + // LOG_ERROR("Unable to set clock speed on WIRE1"); + // } else { + // LOG_INFO("Set clock speed: %d on WIRE1", SCAN_I2C_CLOCK_SPEED); + // } + + // Restore clock speed + if (currentClock != SCAN_I2C_CLOCK_SPEED) { + if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE, currentClock)) { + LOG_ERROR("Unable to restore clock speed on WIRE"); + } else { + currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE); + LOG_INFO("Set clock speed restored to: %uHz on WIRE", currentClock); + } + } +#endif + #if defined(I2C_SDA1) || (defined(NRF52840_XXAA) && (WIRE_INTERFACES_COUNT == 2)) i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1); #endif @@ -539,26 +572,6 @@ 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"); From 8a811b209db77bab3c9ff6b31b3009a86dadf89f Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 23 Jul 2025 17:23:04 +0200 Subject: [PATCH 11/38] Fix minor issues and bring back I2C SPEED def --- src/modules/Telemetry/AirQualityTelemetry.cpp | 9 +++++---- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 7 ++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 9bc41bfa5..431473e05 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -274,10 +274,11 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag; m.time = getTime(); if (getAirQualityTelemetry(&m)) { - LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u, pm10_environmental=%u, pm100_environmental=%u", - 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); + LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u, \ + pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u", \ + 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.pm25_environmental, m.variant.air_quality_metrics.pm100_environmental); meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index d6f12dfbb..ee3258ab1 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -2,13 +2,10 @@ #include "TelemetrySensor.h" -#ifndef PMSA003I_I2C_CLOCK_SPEED #define PMSA003I_I2C_CLOCK_SPEED 100000 -#endif - -#ifndef PMSA003I_ENABLE_PIN #define PMSA003I_FRAME_LENGTH 32 -#endif +#define PMSA003I_WARMUP_MS 30000 + class PMSA003ISensor : public TelemetrySensor { From ec5a752078c6e651ab171bfcf226b19bb438f281 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Fri, 25 Jul 2025 12:29:40 +0200 Subject: [PATCH 12/38] Remove PMSA003I library as its no longer needed --- platformio.ini | 2 -- src/modules/Telemetry/AirQualityTelemetry.cpp | 12 +++++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/platformio.ini b/platformio.ini index 8bf56cf5b..b5f08dd72 100644 --- a/platformio.ini +++ b/platformio.ini @@ -133,8 +133,6 @@ lib_deps = adafruit/Adafruit INA260 Library@1.5.3 # renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219 adafruit/Adafruit INA219@1.2.3 - # renovate: datasource=custom.pio depName=Adafruit PM25 AQI Sensor packageName=adafruit/library/Adafruit PM25 AQI Sensor - adafruit/Adafruit PM25 AQI Sensor@2.0.0 # renovate: datasource=custom.pio depName=Adafruit MPU6050 packageName=adafruit/library/Adafruit MPU6050 adafruit/Adafruit MPU6050@2.2.6 # renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 431473e05..ff5bd22ef 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -16,13 +16,12 @@ #include "main.h" #include "sleep.h" #include - -#if __has_include() +// Sensor includes #include "Sensor/PMSA003ISensor.h" + +// Sensors PMSA003ISensor pmsa003iSensor; -#else -NullSensor pmsa003iSensor; -#endif + #include "graphics/ScreenFonts.h" int32_t AirQualityTelemetryModule::runOnce() @@ -326,6 +325,9 @@ AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule( AdminMessageHandleResult result = AdminMessageHandleResult::NOT_HANDLED; #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL if (pmsa003iSensor.hasSensor()) { + // TODO - Potentially implement an admin message to choose between pm_standard + // and pm_environmental. This could be configurable as it doesn't make sense so + // have both result = pmsa003iSensor.handleAdminMessage(mp, request, response); if (result != AdminMessageHandleResult::NOT_HANDLED) return result; From a129441533a72a52726ffb67f87ee76b2c01eb2a Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 6 Aug 2025 10:40:57 +0200 Subject: [PATCH 13/38] Remove unused I2C speed functions and cleanup * Cleanup of SEN5X specific code added from switching branches * Remove SCAN_I2C_CLOCK_SPEED block as its not needed * Remove associated functions for setting I2C speed --- src/detect/ScanI2CTwoWire.cpp | 69 ------------------- src/detect/ScanI2CTwoWire.h | 3 - src/main.cpp | 33 --------- .../Telemetry/Sensor/PMSA003ISensor.cpp | 16 +++-- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 1 - 5 files changed, 10 insertions(+), 112 deletions(-) diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 23ccdd1ee..8b3670cd9 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -109,75 +109,6 @@ 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__); \ diff --git a/src/detect/ScanI2CTwoWire.h b/src/detect/ScanI2CTwoWire.h index 28b073a17..6988091ad 100644 --- a/src/detect/ScanI2CTwoWire.h +++ b/src/detect/ScanI2CTwoWire.h @@ -29,9 +29,6 @@ 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 4e01f4409..c0276d6a6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -524,39 +524,6 @@ void setup() LOG_INFO("Scan for i2c devices"); #endif -// Scan I2C port at desired speed -#ifdef SCAN_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...", SCAN_I2C_CLOCK_SPEED); - - if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE, SCAN_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); - } - - // TODO Check if necessary - // LOG_DEBUG("Starting Wire with defined clock speed, %d...", SCAN_I2C_CLOCK_SPEED); - // if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE1, SCAN_I2C_CLOCK_SPEED)) { - // LOG_ERROR("Unable to set clock speed on WIRE1"); - // } else { - // LOG_INFO("Set clock speed: %d on WIRE1", SCAN_I2C_CLOCK_SPEED); - // } - - // Restore clock speed - if (currentClock != SCAN_I2C_CLOCK_SPEED) { - if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE, currentClock)) { - LOG_ERROR("Unable to restore clock speed on WIRE"); - } else { - currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE); - LOG_INFO("Set clock speed restored to: %uHz on WIRE", currentClock); - } - } -#endif - #if defined(I2C_SDA1) || (defined(NRF52840_XXAA) && (WIRE_INTERFACES_COUNT == 2)) i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1); #endif diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 39f8269a2..c83c3e01a 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -56,9 +56,11 @@ int32_t PMSA003ISensor::runOnce() return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } +#ifdef PMSA003I_I2C_CLOCK_SPEED restoreClock(currentClock); +#endif - status = 1; + status = 1; LOG_INFO("PMSA003I Enabled"); return initI2CSensor(); @@ -86,7 +88,9 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) return false; } +#ifdef PMSA003I_I2C_CLOCK_SPEED restoreClock(currentClock); +#endif for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) { buffer[i] = bus->read(); @@ -136,19 +140,19 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) measurement->variant.air_quality_metrics.has_particles_05um = true; measurement->variant.air_quality_metrics.particles_05um = read16(buffer, 18); - + measurement->variant.air_quality_metrics.has_particles_10um = true; measurement->variant.air_quality_metrics.particles_10um = read16(buffer, 20); - + measurement->variant.air_quality_metrics.has_particles_25um = true; measurement->variant.air_quality_metrics.particles_25um = read16(buffer, 22); - + measurement->variant.air_quality_metrics.has_particles_50um = true; measurement->variant.air_quality_metrics.particles_50um = read16(buffer, 24); - + measurement->variant.air_quality_metrics.has_particles_100um = true; measurement->variant.air_quality_metrics.particles_100um = read16(buffer, 26); - + return true; } diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index ee3258ab1..35a4df735 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -6,7 +6,6 @@ #define PMSA003I_FRAME_LENGTH 32 #define PMSA003I_WARMUP_MS 30000 - class PMSA003ISensor : public TelemetrySensor { public: From 3f9bf06aee20d68d4f1374042566cedcd476d3e7 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sun, 6 Jul 2025 19:25:46 +0200 Subject: [PATCH 14/38] SEN5X first pass --- platformio.ini | 2 + src/configuration.h | 1 + src/detect/ScanI2C.h | 3 +- src/detect/ScanI2CTwoWire.cpp | 53 +++++++-- src/main.cpp | 2 +- src/modules/Telemetry/AirQualityTelemetry.cpp | 31 +++++ src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 107 ++++++++++++++++++ src/modules/Telemetry/Sensor/SEN5XSensor.h | 43 +++++++ 8 files changed, 232 insertions(+), 10 deletions(-) create mode 100644 src/modules/Telemetry/Sensor/SEN5XSensor.cpp create mode 100644 src/modules/Telemetry/Sensor/SEN5XSensor.h diff --git a/platformio.ini b/platformio.ini index b5f08dd72..c15cd34ee 100644 --- a/platformio.ini +++ b/platformio.ini @@ -199,3 +199,5 @@ lib_deps = 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 + # renovate: datasource=custom.pio depName=Sensirion I2C SEN5X packageName=sensirion/library/Sensirion I2C SEN5X + sensirion/Sensirion I2C SEN5X \ No newline at end of file diff --git a/src/configuration.h b/src/configuration.h index 0e24990b5..bc5aa3b01 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -199,6 +199,7 @@ along with this program. If not, see . #define BQ27220_ADDR 0x55 // same address as TDECK_KB #define BQ25896_ADDR 0x6B #define LTR553ALS_ADDR 0x23 +#define SEN5X_ADDR 0x69 // ----------------------------------------------------------------------------- // ACCELEROMETER diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index c1358861b..2d3cb1ae5 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -79,7 +79,8 @@ class ScanI2C BQ27220, LTR553ALS, BHI260AP, - BMM150 + BMM150, + SEN5X } DeviceType; // typedef uint8_t DeviceAddress; diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 8b3670cd9..3644a765f 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -8,6 +8,11 @@ #endif #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) #include "meshUtils.h" // vformat + +#define SEN50_NAME 48 +#define SEN54_NAME 52 +#define SEN55_NAME 53 + #endif bool in_array(uint8_t *array, int size, uint8_t lookfor) @@ -488,21 +493,53 @@ 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; + // TODO refurbish to find the model + // Just a hack for the hackathon + if (addr.address == SEN5X_ADDR) { + type = SEN5X; + logFoundDevice("SEN5X", (uint8_t)addr.address); + break; + } + + // We can get the 0xD014 register to find the model. This is not a simple task + // There is a buffer returned - getRegisterValue is not enough (maybe) + // registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xD014), 6); + // Important to leave delay + // delay(50); + + // const uint8_t nameSize = 48; + // uint8_t name[nameSize] = ®isterValue; + + // switch(name[4]){ + // case SEN50_NAME: + // type = SEN50; + // break; + // case SEN54_NAME: + // type = SEN54; + // break; + // case SEN55_NAME: + // type = SEN55; + // 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/main.cpp b/src/main.cpp index c0276d6a6..c215fda26 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -691,7 +691,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/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index df193c8a5..1eb125f58 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -24,6 +24,13 @@ PMSA003ISensor pmsa003iSensor; #include "graphics/ScreenFonts.h" +#if __has_include() +#include "Sensor/SEN5XSensor.h" +SEN5XSensor sen5xSensor; +#else +NullSensor sen5xSensor; +#endif + int32_t AirQualityTelemetryModule::runOnce() { if (sleepOnNextExecution == true) { @@ -61,6 +68,9 @@ int32_t AirQualityTelemetryModule::runOnce() if (pmsa003iSensor.hasSensor()) result = pmsa003iSensor.runOnce(); + + if (sen5xSensor.hasSensor()) + result = sen5xSensor.runOnce(); } // it's possible to have this module enabled, only for displaying values on the screen. @@ -78,6 +88,11 @@ 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( moduleConfig.telemetry.air_quality_interval, @@ -98,6 +113,10 @@ int32_t AirQualityTelemetryModule::runOnce() pmsa003iSensor.sleep(); #endif /* PMSA003I_ENABLE_PIN */ +#ifdef SEN5X_ENABLE_PIN + sen5xSensor.sleep(); +#endif /* SEN5X_ENABLE_PIN */ + } return min(sendToPhoneIntervalMs, result); } @@ -236,6 +255,13 @@ bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *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; + } + return valid && hasSensor; } @@ -333,6 +359,11 @@ AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule( return result; } + if (sen5xSensor.hasSensor()) { + result = sen5xSensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } #endif return result; diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp new file mode 100644 index 000000000..b65b3e76d --- /dev/null +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -0,0 +1,107 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "SEN5XSensor.h" +#include "TelemetrySensor.h" +#include + +SEN5XSensor::SEN5XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SEN5X, "SEN5X") {} + +int32_t SEN5XSensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + + sen5x.begin(*nodeTelemetrySensorsMap[sensorType].second); + + delay(25); // without this there is an error on the deviceReset function (NOT WORKING) + + 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; + } + + 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; + } + + return initI2CSensor(); +} + +void SEN5XSensor::setup() +{ +#ifdef SEN5X_ENABLE_PIN + pinMode(SEN5X_ENABLE_PIN, OUTPUT); +#endif /* SEN5X_ENABLE_PIN */ +} + +#ifdef SEN5X_ENABLE_PIN +void SEN5XSensor::sleep() { + digitalWrite(SEN5X_ENABLE_PIN, LOW); + state = State::IDLE; +} + +uint32_t SEN5XSensor::wakeUp() { + digitalWrite(SEN5X_ENABLE_PIN, HIGH); + state = State::ACTIVE; + return SEN5X_WARMUP_MS; +} +#endif /* SEN5X_ENABLE_PIN */ + +bool SEN5XSensor::isActive() { + return state == State::ACTIVE; +} + +bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement) +{ + uint16_t error; + char errorMessage[256]; + + // Read Measurement + float massConcentrationPm1p0; + float massConcentrationPm2p5; + float massConcentrationPm4p0; + float massConcentrationPm10p0; + float ambientHumidity; + float ambientTemperature; + float vocIndex; + float noxIndex; + + error = sen5x.readMeasuredValues( + massConcentrationPm1p0, massConcentrationPm2p5, massConcentrationPm4p0, + massConcentrationPm10p0, ambientHumidity, ambientTemperature, vocIndex, + noxIndex); + + if (error) { + LOG_INFO("Error trying to execute readMeasuredValues(): "); + errorToString(error, errorMessage, 256); + LOG_INFO(errorMessage); + return false; + } + + 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; + + 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..f2b8321ee --- /dev/null +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h @@ -0,0 +1,43 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +#ifndef SEN5X_WARMUP_MS +// from the SEN5X datasheet +#define SEN5X_WARMUP_MS_SMALL 30000 +#endif + +class SEN5XSensor : public TelemetrySensor +{ + private: + SensirionI2CSen5x sen5x; + // PM25_AQI_Data pmsa003iData = {0}; + + protected: + virtual void setup() override; + + public: + enum State { + IDLE = 0, + ACTIVE = 1, + }; + +#ifdef SEN5X_ENABLE_PIN + void sleep(); + uint32_t wakeUp(); + State state = State::IDLE; +#else + State state = State::ACTIVE; +#endif + + SEN5XSensor(); + bool isActive(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; +}; + +#endif \ No newline at end of file From dc0ab1f178c07bf11b5c9a4d8e544eb3fa7ca874 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Mon, 7 Jul 2025 14:34:09 +0200 Subject: [PATCH 15/38] WIP Sen5X functions --- platformio.ini | 4 +- src/detect/ScanI2CTwoWire.cpp | 42 +-- src/modules/Telemetry/AirQualityTelemetry.cpp | 7 +- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 271 +++++++++++++++--- src/modules/Telemetry/Sensor/SEN5XSensor.h | 58 ++-- 5 files changed, 285 insertions(+), 97 deletions(-) diff --git a/platformio.ini b/platformio.ini index c15cd34ee..5b648fb4c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -198,6 +198,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 - # renovate: datasource=custom.pio depName=Sensirion I2C SEN5X packageName=sensirion/library/Sensirion I2C SEN5X - sensirion/Sensirion I2C SEN5X \ No newline at end of file + sensirion/Sensirion I2C SCD4x@1.1.0 \ No newline at end of file diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 3644a765f..b201ea6fe 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -9,10 +9,6 @@ #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) #include "meshUtils.h" // vformat -#define SEN50_NAME 48 -#define SEN54_NAME 52 -#define SEN55_NAME 53 - #endif bool in_array(uint8_t *array, int size, uint8_t lookfor) @@ -131,6 +127,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) DeviceAddress addr(port, 0x00); uint16_t registerValue = 0x00; + String prod = ""; ScanI2C::DeviceType type; TwoWire *i2cBus; #ifdef RV3028_RTC @@ -497,40 +494,25 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) case ICM20948_ADDR_ALT: // same as MPU6050_ADDR // ICM20948 Register check registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); + prod = readSEN5xProductName(i2cBus, addr.address); if (registerValue == 0xEA) { type = ICM20948; logFoundDevice("ICM20948", (uint8_t)addr.address); break; } else { - // TODO refurbish to find the model - // Just a hack for the hackathon - if (addr.address == SEN5X_ADDR) { + if (prod.startsWith("SEN55")) { type = SEN5X; - logFoundDevice("SEN5X", (uint8_t)addr.address); + 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; } - - // We can get the 0xD014 register to find the model. This is not a simple task - // There is a buffer returned - getRegisterValue is not enough (maybe) - // registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xD014), 6); - // Important to leave delay - // delay(50); - - // const uint8_t nameSize = 48; - // uint8_t name[nameSize] = ®isterValue; - - // switch(name[4]){ - // case SEN50_NAME: - // type = SEN50; - // break; - // case SEN54_NAME: - // type = SEN54; - // break; - // case SEN55_NAME: - // type = SEN55; - // break; - // } - if (addr.address == BMX160_ADDR) { type = BMX160; logFoundDevice("BMX160", (uint8_t)addr.address); diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 1eb125f58..97722ff08 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -24,7 +24,12 @@ PMSA003ISensor pmsa003iSensor; #include "graphics/ScreenFonts.h" -#if __has_include() +// Small hack +#ifndef INCLUDE_SEN5X +#define INCLUDE_SEN5X 1 +#endif + +#ifdef INCLUDE_SEN5X #include "Sensor/SEN5XSensor.h" SEN5XSensor sen5xSensor; #else diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index b65b3e76d..a352b6097 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -1,14 +1,169 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "SEN5XSensor.h" #include "TelemetrySensor.h" -#include SEN5XSensor::SEN5XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SEN5X, "SEN5X") {} +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: %d", firmwareVer); + LOG_INFO("SEN5X Hardware Version: %d", hardwareVer); + LOG_INFO("SEN5X Protocol Version: %d", 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 = CRC(&buffer[bi - 2]); + toSend[i++] = calcCRC; + } + } + + // Transmit the data + bus->beginTransmission(address); + size_t writtenBytes = bus->write(toSend, bufferSize); + uint8_t i2c_error = bus->endTransmission(); + + 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) +{ + size_t readedBytes = bus->requestFrom(address, byteNumber); + + if (readedBytes != byteNumber) { + LOG_ERROR("SEN5X: Error reading I2C bus"); + return 0; + } + + uint8_t i = 0; + uint8_t receivedBytes = 0; + while (readedBytes > 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]); + if (recvCRC != calcCRC) { + LOG_ERROR("SEN5X: Checksum error while receiving msg"); + return 0; + } + readedBytes -=3; + receivedBytes += 2; + } + + return receivedBytes; +} + +uint8_t SEN5XSensor::CRC(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; +} + int32_t SEN5XSensor::runOnce() { LOG_INFO("Init sensor: %s", sensorName); @@ -16,30 +171,54 @@ int32_t SEN5XSensor::runOnce() return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } - sen5x.begin(*nodeTelemetrySensorsMap[sensorType].second); + bus = nodeTelemetrySensorsMap[sensorType].second; + // sen5x.begin(*bus); - delay(25); // without this there is an error on the deviceReset function (NOT WORKING) + delay(50); // without this there is an error on the deviceReset function - 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); + 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; } - error = sen5x.startMeasurement(); - if (error) { - LOG_INFO("Error trying to execute startMeasurement(): "); - errorToString(error, errorMessage, 256); - LOG_INFO(errorMessage); + // 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; - } else { - status = 1; } + // Detection succeeded + state = SEN5X_IDLE; + 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; + // } + + // 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; + // } + return initI2CSensor(); } @@ -47,26 +226,24 @@ 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 = State::IDLE; -} +// void SEN5XSensor::sleep() { +// digitalWrite(SEN5X_ENABLE_PIN, LOW); +// state = SSEN5XState::SEN5X_OFF; +// } -uint32_t SEN5XSensor::wakeUp() { - digitalWrite(SEN5X_ENABLE_PIN, HIGH); - state = State::ACTIVE; - return SEN5X_WARMUP_MS; -} +// uint32_t SEN5XSensor::wakeUp() { +// digitalWrite(SEN5X_ENABLE_PIN, HIGH); +// state = SEN5XState::SEN5X_IDLE; +// return SEN5X_WARMUP_MS; +// } #endif /* SEN5X_ENABLE_PIN */ -bool SEN5XSensor::isActive() { - return state == State::ACTIVE; -} - bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement) { uint16_t error; @@ -82,24 +259,24 @@ bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement) float vocIndex; float noxIndex; - error = sen5x.readMeasuredValues( - massConcentrationPm1p0, massConcentrationPm2p5, massConcentrationPm4p0, - massConcentrationPm10p0, ambientHumidity, ambientTemperature, vocIndex, - noxIndex); + // error = sen5x.readMeasuredValues( + // massConcentrationPm1p0, massConcentrationPm2p5, massConcentrationPm4p0, + // massConcentrationPm10p0, ambientHumidity, ambientTemperature, vocIndex, + // noxIndex); - if (error) { - LOG_INFO("Error trying to execute readMeasuredValues(): "); - errorToString(error, errorMessage, 256); - LOG_INFO(errorMessage); - return false; - } + // if (error) { + // LOG_INFO("Error trying to execute readMeasuredValues(): "); + // errorToString(error, errorMessage, 256); + // LOG_INFO(errorMessage); + // return false; + // } - 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; + // 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; return true; } diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.h b/src/modules/Telemetry/Sensor/SEN5XSensor.h index f2b8321ee..14168f587 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.h +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h @@ -1,38 +1,64 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" -#include +#include "Wire.h" +// #include #ifndef SEN5X_WARMUP_MS // from the SEN5X datasheet -#define SEN5X_WARMUP_MS_SMALL 30000 +#define SEN5X_WARMUP_MS 30000 #endif class SEN5XSensor : public TelemetrySensor { private: - SensirionI2CSen5x sen5x; - // PM25_AQI_Data pmsa003iData = {0}; + 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; + + 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); protected: virtual void setup() override; public: - enum State { - IDLE = 0, - ACTIVE = 1, - }; -#ifdef SEN5X_ENABLE_PIN - void sleep(); - uint32_t wakeUp(); - State state = State::IDLE; -#else - State state = State::ACTIVE; -#endif +// #ifdef SEN5X_ENABLE_PIN + // void sleep(); + // uint32_t wakeUp(); +// #endif SEN5XSensor(); bool isActive(); From 9e82c8c4c5c0beeb9751f8004e764bcabc23225f Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 8 Jul 2025 12:07:25 +0200 Subject: [PATCH 16/38] Further (non-working) progress in SEN5X --- src/detect/ScanI2CTwoWire.cpp | 2 +- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 19 ++++++++++++++++++- src/modules/Telemetry/Sensor/SEN5XSensor.h | 1 + 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index b201ea6fe..7efd5bbed 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -494,12 +494,12 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) case ICM20948_ADDR_ALT: // same as MPU6050_ADDR // ICM20948 Register check registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); - prod = readSEN5xProductName(i2cBus, addr.address); if (registerValue == 0xEA) { type = ICM20948; logFoundDevice("ICM20948", (uint8_t)addr.address); break; } else { + prod = readSEN5xProductName(i2cBus, addr.address); if (prod.startsWith("SEN55")) { type = SEN5X; logFoundDevice("Sensirion SEN55", addr.address); diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index a352b6097..795d3bae7 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -101,6 +101,7 @@ bool SEN5XSensor::sendCommand(uint16_t command, uint8_t* buffer, uint8_t byteNum } // 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(); @@ -164,17 +165,33 @@ uint8_t SEN5XSensor::CRC(uint8_t* buffer) 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; +} + 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; // sen5x.begin(*bus); - delay(50); // without this there is an error on the deviceReset function + if (!I2Cdetect(bus, address)) { + LOG_INFO("SEN5X ERROR no device found on adress"); + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + + delay(25); if (!sendCommand(SEN5X_RESET)) { LOG_ERROR("SEN5X: Error reseting device"); diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.h b/src/modules/Telemetry/Sensor/SEN5XSensor.h index 14168f587..9b80e3224 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.h +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h @@ -49,6 +49,7 @@ class SEN5XSensor : public TelemetrySensor 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); + bool I2Cdetect(TwoWire *_Wire, uint8_t address); protected: virtual void setup() override; From aee86f62c515bb1e84b1b9c1c5745a77fbbc698f Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Mon, 7 Jul 2025 14:34:09 +0200 Subject: [PATCH 17/38] WIP Sen5X functions --- src/detect/ScanI2CTwoWire.cpp | 2 +- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 46 +++++++++++++++++--- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 7efd5bbed..b201ea6fe 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -494,12 +494,12 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) case ICM20948_ADDR_ALT: // same as MPU6050_ADDR // ICM20948 Register check registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); + prod = readSEN5xProductName(i2cBus, addr.address); if (registerValue == 0xEA) { type = ICM20948; logFoundDevice("ICM20948", (uint8_t)addr.address); break; } else { - prod = readSEN5xProductName(i2cBus, addr.address); if (prod.startsWith("SEN55")) { type = SEN5X; logFoundDevice("Sensirion SEN55", addr.address); diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index 795d3bae7..b37d5f963 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -183,15 +183,9 @@ int32_t SEN5XSensor::runOnce() } bus = nodeTelemetrySensorsMap[sensorType].second; - address = (uint8_t)nodeTelemetrySensorsMap[sensorType].first; // sen5x.begin(*bus); - if (!I2Cdetect(bus, address)) { - LOG_INFO("SEN5X ERROR no device found on adress"); - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - } - - delay(25); + delay(50); // without this there is an error on the deviceReset function if (!sendCommand(SEN5X_RESET)) { LOG_ERROR("SEN5X: Error reseting device"); @@ -210,6 +204,44 @@ int32_t SEN5XSensor::runOnce() 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 + + 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; + } + + // Detection succeeded + state = SEN5X_IDLE; + 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; + // } + + // 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; + // } // Detection succeeded state = SEN5X_IDLE; From c82e04244a8f8a86d472ae5c202afc0c985de63c Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 15 Jul 2025 19:41:32 +0200 Subject: [PATCH 18/38] Changes on SEN5X library - removing pm_env as well --- src/modules/Telemetry/AirQualityTelemetry.cpp | 26 +- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 481 +++++++++++++++--- src/modules/Telemetry/Sensor/SEN5XSensor.h | 86 +++- src/serialization/MeshPacketSerializer.cpp | 24 +- .../MeshPacketSerializer_nRF52.cpp | 18 +- 5 files changed, 519 insertions(+), 116 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 97722ff08..c14298463 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,6 +299,8 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag; m.time = getTime(); + // TODO - if one sensor fails here, we will stop taking measurements from everything + // Can we do this in a smarter way, for instance checking the nodeTelemetrySensor map and making it dynamic? if (getAirQualityTelemetry(&m)) { LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u, \ pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u", \ 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 5a1f8ed7e..2a8af1778 100644 --- a/src/serialization/MeshPacketSerializer.cpp +++ b/src/serialization/MeshPacketSerializer.cpp @@ -149,18 +149,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; From 0e97e7d004a286c60fdc7b6f36b03e07316fa324 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 15 Jul 2025 19:49:42 +0200 Subject: [PATCH 19/38] Small cleanup of SEN5X sensors --- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 41 +++++--------------- 1 file changed, 9 insertions(+), 32 deletions(-) diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index 36c306d67..f14bf80f8 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -367,38 +367,11 @@ int32_t SEN5XSensor::runOnce() return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } - // Check if firmware version allows The direct switch between Measurement and RHT/Gas-Only Measurement mode - 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; - } - // Detection succeeded state = SEN5X_IDLE; 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; - // } - - // 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; - // } - // Detection succeeded state = SEN5X_IDLE; status = 1; @@ -468,6 +441,7 @@ bool SEN5XSensor::readValues() // 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; @@ -477,8 +451,8 @@ bool SEN5XSensor::readValues() 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", + // 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); @@ -497,7 +471,7 @@ bool SEN5XSensor::readPnValues() uint8_t dataBuffer[30]; size_t receivedNumber = readBuffer(&dataBuffer[0], 30); if (receivedNumber == 0) { - LOG_ERROR("SEN5X: Error getting PM values"); + LOG_ERROR("SEN5X: Error getting PN values"); return false; } @@ -526,6 +500,9 @@ bool SEN5XSensor::readPnValues() sen5xmeasurement.tSize = uint_tSize / 1000.0f; // Convert PN readings from #/cm3 to #/0.1l + // TODO - Decide if those units are right + // TODO Remove accumuluative values: + // https://github.com/fablabbcn/smartcitizen-kit-2x/issues/85 sen5xmeasurement.pN0p5 *= 100; sen5xmeasurement.pN1p0 *= 100; sen5xmeasurement.pN2p5 *= 100; @@ -533,8 +510,8 @@ bool SEN5XSensor::readPnValues() 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", + // 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 From a7ea11b19508792cc6ba46215baa691a4447e122 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 15 Jul 2025 19:53:20 +0200 Subject: [PATCH 20/38] Minor change for SEN5X detection --- src/detect/ScanI2CTwoWire.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index b201ea6fe..1736e2e4a 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -127,7 +127,6 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) DeviceAddress addr(port, 0x00); uint16_t registerValue = 0x00; - String prod = ""; ScanI2C::DeviceType type; TwoWire *i2cBus; #ifdef RV3028_RTC @@ -494,12 +493,13 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) case ICM20948_ADDR_ALT: // same as MPU6050_ADDR // ICM20948 Register check registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); - prod = readSEN5xProductName(i2cBus, addr.address); if (registerValue == 0xEA) { type = ICM20948; logFoundDevice("ICM20948", (uint8_t)addr.address); break; } else { + String prod = ""; + prod = readSEN5xProductName(i2cBus, addr.address); if (prod.startsWith("SEN55")) { type = SEN5X; logFoundDevice("Sensirion SEN55", addr.address); From f9de80272f1a59979e9fe5dc4d9e35523a4d1753 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 15 Jul 2025 19:58:37 +0200 Subject: [PATCH 21/38] Remove dup code --- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index f14bf80f8..66ae77f27 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -362,16 +362,6 @@ int32_t SEN5XSensor::runOnce() } delay(200); // From Sensirion Arduino library - if (!findModel()) { - LOG_ERROR("SEN5X: error finding sensor model"); - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - } - - // Detection succeeded - state = SEN5X_IDLE; - status = 1; - LOG_INFO("SEN5X Enabled"); - // Detection succeeded state = SEN5X_IDLE; status = 1; From 3abd2a27ee9a4a90e40f1605f07d78ecaadebe12 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Thu, 17 Jul 2025 11:05:02 +0200 Subject: [PATCH 22/38] Enable PM sensor before sending telemetry. This enables the PM sensor for a predefined period to allow for warmup. Once telemetry is sent, the sensor shuts down again. --- src/modules/Telemetry/AirQualityTelemetry.cpp | 18 ++++++++++++------ src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 6 +++--- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index c14298463..438f71ff9 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -93,8 +93,14 @@ int32_t AirQualityTelemetryModule::runOnce() return pmsa003iSensor.wakeUp(); #endif /* PMSA003I_ENABLE_PIN */ - if (sen5xSensor.hasSensor() && !sen5xSensor.isActive()) - return sen5xSensor.wakeUp(); + // 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(); + } if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( @@ -112,14 +118,14 @@ 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 + + // 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 */ - // 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(); - + if (sen5xSensor.hasSensor() && sen5xSensor.isActive()) + sen5xSensor.idle(); } return min(sendToPhoneIntervalMs, result); } diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index 66ae77f27..9c25c859b 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -122,7 +122,7 @@ bool SEN5XSensor::sendCommand(uint16_t command, uint8_t* buffer, uint8_t byteNum #endif // Transmit the data - LOG_INFO("Beginning connection to SEN5X: 0x%x", address); + // 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(); @@ -276,14 +276,14 @@ bool SEN5XSensor::isActive(){ } uint32_t SEN5XSensor::wakeUp(){ - LOG_INFO("SEN5X: Attempting to wakeUp sensor"); + // 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"); + // LOG_INFO("SEN5X: Setting measurement mode"); uint32_t now; now = getTime(); measureStarted = now; From bab6fdd7e7e9a257480896c0beb91cff890b8200 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Thu, 17 Jul 2025 16:33:04 +0200 Subject: [PATCH 23/38] Small cleanups in SEN5X sensor --- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index 9c25c859b..7da2e1086 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -395,7 +395,6 @@ int32_t SEN5XSensor::runOnce() LOG_INFO("SEN5X: No valid last cleaning date found, saving it now: %u", lastCleaning); } - // TODO - Should wakeUp happen here? return initI2CSensor(); } @@ -478,10 +477,6 @@ bool SEN5XSensor::readPnValues() 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; @@ -490,7 +485,6 @@ bool SEN5XSensor::readPnValues() sen5xmeasurement.tSize = uint_tSize / 1000.0f; // Convert PN readings from #/cm3 to #/0.1l - // TODO - Decide if those units are right // TODO Remove accumuluative values: // https://github.com/fablabbcn/smartcitizen-kit-2x/issues/85 sen5xmeasurement.pN0p5 *= 100; From 72d75449ac10f37fe32401b979514d3dd0301b8c Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Thu, 17 Jul 2025 21:14:18 +0200 Subject: [PATCH 24/38] Add dynamic measurement interval for SEN5X --- src/modules/Telemetry/AirQualityTelemetry.cpp | 14 +++++-- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 38 +++++++++++++++++++ src/modules/Telemetry/Sensor/SEN5XSensor.h | 22 +++++------ 3 files changed, 58 insertions(+), 16 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 438f71ff9..78a7a6e6e 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -47,6 +47,7 @@ int32_t AirQualityTelemetryModule::runOnce() } uint32_t result = UINT32_MAX; + uint32_t sen5xPendingForReady; /* Uncomment the preferences below if you want to use the module @@ -102,6 +103,14 @@ int32_t AirQualityTelemetryModule::runOnce() 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(); + if (sen5xPendingForReady) { + return sen5xPendingForReady; + } + } + if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( moduleConfig.telemetry.air_quality_interval, @@ -118,14 +127,13 @@ int32_t AirQualityTelemetryModule::runOnce() 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()) - sen5xSensor.idle(); + if (sen5xSensor.hasSensor() && sen5xSensor.isActive() ) + sen5xSensor.idle(); } return min(sendToPhoneIntervalMs, result); } diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index 7da2e1086..0083d20d2 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -570,6 +570,44 @@ uint8_t SEN5XSensor::getMeasurements() return 0; } +int32_t SEN5XSensor::pendingForReady(){ + uint32_t now; + now = getTime(); + uint32_t sinceMeasureStarted = (now - measureStarted)*1000; + LOG_INFO("Since measure started: %u", 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"); diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.h b/src/modules/Telemetry/Sensor/SEN5XSensor.h index b8e0a0ac9..71b32a6f6 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.h +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h @@ -7,15 +7,12 @@ #include "Wire.h" #include "RTC.h" +// Warm up times for SEN5X from the datasheet #ifndef SEN5X_WARMUP_MS_1 -// from the SEN5X datasheet -// #define SEN5X_WARMUP_MS_1 15000 - Change to this -#define SEN5X_WARMUP_MS_1 30000 +#define SEN5X_WARMUP_MS_1 15000 #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 @@ -76,17 +73,10 @@ class SEN5XSensor : public TelemetrySensor 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; - // 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 @@ -131,6 +121,12 @@ class SEN5XSensor : public TelemetrySensor 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(); }; From 89e1532c79cc11f6892e2c169fbe893678db390b Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Fri, 18 Jul 2025 01:03:06 +0200 Subject: [PATCH 25/38] Only disable SEN5X if enough time after reading. --- src/modules/Telemetry/AirQualityTelemetry.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 78a7a6e6e..2a7577ba8 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -132,8 +132,16 @@ int32_t AirQualityTelemetryModule::runOnce() pmsa003iSensor.sleep(); #endif /* PMSA003I_ENABLE_PIN */ - if (sen5xSensor.hasSensor() && sen5xSensor.isActive() ) - sen5xSensor.idle(); + 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); } From e3f345c59b5042370a0ff5bcb3f7fd7308d5cd99 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Fri, 18 Jul 2025 01:03:50 +0200 Subject: [PATCH 26/38] Idle for SEN5X on communication error --- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index 0083d20d2..eda75ae4f 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -657,9 +657,11 @@ bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement) } 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; } From a374bb558c1fd1ed50aa514639528c4682bb0000 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Fri, 18 Jul 2025 09:56:37 +0200 Subject: [PATCH 27/38] Cleanup of logs and remove unnecessary delays --- src/modules/Telemetry/AirQualityTelemetry.cpp | 1 + src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 44 +++++++++---------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 2a7577ba8..eae33da50 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -106,6 +106,7 @@ int32_t AirQualityTelemetryModule::runOnce() // 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; } diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index eda75ae4f..dc505fec8 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -13,7 +13,7 @@ SEN5XSensor::SEN5XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SEN5 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); + // LOG_DEBUG("Restoring I2C clock to %uHz", currentClock); return bus->setClock(currentClock); } return true; @@ -116,7 +116,7 @@ bool SEN5XSensor::sendCommand(uint16_t command, uint8_t* buffer, uint8_t byteNum uint32_t currentClock; currentClock = bus->getClock(); if (currentClock != SEN5X_I2C_CLOCK_SPEED){ - LOG_DEBUG("Changing I2C clock to %u", SEN5X_I2C_CLOCK_SPEED); + // LOG_DEBUG("Changing I2C clock to %u", SEN5X_I2C_CLOCK_SPEED); bus->setClock(SEN5X_I2C_CLOCK_SPEED); } #endif @@ -135,7 +135,7 @@ bool SEN5XSensor::sendCommand(uint16_t command, uint8_t* buffer, uint8_t byteNum } if (i2c_error != 0) { - LOG_ERROR("SEN5X: Error on I2c communication: %x", i2c_error); + LOG_ERROR("SEN5X: Error on I2C communication: %x", i2c_error); return false; } return true; @@ -147,7 +147,7 @@ uint8_t SEN5XSensor::readBuffer(uint8_t* buffer, uint8_t byteNumber) uint32_t currentClock; currentClock = bus->getClock(); if (currentClock != SEN5X_I2C_CLOCK_SPEED){ - LOG_DEBUG("Changing I2C clock to %u", SEN5X_I2C_CLOCK_SPEED); + // LOG_DEBUG("Changing I2C clock to %u", SEN5X_I2C_CLOCK_SPEED); bus->setClock(SEN5X_I2C_CLOCK_SPEED); } #endif @@ -219,7 +219,7 @@ bool SEN5XSensor::idle() LOG_ERROR("SEN5X: Error stoping measurement"); return false; } - delay(200); // From Sensirion Arduino library + // delay(200); // From Sensirion Arduino library LOG_INFO("SEN5X: Stop measurement mode"); @@ -237,13 +237,13 @@ void SEN5XSensor::loadCleaningState() if (file) { file.read(); file.close(); - LOG_INFO("Cleaning state %u read for %s read from %s", lastCleaning, sensorName, sen5XCleaningFileName); + LOG_INFO("SEN5X: Cleaning state %u read for %s read from %s", lastCleaning, sensorName, sen5XCleaningFileName); } else { - LOG_INFO("No %s state found (File: %s)", sensorName, sen5XCleaningFileName); + LOG_INFO("SEN5X: No %s state found (File: %s)", sensorName, sen5XCleaningFileName); } spiLock->unlock(); #else - LOG_ERROR("ERROR: Filesystem not implemented"); + LOG_ERROR("SEN5X: ERROR - Filesystem not implemented"); #endif } @@ -253,21 +253,21 @@ void SEN5XSensor::updateCleaningState() spiLock->lock(); if (FSCom.exists(sen5XCleaningFileName) && !FSCom.remove(sen5XCleaningFileName)) { - LOG_WARN("Can't remove old state file"); + LOG_WARN("SEN5X: 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); + LOG_INFO("SEN5X: 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); + LOG_INFO("SEN5X: Can't write %s state (File: %s)", sensorName, sen5XCleaningFileName); } spiLock->unlock(); #else - LOG_ERROR("ERROR: Filesystem not implemented"); + LOG_ERROR("SEN5X: ERROR: Filesystem not implemented"); #endif } @@ -281,7 +281,7 @@ uint32_t SEN5XSensor::wakeUp(){ LOG_INFO("SEN5X: Error starting measurement"); return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } - delay(50); // From Sensirion Arduino library + // delay(50); // From Sensirion Arduino library // LOG_INFO("SEN5X: Setting measurement mode"); uint32_t now; @@ -308,7 +308,7 @@ bool SEN5XSensor::startCleaning() LOG_ERROR("SEN5X: Error starting fan cleaning"); return false; } - delay(20); // From Sensirion Arduino library + // 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..."); @@ -318,7 +318,7 @@ bool SEN5XSensor::startCleaning() // Serial.print("."); delay(500); } - LOG_INFO(" Cleaning done!!"); + LOG_INFO("SEN5X: Cleaning done!!"); // Save timestamp in flash so we know when a week has passed uint32_t now; @@ -370,24 +370,24 @@ int32_t SEN5XSensor::runOnce() // 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); + LOG_INFO("SEN5X: Last cleaning time: %u", lastCleaning); if (lastCleaning) { - LOG_INFO("Last cleaning is valid"); + LOG_INFO("SEN5X: Last cleaning is valid"); uint32_t now; now = getTime(); - LOG_INFO("Current time %us", now); + LOG_INFO("SEN5X: Current time %us", now); uint32_t passed = now - lastCleaning; - LOG_INFO("Elapsed time since last cleaning: %us", passed); + 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("Last cleaning date (in epoch): %u", lastCleaning); + LOG_INFO("SEN5X: Last cleaning date (in epoch): %u", lastCleaning); } } else { - LOG_INFO("Last cleaning is not valid"); + 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(); @@ -574,7 +574,7 @@ int32_t SEN5XSensor::pendingForReady(){ uint32_t now; now = getTime(); uint32_t sinceMeasureStarted = (now - measureStarted)*1000; - LOG_INFO("Since measure started: %u", sinceMeasureStarted); + LOG_DEBUG("SEN5X: Since measure started: %ums", sinceMeasureStarted); switch (state) { case SEN5X_MEASUREMENT: { From 886866d5e8ddfe2f7936536eadf58155238581cb Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 22 Jul 2025 20:15:23 +0200 Subject: [PATCH 28/38] Small TODO --- src/modules/Telemetry/AirQualityTelemetry.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index eae33da50..9cbe17a76 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -24,7 +24,7 @@ PMSA003ISensor pmsa003iSensor; #include "graphics/ScreenFonts.h" -// Small hack +// TODO - Small hack to review #ifndef INCLUDE_SEN5X #define INCLUDE_SEN5X 1 #endif From 21d21af51c8f3fd976dfe9137e5a790f9d406988 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 23 Jul 2025 16:08:26 +0200 Subject: [PATCH 29/38] Settle on uint16_t for SEN5X PM data --- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 13 ++++++------- src/modules/Telemetry/Sensor/SEN5XSensor.h | 10 ++++------ 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index dc505fec8..d5f04ce94 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -430,18 +430,16 @@ bool SEN5XSensor::readValues() // 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.pM1p0 = uint_pM1p0 / 10; + sen5xmeasurement.pM2p5 = uint_pM2p5 / 10; + sen5xmeasurement.pM4p0 = uint_pM4p0 / 10; + sen5xmeasurement.pM10p0 = uint_pM10p0 / 10; 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", + LOG_DEBUG("Got: pM1p0=%u, pM2p5=%u, pM4p0=%u, pM10p0=%u", sen5xmeasurement.pM1p0, sen5xmeasurement.pM2p5, sen5xmeasurement.pM4p0, sen5xmeasurement.pM10p0); @@ -454,6 +452,7 @@ bool SEN5XSensor::readPnValues() LOG_ERROR("SEN5X: Error sending read command"); return false; } + LOG_DEBUG("SEN5X: Reading PN Values"); delay(20); // From Sensirion Arduino library diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.h b/src/modules/Telemetry/Sensor/SEN5XSensor.h index 71b32a6f6..8782c3b92 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.h +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h @@ -22,13 +22,11 @@ #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; + uint16_t pM1p0; + uint16_t pM2p5; + uint16_t pM4p0; + uint16_t pM10p0; uint32_t pN0p5; uint32_t pN1p0; uint32_t pN2p5; From 76cffe833256ea86d623b58bc1fa9c6835ea4e81 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 23 Jul 2025 16:09:34 +0200 Subject: [PATCH 30/38] Add logic for sleep/wakeUp of PMS. --- src/modules/Telemetry/AirQualityTelemetry.cpp | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 9cbe17a76..732300f54 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -88,19 +88,22 @@ int32_t AirQualityTelemetryModule::runOnce() 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( + (sen5xSensor.hasSensor() && !Throttle::isWithinTimespanMs(lastSentToMesh - SEN5X_WARMUP_MS_1, Default::getConfiguredOrDefaultMsScaled( moduleConfig.telemetry.air_quality_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) { + default_telemetry_broadcast_interval_secs, numOnlineNodes))) || + (pmsa003iSensor.hasSensor() && !Throttle::isWithinTimespanMs(lastSentToMesh - PMSA003I_WARMUP_MS, Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes)))) { + if (sen5xSensor.hasSensor() && !sen5xSensor.isActive()) return sen5xSensor.wakeUp(); + +#ifdef PMSA003I_ENABLE_PIN + if (pmsa003iSensor.hasSensor() && !pmsa003iSensor.isActive()) + return pmsa003iSensor.wakeUp(); +#endif /* PMSA003I_ENABLE_PIN */ } // Check if sen5x is ready to return data, or if it needs more time because of the low concentration threshold @@ -128,9 +131,18 @@ int32_t AirQualityTelemetryModule::runOnce() 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 + // 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(); + if (pmsa003iSensor.hasSensor() && pmsa003iSensor.isActive()) { + if (PMSA003I_WARMUP_MS < Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes)) { + LOG_DEBUG("PMSA003I: Disabling sensor until next period"); + pmsa003iSensor.sleep(); + } else { + LOG_DEBUG("PMSA003I: Sensor stays enabled due to warm up period"); + } + } #endif /* PMSA003I_ENABLE_PIN */ if (sen5xSensor.hasSensor() && sen5xSensor.isActive()) { From 561daa0cd0ff350552f4e17b3cd4f1ba84d66c7e Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 23 Jul 2025 16:10:08 +0200 Subject: [PATCH 31/38] Make AQTelemetry sensors non-exclusive --- src/modules/Telemetry/AirQualityTelemetry.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 732300f54..bd0760f36 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -280,7 +280,7 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) { - bool valid = true; + bool valid = false; bool hasSensor = false; m->time = getTime(); m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag; @@ -289,12 +289,12 @@ bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) // 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); + valid = valid || pmsa003iSensor.getMetrics(m); hasSensor = true; } if (sen5xSensor.hasSensor()) { - valid = valid && sen5xSensor.getMetrics(m); + valid = valid || sen5xSensor.getMetrics(m); hasSensor = true; } From c408d9ccf915b7965579c744a62f8b4f4484c7e1 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 5 Aug 2025 17:32:04 +0200 Subject: [PATCH 32/38] Implementation of cleaning in FS prefs and cleanup * Remove unnecessary LOGS * Add cleaning date storage in FS * Report non-cumulative PN --- src/modules/Telemetry/AirQualityTelemetry.cpp | 30 ++-- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 142 +++++++++++------- src/modules/Telemetry/Sensor/SEN5XSensor.h | 13 +- 3 files changed, 107 insertions(+), 78 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index bd0760f36..33d6399c5 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -16,25 +16,15 @@ #include "main.h" #include "sleep.h" #include + // Sensor includes #include "Sensor/PMSA003ISensor.h" - -// Sensors PMSA003ISensor pmsa003iSensor; -#include "graphics/ScreenFonts.h" - -// TODO - Small hack to review -#ifndef INCLUDE_SEN5X -#define INCLUDE_SEN5X 1 -#endif - -#ifdef INCLUDE_SEN5X #include "Sensor/SEN5XSensor.h" SEN5XSensor sen5xSensor; -#else -NullSensor sen5xSensor; -#endif + +#include "graphics/ScreenFonts.h" int32_t AirQualityTelemetryModule::runOnce() { @@ -286,8 +276,6 @@ 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()) { valid = valid || pmsa003iSensor.getMetrics(m); hasSensor = true; @@ -337,11 +325,13 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) // TODO - if one sensor fails here, we will stop taking measurements from everything // Can we do this in a smarter way, for instance checking the nodeTelemetrySensor map and making it dynamic? if (getAirQualityTelemetry(&m)) { - LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u, \ - pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u", \ - 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.pm25_environmental, m.variant.air_quality_metrics.pm100_environmental); + LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u", + m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard, + m.variant.air_quality_metrics.pm100_standard); + if (m.variant.air_quality_metrics.has_pm10_environmental) + LOG_INFO("pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u", + m.variant.air_quality_metrics.pm10_environmental, m.variant.air_quality_metrics.pm25_environmental, + m.variant.air_quality_metrics.pm100_environmental); meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index d5f04ce94..60313e7e0 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -7,6 +7,11 @@ #include "TelemetrySensor.h" #include "FSCommon.h" #include "SPILock.h" +#include "SafeFile.h" +#include +#include + +meshtastic_SEN5XState sen5xstate = meshtastic_SEN5XState_init_zero; SEN5XSensor::SEN5XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SEN5X, "SEN5X") {} @@ -229,45 +234,59 @@ bool SEN5XSensor::idle() return true; } -void SEN5XSensor::loadCleaningState() +bool SEN5XSensor::loadState() { #ifdef FSCom spiLock->lock(); - auto file = FSCom.open(sen5XCleaningFileName, FILE_O_READ); + auto file = FSCom.open(sen5XStateFileName, FILE_O_READ); + bool okay = false; if (file) { - file.read(); + LOG_INFO("%s state read from %s", sensorName, sen5XStateFileName); + pb_istream_t stream = {&readcb, &file, meshtastic_SEN5XState_size}; + if (!pb_decode(&stream, &meshtastic_SEN5XState_msg, &sen5xstate)) { + LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream)); + } else { + lastCleaning = sen5xstate.last_cleaning_time; + lastCleaningValid = sen5xstate.last_cleaning_valid; + okay = true; + } 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); + LOG_INFO("No %s state found (File: %s)", sensorName, sen5XStateFileName); } spiLock->unlock(); + return okay; #else LOG_ERROR("SEN5X: ERROR - Filesystem not implemented"); #endif } -void SEN5XSensor::updateCleaningState() +bool SEN5XSensor::saveState() { #ifdef FSCom - spiLock->lock(); + auto file = SafeFile(sen5XStateFileName); - 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(); + sen5xstate.last_cleaning_time = lastCleaning; + sen5xstate.last_cleaning_valid = lastCleaningValid; + bool okay = false; + + LOG_INFO("%s: state write to %s", sensorName, sen5XStateFileName); + pb_ostream_t stream = {&writecb, static_cast(&file), meshtastic_SEN5XState_size}; + + if (!pb_encode(&stream, &meshtastic_SEN5XState_msg, &sen5xstate)) { + LOG_ERROR("Error: can't encode protobuf %s", PB_GET_ERROR(&stream)); } else { - LOG_INFO("SEN5X: Can't write %s state (File: %s)", sensorName, sen5XCleaningFileName); + okay = true; } - spiLock->unlock(); + okay &= file.close(); + + if (okay) + LOG_INFO("%s: state write to %s successful", sensorName, sen5XStateFileName); + + return okay; #else - LOG_ERROR("SEN5X: ERROR: Filesystem not implemented"); + LOG_ERROR("%s: ERROR - Filesystem not implemented", sensorName); #endif } @@ -281,6 +300,7 @@ uint32_t SEN5XSensor::wakeUp(){ LOG_INFO("SEN5X: Error starting measurement"); return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } + // Not needed // delay(50); // From Sensirion Arduino library // LOG_INFO("SEN5X: Setting measurement mode"); @@ -295,6 +315,8 @@ uint32_t SEN5XSensor::wakeUp(){ bool SEN5XSensor::startCleaning() { + // Note: we only should enter here if we have a valid RTC with at least + // RTCQuality::RTCQualityDevice state = SEN5X_CLEANING; // Note that this command can only be run when the sensor is in measurement mode @@ -322,9 +344,11 @@ bool SEN5XSensor::startCleaning() // Save timestamp in flash so we know when a week has passed uint32_t now; - now = getTime(); + now = getValidTime(RTCQuality::RTCQualityDevice); + // If time is not RTCQualityNone, it will return non-zero lastCleaning = now; - updateCleaningState(); + lastCleaningValid = true; + saveState(); idle(); return true; @@ -365,34 +389,43 @@ int32_t SEN5XSensor::runOnce() // Detection succeeded state = SEN5X_IDLE; status = 1; - LOG_INFO("SEN5X Enabled"); + + // Load state + loadState(); // 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 = getValidTime(RTCQuality::RTCQualityDevice); + // If time is not RTCQualityNone, it will return non-zero - 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 (now) { + if (lastCleaningValid) { + // LOG_INFO("SEN5X: Last cleaning is valid"); + // LOG_INFO("SEN5X: Current time %us", now); - 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(); + int32_t passed = now - lastCleaning; // in seconds + // 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 (%us) since last cleaning in epoch (%us). Trigger, cleaning...", passed, lastCleaning); + startCleaning(); + } else { + LOG_INFO("SEN5X: Cleaning not needed (%ds passed). Last cleaning date (in epoch): %us", passed, lastCleaning); + } } else { - LOG_INFO("SEN5X: Last cleaning date (in epoch): %u", lastCleaning); - } + // LOG_INFO("SEN5X: Last cleaning time is not valid"); + // We assume the device has just been updated or it is new, so no need to trigger a cleaning. + // Just save the timestamp to do a cleaning one week from now. + // TODO - could we trigger this after getting time? + // Otherwise, we will never trigger cleaning in some cases + lastCleaning = now; + lastCleaningValid = true; + LOG_INFO("SEN5X: No valid last cleaning date found, saving it now: %us", lastCleaning); + saveState(); + } } 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); + // TODO - Should this actually ignore? We could end up never cleaning... + LOG_INFO("SEN5X: Not enough RTCQuality, ignoring cleaning"); } return initI2CSensor(); @@ -429,7 +462,8 @@ bool SEN5XSensor::readValues() 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 + + // Convert values based on Sensirion Arduino lib sen5xmeasurement.pM1p0 = uint_pM1p0 / 10; sen5xmeasurement.pM2p5 = uint_pM2p5 / 10; sen5xmeasurement.pM4p0 = uint_pM4p0 / 10; @@ -446,7 +480,7 @@ bool SEN5XSensor::readValues() return true; } -bool SEN5XSensor::readPnValues() +bool SEN5XSensor::readPnValues(bool cumulative) { if (!sendCommand(SEN5X_READ_PM_VALUES)){ LOG_ERROR("SEN5X: Error sending read command"); @@ -475,7 +509,7 @@ bool SEN5XSensor::readPnValues() 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 + // Convert values based on Sensirion Arduino lib sen5xmeasurement.pN0p5 = uint_pN0p5 / 10; sen5xmeasurement.pN1p0 = uint_pN1p0 / 10; sen5xmeasurement.pN2p5 = uint_pN2p5 / 10; @@ -484,8 +518,6 @@ bool SEN5XSensor::readPnValues() 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; @@ -493,7 +525,15 @@ bool SEN5XSensor::readPnValues() sen5xmeasurement.pN10p0 *= 100; sen5xmeasurement.tSize *= 100; - // TODO - Change depending on the final values + // Remove accumuluative values: + // https://github.com/fablabbcn/smartcitizen-kit-2x/issues/85 + if (!cumulative) { + sen5xmeasurement.pN10p0 -= sen5xmeasurement.pN4p0; + sen5xmeasurement.pN4p0 -= sen5xmeasurement.pN2p5; + sen5xmeasurement.pN2p5 -= sen5xmeasurement.pN1p0; + sen5xmeasurement.pN1p0 -= sen5xmeasurement.pN0p5; + } + LOG_DEBUG("Got: pN0p5=%u, pN1p0=%u, pN2p5=%u, pN4p0=%u, pN10p0=%u, tSize=%.2f", sen5xmeasurement.pN0p5, sen5xmeasurement.pN1p0, sen5xmeasurement.pN2p5, sen5xmeasurement.pN4p0, @@ -556,7 +596,7 @@ uint8_t SEN5XSensor::getMeasurements() return 2; } - if(!readPnValues()) { + if(!readPnValues(false)) { LOG_ERROR("SEN5X: Error getting PM readings"); return 2; } @@ -583,7 +623,7 @@ int32_t SEN5XSensor::pendingForReady(){ } // Get PN values to check if we are above or below threshold - readPnValues(); + readPnValues(true); // 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) { diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.h b/src/modules/Telemetry/Sensor/SEN5XSensor.h index 8782c3b92..a72f606e9 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.h +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h @@ -84,7 +84,7 @@ class SEN5XSensor : public TelemetrySensor bool startCleaning(); uint8_t getMeasurements(); bool readRawValues(); - bool readPnValues(); + bool readPnValues(bool cumulative); bool readValues(); uint32_t measureStarted = 0; @@ -92,15 +92,14 @@ class SEN5XSensor : public TelemetrySensor protected: // Store status of the sensor in this file - const char *sen5XCleaningFileName = "/prefs/sen5XCleaning.dat"; - const char *sen5XVOCFileName = "/prefs/sen5XVOC.dat"; + const char *sen5XStateFileName = "/prefs/sen5X.dat"; + bool loadState(); + bool saveState(); // Cleaning State - #define SEN5X_MAX_CLEANING_SIZE 32 - // Last cleaning status - if > 0 - valid, otherwise 0 + // Last cleaning status uint32_t lastCleaning = 0; - void loadCleaningState(); - void updateCleaningState(); + bool lastCleaningValid = false; // TODO - VOC State // # define SEN5X_VOC_STATE_BUFFER_SIZE 12 From b876b9a9274d446e8c5a6796f53ddb829efaa6df Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 6 Aug 2025 10:47:50 +0200 Subject: [PATCH 33/38] Bring back detection code for SEN5X after branch rebase --- src/detect/ScanI2CTwoWire.cpp | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 1736e2e4a..c10bad8e8 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -110,6 +110,42 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation return value; } +/// for SEN5X detection +// Note, this code needs to be called before setting the I2C bus speed +// for the screen at high speed. The speed needs to be at 100kHz, otherwise +// detection will not work +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__); \ From b3107fd0d5914b749bc97ec6eb47cc3ea75eff63 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Thu, 7 Aug 2025 16:27:41 +0200 Subject: [PATCH 34/38] Add placeholder for admin message --- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 30 ++++++++++++++++++++ src/modules/Telemetry/Sensor/SEN5XSensor.h | 1 + 2 files changed, 31 insertions(+) diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index 60313e7e0..bb82c4fbf 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -707,4 +707,34 @@ bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement) return true; } +AdminMessageHandleResult SEN5XSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) +{ + AdminMessageHandleResult result; + result = AdminMessageHandleResult::NOT_HANDLED; + + // TODO - Add admin command to set temperature offset + // switch (request->which_payload_variant) { + // case meshtastic_AdminMessage_sensor_config_tag: + // if (!request->sensor_config.has_sen5x_config) { + // result = AdminMessageHandleResult::NOT_HANDLED; + // break; + // } + + // // Check for temperature offset + // // if (request->sensor_config.sen5x_config.has_set_temperature) { + // // this->setTemperature(request->sensor_config.sen5x_config.set_temperature); + // // } + + // // result = AdminMessageHandleResult::HANDLED; + // result = AdminMessageHandleResult::NOT_HANDLED; + // break; + + // default: + // result = AdminMessageHandleResult::NOT_HANDLED; + // } + + return result; +} + #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.h b/src/modules/Telemetry/Sensor/SEN5XSensor.h index a72f606e9..f46657f2e 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.h +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h @@ -124,6 +124,7 @@ class SEN5XSensor : public TelemetrySensor #define SEN5X_PN4P0_CONC_THD 100 // This value represents the time needed for pending data int32_t pendingForReady(); + AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) override; }; From 7d1deaf4ce21183dd9c6ee3c6fffab4345c011d2 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Thu, 7 Aug 2025 17:13:26 +0200 Subject: [PATCH 35/38] Add VOC measurements and persistence (WIP) * Adds VOC measurements and state * Still not working on VOC Index persistence * Should it stay in continuous mode? --- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 230 ++++++++++++++++--- src/modules/Telemetry/Sensor/SEN5XSensor.h | 19 +- 2 files changed, 208 insertions(+), 41 deletions(-) diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index bb82c4fbf..77af272fe 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -11,8 +11,6 @@ #include #include -meshtastic_SEN5XState sen5xstate = meshtastic_SEN5XState_init_zero; - SEN5XSensor::SEN5XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SEN5X, "SEN5X") {} bool SEN5XSensor::restoreClock(uint32_t currentClock){ @@ -31,7 +29,7 @@ bool SEN5XSensor::getVersion() LOG_ERROR("SEN5X: Error sending version command"); return false; } - delay(20); // From Sensirion Arduino library + delay(20); // From Sensirion Datasheet uint8_t versionBuffer[12]; size_t charNumber = readBuffer(&versionBuffer[0], 3); @@ -57,7 +55,7 @@ bool SEN5XSensor::findModel() LOG_ERROR("SEN5X: Error asking for product name"); return false; } - delay(50); // From Sensirion Arduino library + delay(50); // From Sensirion Datasheet const uint8_t nameSize = 48; uint8_t name[nameSize]; @@ -127,12 +125,16 @@ bool SEN5XSensor::sendCommand(uint16_t command, uint8_t* buffer, uint8_t byteNum #endif // Transmit the data - // LOG_INFO("Beginning connection to SEN5X: 0x%x", address); + // LOG_DEBUG("Beginning connection to SEN5X: 0x%x. Size: %u", address, bufferSize); + // Note: this is necessary to allow for long-buffers + delay(20); bus->beginTransmission(address); size_t writtenBytes = bus->write(toSend, bufferSize); uint8_t i2c_error = bus->endTransmission(); +#ifdef SEN5X_I2C_CLOCK_SPEED restoreClock(currentClock); +#endif if (writtenBytes != bufferSize) { LOG_ERROR("SEN5X: Error writting on I2C bus"); @@ -177,7 +179,10 @@ uint8_t SEN5XSensor::readBuffer(uint8_t* buffer, uint8_t byteNumber) readBytes -=3; receivedBytes += 2; } +#ifdef SEN5X_I2C_CLOCK_SPEED restoreClock(currentClock); +#endif + return receivedBytes; } @@ -212,24 +217,107 @@ bool SEN5XSensor::I2Cdetect(TwoWire *_Wire, uint8_t address) 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; + + + // Get VOC state before going to idle mode + if (vocStateFromSensor()) { + // TODO Should this be saved with saveState()? + // It so, we can consider not saving it when rebooting as + // we would have likely saved it recently + + // Check if we have time, and store it + uint32_t now; // If time is RTCQualityNone, it will return zero + now = getValidTime(RTCQuality::RTCQualityDevice); + + if (now) { + vocTime = now; + vocValid = true; + // saveState(); + } + } else { + vocValid = 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 + delay(200); // From Sensirion Datasheet LOG_INFO("SEN5X: Stop measurement mode"); state = SEN5X_IDLE; measureStarted = 0; + return true; +} + +bool SEN5XSensor::vocStateToSensor() +{ + if (model != SEN55){ + return true; + } + + if (!sendCommand(SEN5X_STOP_MEASUREMENT)) { + LOG_ERROR("SEN5X: Error stoping measurement"); + return false; + } + delay(200); // From Sensirion Datasheet + + LOG_DEBUG("SEN5X: Sending VOC state to sensor"); + LOG_DEBUG("[%u, %u, %u, %u, %u, %u, %u, %u]", + vocState[0],vocState[1], vocState[2], vocState[3], + vocState[4],vocState[5], vocState[6], vocState[7]); + + // Note: send command already takes into account the CRC + // buffer size increment needed + if (!sendCommand(SEN5X_RW_VOCS_STATE, vocState, SEN5X_VOC_STATE_BUFFER_SIZE)){ + LOG_ERROR("SEN5X: Error sending VOC's state command'"); + return false; + } + + return true; +} + +bool SEN5XSensor::vocStateFromSensor() +{ + if (model != SEN55){ + return true; + } + + LOG_INFO("SEN5X: Getting VOC state from sensor"); + // Ask VOCs state from the sensor + if (!sendCommand(SEN5X_RW_VOCS_STATE)){ + LOG_ERROR("SEN5X: Error sending VOC's state command'"); + return false; + } + + delay(20); // From Sensirion Datasheet + + // Retrieve the data + // Allocate buffer to account for CRC + uint8_t vocBuffer[SEN5X_VOC_STATE_BUFFER_SIZE + (SEN5X_VOC_STATE_BUFFER_SIZE / 2)]; + size_t receivedNumber = readBuffer(&vocBuffer[0], SEN5X_VOC_STATE_BUFFER_SIZE + (SEN5X_VOC_STATE_BUFFER_SIZE / 2)); + delay(20); // From Sensirion Datasheet + + if (receivedNumber == 0) { + LOG_DEBUG("SEN5X: Error getting VOC's state"); + return false; + } + + vocState[0] = vocBuffer[0]; + vocState[1] = vocBuffer[1]; + vocState[2] = vocBuffer[3]; + vocState[3] = vocBuffer[4]; + vocState[4] = vocBuffer[6]; + vocState[5] = vocBuffer[7]; + vocState[6] = vocBuffer[9]; + vocState[7] = vocBuffer[10]; + + // Print the state (if debug is on) + LOG_DEBUG("SEN5X: VOC state retrieved from sensor"); + LOG_DEBUG("[%u, %u, %u, %u, %u, %u, %u, %u]", + vocState[0],vocState[1], vocState[2], vocState[3], + vocState[4],vocState[5], vocState[6], vocState[7]); return true; } @@ -248,6 +336,18 @@ bool SEN5XSensor::loadState() } else { lastCleaning = sen5xstate.last_cleaning_time; lastCleaningValid = sen5xstate.last_cleaning_valid; + // Unpack state + vocState[7] = (uint8_t)(sen5xstate.voc_state >> 56); + vocState[6] = (uint8_t)(sen5xstate.voc_state >> 48); + vocState[5] = (uint8_t)(sen5xstate.voc_state >> 40); + vocState[4] = (uint8_t)(sen5xstate.voc_state >> 32); + vocState[3] = (uint8_t)(sen5xstate.voc_state >> 24); + vocState[2] = (uint8_t)(sen5xstate.voc_state >> 16); + vocState[1] = (uint8_t)(sen5xstate.voc_state >> 8); + vocState[0] = (uint8_t)sen5xstate.voc_state; + + vocTime = sen5xstate.voc_time; + vocValid = sen5xstate.voc_valid; okay = true; } file.close(); @@ -263,11 +363,29 @@ bool SEN5XSensor::loadState() bool SEN5XSensor::saveState() { + // TODO - This should be called before a reboot + // is there a way to get notified? #ifdef FSCom auto file = SafeFile(sen5XStateFileName); sen5xstate.last_cleaning_time = lastCleaning; sen5xstate.last_cleaning_valid = lastCleaningValid; + + // Unpack state (12 bytes in two parts) + sen5xstate.voc_state = ((uint64_t) vocState[7] << 56) | + ((uint64_t) vocState[6] << 48) | + ((uint64_t) vocState[5] << 40) | + ((uint64_t) vocState[4] << 32) | + ((uint32_t) vocState[3] << 24) | + ((uint32_t) vocState[2] << 16) | + ((uint32_t) vocState[1] << 8) | + vocState[0]; + + LOG_INFO("sen5xstate.voc_state %i", sen5xstate.voc_state); + + sen5xstate.voc_time = vocTime; + sen5xstate.voc_valid = vocValid; + bool okay = false; LOG_INFO("%s: state write to %s", sensorName, sen5XStateFileName); @@ -296,17 +414,45 @@ bool SEN5XSensor::isActive(){ uint32_t SEN5XSensor::wakeUp(){ // LOG_INFO("SEN5X: Attempting to wakeUp sensor"); + + // From the datasheet + // By default, the VOC algorithm resets its state to initial + // values each time a measurement is started, + // even if the measurement was stopped only for a short + // time. So, the VOC index output value needs a long time + // until it is stable again. This can be avoided by + // restoring the previously memorized algorithm state before + // starting the measure mode + + // TODO - This needs to be tested + // In SC, the sensor is operated in contionuous mode if + // VOCs are present, increasing battery consumption + // A different approach should be possible as stated on the + // datasheet (see above) + // uint32_t now, passed; + // now = getValidTime(RTCQuality::RTCQualityDevice); + // passed = now - vocTime; //in seconds + // // Check if state is recent, less than 10 minutes (600 seconds) + // if ((passed < SEN5X_VOC_VALID_TIME) && (now > SEN5X_VOC_VALID_DATE) && vocValid) { + // if (!vocStateToSensor()){ + // LOG_ERROR("SEN5X: Sending VOC state to sensor failed"); + // } + // } else { + // LOG_DEBUG("SEN5X: No valid VOC state found. Ignoring"); + // } + if (!sendCommand(SEN5X_START_MEASUREMENT)) { - LOG_INFO("SEN5X: Error starting measurement"); + LOG_ERROR("SEN5X: Error starting measurement"); + // TODO - what should this return?? return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } - // Not needed - // delay(50); // From Sensirion Arduino library + delay(50); // From Sensirion Datasheet // LOG_INFO("SEN5X: Setting measurement mode"); - uint32_t now; - now = getTime(); - measureStarted = now; + // TODO - This is currently "problematic" + // If time is updated in between reads, there is no way to + // keep track of how long it has passed + measureStarted = getTime(); state = SEN5X_MEASUREMENT; if (state == SEN5X_MEASUREMENT) LOG_INFO("SEN5X: Started measurement mode"); @@ -319,18 +465,18 @@ bool SEN5XSensor::startCleaning() // RTCQuality::RTCQualityDevice state = SEN5X_CLEANING; - // Note that this command can only be run when the sensor is in measurement mode + // Note that cleaning 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 + delay(50); // From Sensirion Datasheet if (!sendCommand(SEN5X_START_FAN_CLEANING)) { LOG_ERROR("SEN5X: Error starting fan cleaning"); return false; } - // delay(20); // From Sensirion Arduino library + delay(20); // From Sensirion Datasheet // 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..."); @@ -371,7 +517,7 @@ int32_t SEN5XSensor::runOnce() LOG_ERROR("SEN5X: Error reseting device"); return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } - delay(200); // From Sensirion Arduino library + delay(200); // From Sensirion Datasheet if (!findModel()) { LOG_ERROR("SEN5X: error finding sensor model"); @@ -384,7 +530,7 @@ int32_t SEN5XSensor::runOnce() 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 + delay(200); // From Sensirion Datasheet // Detection succeeded state = SEN5X_IDLE; @@ -395,25 +541,23 @@ int32_t SEN5XSensor::runOnce() // Check if it is time to do a cleaning uint32_t now; + int32_t passed; now = getValidTime(RTCQuality::RTCQualityDevice); // If time is not RTCQualityNone, it will return non-zero if (now) { if (lastCleaningValid) { - // LOG_INFO("SEN5X: Last cleaning is valid"); - // LOG_INFO("SEN5X: Current time %us", now); - int32_t passed = now - lastCleaning; // in seconds - // LOG_INFO("SEN5X: Elapsed time since last cleaning: %us", passed); + passed = now - lastCleaning; // in seconds - if (passed > ONE_WEEK_IN_SECONDS && (now > 1514764800)) { // If current date greater than 01/01/2018 (validity check) + if (passed > ONE_WEEK_IN_SECONDS && (now > SEN5X_VOC_VALID_DATE)) { + // If current date greater than 01/01/2018 (validity check) LOG_INFO("SEN5X: More than a week (%us) since last cleaning in epoch (%us). Trigger, cleaning...", passed, lastCleaning); startCleaning(); } else { LOG_INFO("SEN5X: Cleaning not needed (%ds passed). Last cleaning date (in epoch): %us", passed, lastCleaning); } } else { - // LOG_INFO("SEN5X: Last cleaning time is not valid"); // We assume the device has just been updated or it is new, so no need to trigger a cleaning. // Just save the timestamp to do a cleaning one week from now. // TODO - could we trigger this after getting time? @@ -422,10 +566,28 @@ int32_t SEN5XSensor::runOnce() lastCleaningValid = true; LOG_INFO("SEN5X: No valid last cleaning date found, saving it now: %us", lastCleaning); saveState(); + } + if (model == SEN55) { + if (!vocValid) { + LOG_INFO("SEN5X: No valid VOC's state found"); + } else { + passed = now - vocTime; //in seconds + + // Check if state is recent, less than 10 minutes (600 seconds) + if (passed < SEN5X_VOC_VALID_TIME && (now > SEN5X_VOC_VALID_DATE)) { + // If current date greater than 01/01/2018 (validity check) + // Send it to the sensor + LOG_INFO("SEN5X: VOC state is valid and recent"); + vocStateToSensor(); + } else { + LOG_INFO("SEN5X VOC state is to old or date is invalid"); + } } + } + } else { // TODO - Should this actually ignore? We could end up never cleaning... - LOG_INFO("SEN5X: Not enough RTCQuality, ignoring cleaning"); + LOG_INFO("SEN5X: Not enough RTCQuality, ignoring saved state"); } return initI2CSensor(); @@ -442,7 +604,7 @@ bool SEN5XSensor::readValues() return false; } LOG_DEBUG("SEN5X: Reading PM Values"); - delay(20); // From Sensirion Arduino library + delay(20); // From Sensirion Datasheet uint8_t dataBuffer[24]; size_t receivedNumber = readBuffer(&dataBuffer[0], 24); @@ -488,7 +650,7 @@ bool SEN5XSensor::readPnValues(bool cumulative) } LOG_DEBUG("SEN5X: Reading PN Values"); - delay(20); // From Sensirion Arduino library + delay(20); // From Sensirion Datasheet uint8_t dataBuffer[30]; size_t receivedNumber = readBuffer(&dataBuffer[0], 30); @@ -550,7 +712,7 @@ bool SEN5XSensor::readPnValues(bool cumulative) // LOG_ERROR("SEN5X: Error sending read command"); // return false; // } -// delay(20); // From Sensirion Arduino library +// delay(20); // From Sensirion Datasheet // uint8_t dataBuffer[12]; // size_t receivedNumber = readBuffer(&dataBuffer[0], 12); @@ -575,7 +737,7 @@ uint8_t SEN5XSensor::getMeasurements() LOG_ERROR("SEN5X: Error sending command data ready flag"); return 2; } - delay(20); // From Sensirion Arduino library + delay(20); // From Sensirion Datasheet uint8_t dataReadyBuffer[3]; size_t charNumber = readBuffer(&dataReadyBuffer[0], 3); diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.h b/src/modules/Telemetry/Sensor/SEN5XSensor.h index f46657f2e..e1101ea2e 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.h +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h @@ -66,6 +66,9 @@ class SEN5XSensor : public TelemetrySensor #define SEN5X_READ_RAW_VALUES 0x03D2 #define SEN5X_READ_PM_VALUES 0x0413 + #define SEN5X_VOC_VALID_TIME 600 + #define SEN5X_VOC_VALID_DATE 1514764800 + enum SEN5Xmodel { SEN5X_UNKNOWN = 0, SEN50 = 0b001, SEN54 = 0b010, SEN55 = 0b100 }; SEN5Xmodel model = SEN5X_UNKNOWN; @@ -93,20 +96,22 @@ class SEN5XSensor : public TelemetrySensor protected: // Store status of the sensor in this file const char *sen5XStateFileName = "/prefs/sen5X.dat"; + meshtastic_SEN5XState sen5xstate = meshtastic_SEN5XState_init_zero; + bool loadState(); bool saveState(); // Cleaning State - // Last cleaning status uint32_t lastCleaning = 0; bool lastCleaningValid = false; - // 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(); + // VOC State + #define SEN5X_VOC_STATE_BUFFER_SIZE 8 + uint8_t vocState[SEN5X_VOC_STATE_BUFFER_SIZE]; + uint32_t vocTime; + bool vocValid = true; + bool vocStateFromSensor(); + bool vocStateToSensor(); virtual void setup() override; From c321312e10e7e096ac2f31da076066660c3bfda1 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 26 Aug 2025 19:00:43 +0200 Subject: [PATCH 36/38] Add one-shot mode config flag to SEN5X --- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 50 +++++++++++++------- src/modules/Telemetry/Sensor/SEN5XSensor.h | 6 +-- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index 77af272fe..f3096d82f 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -238,6 +238,11 @@ bool SEN5XSensor::idle() vocValid = false; } + if (!oneShotMode) { + LOG_INFO("SEN5X: Not stopping measurement, continuous mode!"); + return true; + } + if (!sendCommand(SEN5X_STOP_MEASUREMENT)) { LOG_ERROR("SEN5X: Error stoping measurement"); return false; @@ -336,6 +341,7 @@ bool SEN5XSensor::loadState() } else { lastCleaning = sen5xstate.last_cleaning_time; lastCleaningValid = sen5xstate.last_cleaning_valid; + oneShotMode = sen5xstate.one_shot_mode; // Unpack state vocState[7] = (uint8_t)(sen5xstate.voc_state >> 56); vocState[6] = (uint8_t)(sen5xstate.voc_state >> 48); @@ -370,6 +376,7 @@ bool SEN5XSensor::saveState() sen5xstate.last_cleaning_time = lastCleaning; sen5xstate.last_cleaning_valid = lastCleaningValid; + sen5xstate.one_shot_mode = oneShotMode; // Unpack state (12 bytes in two parts) sen5xstate.voc_state = ((uint64_t) vocState[7] << 56) | @@ -869,32 +876,41 @@ bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement) return true; } +void SEN5XSensor::setMode(bool setOneShot) { + oneShotMode = setOneShot; +} + AdminMessageHandleResult SEN5XSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) { AdminMessageHandleResult result; result = AdminMessageHandleResult::NOT_HANDLED; - // TODO - Add admin command to set temperature offset - // switch (request->which_payload_variant) { - // case meshtastic_AdminMessage_sensor_config_tag: - // if (!request->sensor_config.has_sen5x_config) { - // result = AdminMessageHandleResult::NOT_HANDLED; - // break; - // } - // // Check for temperature offset - // // if (request->sensor_config.sen5x_config.has_set_temperature) { - // // this->setTemperature(request->sensor_config.sen5x_config.set_temperature); - // // } + switch (request->which_payload_variant) { + case meshtastic_AdminMessage_sensor_config_tag: + if (!request->sensor_config.has_sen5x_config) { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } - // // result = AdminMessageHandleResult::HANDLED; - // result = AdminMessageHandleResult::NOT_HANDLED; - // break; + // TODO - Add admin command to set temperature offset + // Check for temperature offset + // if (request->sensor_config.sen5x_config.has_set_temperature) { + // this->setTemperature(request->sensor_config.sen5x_config.set_temperature); + // } - // default: - // result = AdminMessageHandleResult::NOT_HANDLED; - // } + // Check for one-shot/continuous mode request + if (request->sensor_config.sen5x_config.has_set_one_shot_mode) { + this->setMode(request->sensor_config.sen5x_config.set_one_shot_mode); + } + + result = AdminMessageHandleResult::HANDLED; + break; + + default: + result = AdminMessageHandleResult::NOT_HANDLED; + } return result; } diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.h b/src/modules/Telemetry/Sensor/SEN5XSensor.h index e1101ea2e..e89470a51 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.h +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h @@ -74,9 +74,9 @@ class SEN5XSensor : public TelemetrySensor 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; + // Flag to work on one-shot (read and sleep), or continuous mode + bool oneShotMode = true; + void setMode(bool setOneShot); bool sendCommand(uint16_t wichCommand); bool sendCommand(uint16_t wichCommand, uint8_t* buffer, uint8_t byteNumber=0); From d5839912483eaa5afb9209a45812d29c641c4d0a Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 26 Aug 2025 19:02:12 +0200 Subject: [PATCH 37/38] Add nan checks on sensor data from SEN5X --- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 130 +++++++++++-------- src/modules/Telemetry/Sensor/SEN5XSensor.h | 6 +- 2 files changed, 79 insertions(+), 57 deletions(-) diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index f3096d82f..2c31d3f1d 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -10,6 +10,7 @@ #include "SafeFile.h" #include #include +#include // FLT_MAX SEN5XSensor::SEN5XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SEN5X, "SEN5X") {} @@ -620,7 +621,7 @@ bool SEN5XSensor::readValues() return false; } - // First get the integers + // 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]); @@ -630,17 +631,15 @@ bool SEN5XSensor::readValues() 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 values based on Sensirion Arduino lib - sen5xmeasurement.pM1p0 = uint_pM1p0 / 10; - sen5xmeasurement.pM2p5 = uint_pM2p5 / 10; - sen5xmeasurement.pM4p0 = uint_pM4p0 / 10; - sen5xmeasurement.pM10p0 = uint_pM10p0 / 10; - sen5xmeasurement.humidity = int_humidity / 100.0f; - sen5xmeasurement.temperature = int_temperature / 200.0f; - sen5xmeasurement.vocIndex = int_vocIndex / 10.0f; - sen5xmeasurement.noxIndex = int_noxIndex / 10.0f; + sen5xmeasurement.pM1p0 = !isnan(uint_pM1p0) ? uint_pM1p0 / 10 : UINT16_MAX; + sen5xmeasurement.pM2p5 = !isnan(uint_pM2p5) ? uint_pM2p5 / 10 : UINT16_MAX; + sen5xmeasurement.pM4p0 = !isnan(uint_pM4p0) ? uint_pM4p0 / 10 : UINT16_MAX; + sen5xmeasurement.pM10p0 = !isnan(uint_pM10p0) ? uint_pM10p0 / 10 : UINT16_MAX; + sen5xmeasurement.humidity = !isnan(int_humidity) ? int_humidity / 100.0f : FLT_MAX; + sen5xmeasurement.temperature = !isnan(int_temperature) ? int_temperature / 200.0f : FLT_MAX; + sen5xmeasurement.vocIndex = !isnan(int_vocIndex) ? int_vocIndex / 10.0f : FLT_MAX; + sen5xmeasurement.noxIndex = !isnan(int_noxIndex) ? int_noxIndex / 10.0f : FLT_MAX; LOG_DEBUG("Got: pM1p0=%u, pM2p5=%u, pM4p0=%u, pM10p0=%u", sen5xmeasurement.pM1p0, sen5xmeasurement.pM2p5, @@ -666,7 +665,7 @@ bool SEN5XSensor::readPnValues(bool cumulative) return false; } - // First get the integers + // 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]); @@ -679,20 +678,13 @@ bool SEN5XSensor::readPnValues(bool cumulative) uint16_t uint_tSize = static_cast((dataBuffer[18] << 8) | dataBuffer[19]); // Convert values 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 - sen5xmeasurement.pN0p5 *= 100; - sen5xmeasurement.pN1p0 *= 100; - sen5xmeasurement.pN2p5 *= 100; - sen5xmeasurement.pN4p0 *= 100; - sen5xmeasurement.pN10p0 *= 100; - sen5xmeasurement.tSize *= 100; + // Multiply by 100 for converting from #/cm3 to #/0.1l for PN values + sen5xmeasurement.pN0p5 = !isnan(uint_pN0p5) ? uint_pN0p5 / 10 * 100: UINT32_MAX; + sen5xmeasurement.pN1p0 = !isnan(uint_pN1p0) ? uint_pN1p0 / 10 * 100: UINT32_MAX; + sen5xmeasurement.pN2p5 = !isnan(uint_pN2p5) ? uint_pN2p5 / 10 * 100: UINT32_MAX; + sen5xmeasurement.pN4p0 = !isnan(uint_pN4p0) ? uint_pN4p0 / 10 * 100: UINT32_MAX; + sen5xmeasurement.pN10p0 = !isnan(uint_pN10p0) ? uint_pN10p0 / 10 * 100: UINT32_MAX; + sen5xmeasurement.tSize = !isnan(uint_tSize) ? uint_tSize / 1000.0f : FLT_MAX; // Remove accumuluative values: // https://github.com/fablabbcn/smartcitizen-kit-2x/issues/85 @@ -766,7 +758,7 @@ uint8_t SEN5XSensor::getMeasurements() } if(!readPnValues(false)) { - LOG_ERROR("SEN5X: Error getting PM readings"); + LOG_ERROR("SEN5X: Error getting PN readings"); return 2; } @@ -828,39 +820,69 @@ bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement) 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 (sen5xmeasurement.pM1p0 != UINT16_MAX) { + measurement->variant.air_quality_metrics.has_pm10_standard = true; + measurement->variant.air_quality_metrics.pm10_standard = sen5xmeasurement.pM1p0; + } + if (sen5xmeasurement.pM2p5 != UINT16_MAX) { + measurement->variant.air_quality_metrics.has_pm25_standard = true; + measurement->variant.air_quality_metrics.pm25_standard = sen5xmeasurement.pM2p5; + } + if (sen5xmeasurement.pM4p0 != UINT16_MAX) { + measurement->variant.air_quality_metrics.has_pm40_standard = true; + measurement->variant.air_quality_metrics.pm40_standard = sen5xmeasurement.pM4p0; + } + if (sen5xmeasurement.pM10p0 != UINT16_MAX) { + measurement->variant.air_quality_metrics.has_pm100_standard = true; + measurement->variant.air_quality_metrics.pm100_standard = sen5xmeasurement.pM10p0; + } + if (sen5xmeasurement.pN0p5 != UINT32_MAX) { + measurement->variant.air_quality_metrics.has_particles_05um = true; + measurement->variant.air_quality_metrics.particles_05um = sen5xmeasurement.pN0p5; + } + if (sen5xmeasurement.pN1p0 != UINT32_MAX) { + measurement->variant.air_quality_metrics.has_particles_10um = true; + measurement->variant.air_quality_metrics.particles_10um = sen5xmeasurement.pN1p0; + } + if (sen5xmeasurement.pN2p5 != UINT32_MAX) { + measurement->variant.air_quality_metrics.has_particles_25um = true; + measurement->variant.air_quality_metrics.particles_25um = sen5xmeasurement.pN2p5; + } + if (sen5xmeasurement.pN4p0 != UINT32_MAX) { + measurement->variant.air_quality_metrics.has_particles_40um = true; + measurement->variant.air_quality_metrics.particles_40um = sen5xmeasurement.pN4p0; + } + if (sen5xmeasurement.pN10p0 != UINT32_MAX) { + measurement->variant.air_quality_metrics.has_particles_100um = true; + measurement->variant.air_quality_metrics.particles_100um = sen5xmeasurement.pN10p0; + } + if (sen5xmeasurement.tSize != FLT_MAX) { + measurement->variant.air_quality_metrics.has_particles_tps = true; + measurement->variant.air_quality_metrics.particles_tps = sen5xmeasurement.tSize; + } 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 (sen5xmeasurement.humidity!= FLT_MAX) { + measurement->variant.air_quality_metrics.has_pm_humidity = true; + measurement->variant.air_quality_metrics.pm_humidity = sen5xmeasurement.humidity; + } + if (sen5xmeasurement.temperature!= FLT_MAX) { + measurement->variant.air_quality_metrics.has_pm_temperature = true; + measurement->variant.air_quality_metrics.pm_temperature = sen5xmeasurement.temperature; + } + if (sen5xmeasurement.noxIndex!= FLT_MAX) { + 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; + if (sen5xmeasurement.noxIndex!= FLT_MAX) { + 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 diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.h b/src/modules/Telemetry/Sensor/SEN5XSensor.h index e89470a51..1f4694a35 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.h +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h @@ -78,15 +78,15 @@ class SEN5XSensor : public TelemetrySensor bool oneShotMode = true; void setMode(bool setOneShot); - bool sendCommand(uint16_t wichCommand); - bool sendCommand(uint16_t wichCommand, uint8_t* buffer, uint8_t byteNumber=0); + bool sendCommand(uint16_t command); + bool sendCommand(uint16_t command, 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 readRawValues(); bool readPnValues(bool cumulative); bool readValues(); From 10387652199b741a3db561f35333795014c1f188 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sat, 30 Aug 2025 13:00:22 +0200 Subject: [PATCH 38/38] Working implementation on VOCState * Adds initial timer for SEN55 to not sleep if VOCstate is not stable (1h) * Adds conditions for stability and sensor state --- src/modules/Telemetry/AirQualityTelemetry.cpp | 13 +- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 229 +++++++++++------- src/modules/Telemetry/Sensor/SEN5XSensor.h | 19 +- 3 files changed, 172 insertions(+), 89 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 33d6399c5..8ecd72563 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -79,13 +79,16 @@ int32_t AirQualityTelemetryModule::runOnce() } // Wake up the sensors that need it, before we need to take telemetry data - if ((lastSentToMesh == 0) || + // TODO - Do it for SENSOR ROLE too? + if (((lastSentToMesh == 0) || (sen5xSensor.hasSensor() && !Throttle::isWithinTimespanMs(lastSentToMesh - SEN5X_WARMUP_MS_1, Default::getConfiguredOrDefaultMsScaled( moduleConfig.telemetry.air_quality_interval, default_telemetry_broadcast_interval_secs, numOnlineNodes))) || (pmsa003iSensor.hasSensor() && !Throttle::isWithinTimespanMs(lastSentToMesh - PMSA003I_WARMUP_MS, Default::getConfiguredOrDefaultMsScaled( moduleConfig.telemetry.air_quality_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes)))) { + default_telemetry_broadcast_interval_secs, numOnlineNodes)))) && + airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && + airTime->isTxAllowedAirUtil()) { if (sen5xSensor.hasSensor() && !sen5xSensor.isActive()) return sen5xSensor.wakeUp(); @@ -100,10 +103,11 @@ int32_t AirQualityTelemetryModule::runOnce() if (sen5xSensor.hasSensor() && sen5xSensor.isActive()) { sen5xPendingForReady = sen5xSensor.pendingForReady(); LOG_DEBUG("SEN5X: Pending for ready %ums", sen5xPendingForReady); - if (sen5xPendingForReady) { + if (sen5xPendingForReady > 0) { return sen5xPendingForReady; } } + LOG_DEBUG("Checking if sending telemetry"); if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( @@ -122,6 +126,7 @@ int32_t AirQualityTelemetryModule::runOnce() } // Send the sensor to idle ONLY if there is enough time to wake it up before the next reading cycle + // TODO - include conditions here for module timing #ifdef PMSA003I_ENABLE_PIN if (pmsa003iSensor.hasSensor() && pmsa003iSensor.isActive()) { if (PMSA003I_WARMUP_MS < Default::getConfiguredOrDefaultMsScaled( @@ -322,8 +327,10 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag; m.time = getTime(); + // TODO - if one sensor fails here, we will stop taking measurements from everything // Can we do this in a smarter way, for instance checking the nodeTelemetrySensor map and making it dynamic? + if (getAirQualityTelemetry(&m)) { LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u", m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard, diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index 2c31d3f1d..6e1ed89c9 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -218,25 +218,36 @@ bool SEN5XSensor::I2Cdetect(TwoWire *_Wire, uint8_t address) bool SEN5XSensor::idle() { + // From the datasheet: + // By default, the VOC algorithm resets its state to initial + // values each time a measurement is started, + // even if the measurement was stopped only for a short + // time. So, the VOC index output value needs a long time + // until it is stable again. This can be avoided by + // restoring the previously memorized algorithm state before + // starting the measure mode - - // Get VOC state before going to idle mode - if (vocStateFromSensor()) { - // TODO Should this be saved with saveState()? - // It so, we can consider not saving it when rebooting as - // we would have likely saved it recently - - // Check if we have time, and store it - uint32_t now; // If time is RTCQualityNone, it will return zero - now = getValidTime(RTCQuality::RTCQualityDevice); - - if (now) { - vocTime = now; - vocValid = true; - // saveState(); - } - } else { + // If the stabilisation period is not passed for SEN55, don't go to idle + if (model == SEN55) { + // Get VOC state before going to idle mode vocValid = false; + if (vocStateFromSensor()) { + vocValid = vocStateValid(); + // Check if we have time, and store it + uint32_t now; // If time is RTCQualityNone, it will return zero + now = getValidTime(RTCQuality::RTCQualityDevice); + if (now) { + // Check if state is valid (non-zero) + vocTime = now; + } + } + + if (vocStateStable() && vocValid) { + saveState(); + } else { + LOG_INFO("SEN5X: Not stopping measurement, vocState is not stable yet!"); + return true; + } } if (!oneShotMode) { @@ -248,8 +259,8 @@ bool SEN5XSensor::idle() LOG_ERROR("SEN5X: Error stoping measurement"); return false; } - delay(200); // From Sensirion Datasheet + delay(200); // From Sensirion Datasheet LOG_INFO("SEN5X: Stop measurement mode"); state = SEN5X_IDLE; @@ -257,12 +268,40 @@ bool SEN5XSensor::idle() return true; } +bool SEN5XSensor::vocStateRecent(uint32_t now){ + if (now) { + uint32_t passed = now - vocTime; //in seconds + + // Check if state is recent, less than 10 minutes (600 seconds) + if (passed < SEN5X_VOC_VALID_TIME && (now > SEN5X_VOC_VALID_DATE)) { + return true; + } + } + return false; +} + +bool SEN5XSensor::vocStateValid() { + if (!vocState[0] && !vocState[1] && !vocState[2] && !vocState[3] && + !vocState[4] && !vocState[5] && !vocState[6] && !vocState[7]) { + LOG_DEBUG("SEN5X: VOC state is all 0, invalid"); + return false; + } else { + LOG_DEBUG("SEN5X: VOC state is valid"); + return true; + } +} + bool SEN5XSensor::vocStateToSensor() { if (model != SEN55){ return true; } + if (!vocStateValid()) { + LOG_INFO("SEN5X: VOC state is invalid, not sending"); + return true; + } + if (!sendCommand(SEN5X_STOP_MEASUREMENT)) { LOG_ERROR("SEN5X: Error stoping measurement"); return false; @@ -320,8 +359,7 @@ bool SEN5XSensor::vocStateFromSensor() vocState[7] = vocBuffer[10]; // Print the state (if debug is on) - LOG_DEBUG("SEN5X: VOC state retrieved from sensor"); - LOG_DEBUG("[%u, %u, %u, %u, %u, %u, %u, %u]", + LOG_DEBUG("SEN5X: VOC state retrieved from sensor: [%u, %u, %u, %u, %u, %u, %u, %u]", vocState[0],vocState[1], vocState[2], vocState[3], vocState[4],vocState[5], vocState[6], vocState[7]); @@ -337,24 +375,36 @@ bool SEN5XSensor::loadState() if (file) { LOG_INFO("%s state read from %s", sensorName, sen5XStateFileName); pb_istream_t stream = {&readcb, &file, meshtastic_SEN5XState_size}; + if (!pb_decode(&stream, &meshtastic_SEN5XState_msg, &sen5xstate)) { LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream)); } else { lastCleaning = sen5xstate.last_cleaning_time; lastCleaningValid = sen5xstate.last_cleaning_valid; oneShotMode = sen5xstate.one_shot_mode; - // Unpack state - vocState[7] = (uint8_t)(sen5xstate.voc_state >> 56); - vocState[6] = (uint8_t)(sen5xstate.voc_state >> 48); - vocState[5] = (uint8_t)(sen5xstate.voc_state >> 40); - vocState[4] = (uint8_t)(sen5xstate.voc_state >> 32); - vocState[3] = (uint8_t)(sen5xstate.voc_state >> 24); - vocState[2] = (uint8_t)(sen5xstate.voc_state >> 16); - vocState[1] = (uint8_t)(sen5xstate.voc_state >> 8); - vocState[0] = (uint8_t)sen5xstate.voc_state; - vocTime = sen5xstate.voc_time; - vocValid = sen5xstate.voc_valid; + if (model == SEN55) { + vocTime = sen5xstate.voc_state_time; + vocValid = sen5xstate.voc_state_valid; + // Unpack state + vocState[7] = (uint8_t)(sen5xstate.voc_state_array >> 56); + vocState[6] = (uint8_t)(sen5xstate.voc_state_array >> 48); + vocState[5] = (uint8_t)(sen5xstate.voc_state_array >> 40); + vocState[4] = (uint8_t)(sen5xstate.voc_state_array >> 32); + vocState[3] = (uint8_t)(sen5xstate.voc_state_array >> 24); + vocState[2] = (uint8_t)(sen5xstate.voc_state_array >> 16); + vocState[1] = (uint8_t)(sen5xstate.voc_state_array >> 8); + vocState[0] = (uint8_t) sen5xstate.voc_state_array; + } + + // LOG_DEBUG("Loaded lastCleaning %u", lastCleaning); + // LOG_DEBUG("Loaded lastCleaningValid %u", lastCleaningValid); + // LOG_DEBUG("Loaded oneShotMode %s", oneShotMode ? "true" : "false"); + // LOG_DEBUG("Loaded vocTime %u", vocTime); + // LOG_DEBUG("Loaded [%u, %u, %u, %u, %u, %u, %u, %u]", + // vocState[7], vocState[6], vocState[5], vocState[4], vocState[3], vocState[2], vocState[1], vocState[0]); + // LOG_DEBUG("Loaded %svalid VOC state", vocValid ? "" : "in"); + okay = true; } file.close(); @@ -370,7 +420,7 @@ bool SEN5XSensor::loadState() bool SEN5XSensor::saveState() { - // TODO - This should be called before a reboot + // TODO - This should be called before a reboot for VOC index storage // is there a way to get notified? #ifdef FSCom auto file = SafeFile(sen5XStateFileName); @@ -379,20 +429,23 @@ bool SEN5XSensor::saveState() sen5xstate.last_cleaning_valid = lastCleaningValid; sen5xstate.one_shot_mode = oneShotMode; - // Unpack state (12 bytes in two parts) - sen5xstate.voc_state = ((uint64_t) vocState[7] << 56) | - ((uint64_t) vocState[6] << 48) | - ((uint64_t) vocState[5] << 40) | - ((uint64_t) vocState[4] << 32) | - ((uint32_t) vocState[3] << 24) | - ((uint32_t) vocState[2] << 16) | - ((uint32_t) vocState[1] << 8) | - vocState[0]; + if (model == SEN55) { + sen5xstate.has_voc_state_time = true; + sen5xstate.has_voc_state_valid = true; + sen5xstate.has_voc_state_array = true; - LOG_INFO("sen5xstate.voc_state %i", sen5xstate.voc_state); - - sen5xstate.voc_time = vocTime; - sen5xstate.voc_valid = vocValid; + sen5xstate.voc_state_time = vocTime; + sen5xstate.voc_state_valid = vocValid; + // Unpack state (8 bytes) + sen5xstate.voc_state_array = (((uint64_t) vocState[7]) << 56) | + ((uint64_t) vocState[6] << 48) | + ((uint64_t) vocState[5] << 40) | + ((uint64_t) vocState[4] << 32) | + ((uint64_t) vocState[3] << 24) | + ((uint64_t) vocState[2] << 16) | + ((uint64_t) vocState[1] << 8) | + ((uint64_t) vocState[0]); + } bool okay = false; @@ -421,42 +474,26 @@ bool SEN5XSensor::isActive(){ } uint32_t SEN5XSensor::wakeUp(){ - // LOG_INFO("SEN5X: Attempting to wakeUp sensor"); + uint32_t now; + now = getValidTime(RTCQuality::RTCQualityDevice); + LOG_DEBUG("SEN5X: Waking up sensor"); - // From the datasheet - // By default, the VOC algorithm resets its state to initial - // values each time a measurement is started, - // even if the measurement was stopped only for a short - // time. So, the VOC index output value needs a long time - // until it is stable again. This can be avoided by - // restoring the previously memorized algorithm state before - // starting the measure mode - - // TODO - This needs to be tested - // In SC, the sensor is operated in contionuous mode if - // VOCs are present, increasing battery consumption - // A different approach should be possible as stated on the - // datasheet (see above) - // uint32_t now, passed; - // now = getValidTime(RTCQuality::RTCQualityDevice); - // passed = now - vocTime; //in seconds - // // Check if state is recent, less than 10 minutes (600 seconds) - // if ((passed < SEN5X_VOC_VALID_TIME) && (now > SEN5X_VOC_VALID_DATE) && vocValid) { - // if (!vocStateToSensor()){ - // LOG_ERROR("SEN5X: Sending VOC state to sensor failed"); - // } - // } else { - // LOG_DEBUG("SEN5X: No valid VOC state found. Ignoring"); - // } + // Check if state is recent, less than 10 minutes (600 seconds) + if (vocStateRecent(now) && vocStateValid()) { + if (!vocStateToSensor()){ + LOG_ERROR("SEN5X: Sending VOC state to sensor failed"); + } + } else { + LOG_DEBUG("SEN5X: No valid VOC state found. Ignoring"); + } if (!sendCommand(SEN5X_START_MEASUREMENT)) { LOG_ERROR("SEN5X: Error starting measurement"); - // TODO - what should this return?? + // TODO - what should this return?? Something actually on the default interval return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } delay(50); // From Sensirion Datasheet - // LOG_INFO("SEN5X: Setting measurement mode"); // TODO - This is currently "problematic" // If time is updated in between reads, there is no way to // keep track of how long it has passed @@ -467,6 +504,15 @@ uint32_t SEN5XSensor::wakeUp(){ return SEN5X_WARMUP_MS_1; } +bool SEN5XSensor::vocStateStable() +{ + uint32_t now; + now = getTime(); + uint32_t sinceFirstMeasureStarted = (now - firstMeasureStarted); + LOG_DEBUG("sinceFirstMeasureStarted: %us", sinceFirstMeasureStarted); + return sinceFirstMeasureStarted > SEN55_VOC_STATE_WARMUP_S; +} + bool SEN5XSensor::startCleaning() { // Note: we only should enter here if we have a valid RTC with at least @@ -532,7 +578,7 @@ int32_t SEN5XSensor::runOnce() return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } - // Check if firmware version allows The direct switch between Measurement and RHT/Gas-Only Measurement mode + // Check the firmware version if (!getVersion()) return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; if (firmwareVer < 2) { LOG_ERROR("SEN5X: error firmware is too old and will not work with this implementation"); @@ -551,8 +597,8 @@ int32_t SEN5XSensor::runOnce() uint32_t now; int32_t passed; now = getValidTime(RTCQuality::RTCQualityDevice); - // If time is not RTCQualityNone, it will return non-zero + // If time is not RTCQualityNone, it will return non-zero if (now) { if (lastCleaningValid) { @@ -566,36 +612,35 @@ int32_t SEN5XSensor::runOnce() LOG_INFO("SEN5X: Cleaning not needed (%ds passed). Last cleaning date (in epoch): %us", passed, lastCleaning); } } else { - // We assume the device has just been updated or it is new, so no need to trigger a cleaning. + // We assume the device has just been updated or it is new, + // so no need to trigger a cleaning. // Just save the timestamp to do a cleaning one week from now. - // TODO - could we trigger this after getting time? // Otherwise, we will never trigger cleaning in some cases lastCleaning = now; lastCleaningValid = true; LOG_INFO("SEN5X: No valid last cleaning date found, saving it now: %us", lastCleaning); saveState(); } + if (model == SEN55) { if (!vocValid) { LOG_INFO("SEN5X: No valid VOC's state found"); } else { - passed = now - vocTime; //in seconds - - // Check if state is recent, less than 10 minutes (600 seconds) - if (passed < SEN5X_VOC_VALID_TIME && (now > SEN5X_VOC_VALID_DATE)) { + // Check if state is recent + if (vocStateRecent(now)) { // If current date greater than 01/01/2018 (validity check) // Send it to the sensor LOG_INFO("SEN5X: VOC state is valid and recent"); vocStateToSensor(); } else { - LOG_INFO("SEN5X VOC state is to old or date is invalid"); + LOG_INFO("SEN5X: VOC state is too old or date is invalid"); + LOG_DEBUG("SEN5X: vocTime %u, Passed %u, and now %u", vocTime, passed, now); } } } - } else { // TODO - Should this actually ignore? We could end up never cleaning... - LOG_INFO("SEN5X: Not enough RTCQuality, ignoring saved state"); + LOG_INFO("SEN5X: Not enough RTCQuality, ignoring saved state. Trying again later"); } return initI2CSensor(); @@ -645,6 +690,17 @@ bool SEN5XSensor::readValues() sen5xmeasurement.pM1p0, sen5xmeasurement.pM2p5, sen5xmeasurement.pM4p0, sen5xmeasurement.pM10p0); + if (model == SEN54 || model == SEN55) { + LOG_DEBUG("Got: humidity=%.2f, temperature=%.2f, noxIndex=%.2f", + sen5xmeasurement.humidity, sen5xmeasurement.temperature, + sen5xmeasurement.noxIndex); + } + + if (model == SEN55) { + LOG_DEBUG("Got: vocIndex=%.2f", + sen5xmeasurement.vocIndex); + } + return true; } @@ -783,6 +839,10 @@ int32_t SEN5XSensor::pendingForReady(){ return SEN5X_WARMUP_MS_1 - sinceMeasureStarted; } + if (!firstMeasureStarted) { + firstMeasureStarted = now; + } + // Get PN values to check if we are above or below threshold readPnValues(true); @@ -883,6 +943,7 @@ bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement) } } + return true; } else if (response == 1) { // TODO return because data was not ready yet diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.h b/src/modules/Telemetry/Sensor/SEN5XSensor.h index 1f4694a35..d94bfe76c 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.h +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h @@ -20,6 +20,16 @@ #define SEN5X_I2C_CLOCK_SPEED 100000 #endif +/* +Time after which the sensor can go to sleep, as the warmup period has passed +and the VOCs sensor will is allowed to stop (although needs to recover the state +each time) +*/ +#ifndef SEN55_VOC_STATE_WARMUP_S +// TODO for Testing 5' - Sensirion recommends 1h. We can try to test a smaller value +#define SEN55_VOC_STATE_WARMUP_S 3600 +#endif + #define ONE_WEEK_IN_SECONDS 604800 struct _SEN5XMeasurements { @@ -77,6 +87,7 @@ class SEN5XSensor : public TelemetrySensor // Flag to work on one-shot (read and sleep), or continuous mode bool oneShotMode = true; void setMode(bool setOneShot); + bool vocStateValid(); bool sendCommand(uint16_t command); bool sendCommand(uint16_t command, uint8_t* buffer, uint8_t byteNumber=0); @@ -91,6 +102,7 @@ class SEN5XSensor : public TelemetrySensor bool readValues(); uint32_t measureStarted = 0; + uint32_t firstMeasureStarted = 0; _SEN5XMeasurements sen5xmeasurement; protected: @@ -108,10 +120,13 @@ class SEN5XSensor : public TelemetrySensor // VOC State #define SEN5X_VOC_STATE_BUFFER_SIZE 8 uint8_t vocState[SEN5X_VOC_STATE_BUFFER_SIZE]; - uint32_t vocTime; - bool vocValid = true; + uint32_t vocTime = 0; + bool vocValid = false; + bool vocStateFromSensor(); bool vocStateToSensor(); + bool vocStateStable(); + bool vocStateRecent(uint32_t now); virtual void setup() override;