From 7f042a011af7d4cbb9e66451fb46d20d239ab0c3 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 1 Jul 2025 17:23:52 +0200 Subject: [PATCH 001/108] Move PMSA003I to separate class and update AQ telemetry --- src/modules/Telemetry/AirQualityTelemetry.cpp | 303 ++++++++++++------ src/modules/Telemetry/AirQualityTelemetry.h | 37 ++- .../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, 362 insertions(+), 122 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 2472b95b144..8b7ab1b24d9 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) + if (!moduleConfig.telemetry.air_quality_enabled && !AIR_QUALITY_TELEMETRY_MODULE_ENABLE) { return disable(); + } - switch (state) { + // Wake up the sensors that need it #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; + if (pmsa003iSensor.hasSensor() && pmsa003iSensor.state == pmsa003iSensor::State::IDLE) + return pmsa003iSensor.wakeUp(); #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); - } + + 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 - // put sensor back to sleep - digitalWrite(PMSA003I_ENABLE_PIN, LOW); - state = State::IDLE; + pmsa003iSensor.sleep(); #endif /* PMSA003I_ENABLE_PIN */ - return sendToPhoneIntervalMs; - default: - return disable(); + } + 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.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; - - 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; + m->variant.air_quality_metrics = meshtastic_AirQualityMetrics_init_zero; + + 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; + } + + 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 0142ee68641..8314c54bc28 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; + setIntervalFromNow(10 * 1000); + } + virtual bool wantUIFrame() override; +#if !HAS_SCREEN + void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); #else - state = State::ACTIVE; + 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 d1b10fa8273..2d6a8a0cbb9 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 d70c063fc66..ffbb229f0fe 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 00000000000..dacdf5ff465 --- /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 00000000000..01b04368ed1 --- /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 002/108] 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 3528f57f57b..2ff5a345ab0 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 003/108] 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 8b7ab1b24d9..75058f849e0 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 004/108] 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 75058f849e0..7689802ea2b 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 dacdf5ff465..8567d7e7014 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 01b04368ed1..db7c9aaa92d 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 005/108] 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 8567d7e7014..67f00574ece 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 006/108] 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 67f00574ece..2b165cd6da3 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 db7c9aaa92d..7e460ce3303 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 007/108] 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 652d50d5122..215efc1d0b0 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 6988091ad3b..28b073a1745 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 1868d98c7c4..132dab9e27e 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 008/108] 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 7689802ea2b..1c8e95a828f 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 009/108] PMSA003I 1st round test --- src/modules/Telemetry/AirQualityTelemetry.cpp | 8 +- .../Telemetry/Sensor/PMSA003ISensor.cpp | 192 ++++++++++++------ src/modules/Telemetry/Sensor/PMSA003ISensor.h | 59 +++--- 3 files changed, 163 insertions(+), 96 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 1c8e95a828f..9bc41bfa576 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 2b165cd6da3..39f8269a2c7 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 + +PMSA003ISensor::PMSA003ISensor() + : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I") +{ +} + +void PMSA003ISensor::setup() +{ +#ifdef PMSA003I_ENABLE_PIN + pinMode(PMSA003I_ENABLE_PIN, OUTPUT); +#endif +} + +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 +} 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(); - } + 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(); } -void PMSA003ISensor::setup() +bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) { -#ifdef PMSA003I_ENABLE_PIN - pinMode(PMSA003I_ENABLE_PIN, OUTPUT); -#endif /* PMSA003I_ENABLE_PIN */ -} + if(!isActive()){ + LOG_WARN("PMSA003I is not active"); + return false; + } -#ifdef PMSA003I_ENABLE_PIN -void PMSA003ISensor::sleep() { - digitalWrite(PMSA003I_ENABLE_PIN, LOW); - state = State::IDLE; -} +#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 -uint32_t PMSA003ISensor::wakeUp() { - digitalWrite(PMSA003I_ENABLE_PIN, HIGH); - state = State::ACTIVE; - return PMSA003I_WARMUP_MS; -} -#endif /* PMSA003I_ENABLE_PIN */ + 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; + } -bool PMSA003ISensor::isActive() { - return state == State::ACTIVE; -} + restoreClock(currentClock); -bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) -{ - if (!pmsa003i.read(&pmsa003iData)) { - LOG_WARN("Skip send measurements. Could not read AQI"); + 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.pm10_environmental = read16(buffer, 10); + measurement->variant.air_quality_metrics.has_pm25_environmental = true; - measurement->variant.air_quality_metrics.pm25_environmental = pmsa003iData.pm25_env; + 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 = pmsa003iData.pm100_env; + 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 7e460ce3303..d6f12dfbb8a 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 010/108] 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 132dab9e27e..deda7f1076d 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 011/108] 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 9bc41bfa576..431473e05b3 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 d6f12dfbb8a..ee3258ab1ce 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 012/108] 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 8bf56cf5bfc..b5f08dd729d 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 431473e05b3..ff5bd22efbb 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 417f5a8ddbd3ea3eb40bbda8aade2c7e1709c3ab Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 2 Jul 2025 11:57:34 +0200 Subject: [PATCH 013/108] Add functional SCD4X --- src/modules/Telemetry/AirQualityTelemetry.cpp | 48 +++++++++++- src/modules/Telemetry/Sensor/SCD4XSensor.cpp | 75 +++++++++++++++++++ src/modules/Telemetry/Sensor/SCD4XSensor.h | 23 ++++++ 3 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 src/modules/Telemetry/Sensor/SCD4XSensor.cpp create mode 100644 src/modules/Telemetry/Sensor/SCD4XSensor.h diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index df193c8a528..6e958010964 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -22,6 +22,14 @@ // Sensors PMSA003ISensor pmsa003iSensor; + +#if __has_include() +#include "Sensor/SCD4XSensor.h" +SCD4XSensor scd4xSensor; +#else +NullSensor scd4xSensor; +#endif + #include "graphics/ScreenFonts.h" int32_t AirQualityTelemetryModule::runOnce() @@ -61,6 +69,9 @@ int32_t AirQualityTelemetryModule::runOnce() if (pmsa003iSensor.hasSensor()) result = pmsa003iSensor.runOnce(); + + if (scd4xSensor.hasSensor()) + result = scd4xSensor.runOnce(); } // it's possible to have this module enabled, only for displaying values on the screen. @@ -143,7 +154,7 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta // 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; + m.has_pm100_environmental || m.has_co2; if (!hasAny) { display->drawString(x, currentY, "No Telemetry"); @@ -170,6 +181,9 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta entries.push_back("PM2.5: " + String(m.pm25_standard) + "ug/m3"); if (m.has_pm100_standard) entries.push_back("PM10.0: " + String(m.pm100_standard) + "ug/m3"); + if (m.has_co2) + entries.push_back("CO2: " + String(m.co2, 0) + "ppm"); + // === Show first available metric on top-right of first line === if (!entries.empty()) { @@ -210,6 +224,10 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack 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); + + LOG_INFO(" | CO2=%i, CO2_T=%f, CO2_H=%f", + t->variant.air_quality_metrics.co2, t->variant.air_quality_metrics.co2_temperature, + t->variant.air_quality_metrics.co2_humidity); #endif // release previous packet before occupying a new spot if (lastMeasurementPacket != nullptr) @@ -236,6 +254,11 @@ bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) hasSensor = true; } + if (scd4xSensor.hasSensor()) { + valid = valid && scd4xSensor.getMetrics(m); + hasSensor = true; + } + return valid && hasSensor; } @@ -273,11 +296,25 @@ 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, \ + + bool hasAnyPM = m.variant.air_quality_metrics.has_pm10_standard || m.variant.air_quality_metrics.has_pm25_standard || m.variant.air_quality_metrics.has_pm100_standard || m.variant.air_quality_metrics.has_pm10_environmental || m.variant.air_quality_metrics.has_pm25_environmental || + m.variant.air_quality_metrics.has_pm100_environmental; + + if (hasAnyPM) { + 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); + m.variant.air_quality_metrics.pm25_environmental, m.variant.air_quality_metrics.pm100_environmental); + } + + bool hasAnyCO2 = m.variant.air_quality_metrics.has_co2 || m.variant.air_quality_metrics.has_co2_temperature || m.variant.air_quality_metrics.has_co2_humidity; + + if (hasAnyCO2) { + LOG_INFO("Send: co2=%i, co2_t=%f, co2_rh=%f", + m.variant.air_quality_metrics.co2, m.variant.air_quality_metrics.co2_temperature, + m.variant.air_quality_metrics.co2_humidity); + } meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; @@ -333,6 +370,11 @@ AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule( return result; } + if (scd4xSensor.hasSensor()) { + result = scd4xSensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } #endif return result; diff --git a/src/modules/Telemetry/Sensor/SCD4XSensor.cpp b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp new file mode 100644 index 00000000000..c3ccb902e84 --- /dev/null +++ b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp @@ -0,0 +1,75 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "SCD4XSensor.h" +#include "TelemetrySensor.h" +#include + +#define SCD4X_NO_ERROR 0 + +SCD4XSensor::SCD4XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SCD4X, "SCD4X") {} + +int32_t SCD4XSensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + + uint16_t error; + + scd4x.begin(*nodeTelemetrySensorsMap[sensorType].second, + nodeTelemetrySensorsMap[sensorType].first); + + delay(30); + // Ensure sensor is in clean state + error = scd4x.wakeUp(); + if (error != SCD4X_NO_ERROR) { + LOG_INFO("Error trying to execute wakeUp()"); + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + + // Stop periodic measurement + error = scd4x.stopPeriodicMeasurement(); + if (error != SCD4X_NO_ERROR) { + LOG_INFO("Error trying to stopPeriodicMeasurement()"); + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + + // TODO - Decide if using Periodic mesaurement or singleshot + // status = scd4x.startLowPowerPeriodicMeasurement(); + + if (!scd4x.startLowPowerPeriodicMeasurement()) { + status = 1; + } else { + status = 0; + } + return initI2CSensor(); +} + +void SCD4XSensor::setup() {} + +bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) +{ + uint16_t co2, error; + float temperature; + float humidity; + + error = scd4x.readMeasurement(co2, temperature, humidity); + if (error != SCD4X_NO_ERROR || co2 == 0) { + LOG_DEBUG("Skipping invalid SCD4X measurement."); + return false; + } else { + measurement->variant.air_quality_metrics.has_co2_temperature = true; + measurement->variant.air_quality_metrics.has_co2_humidity = true; + measurement->variant.air_quality_metrics.has_co2 = true; + measurement->variant.air_quality_metrics.co2_temperature = temperature; + measurement->variant.air_quality_metrics.co2_humidity = humidity; + measurement->variant.air_quality_metrics.co2 = co2; + return true; + } +} + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SCD4XSensor.h b/src/modules/Telemetry/Sensor/SCD4XSensor.h new file mode 100644 index 00000000000..981723edf7d --- /dev/null +++ b/src/modules/Telemetry/Sensor/SCD4XSensor.h @@ -0,0 +1,23 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +class SCD4XSensor : public TelemetrySensor +{ + private: + SensirionI2cScd4x scd4x; + + protected: + virtual void setup() override; + + public: + SCD4XSensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; +}; + +#endif \ No newline at end of file From 00349c3603c4f3cb5874c1fb1d59eb430a401c71 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Fri, 18 Jul 2025 01:18:23 +0200 Subject: [PATCH 014/108] Fix screen frame for CO2 --- 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 6e958010964..7747fda198a 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -182,7 +182,7 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta if (m.has_pm100_standard) entries.push_back("PM10.0: " + String(m.pm100_standard) + "ug/m3"); if (m.has_co2) - entries.push_back("CO2: " + String(m.co2, 0) + "ppm"); + entries.push_back("CO2: " + String(m.co2) + "ppm"); // === Show first available metric on top-right of first line === From 8a4f5c15e706aeb194881396d69a4dea6122e8c5 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 29 Jul 2025 14:34:09 +0200 Subject: [PATCH 015/108] Add admin commands to SCD4X class --- src/modules/Telemetry/Sensor/SCD4XSensor.cpp | 476 ++++++++++++++++++- src/modules/Telemetry/Sensor/SCD4XSensor.h | 20 + 2 files changed, 490 insertions(+), 6 deletions(-) diff --git a/src/modules/Telemetry/Sensor/SCD4XSensor.cpp b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp index c3ccb902e84..c25e6cb1ed6 100644 --- a/src/modules/Telemetry/Sensor/SCD4XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp @@ -11,6 +11,16 @@ SCD4XSensor::SCD4XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SCD4X, "SCD4X") {} +bool SCD4XSensor::restoreClock(uint32_t currentClock){ +#ifdef SCD4X_I2C_CLOCK_SPEED + if (currentClock != SCD4X_I2C_CLOCK_SPEED){ + // LOG_DEBUG("Restoring I2C clock to %uHz", currentClock); + return bus->setClock(currentClock); + } + return true; +#endif +} + int32_t SCD4XSensor::runOnce() { LOG_INFO("Init sensor: %s", sensorName); @@ -20,32 +30,56 @@ int32_t SCD4XSensor::runOnce() uint16_t error; + bus = nodeTelemetrySensorsMap[sensorType].second; + address = (uint8_t)nodeTelemetrySensorsMap[sensorType].first; + +#ifdef SCD4X_I2C_CLOCK_SPEED + uint32_t currentClock; + currentClock = bus->getClock(); + if (currentClock != SCD4X_I2C_CLOCK_SPEED){ + // LOG_DEBUG("Changing I2C clock to %u", SCD4X_I2C_CLOCK_SPEED); + bus->setClock(SCD4X_I2C_CLOCK_SPEED); + } +#endif + + // FIXME - This should be based on bus and address from above scd4x.begin(*nodeTelemetrySensorsMap[sensorType].second, - nodeTelemetrySensorsMap[sensorType].first); + (uint8_t)nodeTelemetrySensorsMap[sensorType].first); delay(30); + // Ensure sensor is in clean state error = scd4x.wakeUp(); if (error != SCD4X_NO_ERROR) { - LOG_INFO("Error trying to execute wakeUp()"); + LOG_ERROR("SCD4X: Error trying to execute wakeUp()"); return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } // Stop periodic measurement error = scd4x.stopPeriodicMeasurement(); if (error != SCD4X_NO_ERROR) { - LOG_INFO("Error trying to stopPeriodicMeasurement()"); + LOG_ERROR("SCD4X: Error trying to stopPeriodicMeasurement()"); return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } + if (!getASC(ascActive)){ + LOG_ERROR("SCD4X: Unable to check if ASC is enabled"); + return false; + } - // TODO - Decide if using Periodic mesaurement or singleshot - // status = scd4x.startLowPowerPeriodicMeasurement(); + if (!ascActive){ + LOG_INFO("SCD4X: ASC is not active"); + } else { + LOG_INFO("SCD4X: ASC is active"); + } if (!scd4x.startLowPowerPeriodicMeasurement()) { status = 1; } else { status = 0; } + + restoreClock(currentClock); + return initI2CSensor(); } @@ -57,9 +91,22 @@ bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) float temperature; float humidity; +#ifdef SCD4X_I2C_CLOCK_SPEED + uint32_t currentClock; + currentClock = bus->getClock(); + if (currentClock != SCD4X_I2C_CLOCK_SPEED){ + // LOG_DEBUG("Changing I2C clock to %u", SCD4X_I2C_CLOCK_SPEED); + bus->setClock(SCD4X_I2C_CLOCK_SPEED); + } +#endif + error = scd4x.readMeasurement(co2, temperature, humidity); + + restoreClock(currentClock); + LOG_DEBUG("SCD4X: Error while getting measurements: %u", error); + LOG_DEBUG("SCD4X readings: %u, %.2f, %.2f", co2, temperature, humidity); if (error != SCD4X_NO_ERROR || co2 == 0) { - LOG_DEBUG("Skipping invalid SCD4X measurement."); + LOG_ERROR("SCD4X: Skipping invalid measurement."); return false; } else { measurement->variant.air_quality_metrics.has_co2_temperature = true; @@ -72,4 +119,421 @@ bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) } } +/** +* @brief Perform a forced recalibration (FRC) of the CO₂ concentration. +* +* From Sensirion SCD4X I2C Library +* +* 1. Operate the SCD4x in the operation mode later used for normal sensor +* operation (e.g. periodic measurement) for at least 3 minutes in an +* environment with a homogenous and constant CO2 concentration. The sensor +* must be operated at the voltage desired for the application when +* performing the FRC sequence. 2. Issue the stop_periodic_measurement +* command. 3. Issue the perform_forced_recalibration command. +*/ +bool SCD4XSensor::performFRC(uint32_t targetCO2) { + uint16_t error; + uint16_t frcCorr; + + LOG_INFO("SCD4X: Issuing FRC. Ensure device has been working at least 3 minutes in stable target environment"); + + error = scd4x.stopPeriodicMeasurement(); + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X."); + return false; + } + + error = scd4x.performForcedRecalibration((uint16_t)targetCO2, frcCorr); + + if (error != SCD4X_NO_ERROR){ + LOG_ERROR("SCD4X: Unable to perform forced recalibration."); + return false; + } + + if (frcCorr == 0xFFFF) { + LOG_ERROR("SCD4X: Error while performing forced recalibration."); + return false; + } + + return true; +} + +/** +* @brief Check the current mode (ASC or FRC) + +* From Sensirion SCD4X I2C Library +*/ +bool SCD4XSensor::getASC(uint16_t &ascEnabled) { + uint16_t error; + LOG_INFO("SCD4X: Getting ASC"); + + error = scd4x.stopPeriodicMeasurement(); + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X."); + return false; + } + + error = scd4x.getAutomaticSelfCalibrationEnabled(ascEnabled); + + if (error != SCD4X_NO_ERROR){ + LOG_ERROR("SCD4X: Unable to send command."); + return false; + } + return true; +} + +/** +* @brief Enable or disable automatic self calibration (ASC). +* +* From Sensirion SCD4X I2C Library +* +* Sets the current state (enabled / disabled) of the ASC. By default, ASC +* is enabled. +*/ +bool SCD4XSensor::setASC(bool ascEnabled){ + uint16_t error; + + if (ascEnabled){ + LOG_INFO("SCD4X: Enabling ASC"); + } else { + LOG_INFO("SCD4X: Disabling ASC"); + } + + error = scd4x.stopPeriodicMeasurement(); + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X."); + return false; + } + + error = scd4x.setAutomaticSelfCalibrationEnabled((uint16_t)ascEnabled); + + if (error != SCD4X_NO_ERROR){ + LOG_ERROR("SCD4X: Unable to send command."); + return false; + } + + error = scd4x.persistSettings(); + if (error != SCD4X_NO_ERROR){ + LOG_ERROR("SCD4X: Unable to make settings persistent."); + return false; + } + + if (!getASC(ascActive)){ + LOG_ERROR("SCD4X: Unable to check if ASC is enabled"); + return false; + } + + if (ascActive){ + LOG_INFO("SCD4X: ASC is enabled"); + } else { + LOG_INFO("SCD4X: ASC is disabled"); + } + + return true; +} + +/** +* @brief Set the value of ASC baseline target in ppm. +* +* From Sensirion SCD4X I2C Library. +* +* Sets the value of the ASC baseline target, i.e. the CO₂ concentration in +* ppm which the ASC algorithm will assume as lower-bound background to +* which the SCD4x is exposed to regularly within one ASC period of +* operation. To save the setting to the EEPROM, the persist_settings +* command must be issued subsequently. The factory default value is 400 +* ppm. +*/ +bool SCD4XSensor::setASCBaseline(uint32_t targetCO2){ + uint16_t error; + LOG_INFO("SCD4X: Setting ASC baseline"); + + getASC(ascActive); + if (!ascActive){ + LOG_ERROR("SCD4X: ASC is not active"); + return false; + } + + error = scd4x.stopPeriodicMeasurement(); + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X."); + return false; + } + + error = scd4x.setAutomaticSelfCalibrationTarget((uint16_t)targetCO2); + + if (error != SCD4X_NO_ERROR){ + LOG_ERROR("SCD4X: Unable to send command."); + return false; + } + + error = scd4x.persistSettings(); + if (error != SCD4X_NO_ERROR){ + LOG_ERROR("SCD4X: Unable to make settings persistent."); + return false; + } + + return true; +} + + +/** +* @brief Set the temperature compensation reference. +* +* From Sensirion SCD4X I2C Library. +* +* Setting the temperature offset of the SCD4x inside the customer device +* allows the user to optimize the RH and T output signal. By default, the temperature offset is set to 4 °C. To save +* the setting to the EEPROM, the persist_settings command may be issued. +* Equation (1) details how the characteristic temperature offset can be +* calculated using the current temperature output of the sensor (TSCD4x), a +* reference temperature value (TReference), and the previous temperature +* offset (Toffset_pervious) obtained using the get_temperature_offset_raw +* command: +* +* Toffset_actual = TSCD4x - TReference + Toffset_pervious. +* +* Recommended temperature offset values are between 0 °C and 20 °C. The +* temperature offset does not impact the accuracy of the CO2 output. +*/ +bool SCD4XSensor::setTemperature(float tempReference){ + uint16_t error; + float prevTempOffset; + float tempOffset; + + uint16_t co2; + float temperature; + float humidity; + LOG_INFO("SCD4X: Setting reference temperature at: %.2f". temperature); + + error = scd4x.readMeasurement(co2, temperature, humidity); + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("SCD4X: Unable read current temperature."); + return false; + } + + LOG_INFO("SCD4X: Current sensor temperature: %.2f", temperature); + + error = scd4x.stopPeriodicMeasurement(); + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X."); + return false; + } + + error = scd4x.getTemperatureOffset(prevTempOffset); + + if (error != SCD4X_NO_ERROR){ + LOG_ERROR("SCD4X: Unable to get temperature offset."); + return false; + } + LOG_INFO("SCD4X: Sensor temperature offset: %.2f", prevTempOffset); + + tempOffset = temperature - tempReference + prevTempOffset; + + LOG_INFO("SCD4X: Setting temperature offset: %.2f", tempOffset); + error = scd4x.setTemperatureOffset(tempOffset); + if (error != SCD4X_NO_ERROR){ + LOG_ERROR("SCD4X: Unable to set temperature offset."); + return false; + } + + error = scd4x.persistSettings(); + if (error != SCD4X_NO_ERROR){ + LOG_ERROR("SCD4X: Unable to make settings persistent."); + return false; + } + + return true; +} + +/** +* @brief Get the sensor altitude. +* +* From Sensirion SCD4X I2C Library. +* +* Altitude in meters above sea level can be set after device installation. +* Valid value between 0 and 3000m. This overrides pressure offset. +*/ +bool SCD4XSensor::getAltitude(uint16_t &altitude){ + uint16_t error; + LOG_INFO("SCD4X: Requesting sensor altitude"); + + error = scd4x.stopPeriodicMeasurement(); + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X."); + return false; + } + + error = scd4x.getSensorAltitude(altitude); + + if (error != SCD4X_NO_ERROR){ + LOG_ERROR("SCD4X: Unable to get altitude."); + return false; + } + LOG_INFO("SCD4X: Sensor altitude: %u", altitude); + + return true; +} + +/** +* @brief Set the sensor altitude. +* +* From Sensirion SCD4X I2C Library. +* +* Altitude in meters above sea level can be set after device installation. +* Valid value between 0 and 3000m. This overrides pressure offset. +*/ +bool SCD4XSensor::setAltitude(uint32_t altitude){ + uint16_t error; + + error = scd4x.stopPeriodicMeasurement(); + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X."); + return false; + } + + error = scd4x.setSensorAltitude(altitude); + + if (error != SCD4X_NO_ERROR){ + LOG_ERROR("SCD4X: Unable to set altitude."); + return false; + } + + error = scd4x.persistSettings(); + if (error != SCD4X_NO_ERROR){ + LOG_ERROR("SCD4X: Unable to make settings persistent."); + return false; + } + + return true; +} + +/** +* @brief Set the ambient pressure around the sensor. +* +* From Sensirion SCD4X I2C Library. +* +* The set_ambient_pressure command can be sent during periodic measurements +* to enable continuous pressure compensation. Note that setting an ambient +* pressure overrides any pressure compensation based on a previously set +* sensor altitude. Use of this command is highly recommended for +* applications experiencing significant ambient pressure changes to ensure +* sensor accuracy. Valid input values are between 70000 - 120000 Pa. The +* default value is 101300 Pa. +*/ +bool SCD4XSensor::setAmbientPressure(uint32_t ambientPressure) { + uint16_t error; + + error = scd4x.setAmbientPressure(ambientPressure); + + if (error != SCD4X_NO_ERROR){ + LOG_ERROR("SCD4X: Unable to set altitude."); + return false; + } + + // Sensirion doesn't indicate if this is necessary. We send it anyway + error = scd4x.persistSettings(); + if (error != SCD4X_NO_ERROR){ + LOG_ERROR("SCD4X: Unable to make settings persistent."); + return false; + } + + return true; +} + +/** +* @brief Perform factory reset to erase the settings stored in the EEPROM. +* +* From Sensirion SCD4X I2C Library. +* +* The perform_factory_reset command resets all configuration settings +* stored in the EEPROM and erases the FRC and ASC algorithm history. +*/ + +bool SCD4XSensor::factoryReset() { + uint16_t error; + + error = scd4x.stopPeriodicMeasurement(); + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X."); + return false; + } + + error = scd4x.performFactoryReset(); + + if (error != SCD4X_NO_ERROR){ + LOG_ERROR("SCD4X: Unable to do factory reset."); + return false; + } + + return true; +} + + + +AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) +{ + AdminMessageHandleResult result; + + switch (request->which_payload_variant) { + case meshtastic_AdminMessage_sensor_config_tag: + // Check for ASC-FRC request first + if (!request->sensor_config.has_scdxx_config) { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + + if (request->sensor_config.scdxx_config.has_factory_reset) { + LOG_DEBUG("SCD4X: Requested factory reset"); + this->factoryReset(); + } else { + + if (request->sensor_config.scdxx_config.has_set_asc) { + this->setASC(request->sensor_config.scdxx_config.set_asc); + if (request->sensor_config.scdxx_config.set_asc == false) { + LOG_DEBUG("SCD4X: Request for FRC"); + if (request->sensor_config.scdxx_config.has_target_co2_conc) { + this->performFRC(request->sensor_config.scdxx_config.target_co2_conc); + } else { + // FRC requested but no target CO2 provided + LOG_ERROR("SCD4X: target CO2 not provided"); + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + } else { + LOG_DEBUG("SCD4X: Request for ASC"); + if (request->sensor_config.scdxx_config.has_target_co2_conc) { + LOG_DEBUG("SCD4X: Request has target CO2"); + this->setASCBaseline(request->sensor_config.scdxx_config.target_co2_conc); + } else { + LOG_DEBUG("SCD4X: Request doesn't have target CO2"); + } + } + } + + // Check for temperature offset + if (request->sensor_config.scdxx_config.has_temperature) { + this->setTemperature(request->sensor_config.scdxx_config.temperature); + } + + // Check for altitude or pressure offset + if (request->sensor_config.scdxx_config.has_altitude) { + this->setAltitude(request->sensor_config.scdxx_config.altitude); + } else if (request->sensor_config.scdxx_config.has_ambient_pressure){ + this->setAmbientPressure(request->sensor_config.scdxx_config.ambient_pressure); + } + + } + + result = AdminMessageHandleResult::HANDLED; + break; + + default: + result = AdminMessageHandleResult::NOT_HANDLED; + } + + return result; +} + #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SCD4XSensor.h b/src/modules/Telemetry/Sensor/SCD4XSensor.h index 981723edf7d..113616897d2 100644 --- a/src/modules/Telemetry/Sensor/SCD4XSensor.h +++ b/src/modules/Telemetry/Sensor/SCD4XSensor.h @@ -6,10 +6,28 @@ #include "TelemetrySensor.h" #include +#define SCD4X_I2C_CLOCK_SPEED 100000 + class SCD4XSensor : public TelemetrySensor { private: SensirionI2cScd4x scd4x; + TwoWire* bus; + uint8_t address; + + bool performFRC(uint32_t targetCO2); + bool setASCBaseline(uint32_t targetCO2); + bool getASC(uint16_t &ascEnabled); + bool setASC(bool ascEnabled); + bool setTemperature(float tempReference); + bool getAltitude(uint16_t &altitude); + bool setAltitude(uint32_t altitude); + bool setAmbientPressure(uint32_t ambientPressure); + bool restoreClock(uint32_t currentClock); + bool factoryReset(); + + // Parameters + uint16_t ascActive; protected: virtual void setup() override; @@ -18,6 +36,8 @@ class SCD4XSensor : public TelemetrySensor SCD4XSensor(); virtual int32_t runOnce() override; virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; }; #endif \ No newline at end of file From 2609f455505afdd60880e8e99c4012b464ab8650 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Mon, 4 Aug 2025 14:55:56 +0200 Subject: [PATCH 016/108] Add further admin commands and fixes. --- src/modules/Telemetry/Sensor/SCD4XSensor.cpp | 390 ++++++++++++++----- src/modules/Telemetry/Sensor/SCD4XSensor.h | 14 +- 2 files changed, 299 insertions(+), 105 deletions(-) diff --git a/src/modules/Telemetry/Sensor/SCD4XSensor.cpp b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp index c25e6cb1ed6..132a1233f6c 100644 --- a/src/modules/Telemetry/Sensor/SCD4XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp @@ -11,15 +11,19 @@ SCD4XSensor::SCD4XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SCD4X, "SCD4X") {} -bool SCD4XSensor::restoreClock(uint32_t currentClock){ #ifdef SCD4X_I2C_CLOCK_SPEED - if (currentClock != SCD4X_I2C_CLOCK_SPEED){ - // LOG_DEBUG("Restoring I2C clock to %uHz", currentClock); - return bus->setClock(currentClock); +uint32_t SCD4XSensor::setI2CClock(uint32_t desiredClock){ + uint32_t currentClock; + currentClock = bus->getClock(); + LOG_DEBUG("Current I2C clock: %uHz", currentClock); + if (currentClock != desiredClock){ + LOG_DEBUG("Setting I2C clock to: %uHz", desiredClock); + bus->setClock(desiredClock); + return currentClock; } - return true; -#endif + return 0; } +#endif int32_t SCD4XSensor::runOnce() { @@ -28,58 +32,60 @@ int32_t SCD4XSensor::runOnce() return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } - uint16_t error; - bus = nodeTelemetrySensorsMap[sensorType].second; address = (uint8_t)nodeTelemetrySensorsMap[sensorType].first; #ifdef SCD4X_I2C_CLOCK_SPEED uint32_t currentClock; - currentClock = bus->getClock(); - if (currentClock != SCD4X_I2C_CLOCK_SPEED){ - // LOG_DEBUG("Changing I2C clock to %u", SCD4X_I2C_CLOCK_SPEED); - bus->setClock(SCD4X_I2C_CLOCK_SPEED); - } + currentClock = setI2CClock(SCD4X_I2C_CLOCK_SPEED); #endif // FIXME - This should be based on bus and address from above scd4x.begin(*nodeTelemetrySensorsMap[sensorType].second, - (uint8_t)nodeTelemetrySensorsMap[sensorType].first); + address); + // SCD4X library delay(30); - // Ensure sensor is in clean state - error = scd4x.wakeUp(); - if (error != SCD4X_NO_ERROR) { - LOG_ERROR("SCD4X: Error trying to execute wakeUp()"); + // Stop periodic measurement + if (!stopMeasurement()) { return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } - // Stop periodic measurement - error = scd4x.stopPeriodicMeasurement(); - if (error != SCD4X_NO_ERROR) { - LOG_ERROR("SCD4X: Error trying to stopPeriodicMeasurement()"); - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + // Get sensor variant + scd4x.getSensorVariant(sensorVariant); + + if (sensorVariant == SCD4X_SENSOR_VARIANT_SCD41){ + LOG_INFO("SCD4X: Found SCD41"); + if (!wakeUp()) { + LOG_ERROR("SCD4X: Error trying to execute wakeUp()"); + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } } + if (!getASC(ascActive)){ LOG_ERROR("SCD4X: Unable to check if ASC is enabled"); - return false; + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } - if (!ascActive){ - LOG_INFO("SCD4X: ASC is not active"); - } else { - LOG_INFO("SCD4X: ASC is active"); + // Start measurement in selected power mode (low power by default) + if (!startMeasurement()){ + LOG_ERROR("SCD4X: Couldn't start measurement"); + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + +#ifdef SCD4X_I2C_CLOCK_SPEED + if (currentClock){ + setI2CClock(currentClock); } +#endif - if (!scd4x.startLowPowerPeriodicMeasurement()) { + if (state == SCD4X_MEASUREMENT){ status = 1; } else { status = 0; } - restoreClock(currentClock); - return initI2CSensor(); } @@ -87,26 +93,41 @@ void SCD4XSensor::setup() {} bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) { + + if (state != SCD4X_MEASUREMENT) { + LOG_ERROR("SCD4X: Not in measurement mode"); + return false; + } + uint16_t co2, error; - float temperature; - float humidity; + float temperature, humidity; #ifdef SCD4X_I2C_CLOCK_SPEED uint32_t currentClock; - currentClock = bus->getClock(); - if (currentClock != SCD4X_I2C_CLOCK_SPEED){ - // LOG_DEBUG("Changing I2C clock to %u", SCD4X_I2C_CLOCK_SPEED); - bus->setClock(SCD4X_I2C_CLOCK_SPEED); - } + currentClock = setI2CClock(SCD4X_I2C_CLOCK_SPEED); #endif + bool dataReady; + error = scd4x.getDataReadyStatus(dataReady); + if (!dataReady) { + LOG_ERROR("SCD4X: Data is not ready"); + return false; + } + error = scd4x.readMeasurement(co2, temperature, humidity); - restoreClock(currentClock); - LOG_DEBUG("SCD4X: Error while getting measurements: %u", error); - LOG_DEBUG("SCD4X readings: %u, %.2f, %.2f", co2, temperature, humidity); - if (error != SCD4X_NO_ERROR || co2 == 0) { - LOG_ERROR("SCD4X: Skipping invalid measurement."); +#ifdef SCD4X_I2C_CLOCK_SPEED + if (currentClock){ + setI2CClock(currentClock); + } +#endif + + LOG_DEBUG("SCD4X readings: %u ppm, %.2f degC, %.2f %rh", co2, temperature, humidity); + if (error != SCD4X_NO_ERROR) { + LOG_DEBUG("SCD4X: Error while getting measurements: %u", error); + if (co2 == 0) { + LOG_ERROR("SCD4X: Skipping invalid measurement."); + } return false; } else { measurement->variant.air_quality_metrics.has_co2_temperature = true; @@ -132,19 +153,20 @@ bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) * command. 3. Issue the perform_forced_recalibration command. */ bool SCD4XSensor::performFRC(uint32_t targetCO2) { - uint16_t error; - uint16_t frcCorr; + uint16_t error, frcCorr; LOG_INFO("SCD4X: Issuing FRC. Ensure device has been working at least 3 minutes in stable target environment"); - error = scd4x.stopPeriodicMeasurement(); - if (error != SCD4X_NO_ERROR) { - LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X."); + if (!stopMeasurement()) { return false; } + LOG_INFO("SCD4X: Target CO2: %u ppm", targetCO2); error = scd4x.performForcedRecalibration((uint16_t)targetCO2, frcCorr); + // SCD4X Sensirion datasheet + delay(400); + if (error != SCD4X_NO_ERROR){ LOG_ERROR("SCD4X: Unable to perform forced recalibration."); return false; @@ -155,6 +177,67 @@ bool SCD4XSensor::performFRC(uint32_t targetCO2) { return false; } + LOG_INFO("SCD4X: FRC Correction successful. Correction output: %u", (uint16_t)(frcCorr-0x8000)); + + return true; +} + +bool SCD4XSensor::startMeasurement() { + uint16_t error; + + if (state == SCD4X_MEASUREMENT){ + LOG_DEBUG("SCD4X: Already in measurement mode"); + return true; + } + + if (lowPower) { + error = scd4x.startLowPowerPeriodicMeasurement(); + } else { + error = scd4x.startPeriodicMeasurement(); + } + + if (error == SCD4X_NO_ERROR) { + LOG_INFO("SCD4X: Started measurement mode"); + if (lowPower) { + LOG_INFO("SCD4X: Low power mode"); + } else { + LOG_INFO("SCD4X: Normal power mode"); + } + + state = SCD4X_MEASUREMENT; + return true; + } else { + LOG_ERROR("SCD4X: Couldn't start measurement mode"); + return false; + } +} + +bool SCD4XSensor::stopMeasurement(){ + uint16_t error; + + error = scd4x.stopPeriodicMeasurement(); + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X."); + return false; + } + + state = SCD4X_IDLE; + return true; +} + +bool SCD4XSensor::setPowerMode(bool _lowPower) { + lowPower = _lowPower; + + if (!stopMeasurement()) { + return false; + } + + if (lowPower) { + LOG_DEBUG("SCD4X: Set low power mode"); + } else { + LOG_DEBUG("SCD4X: Set normal power mode"); + } + return true; } @@ -163,22 +246,26 @@ bool SCD4XSensor::performFRC(uint32_t targetCO2) { * From Sensirion SCD4X I2C Library */ -bool SCD4XSensor::getASC(uint16_t &ascEnabled) { +bool SCD4XSensor::getASC(uint16_t &_ascActive) { uint16_t error; LOG_INFO("SCD4X: Getting ASC"); - error = scd4x.stopPeriodicMeasurement(); - if (error != SCD4X_NO_ERROR) { - LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X."); + if (!stopMeasurement()) { return false; } - - error = scd4x.getAutomaticSelfCalibrationEnabled(ascEnabled); + error = scd4x.getAutomaticSelfCalibrationEnabled(_ascActive); if (error != SCD4X_NO_ERROR){ LOG_ERROR("SCD4X: Unable to send command."); return false; } + + if (_ascActive){ + LOG_INFO("SCD4X: ASC is enabled"); + } else { + LOG_INFO("SCD4X: FRC is enabled"); + } + return true; } @@ -199,9 +286,7 @@ bool SCD4XSensor::setASC(bool ascEnabled){ LOG_INFO("SCD4X: Disabling ASC"); } - error = scd4x.stopPeriodicMeasurement(); - if (error != SCD4X_NO_ERROR) { - LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X."); + if (!stopMeasurement()) { return false; } @@ -245,18 +330,18 @@ bool SCD4XSensor::setASC(bool ascEnabled){ * ppm. */ bool SCD4XSensor::setASCBaseline(uint32_t targetCO2){ + // TODO - Remove? + // Available in library, but not described in datasheet. uint16_t error; - LOG_INFO("SCD4X: Setting ASC baseline"); + LOG_INFO("SCD4X: Setting ASC baseline to: %u", targetCO2); getASC(ascActive); if (!ascActive){ - LOG_ERROR("SCD4X: ASC is not active"); + LOG_ERROR("SCD4X: Can't set ASC baseline. ASC is not active"); return false; } - error = scd4x.stopPeriodicMeasurement(); - if (error != SCD4X_NO_ERROR) { - LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X."); + if (!stopMeasurement()) { return false; } @@ -273,6 +358,8 @@ bool SCD4XSensor::setASCBaseline(uint32_t targetCO2){ return false; } + LOG_INFO("SCD4X: Setting ASC baseline successful"); + return true; } @@ -283,7 +370,8 @@ bool SCD4XSensor::setASCBaseline(uint32_t targetCO2){ * From Sensirion SCD4X I2C Library. * * Setting the temperature offset of the SCD4x inside the customer device -* allows the user to optimize the RH and T output signal. By default, the temperature offset is set to 4 °C. To save +* allows the user to optimize the RH and T output signal. +* By default, the temperature offset is set to 4 °C. To save * the setting to the EEPROM, the persist_settings command may be issued. * Equation (1) details how the characteristic temperature offset can be * calculated using the current temperature output of the sensor (TSCD4x), a @@ -299,50 +387,59 @@ bool SCD4XSensor::setASCBaseline(uint32_t targetCO2){ bool SCD4XSensor::setTemperature(float tempReference){ uint16_t error; float prevTempOffset; + float updatedTempOffset; float tempOffset; - + bool dataReady; uint16_t co2; float temperature; float humidity; - LOG_INFO("SCD4X: Setting reference temperature at: %.2f". temperature); + + LOG_INFO("SCD4X: Setting reference temperature at: %.2f", tempReference); + + error = scd4x.getDataReadyStatus(dataReady); + if (!dataReady) { + LOG_ERROR("SCD4X: Data is not ready"); + return false; + } error = scd4x.readMeasurement(co2, temperature, humidity); if (error != SCD4X_NO_ERROR) { - LOG_ERROR("SCD4X: Unable read current temperature."); + LOG_ERROR("SCD4X: Unable to read current temperature. Error code: %u", error); return false; } LOG_INFO("SCD4X: Current sensor temperature: %.2f", temperature); - error = scd4x.stopPeriodicMeasurement(); - if (error != SCD4X_NO_ERROR) { - LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X."); + if (!stopMeasurement()) { return false; } error = scd4x.getTemperatureOffset(prevTempOffset); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to get temperature offset."); + LOG_ERROR("SCD4X: Unable to get temperature offset. Error code: %u", error); return false; } - LOG_INFO("SCD4X: Sensor temperature offset: %.2f", prevTempOffset); + LOG_INFO("SCD4X: Current sensor temperature offset: %.2f", prevTempOffset); tempOffset = temperature - tempReference + prevTempOffset; LOG_INFO("SCD4X: Setting temperature offset: %.2f", tempOffset); error = scd4x.setTemperatureOffset(tempOffset); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to set temperature offset."); + LOG_ERROR("SCD4X: Unable to set temperature offset. Error code: %u", error); return false; } error = scd4x.persistSettings(); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to make settings persistent."); + LOG_ERROR("SCD4X: Unable to make settings persistent. Error code: %u", error); return false; } + scd4x.getTemperatureOffset(updatedTempOffset); + LOG_INFO("SCD4X: Updated sensor temperature offset: %.2f", updatedTempOffset); + return true; } @@ -358,16 +455,14 @@ bool SCD4XSensor::getAltitude(uint16_t &altitude){ uint16_t error; LOG_INFO("SCD4X: Requesting sensor altitude"); - error = scd4x.stopPeriodicMeasurement(); - if (error != SCD4X_NO_ERROR) { - LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X."); + if (!stopMeasurement()) { return false; } error = scd4x.getSensorAltitude(altitude); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to get altitude."); + LOG_ERROR("SCD4X: Unable to get altitude. Error code: %u", error); return false; } LOG_INFO("SCD4X: Sensor altitude: %u", altitude); @@ -375,6 +470,28 @@ bool SCD4XSensor::getAltitude(uint16_t &altitude){ return true; } +/** +* @brief Get the ambient pressure around the sensor. +* +* From Sensirion SCD4X I2C Library. +* +* Gets the ambient pressure in Pa. +*/ +bool SCD4XSensor::getAmbientPressure(uint32_t &ambientPressure){ + uint16_t error; + LOG_INFO("SCD4X: Requesting sensor ambient pressure"); + + error = scd4x.getAmbientPressure(ambientPressure); + + if (error != SCD4X_NO_ERROR){ + LOG_ERROR("SCD4X: Unable to get altitude. Error code: %u", error); + return false; + } + LOG_INFO("SCD4X: Sensor ambient pressure: %u", ambientPressure); + + return true; +} + /** * @brief Set the sensor altitude. * @@ -386,22 +503,20 @@ bool SCD4XSensor::getAltitude(uint16_t &altitude){ bool SCD4XSensor::setAltitude(uint32_t altitude){ uint16_t error; - error = scd4x.stopPeriodicMeasurement(); - if (error != SCD4X_NO_ERROR) { - LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X."); + if (!stopMeasurement()) { return false; } error = scd4x.setSensorAltitude(altitude); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to set altitude."); + LOG_ERROR("SCD4X: Unable to set altitude. Error code: %u", error); return false; } error = scd4x.persistSettings(); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to make settings persistent."); + LOG_ERROR("SCD4X: Unable to make settings persistent. Error code: %u", error); return false; } @@ -427,14 +542,14 @@ bool SCD4XSensor::setAmbientPressure(uint32_t ambientPressure) { error = scd4x.setAmbientPressure(ambientPressure); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to set altitude."); + LOG_ERROR("SCD4X: Unable to set altitude. Error code: %u", error); return false; } // Sensirion doesn't indicate if this is necessary. We send it anyway error = scd4x.persistSettings(); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to make settings persistent."); + LOG_ERROR("SCD4X: Unable to make settings persistent. Error code: %u", error); return false; } @@ -449,52 +564,106 @@ bool SCD4XSensor::setAmbientPressure(uint32_t ambientPressure) { * The perform_factory_reset command resets all configuration settings * stored in the EEPROM and erases the FRC and ASC algorithm history. */ - bool SCD4XSensor::factoryReset() { uint16_t error; - error = scd4x.stopPeriodicMeasurement(); - if (error != SCD4X_NO_ERROR) { - LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X."); + LOG_INFO("SCD4X: Requesting factory reset"); + + if (!stopMeasurement()) { return false; } error = scd4x.performFactoryReset(); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to do factory reset."); + LOG_ERROR("SCD4X: Unable to do factory reset. Error code: %u", error); return false; } + LOG_INFO("SCD4X: Factory reset successful"); + return true; } +/** +* @brief Put the sensor into sleep mode from idle mode. +* +* From Sensirion SCD4X I2C Library. +* +* Put the sensor from idle to sleep to reduce power consumption. Can be +* used to power down when operating the sensor in power-cycled single shot +* mode. +* +* @note This command is only available in idle mode. Only for SCD41. +*/ +bool SCD4XSensor::sleep() { + LOG_INFO("SCD4X: Powering down"); + + if (sensorVariant != SCD4X_SENSOR_VARIANT_SCD41) { + LOG_WARN("SCD4X: Can't send sensor to sleep. Incorrect variant. Ignoring"); + return true; + } + if (!stopMeasurement()) { + return false; + } + + if (scd4x.powerDown() != SCD4X_NO_ERROR) { + LOG_ERROR("SCD4X: Error trying to execute wakeUp()"); + return false; + } + state = SCD4X_OFF; + return true; +} + +/** +* @brief Wake up sensor from sleep mode to idle mode. +* +* From Sensirion SCD4X I2C Library. +* +* Wake up the sensor from sleep mode into idle mode. Note that the SCD4x +* does not acknowledge the wake_up command. The sensor's idle state after +* wake up can be verified by reading out the serial number. +* +* @note This command is only available for SCD41. +*/ +bool SCD4XSensor::wakeUp(){ + LOG_INFO("SCD4X: Waking up"); + + if (scd4x.wakeUp() != SCD4X_NO_ERROR) { + LOG_ERROR("SCD4X: Error trying to execute wakeUp()"); + return false; + } + state = SCD4X_IDLE; + return true; +} AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) { AdminMessageHandleResult result; + // TODO: potentially add selftest command? + switch (request->which_payload_variant) { case meshtastic_AdminMessage_sensor_config_tag: // Check for ASC-FRC request first - if (!request->sensor_config.has_scdxx_config) { + if (!request->sensor_config.has_scd4x_config) { result = AdminMessageHandleResult::NOT_HANDLED; break; } - if (request->sensor_config.scdxx_config.has_factory_reset) { + if (request->sensor_config.scd4x_config.has_factory_reset) { LOG_DEBUG("SCD4X: Requested factory reset"); this->factoryReset(); } else { - if (request->sensor_config.scdxx_config.has_set_asc) { - this->setASC(request->sensor_config.scdxx_config.set_asc); - if (request->sensor_config.scdxx_config.set_asc == false) { + if (request->sensor_config.scd4x_config.has_set_asc) { + this->setASC(request->sensor_config.scd4x_config.set_asc); + if (request->sensor_config.scd4x_config.set_asc == false) { LOG_DEBUG("SCD4X: Request for FRC"); - if (request->sensor_config.scdxx_config.has_target_co2_conc) { - this->performFRC(request->sensor_config.scdxx_config.target_co2_conc); + if (request->sensor_config.scd4x_config.has_set_target_co2_conc) { + this->performFRC(request->sensor_config.scd4x_config.set_target_co2_conc); } else { // FRC requested but no target CO2 provided LOG_ERROR("SCD4X: target CO2 not provided"); @@ -503,9 +672,10 @@ AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPa } } else { LOG_DEBUG("SCD4X: Request for ASC"); - if (request->sensor_config.scdxx_config.has_target_co2_conc) { + if (request->sensor_config.scd4x_config.has_set_target_co2_conc) { LOG_DEBUG("SCD4X: Request has target CO2"); - this->setASCBaseline(request->sensor_config.scdxx_config.target_co2_conc); + // TODO - Remove? see setASCBaseline function + this->setASCBaseline(request->sensor_config.scd4x_config.set_target_co2_conc); } else { LOG_DEBUG("SCD4X: Request doesn't have target CO2"); } @@ -513,19 +683,31 @@ AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPa } // Check for temperature offset - if (request->sensor_config.scdxx_config.has_temperature) { - this->setTemperature(request->sensor_config.scdxx_config.temperature); + // NOTE: this requires to have a sensor working on stable environment + // And to make it between readings + if (request->sensor_config.scd4x_config.has_set_temperature) { + this->setTemperature(request->sensor_config.scd4x_config.set_temperature); } // Check for altitude or pressure offset - if (request->sensor_config.scdxx_config.has_altitude) { - this->setAltitude(request->sensor_config.scdxx_config.altitude); - } else if (request->sensor_config.scdxx_config.has_ambient_pressure){ - this->setAmbientPressure(request->sensor_config.scdxx_config.ambient_pressure); + if (request->sensor_config.scd4x_config.has_set_altitude) { + this->setAltitude(request->sensor_config.scd4x_config.set_altitude); + } else if (request->sensor_config.scd4x_config.has_set_ambient_pressure){ + this->setAmbientPressure(request->sensor_config.scd4x_config.set_ambient_pressure); + } + + // Check for low power mode + // NOTE: to switch from one mode to another do: + // setPowerMode -> startMeasurement + if (request->sensor_config.scd4x_config.has_set_power_mode) { + this->setPowerMode(request->sensor_config.scd4x_config.set_power_mode); } } + // Start measurement mode + this->startMeasurement(); + result = AdminMessageHandleResult::HANDLED; break; diff --git a/src/modules/Telemetry/Sensor/SCD4XSensor.h b/src/modules/Telemetry/Sensor/SCD4XSensor.h index 113616897d2..52a0b7046f2 100644 --- a/src/modules/Telemetry/Sensor/SCD4XSensor.h +++ b/src/modules/Telemetry/Sensor/SCD4XSensor.h @@ -22,18 +22,30 @@ class SCD4XSensor : public TelemetrySensor bool setTemperature(float tempReference); bool getAltitude(uint16_t &altitude); bool setAltitude(uint32_t altitude); + bool getAmbientPressure(uint32_t &ambientPressure); bool setAmbientPressure(uint32_t ambientPressure); - bool restoreClock(uint32_t currentClock); +#ifdef SCD4X_I2C_CLOCK_SPEED + uint32_t setI2CClock(uint32_t currentClock); +#endif bool factoryReset(); + bool setPowerMode(bool _lowPower); + bool startMeasurement(); + bool stopMeasurement(); // Parameters uint16_t ascActive; + bool lowPower = true; protected: virtual void setup() override; public: SCD4XSensor(); + enum SCD4XState { SCD4X_OFF, SCD4X_IDLE, SCD4X_MEASUREMENT }; + SCD4XState state = SCD4X_OFF; + SCD4xSensorVariant sensorVariant; + bool sleep(); + bool wakeUp(); virtual int32_t runOnce() override; virtual bool getMetrics(meshtastic_Telemetry *measurement) override; AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, From a129441533a72a52726ffb67f87ee76b2c01eb2a Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 6 Aug 2025 10:40:57 +0200 Subject: [PATCH 017/108] 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 23ccdd1ee5f..8b3670cd989 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 28b073a1745..6988091ad3b 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 4e01f4409b0..c0276d6a662 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 39f8269a2c7..c83c3e01aca 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 ee3258ab1ce..35a4df735e1 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 24b3a2225ca79eec91a39ac5bc37a5d605c6d4d8 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 10 Sep 2025 15:29:50 -0500 Subject: [PATCH 018/108] Unify build epoch to add flag in platformio-custom.py (#7917) * Unify build_epoch replacement logic in platformio-custom * Missed one --- .github/actions/setup-base/action.yml | 5 ----- bin/build-firmware.sh | 2 -- bin/platformio-custom.py | 7 +++++++ platformio.ini | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/actions/setup-base/action.yml b/.github/actions/setup-base/action.yml index 350ca290c81..f6c1fd80c8a 100644 --- a/.github/actions/setup-base/action.yml +++ b/.github/actions/setup-base/action.yml @@ -11,11 +11,6 @@ runs: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - - name: Uncomment build epoch - shell: bash - run: | - sed -i 's/#-DBUILD_EPOCH=$UNIX_TIME/-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini - - name: Install dependencies shell: bash run: | diff --git a/bin/build-firmware.sh b/bin/build-firmware.sh index fdd7caa11c4..7bd19aaa90f 100644 --- a/bin/build-firmware.sh +++ b/bin/build-firmware.sh @@ -1,7 +1,5 @@ #!/usr/bin/env bash -sed -i 's/#-DBUILD_EPOCH=$UNIX_TIME/-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini - export PIP_BREAK_SYSTEM_PACKAGES=1 if (echo $2 | grep -q "esp32"); then diff --git a/bin/platformio-custom.py b/bin/platformio-custom.py index fc1b4bc2e54..e54d1586f28 100644 --- a/bin/platformio-custom.py +++ b/bin/platformio-custom.py @@ -6,6 +6,8 @@ import subprocess import json import re +import time +from datetime import datetime from readprops import readProps @@ -125,11 +127,16 @@ def esp32_create_combined_bin(source, target, env): pref_flags.append("-D" + pref + "=" + env.StringifyMacro(userPrefs[pref]) + "") # General options that are passed to the C and C++ compilers +# Calculate unix epoch for current day (midnight) +current_date = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) +build_epoch = int(current_date.timestamp()) + flags = [ "-DAPP_VERSION=" + verObj["long"], "-DAPP_VERSION_SHORT=" + verObj["short"], "-DAPP_ENV=" + env.get("PIOENV"), "-DAPP_REPO=" + repo_owner, + "-DBUILD_EPOCH=" + str(build_epoch), ] + pref_flags print ("Using flags:") diff --git a/platformio.ini b/platformio.ini index d1089a4f3be..81bda239c80 100644 --- a/platformio.ini +++ b/platformio.ini @@ -53,7 +53,7 @@ build_flags = -Wno-missing-field-initializers -DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware -DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1 -D MAX_THREADS=40 ; As we've split modules, we have more threads to manage - #-DBUILD_EPOCH=$UNIX_TIME + #-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now #-D OLED_PL=1 monitor_speed = 115200 From 0827bd02c0ce9dca5f3a7fb662d7c657b78a3e3e Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Mon, 8 Sep 2025 20:54:08 +1000 Subject: [PATCH 019/108] Fix build error in rak_wismesh_tap_v2 (#7905) In the logs was: "No screen resolution defined in build_flags. Please define DISPLAY_SIZE." set according to similar devices. --- variants/esp32s3/rak_wismesh_tap_v2/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini index 8b86e021766..de4714efaa1 100644 --- a/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini +++ b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini @@ -70,6 +70,7 @@ build_flags = ${ft5x06.build_flags} -D LGFX_SCREEN_WIDTH=240 -D LGFX_SCREEN_HEIGHT=320 + -D DISPLAY_SIZE=320x240 ; landscape mode -D LGFX_PANEL=ST7789 -D LGFX_ROTATION=1 -D LGFX_TOUCH_X_MIN=0 From 0b900dd42e50f1c7de15d7529a776ecd6dc32c4a Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 11 Sep 2025 07:57:42 -0500 Subject: [PATCH 020/108] Put guards in place around debug heap operations (#7955) * Put guards in place around debug heap operations * Add macros to clean up code * Add pointer as well --- src/DebugConfiguration.h | 19 +++++++++++++++++++ src/memGet.cpp | 12 ++++++++++++ src/mesh/MeshService.cpp | 11 ++++------- src/mesh/ReliableRouter.cpp | 5 ++--- src/mesh/Router.cpp | 12 +++++------- src/modules/NodeInfoModule.cpp | 5 ++--- src/modules/Telemetry/DeviceTelemetry.cpp | 6 ++---- 7 files changed, 46 insertions(+), 24 deletions(-) diff --git a/src/DebugConfiguration.h b/src/DebugConfiguration.h index 26f2db1f426..98bbe0f729a 100644 --- a/src/DebugConfiguration.h +++ b/src/DebugConfiguration.h @@ -2,6 +2,12 @@ #include "configuration.h" +// Forward declarations +#if defined(DEBUG_HEAP) +class MemGet; +extern MemGet memGet; +#endif + // DEBUG LED #ifndef LED_STATE_ON #define LED_STATE_ON 1 @@ -65,8 +71,21 @@ #if defined(DEBUG_HEAP) #define LOG_HEAP(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_HEAP, __VA_ARGS__) + +// Macro-based heap debugging +#define DEBUG_HEAP_BEFORE auto heapBefore = memGet.getFreeHeap(); +#define DEBUG_HEAP_AFTER(context, ptr) \ + do { \ + auto heapAfter = memGet.getFreeHeap(); \ + if (heapBefore != heapAfter) { \ + LOG_HEAP("Alloc in %s pointer 0x%x, size: %u, free: %u", context, ptr, heapBefore - heapAfter, heapAfter); \ + } \ + } while (0) + #else #define LOG_HEAP(...) +#define DEBUG_HEAP_BEFORE +#define DEBUG_HEAP_AFTER(context, ptr) #endif /// A C wrapper for LOG_DEBUG that can be used from arduino C libs that don't know about C++ or meshtastic diff --git a/src/memGet.cpp b/src/memGet.cpp index e8cd177dd93..14e61401495 100644 --- a/src/memGet.cpp +++ b/src/memGet.cpp @@ -88,4 +88,16 @@ uint32_t MemGet::getPsramSize() #else return 0; #endif +} + +void displayPercentHeapFree() +{ + uint32_t freeHeap = memGet.getFreeHeap(); + uint32_t totalHeap = memGet.getHeapSize(); + if (totalHeap == 0 || totalHeap == UINT32_MAX) { + LOG_INFO("Heap size unavailable"); + return; + } + int percent = (int)((freeHeap * 100) / totalHeap); + LOG_INFO("Heap free: %d%% (%u/%u bytes)", percent, freeHeap, totalHeap); } \ No newline at end of file diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index 157a2eda3cd..607766ab6aa 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -193,11 +193,9 @@ void MeshService::handleToRadio(meshtastic_MeshPacket &p) // (so we update our nodedb for the local node) // Send the packet into the mesh - auto heapBefore = memGet.getFreeHeap(); + DEBUG_HEAP_BEFORE; auto a = packetPool.allocCopy(p); - auto heapAfter = memGet.getFreeHeap(); - LOG_HEAP("Alloc in MeshService::handleToRadio() pointer 0x%x, size: %u, free: %u", a, heapBefore - heapAfter, heapAfter); - + DEBUG_HEAP_AFTER("MeshService::handleToRadio", a); sendToMesh(a, RX_SRC_USER); bool loopback = false; // if true send any packet the phone sends back itself (for testing) @@ -254,10 +252,9 @@ void MeshService::sendToMesh(meshtastic_MeshPacket *p, RxSource src, bool ccToPh } if ((res == ERRNO_OK || res == ERRNO_SHOULD_RELEASE) && ccToPhone) { // Check if p is not released in case it couldn't be sent - auto heapBefore = memGet.getFreeHeap(); + DEBUG_HEAP_BEFORE; auto a = packetPool.allocCopy(*p); - auto heapAfter = memGet.getFreeHeap(); - LOG_HEAP("Alloc in MeshService::sendToMesh() pointer 0x%x, size: %u, free: %u", a, heapBefore - heapAfter, heapAfter); + DEBUG_HEAP_AFTER("MeshService::sendToMesh", a); sendToPhone(a); } diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp index 890d42b00a3..6d098b6696a 100644 --- a/src/mesh/ReliableRouter.cpp +++ b/src/mesh/ReliableRouter.cpp @@ -22,10 +22,9 @@ ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p) if (p->hop_limit == 0) { p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); } - auto heapBefore = memGet.getFreeHeap(); + DEBUG_HEAP_BEFORE; auto copy = packetPool.allocCopy(*p); - auto heapAfter = memGet.getFreeHeap(); - LOG_HEAP("Alloc in ReliableRouter::send() pointer 0x%x, size: %u, free: %u", copy, heapBefore - heapAfter, heapAfter); + DEBUG_HEAP_AFTER("ReliableRouter::send", copy); startRetransmission(copy, NUM_RELIABLE_RETX); } diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 603dfda4a86..4442b5d506a 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -276,11 +276,9 @@ ErrorCode Router::send(meshtastic_MeshPacket *p) if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it - auto heapBefore = memGet.getFreeHeap(); + DEBUG_HEAP_BEFORE; meshtastic_MeshPacket *p_decoded = packetPool.allocCopy(*p); - auto heapAfter = memGet.getFreeHeap(); - - LOG_HEAP("Alloc in Router::send pointer 0x%x, size: %u, free: %u", p_decoded, heapBefore - heapAfter, heapAfter); + DEBUG_HEAP_AFTER("Router::send", p_decoded); auto encodeResult = perhapsEncode(p); if (encodeResult != meshtastic_Routing_Error_NONE) { @@ -612,11 +610,11 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) bool skipHandle = false; // Also, we should set the time from the ISR and it should have msec level resolution p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone + // Store a copy of encrypted packet for MQTT - auto heapBefore = memGet.getFreeHeap(); + DEBUG_HEAP_BEFORE; meshtastic_MeshPacket *p_encrypted = packetPool.allocCopy(*p); - auto heapAfter = memGet.getFreeHeap(); - LOG_HEAP("Alloc in Router::handleReceived pointer 0x%x, size: %u, free: %u", p_encrypted, heapBefore - heapAfter, heapAfter); + DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted); // Take those raw bytes and convert them back into a well structured protobuf we can understand auto decodedState = perhapsDecode(p); diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp index 82632f66714..86ceaae24d1 100644 --- a/src/modules/NodeInfoModule.cpp +++ b/src/modules/NodeInfoModule.cpp @@ -44,11 +44,10 @@ void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t cha if (prevPacketId) // if we wrap around to zero, we'll simply fail to cancel in that rare case (no big deal) service->cancelSending(prevPacketId); shorterTimeout = _shorterTimeout; - auto heapBefore = memGet.getFreeHeap(); + DEBUG_HEAP_BEFORE; meshtastic_MeshPacket *p = allocReply(); - auto heapAfter = memGet.getFreeHeap(); + DEBUG_HEAP_AFTER("NodeInfoModule::sendOurNodeInfo", p); - LOG_HEAP("Alloc in NodeInfoModule::sendOurNodeInfo pointer 0x%x, size: %u, free: %u", p, heapBefore - heapAfter, heapAfter); if (p) { // Check whether we didn't ignore it p->to = dest; p->decoded.want_response = (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER && diff --git a/src/modules/Telemetry/DeviceTelemetry.cpp b/src/modules/Telemetry/DeviceTelemetry.cpp index 8694de99387..98d5b19d0a2 100644 --- a/src/modules/Telemetry/DeviceTelemetry.cpp +++ b/src/modules/Telemetry/DeviceTelemetry.cpp @@ -172,11 +172,9 @@ bool DeviceTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) telemetry.variant.device_metrics.battery_level, telemetry.variant.device_metrics.voltage, telemetry.variant.device_metrics.uptime_seconds); - auto heapBefore = memGet.getFreeHeap(); + DEBUG_HEAP_BEFORE; meshtastic_MeshPacket *p = allocDataProtobuf(telemetry); - auto heapAfter = memGet.getFreeHeap(); - LOG_HEAP("Alloc in DeviceTelemetryModule::sendTelemetry() pointer 0x%x, size: %u, free: %u", p, heapBefore - heapAfter, - heapAfter); + DEBUG_HEAP_AFTER("DeviceTelemetryModule::sendTelemetry", p); p->to = dest; p->decoded.want_response = false; From 7b75468d64d68dc12c73a21239b2c7042467fa40 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 11 Sep 2025 18:57:30 -0500 Subject: [PATCH 021/108] Cleanup --- src/mesh/MeshModule.cpp | 1 - src/mesh/Router.cpp | 4 +--- src/modules/NodeInfoModule.cpp | 4 ++-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/mesh/MeshModule.cpp b/src/mesh/MeshModule.cpp index 22fcec663f1..c5748a56066 100644 --- a/src/mesh/MeshModule.cpp +++ b/src/mesh/MeshModule.cpp @@ -100,7 +100,6 @@ void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src) // Was this message directed to us specifically? Will be false if we are sniffing someone elses packets auto ourNodeNum = nodeDB->getNodeNum(); bool toUs = isBroadcast(mp.to) || isToUs(&mp); - bool fromUs = mp.from == ourNodeNum; for (auto i = modules->begin(); i != modules->end(); ++i) { auto &pi = **i; diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 4442b5d506a..44d09637f26 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -662,7 +662,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) // call modules here // If this could be a spoofed packet, don't let the modules see it. - if (!skipHandle && p->from != nodeDB->getNodeNum()) { + if (!skipHandle) { MeshModule::callModules(*p, src); #if !MESHTASTIC_EXCLUDE_MQTT @@ -676,8 +676,6 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) !isFromUs(p) && mqtt) mqtt->onSend(*p_encrypted, *p, p->channel); #endif - } else if (p->from == nodeDB->getNodeNum() && !skipHandle) { - MeshModule::callModules(*p, src); } packetPool.release(p_encrypted); // Release the encrypted packet diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp index 86ceaae24d1..276a11b3a40 100644 --- a/src/modules/NodeInfoModule.cpp +++ b/src/modules/NodeInfoModule.cpp @@ -12,12 +12,12 @@ NodeInfoModule *nodeInfoModule; bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *pptr) { - auto p = *pptr; - if (mp.from == nodeDB->getNodeNum()) { LOG_WARN("Ignoring packet supposed to be from our own node: %08x", mp.from); return false; } + + auto p = *pptr; if (p.is_licensed != owner.is_licensed) { LOG_WARN("Invalid nodeInfo detected, is_licensed mismatch!"); return true; From d7ef19b19c2a4711a7b3e35903575473ccf82b9e Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Fri, 12 Sep 2025 10:40:13 -0700 Subject: [PATCH 022/108] Fix memory leak in NextHopRouter: always free packet copy when removing from pending --- src/mesh/NextHopRouter.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp index 7ceca219578..db3d62038b8 100644 --- a/src/mesh/NextHopRouter.cpp +++ b/src/mesh/NextHopRouter.cpp @@ -175,12 +175,18 @@ bool NextHopRouter::stopRetransmission(GlobalPacketId key) config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) { // remove the 'original' (identified by originator and packet->id) from the txqueue and free it cancelSending(getFrom(p), p->id); - // now free the pooled copy for retransmission too - packetPool.release(p); } } + + // Regardless of whether or not we canceled this packet from the txQueue, remove it from our pending list so it doesn't + // get scheduled again. (This is the core of stopRetransmission.) auto numErased = pending.erase(key); assert(numErased == 1); + + // When we remove an entry from pending, always be sure to release the copy of the packet that was allocated in the call + // to startRetransmission. + packetPool.release(p); + return true; } else return false; From ab09d9bc37036609549c8975fa1aaec1164e7d4c Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 12 Sep 2025 15:49:56 -0500 Subject: [PATCH 023/108] Formatting --- src/mesh/MemoryPool.h | 76 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 7 deletions(-) diff --git a/src/mesh/MemoryPool.h b/src/mesh/MemoryPool.h index ea7c8f5832a..0c5ba6c71ce 100644 --- a/src/mesh/MemoryPool.h +++ b/src/mesh/MemoryPool.h @@ -6,6 +6,7 @@ #include #include "PointerQueue.h" +#include "configuration.h" // For LOG_WARN, LOG_DEBUG, LOG_HEAP template class Allocator { @@ -14,13 +15,14 @@ template class Allocator Allocator() : deleter([this](T *p) { this->release(p); }) {} virtual ~Allocator() {} - /// Return a queable object which has been prefilled with zeros. Panic if no buffer is available + /// Return a queable object which has been prefilled with zeros. Return nullptr if no buffer is available /// Note: this method is safe to call from regular OR ISR code T *allocZeroed() { T *p = allocZeroed(0); - - assert(p); // FIXME panic instead + if (!p) { + LOG_WARN("Failed to allocate zeroed memory"); + } return p; } @@ -39,10 +41,12 @@ template class Allocator T *allocCopy(const T &src, TickType_t maxWait = portMAX_DELAY) { T *p = alloc(maxWait); - assert(p); + if (!p) { + LOG_WARN("Failed to allocate memory for copy"); + return nullptr; + } - if (p) - *p = src; + *p = src; return p; } @@ -83,7 +87,9 @@ template class MemoryDynamic : public Allocator /// Return a buffer for use by others virtual void release(T *p) override { - assert(p); + if (p == nullptr) + return; + LOG_HEAP("Freeing 0x%x", p); free(p); @@ -98,3 +104,59 @@ template class MemoryDynamic : public Allocator return p; } }; + +/** + * A static memory pool that uses a fixed buffer instead of heap allocation + */ +template class MemoryPool : public Allocator +{ + private: + T pool[MaxSize]; + bool used[MaxSize]; + + public: + MemoryPool() + { + // Initialize the used array to false (all slots free) + for (int i = 0; i < MaxSize; i++) { + used[i] = false; + } + } + + /// Return a buffer for use by others + virtual void release(T *p) override + { + if (!p) { + LOG_DEBUG("Failed to release memory, pointer is null"); + return; + } + + // Find the index of this pointer in our pool + int index = p - pool; + if (index >= 0 && index < MaxSize) { + assert(used[index]); // Should be marked as used + used[index] = false; + LOG_HEAP("Released static pool item %d at 0x%x", index, p); + } else { + LOG_WARN("Pointer 0x%x not from our pool!", p); + } + } + + protected: + // Alloc some storage from our static pool + virtual T *alloc(TickType_t maxWait) override + { + // Find first free slot + for (int i = 0; i < MaxSize; i++) { + if (!used[i]) { + used[i] = true; + LOG_HEAP("Allocated static pool item %d at 0x%x", i, &pool[i]); + return &pool[i]; + } + } + + // No free slots available - return nullptr instead of asserting + LOG_WARN("No free slots available in static memory pool!"); + return nullptr; + } +}; From 4aa5e91c3117469ca8ab70088a75e897c3376487 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 12 Sep 2025 16:07:27 -0500 Subject: [PATCH 024/108] Only queue 2 client notification --- src/mesh/mesh-pb-constants.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/mesh-pb-constants.h b/src/mesh/mesh-pb-constants.h index 224f45de251..12aec98cd5d 100644 --- a/src/mesh/mesh-pb-constants.h +++ b/src/mesh/mesh-pb-constants.h @@ -30,7 +30,7 @@ /// max number of ClientNotification packets which can be waiting for delivery to phone #ifndef MAX_RX_NOTIFICATION_TOPHONE -#define MAX_RX_NOTIFICATION_TOPHONE 4 +#define MAX_RX_NOTIFICATION_TOPHONE 2 #endif /// Verify baseline assumption of node size. If it increases, we need to reevaluate From 8468bf9a13e17e3fe78dfa4190199d01b8517f69 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 12 Sep 2025 17:12:18 -0500 Subject: [PATCH 025/108] Merge pull request #7965 from compumike/compumike/fix-nrf52-bluetooth-memory-leak Fix memory leak in `NRF52Bluetooth`: allocate `BluetoothStatus` on stack, not heap --- src/platform/nrf52/NRF52Bluetooth.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index 6f0e7250fdd..f8366ae32eb 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -59,7 +59,8 @@ void onConnect(uint16_t conn_handle) LOG_INFO("BLE Connected to %s", central_name); // Notify UI (or any other interested firmware components) - bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED)); + meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); + bluetoothStatus->updateStatus(&newStatus); } /** * Callback invoked when a connection is dropped @@ -74,7 +75,8 @@ void onDisconnect(uint16_t conn_handle, uint8_t reason) } // Notify UI (or any other interested firmware components) - bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED)); + meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); + bluetoothStatus->updateStatus(&newStatus); } void onCccd(uint16_t conn_hdl, BLECharacteristic *chr, uint16_t cccd_value) { @@ -326,7 +328,8 @@ bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passke textkey += (char)passkey[i]; // Notify UI (or other components) of pairing event and passkey - bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(textkey)); + meshtastic::BluetoothStatus newStatus(textkey); + bluetoothStatus->updateStatus(&newStatus); #if !defined(MESHTASTIC_EXCLUDE_SCREEN) // Todo: migrate this display code back into Screen class, and observe bluetoothStatus if (screen) { @@ -398,12 +401,13 @@ void NRF52Bluetooth::onPairingCompleted(uint16_t conn_handle, uint8_t auth_statu { if (auth_status == BLE_GAP_SEC_STATUS_SUCCESS) { LOG_INFO("BLE pair success"); - bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED)); + meshtastic::BluetoothStatus newConnectedStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); + bluetoothStatus->updateStatus(&newConnectedStatus); } else { LOG_INFO("BLE pair failed"); // Notify UI (or any other interested firmware components) - bluetoothStatus->updateStatus( - new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED)); + meshtastic::BluetoothStatus newDisconnectedStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); + bluetoothStatus->updateStatus(&newDisconnectedStatus); } // Todo: migrate this display code back into Screen class, and observe bluetoothStatus From 8d1b20db04f928c7ed2deebd639861165b57834e Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 12 Sep 2025 17:12:27 -0500 Subject: [PATCH 026/108] Merge pull request #7964 from compumike/compumike/fix-nimble-bluetooth-memory-leak Fix memory leak in `NimbleBluetooth`: allocate `BluetoothStatus` on stack, not heap --- src/nimble/NimbleBluetooth.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index 95e191c8e62..ee95168c3b6 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -133,7 +133,8 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks LOG_INFO("*** Enter passkey %d on the peer side ***", passkey); powerFSM.trigger(EVENT_BLUETOOTH_PAIR); - bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(std::to_string(passkey))); + meshtastic::BluetoothStatus newStatus(std::to_string(passkey)); + bluetoothStatus->updateStatus(&newStatus); #if HAS_SCREEN // Todo: migrate this display code back into Screen class, and observe bluetoothStatus if (screen) { @@ -173,7 +174,8 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks { LOG_INFO("BLE authentication complete"); - bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED)); + meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); + bluetoothStatus->updateStatus(&newStatus); // Todo: migrate this display code back into Screen class, and observe bluetoothStatus if (passkeyShowing) { @@ -187,8 +189,8 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks { LOG_INFO("BLE disconnect"); - bluetoothStatus->updateStatus( - new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED)); + meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); + bluetoothStatus->updateStatus(&newStatus); if (bluetoothPhoneAPI) { std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex); From 9f25738ea07d16b6acc04778d37935e232210e01 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 13 Sep 2025 06:37:58 -0500 Subject: [PATCH 027/108] Update protobufs (#7973) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- protobufs | 2 +- src/mesh/generated/meshtastic/device_ui.pb.h | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/protobufs b/protobufs index a84657c2204..8caf4239643 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit a84657c220421536f18d11fc5edf680efadbceeb +Subproject commit 8caf42396438f0d8a0305143485fd671c1fc7126 diff --git a/src/mesh/generated/meshtastic/device_ui.pb.h b/src/mesh/generated/meshtastic/device_ui.pb.h index 8313438f871..8f693e5703d 100644 --- a/src/mesh/generated/meshtastic/device_ui.pb.h +++ b/src/mesh/generated/meshtastic/device_ui.pb.h @@ -66,6 +66,8 @@ typedef enum _meshtastic_Language { meshtastic_Language_UKRAINIAN = 16, /* Bulgarian */ meshtastic_Language_BULGARIAN = 17, + /* Czech */ + meshtastic_Language_CZECH = 18, /* Simplified Chinese (experimental) */ meshtastic_Language_SIMPLIFIED_CHINESE = 30, /* Traditional Chinese (experimental) */ From a5876f86df6566a1a482a1acd6eb1f116391201e Mon Sep 17 00:00:00 2001 From: WillyJL Date: Sat, 13 Sep 2025 13:50:02 +0200 Subject: [PATCH 028/108] T-Lora Pager: Support LR1121 and SX1280 models (#7956) * T-Lora Pager: Support LR1121 and SX1280 models * Remove ifdefs --- variants/esp32s3/tlora-pager/rfswitch.h | 18 ++++++++++++++++++ variants/esp32s3/tlora-pager/variant.h | 19 ++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 variants/esp32s3/tlora-pager/rfswitch.h diff --git a/variants/esp32s3/tlora-pager/rfswitch.h b/variants/esp32s3/tlora-pager/rfswitch.h new file mode 100644 index 00000000000..1e5eb7a9e20 --- /dev/null +++ b/variants/esp32s3/tlora-pager/rfswitch.h @@ -0,0 +1,18 @@ +#include "RadioLib.h" + +static const uint32_t rfswitch_dio_pins[] = { + RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, + RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC +}; + +static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 + { LR11x0::MODE_STBY, { LOW, LOW } }, + { LR11x0::MODE_RX, { LOW, HIGH } }, + { LR11x0::MODE_TX, { HIGH, LOW } }, + { LR11x0::MODE_TX_HP, { HIGH, LOW } }, + { LR11x0::MODE_TX_HF, { LOW, LOW } }, + { LR11x0::MODE_GNSS, { LOW, LOW } }, + { LR11x0::MODE_WIFI, { LOW, LOW } }, + END_OF_MODE_TABLE, +}; \ No newline at end of file diff --git a/variants/esp32s3/tlora-pager/variant.h b/variants/esp32s3/tlora-pager/variant.h index ee48088c885..2875f6804f0 100644 --- a/variants/esp32s3/tlora-pager/variant.h +++ b/variants/esp32s3/tlora-pager/variant.h @@ -105,14 +105,16 @@ // LoRa #define USE_SX1262 #define USE_SX1268 +#define USE_SX1280 +#define USE_LR1121 #define LORA_SCK 35 #define LORA_MISO 33 #define LORA_MOSI 34 #define LORA_CS 36 +#define LORA_RESET 47 #define LORA_DIO0 -1 // a No connect on the SX1262 module -#define LORA_RESET 47 #define LORA_DIO1 14 // SX1262 IRQ #define LORA_DIO2 48 // SX1262 BUSY #define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled @@ -123,3 +125,18 @@ #define SX126X_RESET LORA_RESET #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 3.0 + +#define SX128X_CS LORA_CS +#define SX128X_DIO1 LORA_DIO1 +#define SX128X_BUSY LORA_DIO2 +#define SX128X_RESET LORA_RESET + +#define LR1121_IRQ_PIN LORA_DIO1 +#define LR1121_NRESET_PIN LORA_RESET +#define LR1121_BUSY_PIN LORA_DIO2 +#define LR1121_SPI_NSS_PIN LORA_CS +#define LR1121_SPI_SCK_PIN LORA_SCK +#define LR1121_SPI_MOSI_PIN LORA_MOSI +#define LR1121_SPI_MISO_PIN LORA_MISO +#define LR11X0_DIO3_TCXO_VOLTAGE 3.0 +#define LR11X0_DIO_AS_RF_SWITCH From 1d757ba739660e23a123f9665b42010a7730c6ad Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 06:51:18 -0500 Subject: [PATCH 029/108] Trunk --- variants/esp32s3/tlora-pager/rfswitch.h | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/variants/esp32s3/tlora-pager/rfswitch.h b/variants/esp32s3/tlora-pager/rfswitch.h index 1e5eb7a9e20..0fba5a30579 100644 --- a/variants/esp32s3/tlora-pager/rfswitch.h +++ b/variants/esp32s3/tlora-pager/rfswitch.h @@ -1,18 +1,11 @@ #include "RadioLib.h" -static const uint32_t rfswitch_dio_pins[] = { - RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, - RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC -}; +static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - { LR11x0::MODE_STBY, { LOW, LOW } }, - { LR11x0::MODE_RX, { LOW, HIGH } }, - { LR11x0::MODE_TX, { HIGH, LOW } }, - { LR11x0::MODE_TX_HP, { HIGH, LOW } }, - { LR11x0::MODE_TX_HF, { LOW, LOW } }, - { LR11x0::MODE_GNSS, { LOW, LOW } }, - { LR11x0::MODE_WIFI, { LOW, LOW } }, - END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {LOW, HIGH}}, + {LR11x0::MODE_TX, {HIGH, LOW}}, {LR11x0::MODE_TX_HP, {HIGH, LOW}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, + {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; \ No newline at end of file From 375ab363b6821a6ebd4184e8c2b3d07690002f22 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 06:57:12 -0500 Subject: [PATCH 030/108] Trunk From 33ffa8df9ea066feb781b11c3ea3cdbd61ef8c19 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 07:01:07 -0500 Subject: [PATCH 031/108] Static memory pool allocation (#7966) * Static memory pool * Initializer * T-Lora Pager: Support LR1121 and SX1280 models (#7956) * T-Lora Pager: Support LR1121 and SX1280 models * Remove ifdefs --------- Co-authored-by: WillyJL --- src/mesh/MemoryPool.h | 9 ++++----- src/mesh/MeshService.cpp | 9 ++++++--- src/mesh/Router.cpp | 3 +-- variants/esp32s3/tlora-pager/rfswitch.h | 12 ++++++++---- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/mesh/MemoryPool.h b/src/mesh/MemoryPool.h index 0c5ba6c71ce..eb5ac5109de 100644 --- a/src/mesh/MemoryPool.h +++ b/src/mesh/MemoryPool.h @@ -115,12 +115,11 @@ template class MemoryPool : public Allocator bool used[MaxSize]; public: - MemoryPool() + MemoryPool() : pool{}, used{} { - // Initialize the used array to false (all slots free) - for (int i = 0; i < MaxSize; i++) { - used[i] = false; - } + // Arrays are now zero-initialized by member initializer list + // pool array: all elements are default-constructed (zero for POD types) + // used array: all elements are false (zero-initialized) } /// Return a buffer for use by others diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index 607766ab6aa..96782cda550 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -46,11 +46,14 @@ the new node can build its node db) MeshService *service; -static MemoryDynamic staticMqttClientProxyMessagePool; +#define MAX_MQTT_PROXY_MESSAGES 16 +static MemoryPool staticMqttClientProxyMessagePool; -static MemoryDynamic staticQueueStatusPool; +#define MAX_QUEUE_STATUS 4 +static MemoryPool staticQueueStatusPool; -static MemoryDynamic staticClientNotificationPool; +#define MAX_CLIENT_NOTIFICATIONS 4 +static MemoryPool staticClientNotificationPool; Allocator &mqttClientProxyMessagePool = staticMqttClientProxyMessagePool; diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 44d09637f26..b5ae1ec0ccc 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -31,8 +31,7 @@ (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ 2) // max number of packets which can be in flight (either queued from reception or queued for sending) -// static MemoryPool staticPool(MAX_PACKETS); -static MemoryDynamic staticPool; +static MemoryPool staticPool; Allocator &packetPool = staticPool; diff --git a/variants/esp32s3/tlora-pager/rfswitch.h b/variants/esp32s3/tlora-pager/rfswitch.h index 0fba5a30579..337346ec597 100644 --- a/variants/esp32s3/tlora-pager/rfswitch.h +++ b/variants/esp32s3/tlora-pager/rfswitch.h @@ -4,8 +4,12 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {LOW, HIGH}}, - {LR11x0::MODE_TX, {HIGH, LOW}}, {LR11x0::MODE_TX_HP, {HIGH, LOW}}, - {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, - {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, + {LR11x0::MODE_RX, {LOW, HIGH}}, + {LR11x0::MODE_TX, {HIGH, LOW}}, + {LR11x0::MODE_TX_HP, {HIGH, LOW}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, + {LR11x0::MODE_GNSS, {LOW, LOW}}, + {LR11x0::MODE_WIFI, {LOW, LOW}}, + END_OF_MODE_TABLE, }; \ No newline at end of file From 55b42b95e19330a101c4638fa80fdc8c461c8727 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 11:59:50 -0500 Subject: [PATCH 032/108] Portduino dynamic alloc --- src/mesh/Router.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index b5ae1ec0ccc..c5eed5180ea 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -5,6 +5,7 @@ #include "MeshService.h" #include "NodeDB.h" #include "RTC.h" + #include "configuration.h" #include "detect/LoRaRadioType.h" #include "main.h" @@ -27,13 +28,24 @@ // I think this is right, one packet for each of the three fifos + one packet being currently assembled for TX or RX // And every TX packet might have a retransmission packet or an ack alive at any moment + +#ifdef ARCH_PORTDUINO +// Portduino (native) targets can use dynamic memory pools with runtime-configurable sizes #define MAX_PACKETS \ (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ 2) // max number of packets which can be in flight (either queued from reception or queued for sending) -static MemoryPool staticPool; +static MemoryDynamic dynamicPool(MAX_PACKETS); +Allocator &packetPool = dynamicPool; +#else +// Embedded targets use static memory pools with compile-time constants +#define MAX_PACKETS_STATIC \ + (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ + 2) // max number of packets which can be in flight (either queued from reception or queued for sending) +static MemoryPool staticPool; Allocator &packetPool = staticPool; +#endif static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1] __attribute__((__aligned__)); From 1da6d2832f5db50ec9ae04468a2148037abab4eb Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 11:59:58 -0500 Subject: [PATCH 033/108] Missed From 3092ec7a0f3df3af59feb7c4283d35619b42900e Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 12:07:14 -0500 Subject: [PATCH 034/108] Drop the limit --- src/mesh/Router.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index c5eed5180ea..6c5d08a93a4 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -35,7 +35,7 @@ (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ 2) // max number of packets which can be in flight (either queued from reception or queued for sending) -static MemoryDynamic dynamicPool(MAX_PACKETS); +static MemoryDynamic dynamicPool; Allocator &packetPool = dynamicPool; #else // Embedded targets use static memory pools with compile-time constants From 569c0d22067beb7860c0e67b43b4e728fc10a8b2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 13 Sep 2025 16:31:56 -0500 Subject: [PATCH 035/108] Update meshtastic-esp8266-oled-ssd1306 digest to 0cbc26b (#7977) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 81bda239c80..64e827b3efb 100644 --- a/platformio.ini +++ b/platformio.ini @@ -60,7 +60,7 @@ monitor_speed = 115200 monitor_filters = direct lib_deps = # renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master - https://github.com/meshtastic/esp8266-oled-ssd1306/archive/9573abb64dc9c94f3051348f2bf4fc5cedf03c22.zip + https://github.com/meshtastic/esp8266-oled-ssd1306/archive/0cbc26b1f8f61957af0475f486b362eafe7cc4e2.zip # renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip # renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master From 2fe065ddc9b17a7e11fbede0c1a2acee0113d744 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 18:52:46 -0500 Subject: [PATCH 036/108] Fix json report crashes on esp32 (#7978) --- src/mesh/http/ContentHandler.cpp | 39 ++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/mesh/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp index 74953d8fcc8..fb66dae7c6e 100644 --- a/src/mesh/http/ContentHandler.cpp +++ b/src/mesh/http/ContentHandler.cpp @@ -292,11 +292,14 @@ JSONArray htmlListDir(const char *dirname, uint8_t levels) JSONObject thisFileMap; thisFileMap["size"] = new JSONValue((int)file.size()); #ifdef ARCH_ESP32 - thisFileMap["name"] = new JSONValue(String(file.path()).substring(1).c_str()); + String fileName = String(file.path()).substring(1); + thisFileMap["name"] = new JSONValue(fileName.c_str()); #else - thisFileMap["name"] = new JSONValue(String(file.name()).substring(1).c_str()); + String fileName = String(file.name()).substring(1); + thisFileMap["name"] = new JSONValue(fileName.c_str()); #endif - if (String(file.name()).substring(1).endsWith(".gz")) { + String tempName = String(file.name()).substring(1); + if (tempName.endsWith(".gz")) { #ifdef ARCH_ESP32 String modifiedFile = String(file.path()).substring(1); #else @@ -339,7 +342,8 @@ void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res) JSONValue *value = new JSONValue(jsonObjOuter); - res->print(value->Stringify().c_str()); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); delete value; @@ -367,7 +371,8 @@ void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res) JSONObject jsonObjOuter; jsonObjOuter["status"] = new JSONValue("ok"); JSONValue *value = new JSONValue(jsonObjOuter); - res->print(value->Stringify().c_str()); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); delete value; return; } else { @@ -376,7 +381,8 @@ void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res) JSONObject jsonObjOuter; jsonObjOuter["status"] = new JSONValue("Error"); JSONValue *value = new JSONValue(jsonObjOuter); - res->print(value->Stringify().c_str()); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); delete value; return; } @@ -622,10 +628,7 @@ void handleReport(HTTPRequest *req, HTTPResponse *res) tempArray.push_back(new JSONValue((int)logArray[i])); } JSONValue *result = new JSONValue(tempArray); - // Clean up original array to prevent memory leak - for (auto *val : tempArray) { - delete val; - } + // Note: Don't delete tempArray elements here - JSONValue now owns them return result; }; @@ -656,7 +659,9 @@ void handleReport(HTTPRequest *req, HTTPResponse *res) // data->wifi JSONObject jsonObjWifi; jsonObjWifi["rssi"] = new JSONValue(WiFi.RSSI()); - jsonObjWifi["ip"] = new JSONValue(WiFi.localIP().toString().c_str()); + String wifiIPString = WiFi.localIP().toString(); + std::string wifiIP = wifiIPString.c_str(); + jsonObjWifi["ip"] = new JSONValue(wifiIP.c_str()); // data->memory JSONObject jsonObjMemory; @@ -702,7 +707,8 @@ void handleReport(HTTPRequest *req, HTTPResponse *res) jsonObjOuter["status"] = new JSONValue("ok"); // serialize and write it to the stream JSONValue *value = new JSONValue(jsonObjOuter); - res->print(value->Stringify().c_str()); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); delete value; } @@ -773,7 +779,8 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res) jsonObjOuter["status"] = new JSONValue("ok"); // serialize and write it to the stream JSONValue *value = new JSONValue(jsonObjOuter); - res->print(value->Stringify().c_str()); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); delete value; // Clean up the nodesArray to prevent memory leak @@ -926,7 +933,8 @@ void handleBlinkLED(HTTPRequest *req, HTTPResponse *res) JSONObject jsonObjOuter; jsonObjOuter["status"] = new JSONValue("ok"); JSONValue *value = new JSONValue(jsonObjOuter); - res->print(value->Stringify().c_str()); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); delete value; } @@ -968,7 +976,8 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res) // serialize and write it to the stream JSONValue *value = new JSONValue(jsonObjOuter); - res->print(value->Stringify().c_str()); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); delete value; // Clean up the networkObjs to prevent memory leak From d9eb18fdff5eab0f78e065644eb8d05fa5ed0801 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 18:57:00 -0500 Subject: [PATCH 037/108] Tweak maximums --- src/mesh/mesh-pb-constants.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mesh/mesh-pb-constants.h b/src/mesh/mesh-pb-constants.h index 12aec98cd5d..868670f42b1 100644 --- a/src/mesh/mesh-pb-constants.h +++ b/src/mesh/mesh-pb-constants.h @@ -20,12 +20,12 @@ /// max number of QueueStatus packets which can be waiting for delivery to phone #ifndef MAX_RX_QUEUESTATUS_TOPHONE -#define MAX_RX_QUEUESTATUS_TOPHONE 4 +#define MAX_RX_QUEUESTATUS_TOPHONE 2 #endif /// max number of MqttClientProxyMessage packets which can be waiting for delivery to phone #ifndef MAX_RX_MQTTPROXY_TOPHONE -#define MAX_RX_MQTTPROXY_TOPHONE 32 +#define MAX_RX_MQTTPROXY_TOPHONE 16 #endif /// max number of ClientNotification packets which can be waiting for delivery to phone From 9fdd31a0484c0b594467a669cf837545c4d0f3a6 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 20:14:10 -0500 Subject: [PATCH 038/108] Fix DRAM overflow on old esp32 targets --- src/mesh/mesh-pb-constants.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/mesh/mesh-pb-constants.h b/src/mesh/mesh-pb-constants.h index 868670f42b1..e4f65aa283e 100644 --- a/src/mesh/mesh-pb-constants.h +++ b/src/mesh/mesh-pb-constants.h @@ -15,8 +15,12 @@ // FIXME - max_count is actually 32 but we save/load this as one long string of preencoded MeshPacket bytes - not a big array in // RAM #define MAX_RX_TOPHONE (member_size(DeviceState, receive_queue) / member_size(DeviceState, receive_queue[0])) #ifndef MAX_RX_TOPHONE +#if defined(ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3)) +#define MAX_RX_TOPHONE 8 +#else #define MAX_RX_TOPHONE 32 #endif +#endif /// max number of QueueStatus packets which can be waiting for delivery to phone #ifndef MAX_RX_QUEUESTATUS_TOPHONE @@ -25,7 +29,7 @@ /// max number of MqttClientProxyMessage packets which can be waiting for delivery to phone #ifndef MAX_RX_MQTTPROXY_TOPHONE -#define MAX_RX_MQTTPROXY_TOPHONE 16 +#define MAX_RX_MQTTPROXY_TOPHONE 8 #endif /// max number of ClientNotification packets which can be waiting for delivery to phone From e9fb1b5ff6e0db9d07683700c01341a467dc2874 Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Mon, 8 Sep 2025 10:29:26 +1000 Subject: [PATCH 039/108] Guard bad time warning logs using GPS_DEBUG (#7897) In 2.7.7 / 2.7.8 we introduced some new checks for time accuracy. In combination, these result in a spamming of the logs when a bad time is found When the GPS is active, we're calling the GPS thread every 0.2secs. So this log could be printed 4,500 times in a no-lock scenario :) Reserve this experience for developers using GPS_DEBUG. Fixes https://github.com/meshtastic/firmware/issues/7896 --- src/gps/RTC.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index e208e2df9cc..39b633e477e 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -130,11 +130,15 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd uint32_t printableEpoch = tv->tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH if (tv->tv_sec < BUILD_EPOCH) { +#ifdef GPS_DEBUG LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); +#endif return RTCSetResultInvalidTime; } else if (tv->tv_sec > (BUILD_EPOCH + FORTY_YEARS)) { +#ifdef GPS_DEBUG LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH, BUILD_EPOCH + FORTY_YEARS); +#endif return RTCSetResultInvalidTime; } #endif @@ -252,11 +256,15 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t) uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH if (tv.tv_sec < BUILD_EPOCH) { +#ifdef GPS_DEBUG LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); +#endif return RTCSetResultInvalidTime; } else if (tv.tv_sec > (BUILD_EPOCH + FORTY_YEARS)) { +#ifdef GPS_DEBUG LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH, BUILD_EPOCH + FORTY_YEARS); +#endif return RTCSetResultInvalidTime; } #endif From 4b4609f2645baadcecad865f5d677d8125d220c3 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 14 Sep 2025 06:31:17 -0500 Subject: [PATCH 040/108] Scale probe buffer size based on current baud rate (#7975) * Scale probe buffer size based on current baud rate * Throttle bad time validation logging and fix time comparison logic * Remove comment * Missed the other instances * Copy pasta --- src/gps/GPS.cpp | 19 +++++++++++++---- src/gps/GPS.h | 2 +- src/gps/RTC.cpp | 54 ++++++++++++++++++++++++++++++++----------------- src/gps/RTC.h | 2 +- 4 files changed, 53 insertions(+), 24 deletions(-) diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index d4e9076d9e0..a663f46c489 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -1205,7 +1205,7 @@ static const char *DETECTED_MESSAGE = "%s detected"; LOG_DEBUG(PROBE_MESSAGE, COMMAND, FAMILY_NAME); \ clearBuffer(); \ _serial_gps->write(COMMAND "\r\n"); \ - GnssModel_t detectedDriver = getProbeResponse(TIMEOUT, RESPONSE_MAP); \ + GnssModel_t detectedDriver = getProbeResponse(TIMEOUT, RESPONSE_MAP, serialSpeed); \ if (detectedDriver != GNSS_MODEL_UNKNOWN) { \ return detectedDriver; \ } \ @@ -1367,9 +1367,18 @@ GnssModel_t GPS::probe(int serialSpeed) return GNSS_MODEL_UNKNOWN; } -GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector &responseMap) +GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector &responseMap, int serialSpeed) { - char response[256] = {0}; // Fixed buffer instead of String + // Calculate buffer size based on baud rate - 256 bytes for 9600 baud as baseline + // Higher baud rates get proportionally larger buffers to handle more data + int bufferSize = (serialSpeed * 256) / 9600; + // Clamp buffer size between reasonable limits + if (bufferSize < 128) + bufferSize = 128; + if (bufferSize > 2048) + bufferSize = 2048; + + char *response = new char[bufferSize](); // Dynamically allocate based on baud rate uint16_t responseLen = 0; unsigned long start = millis(); while (millis() - start < timeout) { @@ -1377,7 +1386,7 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vectorread(); // Add char to buffer if there's space - if (responseLen < sizeof(response) - 1) { + if (responseLen < bufferSize - 1) { response[responseLen++] = c; response[responseLen] = '\0'; } @@ -1390,6 +1399,7 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector &responseMap); + GnssModel_t getProbeResponse(unsigned long timeout, const std::vector &responseMap, int serialSpeed); // Get GNSS model GnssModel_t probe(int serialSpeed); diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index 39b633e477e..3e410d236b7 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -9,6 +9,9 @@ static RTCQuality currentQuality = RTCQualityNone; uint32_t lastSetFromPhoneNtpOrGps = 0; +static uint32_t lastTimeValidationWarning = 0; +static const uint32_t TIME_VALIDATION_WARNING_INTERVAL_MS = 15000; // 15 seconds + RTCQuality getRTCQuality() { return currentQuality; @@ -48,7 +51,9 @@ RTCSetResult readFromRTC() #ifdef BUILD_EPOCH if (tv.tv_sec < BUILD_EPOCH) { - LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + } return RTCSetResultInvalidTime; } #endif @@ -87,7 +92,10 @@ RTCSetResult readFromRTC() #ifdef BUILD_EPOCH if (tv.tv_sec < BUILD_EPOCH) { - LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + lastTimeValidationWarning = millis(); + } return RTCSetResultInvalidTime; } #endif @@ -130,15 +138,20 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd uint32_t printableEpoch = tv->tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH if (tv->tv_sec < BUILD_EPOCH) { -#ifdef GPS_DEBUG - LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); -#endif + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + lastTimeValidationWarning = millis(); + } return RTCSetResultInvalidTime; - } else if (tv->tv_sec > (BUILD_EPOCH + FORTY_YEARS)) { -#ifdef GPS_DEBUG - LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH, - BUILD_EPOCH + FORTY_YEARS); -#endif + } else if (tv->tv_sec > (time_t)(BUILD_EPOCH + FORTY_YEARS)) { + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + // Calculate max allowed time safely to avoid overflow in logging + uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS; + uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime; + LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, + (uint32_t)BUILD_EPOCH, maxAllowedPrintable); + lastTimeValidationWarning = millis(); + } return RTCSetResultInvalidTime; } #endif @@ -256,15 +269,20 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t) uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH if (tv.tv_sec < BUILD_EPOCH) { -#ifdef GPS_DEBUG - LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); -#endif + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + lastTimeValidationWarning = millis(); + } return RTCSetResultInvalidTime; - } else if (tv.tv_sec > (BUILD_EPOCH + FORTY_YEARS)) { -#ifdef GPS_DEBUG - LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH, - BUILD_EPOCH + FORTY_YEARS); -#endif + } else if (tv.tv_sec > (time_t)(BUILD_EPOCH + FORTY_YEARS)) { + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + // Calculate max allowed time safely to avoid overflow in logging + uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS; + uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime; + LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, + (uint32_t)BUILD_EPOCH, maxAllowedPrintable); + lastTimeValidationWarning = millis(); + } return RTCSetResultInvalidTime; } #endif diff --git a/src/gps/RTC.h b/src/gps/RTC.h index 03350823cb9..1ecde79aee8 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -56,5 +56,5 @@ time_t gm_mktime(struct tm *tm); #define SEC_PER_HOUR 3600 #define SEC_PER_MIN 60 #ifdef BUILD_EPOCH -#define FORTY_YEARS (40UL * 365 * SEC_PER_DAY) // probably time to update your firmware +#define FORTY_YEARS (40ULL * 365 * SEC_PER_DAY) // Use 64-bit arithmetic to prevent overflow #endif From aea2072128ea6c3be80effc2fba3e797a683a7cc Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Sun, 14 Sep 2025 03:05:06 -0700 Subject: [PATCH 041/108] Fix GPS gm_mktime memory leak (#7981) --- src/gps/RTC.cpp | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index 3e410d236b7..4a629d755d1 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -342,14 +342,40 @@ uint32_t getValidTime(RTCQuality minQuality, bool local) time_t gm_mktime(struct tm *tm) { #if !MESHTASTIC_EXCLUDE_TZ - setenv("TZ", "GMT0", 1); - time_t res = mktime(tm); - if (*config.device.tzdef) { - setenv("TZ", config.device.tzdef, 1); - } else { - setenv("TZ", "UTC0", 1); + time_t result = 0; + + // First, get us to the start of tm->year, by calcuating the number of days since the Unix epoch. + int year = 1900 + tm->tm_year; // tm_year is years since 1900 + int year_minus_one = year - 1; + int days_before_this_year = 0; + days_before_this_year += year_minus_one * 365; + // leap days: every 4 years, except 100s, but including 400s. + days_before_this_year += year_minus_one / 4 - year_minus_one / 100 + year_minus_one / 400; + // subtract from 1970-01-01 to get days since epoch + days_before_this_year -= 719162; // (1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400); + + // Now, within this tm->year, compute the days *before* this tm->month starts. + int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year + int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11 + + // If this is a leap year, and we're past February, add a day: + if (tm->tm_mon >= 2 && (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0)) { + days_this_year_before_this_month += 1; } - return res; + + // And within this month: + int days_this_month_before_today = tm->tm_mday - 1; // tm->tm_mday is 1..31 + + // Now combine them all together, and convert days to seconds: + result += (days_before_this_year + days_this_year_before_this_month + days_this_month_before_today); + result *= 86400L; + + // Finally, add in the hours, minutes, and seconds of today: + result += tm->tm_hour * 3600; + result += tm->tm_min * 60; + result += tm->tm_sec; + + return result; #else return mktime(tm); #endif From 520c47956522870e71ff101184cdbeffce6bf763 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 14 Sep 2025 08:12:38 -0500 Subject: [PATCH 042/108] Fix overflow of time value (#7984) * Fix overflow of time value * Revert "Fix overflow of time value" This reverts commit 084796920179e80a7500d36c25fd4d82b3ef4214. * That got boogered up --- src/gps/RTC.cpp | 4 ++-- src/gps/RTC.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index 4a629d755d1..da20e28eb96 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -143,7 +143,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd lastTimeValidationWarning = millis(); } return RTCSetResultInvalidTime; - } else if (tv->tv_sec > (time_t)(BUILD_EPOCH + FORTY_YEARS)) { + } else if ((uint64_t)tv->tv_sec > ((uint64_t)BUILD_EPOCH + FORTY_YEARS)) { if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { // Calculate max allowed time safely to avoid overflow in logging uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS; @@ -274,7 +274,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t) lastTimeValidationWarning = millis(); } return RTCSetResultInvalidTime; - } else if (tv.tv_sec > (time_t)(BUILD_EPOCH + FORTY_YEARS)) { + } else if ((uint64_t)tv.tv_sec > ((uint64_t)BUILD_EPOCH + FORTY_YEARS)) { if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { // Calculate max allowed time safely to avoid overflow in logging uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS; diff --git a/src/gps/RTC.h b/src/gps/RTC.h index 1ecde79aee8..eca17bf3527 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -56,5 +56,5 @@ time_t gm_mktime(struct tm *tm); #define SEC_PER_HOUR 3600 #define SEC_PER_MIN 60 #ifdef BUILD_EPOCH -#define FORTY_YEARS (40ULL * 365 * SEC_PER_DAY) // Use 64-bit arithmetic to prevent overflow +static constexpr uint64_t FORTY_YEARS = (40ULL * 365 * SEC_PER_DAY); // Use 64-bit arithmetic to prevent overflow #endif From 7c1fea039b5bbf9c854d8637115dcbe9cd74ecb9 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sun, 14 Sep 2025 19:38:18 +0200 Subject: [PATCH 043/108] Remove PMSA003 include from modules --- src/modules/Modules.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index f8ac9f2585b..15cd87d660b 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -242,11 +242,9 @@ void setupModules() (moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled)) { new EnvironmentTelemetryModule(); } -#if __has_include("Adafruit_PM25AQI.h") if (moduleConfig.has_telemetry && moduleConfig.telemetry.air_quality_enabled) { new AirQualityTelemetryModule(); } -#endif #if !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX30102].first > 0 || nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MLX90614].first > 0) { From 44f5e060ff2598b1df837aab7f23d6fb7d2a4b4a Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 7 Jan 2026 12:38:09 +0100 Subject: [PATCH 044/108] Add flag to exclude air quality module --- src/configuration.h | 1 + src/modules/Modules.cpp | 8 ++++---- src/modules/Telemetry/AirQualityTelemetry.cpp | 9 ++++----- src/modules/Telemetry/AirQualityTelemetry.h | 2 +- src/modules/Telemetry/Sensor/PMSA003ISensor.cpp | 2 +- variants/esp32/heltec_wireless_bridge/platformio.ini | 5 +++-- 6 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/configuration.h b/src/configuration.h index b4ab570531e..73d9fb0fd70 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -468,6 +468,7 @@ along with this program. If not, see . #define MESHTASTIC_EXCLUDE_AUDIO 1 #define MESHTASTIC_EXCLUDE_DETECTIONSENSOR 1 #define MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR 1 +#define MESHTASTIC_EXCLUDE_AIR_QUALITY 1 #define MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY 1 #define MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION 1 #define MESHTASTIC_EXCLUDE_PAXCOUNTER 1 diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index d17730178ab..10a1a9a1eb2 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -252,12 +252,12 @@ void setupModules() (moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled)) { new EnvironmentTelemetryModule(); } - - if (moduleConfig.has_telemetry && moduleConfig.telemetry.air_quality_enabled && - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) { +#if HAS_TELEMETRY && HAS_SENSOR && !MESHTASTIC_EXCLUDE_AIR_QUALITY + if (moduleConfig.has_telemetry && + (moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_screen_enabled)) { new AirQualityTelemetryModule(); } - +#endif #if !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX30102].first > 0 || nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MLX90614].first > 0) { diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index df193c8a528..6e99205a69f 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "Default.h" @@ -42,11 +42,10 @@ int32_t AirQualityTelemetryModule::runOnce() */ // 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_screen_enabled = 1; // moduleConfig.telemetry.air_quality_interval = 15; - if (!(moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.environment_screen_enabled || + if (!(moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_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(); @@ -104,7 +103,7 @@ int32_t AirQualityTelemetryModule::runOnce() bool AirQualityTelemetryModule::wantUIFrame() { - return moduleConfig.telemetry.environment_screen_enabled; + return moduleConfig.telemetry.air_quality_screen_enabled; } void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) diff --git a/src/modules/Telemetry/AirQualityTelemetry.h b/src/modules/Telemetry/AirQualityTelemetry.h index 8314c54bc28..b76ae589790 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.h +++ b/src/modules/Telemetry/AirQualityTelemetry.h @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY #pragma once diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index c83c3e01aca..8dc4c7dcee1 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "PMSA003ISensor.h" diff --git a/variants/esp32/heltec_wireless_bridge/platformio.ini b/variants/esp32/heltec_wireless_bridge/platformio.ini index 93c3e3394c2..62be4b45ed4 100644 --- a/variants/esp32/heltec_wireless_bridge/platformio.ini +++ b/variants/esp32/heltec_wireless_bridge/platformio.ini @@ -1,9 +1,9 @@ [env:heltec-wireless-bridge] -;build_type = debug ; to make it possible to step through our jtag debugger +;build_type = debug ; to make it possible to step through our jtag debugger extends = esp32_base board_level = extra board = heltec_wifi_lora_32 -build_flags = +build_flags = ${esp32_base.build_flags} -I variants/esp32/heltec_wireless_bridge -D HELTEC_WIRELESS_BRIDGE @@ -13,6 +13,7 @@ build_flags = -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 -D MESHTASTIC_EXCLUDE_DETECTIONSENSOR=1 -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 + -D MESHTASTIC_EXCLUDE_AIR_QUALITY=1 -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL=1 -D MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION=1 -D MESHTASTIC_EXCLUDE_GPS=1 From d893954daa775b8ec413d1525ce3c37b1ad5b20d Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Thu, 8 Jan 2026 17:52:38 +0100 Subject: [PATCH 045/108] Rework PMSA003I to align with new I2C scanner * Reworks AQ telemetry to match new dynamic allocation method * Adds VBLE_I2C_CLOCK_SPEED build flag for sensors with different I2C speed requirements * Reworks PMSA003I --- src/configuration.h | 4 +- src/detect/ScanI2C.cpp | 2 +- src/detect/ScanI2C.h | 2 +- src/detect/ScanI2CTwoWire.cpp | 2 +- src/main.cpp | 1 - src/modules/Modules.cpp | 2 +- src/modules/Telemetry/AirQualityTelemetry.cpp | 87 +++++++++++++++---- src/modules/Telemetry/AirQualityTelemetry.h | 11 ++- .../Telemetry/Sensor/PMSA003ISensor.cpp | 64 +++++++------- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 14 +-- .../heltec_wireless_bridge/platformio.ini | 2 +- 11 files changed, 121 insertions(+), 70 deletions(-) diff --git a/src/configuration.h b/src/configuration.h index 73d9fb0fd70..e81caf89d5e 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -214,7 +214,7 @@ along with this program. If not, see . #define LPS22HB_ADDR_ALT 0x5D #define SHT31_4x_ADDR 0x44 #define SHT31_4x_ADDR_ALT 0x45 -#define PMSA0031_ADDR 0x12 +#define PMSA003I_ADDR 0x12 #define QMA6100P_ADDR 0x12 #define AHT10_ADDR 0x38 #define RCWL9620_ADDR 0x57 @@ -468,7 +468,7 @@ along with this program. If not, see . #define MESHTASTIC_EXCLUDE_AUDIO 1 #define MESHTASTIC_EXCLUDE_DETECTIONSENSOR 1 #define MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR 1 -#define MESHTASTIC_EXCLUDE_AIR_QUALITY 1 +#define MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR 1 #define MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY 1 #define MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION 1 #define MESHTASTIC_EXCLUDE_PAXCOUNTER 1 diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp index 8ac503b83fe..165f7a99bca 100644 --- a/src/detect/ScanI2C.cpp +++ b/src/detect/ScanI2C.cpp @@ -43,7 +43,7 @@ ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const ScanI2C::FoundDevice ScanI2C::firstAQI() const { - ScanI2C::DeviceType types[] = {PMSA0031, SCD4X}; + ScanI2C::DeviceType types[] = {PMSA003I, SCD4X}; return firstOfOrNONE(2, types); } diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index cced980a66d..8c6880d7d32 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -38,7 +38,7 @@ class ScanI2C QMI8658, QMC5883L, HMC5883L, - PMSA0031, + PMSA003I, QMA6100P, MPU6050, LIS3DH, diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 45e7fda2d1b..78caa2e7623 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -438,7 +438,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) #ifdef HAS_QMA6100P SCAN_SIMPLE_CASE(QMA6100P_ADDR, QMA6100P, "QMA6100P", (uint8_t)addr.address) #else - SCAN_SIMPLE_CASE(PMSA0031_ADDR, PMSA0031, "PMSA0031", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(PMSA003I_ADDR, PMSA003I, "PMSA003I", (uint8_t)addr.address) #endif case BMA423_ADDR: // this can also be LIS3DH_ADDR_ALT registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2); diff --git a/src/main.cpp b/src/main.cpp index 4af275bf4c3..8986fb417dc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -753,7 +753,6 @@ void setup() scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMI8658, meshtastic_TelemetrySensorType_QMI8658); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC5883L, meshtastic_TelemetrySensorType_QMC5883L); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::HMC5883L, meshtastic_TelemetrySensorType_QMC5883L); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::PMSA0031, meshtastic_TelemetrySensorType_PMSA003I); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102); diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 10a1a9a1eb2..e17868bafd6 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -252,7 +252,7 @@ void setupModules() (moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled)) { new EnvironmentTelemetryModule(); } -#if HAS_TELEMETRY && HAS_SENSOR && !MESHTASTIC_EXCLUDE_AIR_QUALITY +#if HAS_TELEMETRY && HAS_SENSOR && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR if (moduleConfig.has_telemetry && (moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_screen_enabled)) { new AirQualityTelemetryModule(); diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 6e99205a69f..5966d461392 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "Default.h" @@ -13,16 +13,61 @@ #include "UnitConversions.h" #include "graphics/SharedUIDisplay.h" #include "graphics/images.h" +#include "graphics/ScreenFonts.h" #include "main.h" #include "sleep.h" #include -// Sensor includes -#include "Sensor/PMSA003ISensor.h" // Sensors -PMSA003ISensor pmsa003iSensor; +#ifdef VBLE_I2C_CLOCK_SPEED +#include "Sensor/PMSA003ISensor.h" +#endif -#include "graphics/ScreenFonts.h" +#include + +static std::forward_list sensors; + +template void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type) +{ + ScanI2C::FoundDevice dev = i2cScanner->find(type); + if (dev.type != ScanI2C::DeviceType::NONE || type == ScanI2C::DeviceType::NONE) { + TelemetrySensor *sensor = new T(); +#if WIRE_INTERFACES_COUNT > 1 + TwoWire *bus = ScanI2CTwoWire::fetchI2CBus(dev.address); + if (dev.address.port != ScanI2C::I2CPort::WIRE1 && sensor->onlyWire1()) { + // This sensor only works on Wire (Wire1 is not supported) + delete sensor; + return; + } +#else + TwoWire *bus = &Wire; +#endif + if (sensor->initDevice(bus, &dev)) { + sensors.push_front(sensor); + return; + } + // destroy sensor + delete sensor; + } +} + +void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) +{ + if (!moduleConfig.telemetry.air_quality_enabled && !AIR_QUALITY_TELEMETRY_MODULE_ENABLE) { + return; + } + LOG_INFO("Air Quality Telemetry adding I2C devices..."); + + // order by priority of metrics/values (low top, high bottom) + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR +// Sensors that require variable I2C clock speed +#ifdef VBLE_I2C_CLOCK_SPEED + addSensor(i2cScanner, ScanI2C::DeviceType::PMSA003I); +#endif +#endif + +} int32_t AirQualityTelemetryModule::runOnce() { @@ -58,8 +103,11 @@ int32_t AirQualityTelemetryModule::runOnce() if (moduleConfig.telemetry.air_quality_enabled) { LOG_INFO("Air quality Telemetry: init"); - if (pmsa003iSensor.hasSensor()) - result = pmsa003iSensor.runOnce(); + // check if we have at least one sensor + if (!sensors.empty()) { + result = DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + } // it's possible to have this module enabled, only for displaying values on the screen. @@ -72,10 +120,12 @@ int32_t AirQualityTelemetryModule::runOnce() } // Wake up the sensors that need it +// #ifdef VBLE_I2C_CLOCK_SPEED #ifdef PMSA003I_ENABLE_PIN if (pmsa003iSensor.hasSensor() && !pmsa003iSensor.isActive()) return pmsa003iSensor.wakeUp(); #endif /* PMSA003I_ENABLE_PIN */ +// #endif if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( @@ -93,9 +143,12 @@ int32_t AirQualityTelemetryModule::runOnce() lastSentToPhone = millis(); } +// Send to sleep sensors that consume power +// #ifdef VBLE_I2C_CLOCK_SPEED #ifdef PMSA003I_ENABLE_PIN pmsa003iSensor.sleep(); #endif /* PMSA003I_ENABLE_PIN */ +// #endif } return min(sendToPhoneIntervalMs, result); @@ -228,10 +281,11 @@ bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag; m->variant.air_quality_metrics = meshtastic_AirQualityMetrics_init_zero; - 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); + // TODO - Should we check for sensor state here? + // If a sensor is sleeping, we should know and check to wake it up + for (TelemetrySensor *sensor : sensors) { + LOG_INFO("Reading AQ sensors"); + valid = valid && sensor->getMetrics(m); hasSensor = true; } @@ -322,18 +376,13 @@ AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule( meshtastic_AdminMessage *response) { 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); + + for (TelemetrySensor *sensor : sensors) { + result = sensor->handleAdminMessage(mp, request, response); if (result != AdminMessageHandleResult::NOT_HANDLED) return result; } - -#endif return result; } diff --git a/src/modules/Telemetry/AirQualityTelemetry.h b/src/modules/Telemetry/AirQualityTelemetry.h index b76ae589790..af9c4ebc000 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.h +++ b/src/modules/Telemetry/AirQualityTelemetry.h @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_AIR_QUALITY +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #pragma once @@ -11,10 +11,13 @@ #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "NodeDB.h" #include "ProtobufModule.h" +#include "detect/ScanI2CConsumer.h" #include #include -class AirQualityTelemetryModule : private concurrency::OSThread, public ProtobufModule +class AirQualityTelemetryModule : private concurrency::OSThread, + public ScanI2CConsumer, + public ProtobufModule { CallbackObserver nodeStatusObserver = CallbackObserver(this, @@ -22,7 +25,7 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public Protobuf public: AirQualityTelemetryModule() - : concurrency::OSThread("AirQualityTelemetry"), + : concurrency::OSThread("AirQualityTelemetry"), ScanI2CConsumer(), ProtobufModule("AirQualityTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { lastMeasurementPacket = nullptr; @@ -55,6 +58,8 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public Protobuf virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) override; + void i2cScanFinished(ScanI2C *i2cScanner); + private: bool firstTime = true; meshtastic_MeshPacket *lastMeasurementPacket; diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 8dc4c7dcee1..25988380345 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_AIR_QUALITY +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && defined(VBLE_I2C_CLOCK_SPEED) #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "PMSA003ISensor.h" @@ -13,47 +13,29 @@ PMSA003ISensor::PMSA003ISensor() { } -void PMSA003ISensor::setup() +bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); #ifdef PMSA003I_ENABLE_PIN pinMode(PMSA003I_ENABLE_PIN, OUTPUT); #endif -} - -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 -} -int32_t PMSA003ISensor::runOnce() -{ - 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; + _bus = bus; + _address = dev->address.address; #ifdef PMSA003I_I2C_CLOCK_SPEED uint32_t currentClock; - currentClock = bus->getClock(); + 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); + _bus->setClock(PMSA003I_I2C_CLOCK_SPEED); } #endif - bus->beginTransmission(address); - if (bus->endTransmission() != 0) { + _bus->beginTransmission(_address); + if (_bus->endTransmission() != 0) { LOG_WARN("PMSA003I not found on I2C at 0x12"); - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + return false; } #ifdef PMSA003I_I2C_CLOCK_SPEED @@ -63,7 +45,18 @@ int32_t PMSA003ISensor::runOnce() status = 1; LOG_INFO("PMSA003I Enabled"); - return initI2CSensor(); + initI2CSensor(); + return true; +} + +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 } bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) @@ -75,16 +68,16 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) #ifdef PMSA003I_I2C_CLOCK_SPEED uint32_t currentClock; - currentClock = bus->getClock(); + 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); + _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()); + _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; } @@ -93,7 +86,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) #endif for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) { - buffer[i] = bus->read(); + buffer[i] = _bus->read(); } if (buffer[0] != 0x42 || buffer[1] != 0x4D) { @@ -170,6 +163,7 @@ void PMSA003ISensor::sleep() uint32_t PMSA003ISensor::wakeUp() { + LOG_INFO('Waking up PMSA003I') digitalWrite(PMSA003I_ENABLE_PIN, HIGH); state = State::ACTIVE; return PMSA003I_WARMUP_MS; diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index 35a4df735e1..aa926028e42 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -1,5 +1,8 @@ -#pragma once +#include "configuration.h" +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && defined(VBLE_I2C_CLOCK_SPEED) + +#include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #define PMSA003I_I2C_CLOCK_SPEED 100000 @@ -10,11 +13,10 @@ class PMSA003ISensor : public TelemetrySensor { public: PMSA003ISensor(); - virtual void setup() override; - virtual int32_t runOnce() override; virtual bool restoreClock(uint32_t currentClock); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool isActive(); + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; #ifdef PMSA003I_ENABLE_PIN void sleep(); @@ -24,11 +26,13 @@ class PMSA003ISensor : public TelemetrySensor private: enum class State { IDLE, ACTIVE }; State state = State::ACTIVE; - TwoWire * bus; - uint8_t address; + TwoWire * _bus; + uint8_t _address; uint16_t computedChecksum = 0; uint16_t receivedChecksum = 0; uint8_t buffer[PMSA003I_FRAME_LENGTH]; }; + +#endif \ No newline at end of file diff --git a/variants/esp32/heltec_wireless_bridge/platformio.ini b/variants/esp32/heltec_wireless_bridge/platformio.ini index 62be4b45ed4..6f9de7a8472 100644 --- a/variants/esp32/heltec_wireless_bridge/platformio.ini +++ b/variants/esp32/heltec_wireless_bridge/platformio.ini @@ -13,7 +13,7 @@ build_flags = -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 -D MESHTASTIC_EXCLUDE_DETECTIONSENSOR=1 -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 - -D MESHTASTIC_EXCLUDE_AIR_QUALITY=1 + -D MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1 -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL=1 -D MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION=1 -D MESHTASTIC_EXCLUDE_GPS=1 From 23f82c92a7df72ec60aba889d5f77a62a1a5953e Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sat, 10 Jan 2026 11:26:50 +0100 Subject: [PATCH 046/108] Move add sensor template to separate file --- src/modules/Telemetry/AirQualityTelemetry.cpp | 29 +------------------ .../Telemetry/EnvironmentTelemetry.cpp | 29 +------------------ .../Telemetry/Sensor/AddI2CSensorTemplate.h | 26 +++++++++++++++++ 3 files changed, 28 insertions(+), 56 deletions(-) create mode 100644 src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 5966d461392..2d2cfa0b67f 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -17,40 +17,13 @@ #include "main.h" #include "sleep.h" #include +#include "Sensor/AddI2CSensorTemplate.h" // Sensors #ifdef VBLE_I2C_CLOCK_SPEED #include "Sensor/PMSA003ISensor.h" #endif -#include - -static std::forward_list sensors; - -template void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type) -{ - ScanI2C::FoundDevice dev = i2cScanner->find(type); - if (dev.type != ScanI2C::DeviceType::NONE || type == ScanI2C::DeviceType::NONE) { - TelemetrySensor *sensor = new T(); -#if WIRE_INTERFACES_COUNT > 1 - TwoWire *bus = ScanI2CTwoWire::fetchI2CBus(dev.address); - if (dev.address.port != ScanI2C::I2CPort::WIRE1 && sensor->onlyWire1()) { - // This sensor only works on Wire (Wire1 is not supported) - delete sensor; - return; - } -#else - TwoWire *bus = &Wire; -#endif - if (sensor->initDevice(bus, &dev)) { - sensors.push_front(sensor); - return; - } - // destroy sensor - delete sensor; - } -} - void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { if (!moduleConfig.telemetry.air_quality_enabled && !AIR_QUALITY_TELEMETRY_MODULE_ENABLE) { diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 5ded83a29ee..c394c6bb118 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -143,34 +143,7 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c #include "graphics/ScreenFonts.h" #include - -#include - -static std::forward_list sensors; - -template void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type) -{ - ScanI2C::FoundDevice dev = i2cScanner->find(type); - if (dev.type != ScanI2C::DeviceType::NONE || type == ScanI2C::DeviceType::NONE) { - TelemetrySensor *sensor = new T(); -#if WIRE_INTERFACES_COUNT > 1 - TwoWire *bus = ScanI2CTwoWire::fetchI2CBus(dev.address); - if (dev.address.port != ScanI2C::I2CPort::WIRE1 && sensor->onlyWire1()) { - // This sensor only works on Wire (Wire1 is not supported) - delete sensor; - return; - } -#else - TwoWire *bus = &Wire; -#endif - if (sensor->initDevice(bus, &dev)) { - sensors.push_front(sensor); - return; - } - // destroy sensor - delete sensor; - } -} +#include "Sensor/AddI2CSensorTemplate.h" void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { diff --git a/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h b/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h new file mode 100644 index 00000000000..3e213954d1c --- /dev/null +++ b/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h @@ -0,0 +1,26 @@ +#include +static std::forward_list sensors; + +template void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type) +{ + ScanI2C::FoundDevice dev = i2cScanner->find(type); + if (dev.type != ScanI2C::DeviceType::NONE || type == ScanI2C::DeviceType::NONE) { + TelemetrySensor *sensor = new T(); +#if WIRE_INTERFACES_COUNT > 1 + TwoWire *bus = ScanI2CTwoWire::fetchI2CBus(dev.address); + if (dev.address.port != ScanI2C::I2CPort::WIRE1 && sensor->onlyWire1()) { + // This sensor only works on Wire (Wire1 is not supported) + delete sensor; + return; + } +#else + TwoWire *bus = &Wire; +#endif + if (sensor->initDevice(bus, &dev)) { + sensors.push_front(sensor); + return; + } + // destroy sensor + delete sensor; + } +} \ No newline at end of file From 80f63b649823f6dfe430c8c60a4d3ccfed6c215a Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sat, 10 Jan 2026 11:28:22 +0100 Subject: [PATCH 047/108] Split telemetry on screen options --- src/graphics/draw/MenuHandler.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 586bdd4a66c..ef49a896e10 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -1541,7 +1541,8 @@ void menuHandler::FrameToggles_menu() lora, clock, show_favorites, - show_telemetry, + show_env_telemetry, + show_aq_telemetry, show_power, enumEnd }; @@ -1581,8 +1582,11 @@ void menuHandler::FrameToggles_menu() optionsArray[options] = screen->isFrameHidden("show_favorites") ? "Show Favorites" : "Hide Favorites"; optionsEnumArray[options++] = show_favorites; - optionsArray[options] = moduleConfig.telemetry.environment_screen_enabled ? "Hide Telemetry" : "Show Telemetry"; - optionsEnumArray[options++] = show_telemetry; + optionsArray[options] = moduleConfig.telemetry.environment_screen_enabled ? "Hide Env. Telemetry" : "Show Env. Telemetry"; + optionsEnumArray[options++] = show_env_telemetry; + + optionsArray[options] = moduleConfig.telemetry.air_quality_screen_enabled ? "Hide AQ Telemetry" : "Show AQ Telemetry"; + optionsEnumArray[options++] = show_aq_telemetry; optionsArray[options] = moduleConfig.telemetry.power_screen_enabled ? "Hide Power" : "Show Power"; optionsEnumArray[options++] = show_power; @@ -1641,10 +1645,14 @@ void menuHandler::FrameToggles_menu() screen->toggleFrameVisibility("show_favorites"); menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); - } else if (selected == show_telemetry) { + } else if (selected == show_env_telemetry) { moduleConfig.telemetry.environment_screen_enabled = !moduleConfig.telemetry.environment_screen_enabled; menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); + } else if (selected == show_aq_telemetry) { + moduleConfig.telemetry.air_quality_screen_enabled = !moduleConfig.telemetry.air_quality_screen_enabled; + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); } else if (selected == show_power) { moduleConfig.telemetry.power_screen_enabled = !moduleConfig.telemetry.power_screen_enabled; menuHandler::menuQueue = menuHandler::FrameToggles; From 998726e54e499e91b87a3a763f14e52d1e3fc792 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Mon, 12 Jan 2026 11:06:52 +0100 Subject: [PATCH 048/108] Add variable I2C clock compile flag * Added to Seeed Xiao S3 as demo --- variants/esp32s3/seeed_xiao_s3/variant.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/variants/esp32s3/seeed_xiao_s3/variant.h b/variants/esp32s3/seeed_xiao_s3/variant.h index d8dcbc8d4f9..40060052614 100644 --- a/variants/esp32s3/seeed_xiao_s3/variant.h +++ b/variants/esp32s3/seeed_xiao_s3/variant.h @@ -88,3 +88,5 @@ L76K GPS Module Information : https://www.seeedstudio.com/L76K-GNSS-Module-for-S #define SX126X_TXEN RADIOLIB_NC #define SX126X_DIO3_TCXO_VOLTAGE 1.8 #endif + +#define VBLE_I2C_CLOCK_SPEED From aa943a9e029f16f2801323881684ce3cd56b7d40 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Mon, 12 Jan 2026 12:07:33 +0100 Subject: [PATCH 049/108] Fix drawFrame in AQ module --- src/modules/Telemetry/AirQualityTelemetry.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 2d2cfa0b67f..4e648192099 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -132,6 +132,7 @@ bool AirQualityTelemetryModule::wantUIFrame() return moduleConfig.telemetry.air_quality_screen_enabled; } +#if HAS_SCREEN void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { // === Setup display === @@ -141,7 +142,7 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta int line = 1; // === Set Title - const char *titleStr = (graphics::isHighResolution) ? "Environment" : "Env."; + const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Air Quality" : "AQ."; // === Header === graphics::drawCommonHeader(display, x, y, titleStr); @@ -190,11 +191,11 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta std::vector entries; if (m.has_pm10_standard) - entries.push_back("PM1.0: " + String(m.pm10_standard) + "ug/m3"); + entries.push_back("PM1: " + String(m.pm10_standard) + "ug/m3"); if (m.has_pm25_standard) entries.push_back("PM2.5: " + String(m.pm25_standard) + "ug/m3"); if (m.has_pm100_standard) - entries.push_back("PM10.0: " + String(m.pm100_standard) + "ug/m3"); + entries.push_back("PM10: " + String(m.pm100_standard) + "ug/m3"); // === Show first available metric on top-right of first line === if (!entries.empty()) { @@ -220,7 +221,9 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta currentY += rowHeight; } + graphics::drawCommonFooter(display, x, y); } +#endif bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { From 7f35c38d848af1c5ff948b9e5481a0673c1a2f02 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Mon, 12 Jan 2026 12:08:39 +0100 Subject: [PATCH 050/108] Module settings override to i2cScan module function --- src/modules/Telemetry/AirQualityTelemetry.cpp | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 4e648192099..bfd90b69662 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -31,15 +31,24 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) } LOG_INFO("Air Quality Telemetry adding I2C devices..."); - // order by priority of metrics/values (low top, high bottom) + /* + Uncomment the preferences below if you want to use the module + without having to configure it from the PythonAPI or WebUI. + Note: this was previously on runOnce, which didnt take effect + as other modules already had already been initialized (screen) + */ + // moduleConfig.telemetry.air_quality_enabled = 1; + // moduleConfig.telemetry.air_quality_screen_enabled = 1; + // moduleConfig.telemetry.air_quality_interval = 15; + + // order by priority of metrics/values (low top, high bottom) #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR // Sensors that require variable I2C clock speed #ifdef VBLE_I2C_CLOCK_SPEED addSensor(i2cScanner, ScanI2C::DeviceType::PMSA003I); #endif #endif - } int32_t AirQualityTelemetryModule::runOnce() @@ -54,15 +63,6 @@ int32_t AirQualityTelemetryModule::runOnce() 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; - // moduleConfig.telemetry.air_quality_screen_enabled = 1; - // moduleConfig.telemetry.air_quality_interval = 15; - if (!(moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_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 From 25e431969e3fd34a6f6a7082a518a58cfd4e8d2e Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Mon, 12 Jan 2026 15:46:49 +0100 Subject: [PATCH 051/108] Move to CAN_RECLOCK_I2C per architecture * Add reclock function in TelemetrySensor.cpp * Add flag in ESP32 common --- src/modules/Telemetry/AirQualityTelemetry.cpp | 9 +--- .../Telemetry/Sensor/PMSA003ISensor.cpp | 43 ++++++------------- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 5 +-- .../Telemetry/Sensor/TelemetrySensor.cpp | 38 +++++++++++++++- .../Telemetry/Sensor/TelemetrySensor.h | 5 +++ variants/esp32/esp32-common.ini | 1 + variants/esp32s3/seeed_xiao_s3/variant.h | 2 - 7 files changed, 59 insertions(+), 44 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index bfd90b69662..58b66e1c347 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -20,9 +20,8 @@ #include "Sensor/AddI2CSensorTemplate.h" // Sensors -#ifdef VBLE_I2C_CLOCK_SPEED #include "Sensor/PMSA003ISensor.h" -#endif + void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { @@ -45,10 +44,8 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) // order by priority of metrics/values (low top, high bottom) #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR // Sensors that require variable I2C clock speed -#ifdef VBLE_I2C_CLOCK_SPEED addSensor(i2cScanner, ScanI2C::DeviceType::PMSA003I); #endif -#endif } int32_t AirQualityTelemetryModule::runOnce() @@ -93,12 +90,10 @@ int32_t AirQualityTelemetryModule::runOnce() } // Wake up the sensors that need it -// #ifdef VBLE_I2C_CLOCK_SPEED #ifdef PMSA003I_ENABLE_PIN if (pmsa003iSensor.hasSensor() && !pmsa003iSensor.isActive()) return pmsa003iSensor.wakeUp(); #endif /* PMSA003I_ENABLE_PIN */ -// #endif if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( @@ -117,11 +112,9 @@ int32_t AirQualityTelemetryModule::runOnce() } // Send to sleep sensors that consume power -// #ifdef VBLE_I2C_CLOCK_SPEED #ifdef PMSA003I_ENABLE_PIN pmsa003iSensor.sleep(); #endif /* PMSA003I_ENABLE_PIN */ -// #endif } return min(sendToPhoneIntervalMs, result); diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 25988380345..37b88090ca3 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && defined(VBLE_I2C_CLOCK_SPEED) +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "PMSA003ISensor.h" @@ -23,12 +23,11 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) _bus = bus; _address = dev->address.address; -#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); +#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + uint32_t currentClock = setClock(PMSA003I_I2C_CLOCK_SPEED); + if (!currentClock){ + LOG_WARN("PMSA003I can't be used at this clock speed"); + return false; } #endif @@ -38,8 +37,8 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) return false; } -#ifdef PMSA003I_I2C_CLOCK_SPEED - restoreClock(currentClock); +#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + setClock(currentClock); #endif status = 1; @@ -49,16 +48,6 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) return true; } -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 -} - bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) { if(!isActive()){ @@ -66,13 +55,8 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) 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); - } +#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + uint32_t currentClock = setClock(PMSA003I_I2C_CLOCK_SPEED); #endif _bus->requestFrom(_address, PMSA003I_FRAME_LENGTH); @@ -81,8 +65,8 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) return false; } -#ifdef PMSA003I_I2C_CLOCK_SPEED - restoreClock(currentClock); +#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + setClock(currentClock); #endif for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) { @@ -119,6 +103,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) measurement->variant.air_quality_metrics.has_pm100_standard = true; measurement->variant.air_quality_metrics.pm100_standard = read16(buffer, 8); + // TODO - Add admin command to remove environmental metrics to save protobuf space measurement->variant.air_quality_metrics.has_pm10_environmental = true; measurement->variant.air_quality_metrics.pm10_environmental = read16(buffer, 10); @@ -128,6 +113,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) measurement->variant.air_quality_metrics.has_pm100_environmental = true; measurement->variant.air_quality_metrics.pm100_environmental = read16(buffer, 14); + // TODO - Add admin command to remove PN to save protobuf space measurement->variant.air_quality_metrics.has_particles_03um = true; measurement->variant.air_quality_metrics.particles_03um = read16(buffer, 16); @@ -169,5 +155,4 @@ uint32_t PMSA003ISensor::wakeUp() return PMSA003I_WARMUP_MS; } #endif - #endif diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index aa926028e42..64a47ccaf3c 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && defined(VBLE_I2C_CLOCK_SPEED) +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" @@ -13,7 +13,6 @@ class PMSA003ISensor : public TelemetrySensor { public: PMSA003ISensor(); - virtual bool restoreClock(uint32_t currentClock); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool isActive(); virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; @@ -26,8 +25,6 @@ class PMSA003ISensor : public TelemetrySensor private: enum class State { IDLE, ACTIVE }; State state = State::ACTIVE; - TwoWire * _bus; - uint8_t _address; uint16_t computedChecksum = 0; uint16_t receivedChecksum = 0; diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.cpp b/src/modules/Telemetry/Sensor/TelemetrySensor.cpp index d6e7d1fac9d..c91f63a4c4c 100644 --- a/src/modules/Telemetry/Sensor/TelemetrySensor.cpp +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.cpp @@ -1,10 +1,46 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "NodeDB.h" #include "TelemetrySensor.h" #include "main.h" +#ifdef CAN_RECLOCK_I2C + +uint32_t TelemetrySensor::setClock(uint32_t desiredClock) { + + uint32_t currentClock; + + // See https://github.com/arduino/Arduino/issues/11457 + // Currently, only ESP32 can getClock + // While all cores can setClock() + // https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50 + // https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60 + // https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103 + +#ifdef ARCH_ESP32 + currentClock = _bus->getClock(); +#elif defined(ARCH_NRF52) + // TODO add getClock function or return a predefined clock speed per variant + return 0; +#elif defined(ARCH_RP2040) + // TODO add getClock function or return a predefined clock speed per variant + return 0; +#elif defined(ARCH_STM32WL) + // TODO add getClock function or return a predefined clock speed per variant + return 0; +#else + return 0; +#endif + + if (currentClock != desiredClock){ + LOG_DEBUG("Changing I2C clock to %u", desiredClock); + _bus->setClock(desiredClock); + } + return currentClock; +} +#endif + #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.h b/src/modules/Telemetry/Sensor/TelemetrySensor.h index 3c3e6180890..268a0c8e1ff 100644 --- a/src/modules/Telemetry/Sensor/TelemetrySensor.h +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.h @@ -30,6 +30,8 @@ class TelemetrySensor meshtastic_TelemetrySensorType sensorType = meshtastic_TelemetrySensorType_SENSOR_UNSET; unsigned status; bool initialized = false; + TwoWire * _bus; + uint8_t _address; int32_t initI2CSensor() { @@ -69,6 +71,9 @@ class TelemetrySensor virtual bool getMetrics(meshtastic_Telemetry *measurement) = 0; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { return false; }; +#ifdef CAN_RECLOCK_I2C + virtual uint32_t setClock(uint32_t desiredClock); +#endif }; #endif \ No newline at end of file diff --git a/variants/esp32/esp32-common.ini b/variants/esp32/esp32-common.ini index e582b68803c..1710f9b0175 100644 --- a/variants/esp32/esp32-common.ini +++ b/variants/esp32/esp32-common.ini @@ -49,6 +49,7 @@ build_flags = -DLIBPAX_BLE -DHAS_UDP_MULTICAST=1 ;-DDEBUG_HEAP + -DCAN_RECLOCK_I2C lib_deps = ${arduino_base.lib_deps} diff --git a/variants/esp32s3/seeed_xiao_s3/variant.h b/variants/esp32s3/seeed_xiao_s3/variant.h index 40060052614..d8dcbc8d4f9 100644 --- a/variants/esp32s3/seeed_xiao_s3/variant.h +++ b/variants/esp32s3/seeed_xiao_s3/variant.h @@ -88,5 +88,3 @@ L76K GPS Module Information : https://www.seeedstudio.com/L76K-GNSS-Module-for-S #define SX126X_TXEN RADIOLIB_NC #define SX126X_DIO3_TCXO_VOLTAGE 1.8 #endif - -#define VBLE_I2C_CLOCK_SPEED From 0d420ebc26f526b2e06e8f77abd8b8e42e4c429f Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Mon, 12 Jan 2026 17:42:12 +0100 Subject: [PATCH 052/108] Minor fix --- src/modules/Telemetry/Sensor/PMSA003ISensor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 37b88090ca3..5dee5ddab4c 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -149,7 +149,7 @@ void PMSA003ISensor::sleep() uint32_t PMSA003ISensor::wakeUp() { - LOG_INFO('Waking up PMSA003I') + LOG_INFO('Waking up PMSA003I'); digitalWrite(PMSA003I_ENABLE_PIN, HIGH); state = State::ACTIVE; return PMSA003I_WARMUP_MS; From a134f413e50934e69ae3bacd0d3533ef80c9e5eb Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 14 Jan 2026 09:14:41 +0100 Subject: [PATCH 053/108] Move I2C reclock function to src/detect --- src/detect/reClockI2C.h | 40 +++++++++++++++++++ .../Telemetry/Sensor/PMSA003ISensor.cpp | 9 +++-- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 2 + .../Telemetry/Sensor/TelemetrySensor.cpp | 36 ----------------- .../Telemetry/Sensor/TelemetrySensor.h | 5 --- 5 files changed, 47 insertions(+), 45 deletions(-) create mode 100644 src/detect/reClockI2C.h diff --git a/src/detect/reClockI2C.h b/src/detect/reClockI2C.h new file mode 100644 index 00000000000..edcd0afb601 --- /dev/null +++ b/src/detect/reClockI2C.h @@ -0,0 +1,40 @@ +#ifdef CAN_RECLOCK_I2C +#include "ScanI2CTwoWire.h" + +uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus) { + + uint32_t currentClock; + + /* See https://github.com/arduino/Arduino/issues/11457 + Currently, only ESP32 can getClock() + While all cores can setClock() + https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50 + https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60 + https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103 + For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes) + we need to reclock I2C and set it back to the previous desired speed. + Only for cases where we can know OR predefine the speed, we can do this. + */ + +#ifdef ARCH_ESP32 + currentClock = i2cBus->getClock(); +#elif defined(ARCH_NRF52) + // TODO add getClock function or return a predefined clock speed per variant? + return 0; +#elif defined(ARCH_RP2040) + // TODO add getClock function or return a predefined clock speed per variant + return 0; +#elif defined(ARCH_STM32WL) + // TODO add getClock function or return a predefined clock speed per variant + return 0; +#else + return 0; +#endif + + if (currentClock != desiredClock){ + LOG_DEBUG("Changing I2C clock to %u", desiredClock); + i2cBus->setClock(desiredClock); + } + return currentClock; +} +#endif diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 5dee5ddab4c..231ba4eb76f 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -5,6 +5,7 @@ #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "PMSA003ISensor.h" #include "TelemetrySensor.h" +#include "../detect/reClockI2C.h" #include @@ -24,7 +25,7 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) _address = dev->address.address; #if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) - uint32_t currentClock = setClock(PMSA003I_I2C_CLOCK_SPEED); + uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus); if (!currentClock){ LOG_WARN("PMSA003I can't be used at this clock speed"); return false; @@ -38,7 +39,7 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) } #if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) - setClock(currentClock); + reClockI2C(currentClock, _bus); #endif status = 1; @@ -56,7 +57,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) } #if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) - uint32_t currentClock = setClock(PMSA003I_I2C_CLOCK_SPEED); + uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus); #endif _bus->requestFrom(_address, PMSA003I_FRAME_LENGTH); @@ -66,7 +67,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) } #if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) - setClock(currentClock); + reClockI2C(currentClock, _bus); #endif for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) { diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index 64a47ccaf3c..fa1518a51ac 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -30,6 +30,8 @@ class PMSA003ISensor : public TelemetrySensor uint16_t receivedChecksum = 0; uint8_t buffer[PMSA003I_FRAME_LENGTH]; + TwoWire * _bus; + uint8_t _address; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.cpp b/src/modules/Telemetry/Sensor/TelemetrySensor.cpp index c91f63a4c4c..f854cb5feec 100644 --- a/src/modules/Telemetry/Sensor/TelemetrySensor.cpp +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.cpp @@ -7,40 +7,4 @@ #include "TelemetrySensor.h" #include "main.h" -#ifdef CAN_RECLOCK_I2C - -uint32_t TelemetrySensor::setClock(uint32_t desiredClock) { - - uint32_t currentClock; - - // See https://github.com/arduino/Arduino/issues/11457 - // Currently, only ESP32 can getClock - // While all cores can setClock() - // https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50 - // https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60 - // https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103 - -#ifdef ARCH_ESP32 - currentClock = _bus->getClock(); -#elif defined(ARCH_NRF52) - // TODO add getClock function or return a predefined clock speed per variant - return 0; -#elif defined(ARCH_RP2040) - // TODO add getClock function or return a predefined clock speed per variant - return 0; -#elif defined(ARCH_STM32WL) - // TODO add getClock function or return a predefined clock speed per variant - return 0; -#else - return 0; -#endif - - if (currentClock != desiredClock){ - LOG_DEBUG("Changing I2C clock to %u", desiredClock); - _bus->setClock(desiredClock); - } - return currentClock; -} -#endif - #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.h b/src/modules/Telemetry/Sensor/TelemetrySensor.h index 268a0c8e1ff..3c3e6180890 100644 --- a/src/modules/Telemetry/Sensor/TelemetrySensor.h +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.h @@ -30,8 +30,6 @@ class TelemetrySensor meshtastic_TelemetrySensorType sensorType = meshtastic_TelemetrySensorType_SENSOR_UNSET; unsigned status; bool initialized = false; - TwoWire * _bus; - uint8_t _address; int32_t initI2CSensor() { @@ -71,9 +69,6 @@ class TelemetrySensor virtual bool getMetrics(meshtastic_Telemetry *measurement) = 0; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { return false; }; -#ifdef CAN_RECLOCK_I2C - virtual uint32_t setClock(uint32_t desiredClock); -#endif }; #endif \ No newline at end of file From 886d5428e33775acff19ae33f0854d27c3874d88 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 14 Jan 2026 09:34:04 +0100 Subject: [PATCH 054/108] Fix uninitMemberVar errors and compile issue --- src/modules/Telemetry/Sensor/PMSA003ISensor.cpp | 2 +- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 231ba4eb76f..1ad9e87244f 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -150,7 +150,7 @@ void PMSA003ISensor::sleep() uint32_t PMSA003ISensor::wakeUp() { - LOG_INFO('Waking up PMSA003I'); + LOG_INFO("Waking up PMSA003I"); digitalWrite(PMSA003I_ENABLE_PIN, HIGH); state = State::ACTIVE; return PMSA003I_WARMUP_MS; diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index fa1518a51ac..05431b9d3d6 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -29,9 +29,9 @@ class PMSA003ISensor : public TelemetrySensor uint16_t computedChecksum = 0; uint16_t receivedChecksum = 0; - uint8_t buffer[PMSA003I_FRAME_LENGTH]; - TwoWire * _bus; - uint8_t _address; + uint8_t buffer[PMSA003I_FRAME_LENGTH]{}; + TwoWire * _bus{}; + uint8_t _address{}; }; #endif \ No newline at end of file From 068009ece6336b1956e5177398902d7fa5361eda Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 14 Jan 2026 14:10:13 +0100 Subject: [PATCH 055/108] Make sleep, wakeUp functions generic --- src/modules/Telemetry/AirQualityTelemetry.cpp | 22 +++++++++---------- .../Telemetry/Sensor/PMSA003ISensor.cpp | 9 ++++++-- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 8 +++---- .../Telemetry/Sensor/TelemetrySensor.h | 6 +++++ 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 58b66e1c347..dff23abf1b3 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -42,10 +42,7 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) // moduleConfig.telemetry.air_quality_interval = 15; // order by priority of metrics/values (low top, high bottom) -#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR -// Sensors that require variable I2C clock speed addSensor(i2cScanner, ScanI2C::DeviceType::PMSA003I); -#endif } int32_t AirQualityTelemetryModule::runOnce() @@ -90,10 +87,12 @@ int32_t AirQualityTelemetryModule::runOnce() } // Wake up the sensors that need it -#ifdef PMSA003I_ENABLE_PIN - if (pmsa003iSensor.hasSensor() && !pmsa003iSensor.isActive()) - return pmsa003iSensor.wakeUp(); -#endif /* PMSA003I_ENABLE_PIN */ + LOG_INFO("Waking up sensors"); + for (TelemetrySensor *sensor : sensors) { + if (!sensor->isActive()) { + return sensor->wakeUp(); + } + } if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( @@ -111,10 +110,11 @@ int32_t AirQualityTelemetryModule::runOnce() lastSentToPhone = millis(); } -// Send to sleep sensors that consume power -#ifdef PMSA003I_ENABLE_PIN - pmsa003iSensor.sleep(); -#endif /* PMSA003I_ENABLE_PIN */ + // Send to sleep sensors that consume power + LOG_INFO("Sending sensors to sleep"); + for (TelemetrySensor *sensor : sensors) { + sensor->sleep(); + } } return min(sendToPhoneIntervalMs, result); diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 1ad9e87244f..467659efe6c 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -141,19 +141,24 @@ bool PMSA003ISensor::isActive() return state == State::ACTIVE; } -#ifdef PMSA003I_ENABLE_PIN + void PMSA003ISensor::sleep() { +#ifdef PMSA003I_ENABLE_PIN digitalWrite(PMSA003I_ENABLE_PIN, LOW); state = State::IDLE; +#endif } uint32_t PMSA003ISensor::wakeUp() { +#ifdef PMSA003I_ENABLE_PIN LOG_INFO("Waking up PMSA003I"); digitalWrite(PMSA003I_ENABLE_PIN, HIGH); state = State::ACTIVE; return PMSA003I_WARMUP_MS; -} #endif + // No need to wait for warmup if already active + return 0; +} #endif diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index 05431b9d3d6..47c8a05cc5d 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -14,13 +14,11 @@ class PMSA003ISensor : public TelemetrySensor public: PMSA003ISensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool isActive(); virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; -#ifdef PMSA003I_ENABLE_PIN - void sleep(); - uint32_t wakeUp(); -#endif + virtual bool isActive() override; + virtual void sleep() override; + virtual uint32_t wakeUp() override; private: enum class State { IDLE, ACTIVE }; diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.h b/src/modules/Telemetry/Sensor/TelemetrySensor.h index 3c3e6180890..4a325aeed79 100644 --- a/src/modules/Telemetry/Sensor/TelemetrySensor.h +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.h @@ -58,6 +58,11 @@ class TelemetrySensor // TODO: delete after migration bool hasSensor() { return nodeTelemetrySensorsMap[sensorType].first > 0; } + // Functions to sleep / wakeup sensors that support it + virtual void sleep() {}; + virtual uint32_t wakeUp() { return 0; } + // Return active by default, override per sensor + virtual bool isActive() { return true; } #if WIRE_INTERFACES_COUNT > 1 // Set to true if Implementation only works first I2C port (Wire) @@ -65,6 +70,7 @@ class TelemetrySensor #endif virtual int32_t runOnce() { return INT32_MAX; } virtual bool isInitialized() { return initialized; } + // TODO: is this used? virtual bool isRunning() { return status > 0; } virtual bool getMetrics(meshtastic_Telemetry *measurement) = 0; From d641206164359b7fd9bb690d12a58718d0c7d81b Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 14 Jan 2026 18:39:05 +0100 Subject: [PATCH 056/108] Fix STM32 builds * Add exclude AQ sensor to builds that have environmental sensor excludes * Add includes to AddI2CSensorTemplate.h --- src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h | 10 +++++++++- variants/stm32/CDEBYTE_E77-MBL/platformio.ini | 1 + variants/stm32/rak3172/platformio.ini | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h b/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h index 3e213954d1c..01aacc6741b 100644 --- a/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h +++ b/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h @@ -1,4 +1,11 @@ +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR + #include +#include "TelemetrySensor.h" +#include "detect/ScanI2C.h" +#include "detect/ScanI2CTwoWire.h" +#include + static std::forward_list sensors; template void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type) @@ -23,4 +30,5 @@ template void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType ty // destroy sensor delete sensor; } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/variants/stm32/CDEBYTE_E77-MBL/platformio.ini b/variants/stm32/CDEBYTE_E77-MBL/platformio.ini index c5af9a4a4de..b4c0c958ff4 100644 --- a/variants/stm32/CDEBYTE_E77-MBL/platformio.ini +++ b/variants/stm32/CDEBYTE_E77-MBL/platformio.ini @@ -13,6 +13,7 @@ build_flags = -DPIN_SERIAL1_RX=PB7 -DPIN_SERIAL1_TX=PB6 -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 + -DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1 -DMESHTASTIC_EXCLUDE_I2C=1 -DMESHTASTIC_EXCLUDE_GPS=1 diff --git a/variants/stm32/rak3172/platformio.ini b/variants/stm32/rak3172/platformio.ini index b9a4b8a0440..4d96e98f93f 100644 --- a/variants/stm32/rak3172/platformio.ini +++ b/variants/stm32/rak3172/platformio.ini @@ -12,6 +12,7 @@ build_flags = -DPIN_WIRE_SDA=PA11 -DPIN_WIRE_SCL=PA12 -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 + -DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1 -DMESHTASTIC_EXCLUDE_I2C=1 -DMESHTASTIC_EXCLUDE_GPS=1 From f61e4c90fac62a3a44e6a7ea74fe43c80d6888d1 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sun, 6 Jul 2025 19:25:46 +0200 Subject: [PATCH 057/108] SEN5X first pass --- platformio.ini | 2 + src/configuration.h | 1 + src/detect/ScanI2C.h | 3 +- src/detect/ScanI2CTwoWire.cpp | 53 +++++++-- src/main.cpp | 3 +- src/modules/Telemetry/AirQualityTelemetry.cpp | 14 +++ src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 107 ++++++++++++++++++ src/modules/Telemetry/Sensor/SEN5XSensor.h | 43 +++++++ 8 files changed, 216 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 b72d9b5b15a..33e83a5496b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -212,3 +212,5 @@ lib_deps = sensirion/Sensirion Core@0.7.2 # 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 be483b92469..cbe37f9bf1a 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -236,6 +236,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 ceb89430454..cf568e4db00 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -87,7 +87,8 @@ class ScanI2C BH1750, DA217, CHSC6X, - CST226SE + CST226SE, + SEN5X } DeviceType; // typedef uint8_t DeviceAddress; diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 202d73d8421..854aaeaa3f7 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) @@ -563,8 +568,9 @@ 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); #ifdef HAS_ICM20948 type = ICM20948; @@ -575,14 +581,45 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) 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 cdaf1ce37a1..0e812ba1244 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -767,7 +767,8 @@ void setup() scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X); - + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SEN5X, meshtastic_TelemetrySensorType_SEN5X); + i2cScanner.reset(); #endif #ifdef HAS_SDCARD diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index dff23abf1b3..17d985efbc0 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -45,6 +45,13 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) addSensor(i2cScanner, ScanI2C::DeviceType::PMSA003I); } +#if __has_include() +#include "Sensor/SEN5XSensor.h" +SEN5XSensor sen5xSensor; +#else +NullSensor sen5xSensor; +#endif + int32_t AirQualityTelemetryModule::runOnce() { if (sleepOnNextExecution == true) { @@ -352,6 +359,13 @@ 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 00000000000..b65b3e76d1b --- /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 00000000000..f2b8321eeff --- /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 f9924d0bd3799685f5788cba5b35d4b410e59f41 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Mon, 7 Jul 2025 14:34:09 +0200 Subject: [PATCH 058/108] 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 | 60 ++-- 5 files changed, 286 insertions(+), 98 deletions(-) diff --git a/platformio.ini b/platformio.ini index 33e83a5496b..84a9dffb1f3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -211,6 +211,4 @@ lib_deps = # renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core sensirion/Sensirion Core@0.7.2 # 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 854aaeaa3f7..1a36dc0824a 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) @@ -132,6 +128,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 @@ -577,40 +574,25 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) logFoundDevice("ICM20948", (uint8_t)addr.address); break; #endif + 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("Sensirion SEN55", addr.address); + break; + } else if (prod.startsWith("SEN54")) { type = SEN5X; - logFoundDevice("SEN5X", (uint8_t)addr.address); + 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 17d985efbc0..c87cf8f75dd 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -45,7 +45,12 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) addSensor(i2cScanner, ScanI2C::DeviceType::PMSA003I); } -#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 b65b3e76d1b..a352b60971c 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 - error = sen5x.startMeasurement(); - if (error) { - LOG_INFO("Error trying to execute startMeasurement(): "); - errorToString(error, errorMessage, 256); - LOG_INFO(errorMessage); + if (!findModel()) { + LOG_ERROR("SEN5X: error finding sensor model"); return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - } else { - status = 1; } + // 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; + // } + 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 f2b8321eeff..14168f5872e 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 aa02a24601b105ed4168ae63b13f73c98082db7b Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 8 Jul 2025 12:07:25 +0200 Subject: [PATCH 059/108] 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 1a36dc0824a..b77b4715d14 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -574,12 +574,12 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) logFoundDevice("ICM20948", (uint8_t)addr.address); break; #endif - 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 a352b60971c..795d3bae783 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 14168f5872e..9b80e3224bb 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 2261ae53d1c41672fa98c8ff6f5b9c027f52cbdc Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Mon, 7 Jul 2025 14:34:09 +0200 Subject: [PATCH 060/108] WIP Sen5X functions --- src/detect/ScanI2CTwoWire.cpp | 2 +- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 44 +++++++++++++++++--- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index b77b4715d14..1a36dc0824a 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -574,12 +574,12 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) logFoundDevice("ICM20948", (uint8_t)addr.address); break; #endif + 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 795d3bae783..b37d5f963e3 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -183,18 +183,25 @@ 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"); + delay(50); // without this there is an error on the deviceReset function + + if (!sendCommand(SEN5X_RESET)) { + LOG_ERROR("SEN5X: Error reseting device"); return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } + delay(200); // From Sensirion Arduino library - delay(25); + if (!findModel()) { + LOG_ERROR("SEN5X: error finding sensor model"); + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } - if (!sendCommand(SEN5X_RESET)) { - LOG_ERROR("SEN5X: Error reseting device"); + // Check if firmware version allows The direct switch between Measurement and RHT/Gas-Only Measurement mode + if (!getVersion()) return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + if (firmwareVer < 2) { + LOG_ERROR("SEN5X: error firmware is too old and will not work with this implementation"); return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } delay(200); // From Sensirion Arduino library @@ -236,6 +243,31 @@ int32_t SEN5XSensor::runOnce() // 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(); } From 5788a7bcc9202bcdb4eaafad32fa94dc5b33224a Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 15 Jul 2025 19:41:32 +0200 Subject: [PATCH 061/108] Changes on SEN5X library - removing pm_env as well --- src/modules/Telemetry/AirQualityTelemetry.cpp | 12 +- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 487 +++++++++++++++--- src/modules/Telemetry/Sensor/SEN5XSensor.h | 86 +++- src/serialization/MeshPacketSerializer.cpp | 24 +- .../MeshPacketSerializer_nRF52.cpp | 18 +- 5 files changed, 517 insertions(+), 110 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index c87cf8f75dd..05acb56fb1b 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -173,8 +173,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"); @@ -240,9 +239,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) @@ -306,6 +306,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 b37d5f963e3..36c306d67f4 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; - // } - - // 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; - // } + // 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"); + + 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; + } -// uint32_t SEN5XSensor::wakeUp() { -// digitalWrite(SEN5X_ENABLE_PIN, HIGH); -// state = SEN5XState::SEN5X_IDLE; -// return SEN5X_WARMUP_MS; + // 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; // } -#endif /* SEN5X_ENABLE_PIN */ -bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement) +uint8_t SEN5XSensor::getMeasurements() { - 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); + // 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 - // if (error) { - // LOG_INFO("Error trying to execute readMeasuredValues(): "); - // errorToString(error, errorMessage, 256); - // LOG_INFO(errorMessage); - // return false; + 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; // } - // 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 0; +} + +bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement) +{ + LOG_INFO("SEN5X: Attempting to get metrics"); + if (!isActive()){ + LOG_INFO("SEN5X: not in measurement mode"); + return false; + } + + uint8_t response; + response = getMeasurements(); + + if (response == 0) { + measurement->variant.air_quality_metrics.has_pm10_standard = true; + measurement->variant.air_quality_metrics.pm10_standard = sen5xmeasurement.pM1p0; + measurement->variant.air_quality_metrics.has_pm25_standard = true; + measurement->variant.air_quality_metrics.pm25_standard = sen5xmeasurement.pM2p5; + measurement->variant.air_quality_metrics.has_pm40_standard = true; + measurement->variant.air_quality_metrics.pm40_standard = sen5xmeasurement.pM4p0; + measurement->variant.air_quality_metrics.has_pm100_standard = true; + measurement->variant.air_quality_metrics.pm100_standard = sen5xmeasurement.pM10p0; + + measurement->variant.air_quality_metrics.has_particles_05um = true; + measurement->variant.air_quality_metrics.particles_05um = sen5xmeasurement.pN0p5; + measurement->variant.air_quality_metrics.has_particles_10um = true; + measurement->variant.air_quality_metrics.particles_10um = sen5xmeasurement.pN1p0; + measurement->variant.air_quality_metrics.has_particles_25um = true; + measurement->variant.air_quality_metrics.particles_25um = sen5xmeasurement.pN2p5; + measurement->variant.air_quality_metrics.has_particles_40um = true; + measurement->variant.air_quality_metrics.particles_40um = sen5xmeasurement.pN4p0; + measurement->variant.air_quality_metrics.has_particles_100um = true; + measurement->variant.air_quality_metrics.particles_100um = sen5xmeasurement.pN10p0; + + if (model == SEN54 || model == SEN55) { + measurement->variant.air_quality_metrics.has_pm_humidity = true; + measurement->variant.air_quality_metrics.pm_humidity = sen5xmeasurement.humidity; + measurement->variant.air_quality_metrics.has_pm_temperature = true; + measurement->variant.air_quality_metrics.pm_temperature = sen5xmeasurement.temperature; + measurement->variant.air_quality_metrics.has_pm_nox_idx = true; + measurement->variant.air_quality_metrics.pm_nox_idx = sen5xmeasurement.noxIndex; + } + + if (model == SEN55) { + measurement->variant.air_quality_metrics.has_pm_voc_idx = true; + measurement->variant.air_quality_metrics.pm_voc_idx = sen5xmeasurement.vocIndex; + } + return true; + } else if (response == 1) { + // TODO return because data was not ready yet + // Should this return false? + 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 9b80e3224bb..b8e0a0ac982 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 a12972cb002..92e70036811 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 41f505b94e5..80aef0e9477 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 5068c8b5b99b2251f048de61f254eff44ca47fc3 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 15 Jul 2025 19:49:42 +0200 Subject: [PATCH 062/108] 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 36c306d67f4..f14bf80f82e 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 7988af4f3e6033e89030d36de1bf1a82e02d5b08 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 15 Jul 2025 19:53:20 +0200 Subject: [PATCH 063/108] 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 1a36dc0824a..ada67cd366d 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -128,7 +128,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 @@ -574,12 +573,13 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) logFoundDevice("ICM20948", (uint8_t)addr.address); break; #endif - 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 697c120bf904a8f371f5e0cc2d3f3ed6089ee815 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 15 Jul 2025 19:58:37 +0200 Subject: [PATCH 064/108] 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 f14bf80f82e..66ae77f27f2 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 e4de0d02aa636fc9c0f820c26bedbfa024131e27 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Thu, 17 Jul 2025 11:05:02 +0200 Subject: [PATCH 065/108] 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 | 9 +++++++-- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 6 +++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 05acb56fb1b..bf51b745c2d 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -101,8 +101,13 @@ int32_t AirQualityTelemetryModule::runOnce() // Wake up the sensors that need it LOG_INFO("Waking up sensors"); for (TelemetrySensor *sensor : sensors) { - if (!sensor->isActive()) { - return sensor->wakeUp(); + if ((lastSentToMesh == 0) || + !Throttle::isWithinTimespanMs(lastSentToMesh - sensor->warmup_time, Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) { + if (!sensor->isActive()) { + return sensor->wakeUp(); + } } } diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index 66ae77f27f2..9c25c859b54 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 03b10c4743f5674031a60843ae25ea4fa99e82bd Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Thu, 17 Jul 2025 16:33:04 +0200 Subject: [PATCH 066/108] 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 9c25c859b54..7da2e108625 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 4e9b66a79c693149f0fbb8d45bde9d1fff8c79e6 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Thu, 17 Jul 2025 21:14:18 +0200 Subject: [PATCH 067/108] Add dynamic measurement interval for SEN5X --- src/modules/Telemetry/AirQualityTelemetry.cpp | 10 +++++ src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 38 +++++++++++++++++++ src/modules/Telemetry/Sensor/SEN5XSensor.h | 22 +++++------ 3 files changed, 57 insertions(+), 13 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index bf51b745c2d..a00e77b5d4b 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -68,6 +68,7 @@ int32_t AirQualityTelemetryModule::runOnce() } uint32_t result = UINT32_MAX; + uint32_t sen5xPendingForReady; if (!(moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_screen_enabled || AIR_QUALITY_TELEMETRY_MODULE_ENABLE)) { @@ -111,6 +112,15 @@ int32_t AirQualityTelemetryModule::runOnce() } } + // TODO - FIX + // 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, diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index 7da2e108625..0083d20d2e5 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 b8e0a0ac982..71b32a6f649 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 6df0f88ade731029bf3a699b99b27f8caded9577 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Fri, 18 Jul 2025 01:03:06 +0200 Subject: [PATCH 068/108] Only disable SEN5X if enough time after reading. --- src/modules/Telemetry/AirQualityTelemetry.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index a00e77b5d4b..106a5d61625 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -140,9 +140,22 @@ int32_t AirQualityTelemetryModule::runOnce() // Send to sleep sensors that consume power LOG_INFO("Sending sensors to sleep"); for (TelemetrySensor *sensor : sensors) { - sensor->sleep(); + // TODO FIX + if (sensor->isActive()) { + if (sensor.warmup_time < Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes)) { + LOG_DEBUG("SEN5X: Disabling sensor until next period"); + sensor->sleep(); + } else { + LOG_DEBUG("SEN5X: Sensor stays enabled due to warm up period"); + } + } } + + + } return min(sendToPhoneIntervalMs, result); } From c9fd77aa29e9b9b06ca588a004017783bc3e57e4 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Fri, 18 Jul 2025 01:03:50 +0200 Subject: [PATCH 069/108] 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 0083d20d2e5..eda75ae4fd8 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 e54acf5bbf49c622b5f776a07cc764120bfb71ca Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Fri, 18 Jul 2025 09:56:37 +0200 Subject: [PATCH 070/108] 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 106a5d61625..5fd8239f708 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -116,6 +116,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 eda75ae4fd8..dc505fec87d 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 5284215e843c97762b0aa6d1aab11a61c84886e6 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 22 Jul 2025 20:15:23 +0200 Subject: [PATCH 071/108] 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 5fd8239f708..5f07da0e130 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -45,7 +45,7 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) addSensor(i2cScanner, ScanI2C::DeviceType::PMSA003I); } -// Small hack +// TODO - Small hack to review #ifndef INCLUDE_SEN5X #define INCLUDE_SEN5X 1 #endif From eaf6cb51567d0b23295e371e8a5f6452ce058ca2 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 23 Jul 2025 16:08:26 +0200 Subject: [PATCH 072/108] 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 dc505fec87d..d5f04ce9401 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 71b32a6f649..8782c3b92cb 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 c45e0bfd1cd3e13559fbd83b8a39ff399256bf6a Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 23 Jul 2025 16:10:08 +0200 Subject: [PATCH 073/108] Make AQTelemetry sensors non-exclusive --- 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 5f07da0e130..4162ff03bd6 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -285,7 +285,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; From 94750ed4c7b683b415426d2f0dd7cd158898f82f Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 5 Aug 2025 17:32:04 +0200 Subject: [PATCH 074/108] 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 | 17 +- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 146 +++++++++++------- src/modules/Telemetry/Sensor/SEN5XSensor.h | 13 +- 3 files changed, 108 insertions(+), 68 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 4162ff03bd6..75e9f00a312 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -53,9 +53,8 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) #ifdef INCLUDE_SEN5X #include "Sensor/SEN5XSensor.h" SEN5XSensor sen5xSensor; -#else -NullSensor sen5xSensor; -#endif + +#include "graphics/ScreenFonts.h" int32_t AirQualityTelemetryModule::runOnce() { @@ -338,11 +337,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 d5f04ce9401..60313e7e047 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 = getTime(); - LOG_INFO("SEN5X: Current time %us", now); - uint32_t passed = now - lastCleaning; - LOG_INFO("SEN5X: Elapsed time since last cleaning: %us", passed); - - if (passed > ONE_WEEK_IN_SECONDS && (now > 1514764800)) { // If current date greater than 01/01/2018 (validity check) - LOG_INFO("SEN5X: More than a week since las cleaning, cleaning..."); - startCleaning(); + uint32_t now; + 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); + + 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 8782c3b92cb..a72f606e97a 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 7f51253210c5ed7d0c6a43261752a49ba7dc5ae1 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 6 Aug 2025 10:47:50 +0200 Subject: [PATCH 075/108] 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 ada67cd366d..3a442d155a4 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -111,6 +111,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 456ee9574c50ed4e282951a1921f7b21caf5ff04 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Thu, 7 Aug 2025 16:27:41 +0200 Subject: [PATCH 076/108] 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 60313e7e047..bb82c4fbfd9 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 a72f606e97a..f46657f2e3c 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 f722ce0a815adbfdb61b8806fbfc8c720a03a099 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Thu, 7 Aug 2025 17:13:26 +0200 Subject: [PATCH 077/108] 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 bb82c4fbfd9..77af272fe69 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 f46657f2e3c..e1101ea2e50 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 8194be0f01db7c925c5008e3afe779ee924799f5 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 26 Aug 2025 19:00:43 +0200 Subject: [PATCH 078/108] Add one-shot mode config flag to SEN5X --- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 56 +++++++++++++------- src/modules/Telemetry/Sensor/SEN5XSensor.h | 6 +-- 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index 77af272fe69..f3096d82f67 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); - // // } - - // // result = AdminMessageHandleResult::HANDLED; - // result = AdminMessageHandleResult::NOT_HANDLED; - // break; - - // default: - // result = AdminMessageHandleResult::NOT_HANDLED; - // } + + switch (request->which_payload_variant) { + case meshtastic_AdminMessage_sensor_config_tag: + if (!request->sensor_config.has_sen5x_config) { + 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); + // } + + // 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 e1101ea2e50..e89470a5164 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 53f43dcba997126facc40a9e4ea535de16356598 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 26 Aug 2025 19:02:12 +0200 Subject: [PATCH 079/108] 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 f3096d82f67..2c31d3f1d68 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 e89470a5164..1f4694a3569 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 972aec1d8f10b51225d0a8526fc684ed31a8a318 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sat, 30 Aug 2025 13:00:22 +0200 Subject: [PATCH 080/108] 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 | 46 ++-- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 229 +++++++++++------- src/modules/Telemetry/Sensor/SEN5XSensor.h | 19 +- 3 files changed, 185 insertions(+), 109 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 75e9f00a312..ea5733fa62d 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -86,7 +86,6 @@ int32_t AirQualityTelemetryModule::runOnce() if (!sensors.empty()) { result = DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } - } // it's possible to have this module enabled, only for displaying values on the screen. @@ -101,25 +100,28 @@ int32_t AirQualityTelemetryModule::runOnce() // Wake up the sensors that need it LOG_INFO("Waking up sensors"); for (TelemetrySensor *sensor : sensors) { - if ((lastSentToMesh == 0) || - !Throttle::isWithinTimespanMs(lastSentToMesh - sensor->warmup_time, Default::getConfiguredOrDefaultMsScaled( + if (((lastSentToMesh == 0) || + !Throttle::isWithinTimespanMs(lastSentToMesh - sensor->warmup_time, 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 (!sensor->isActive()) { return sensor->wakeUp(); } } } - // TODO - FIX + // 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) { + if (sen5xPendingForReady > 0) { return sen5xPendingForReady; } } + LOG_DEBUG("Checking if sending telemetry"); if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( @@ -137,25 +139,21 @@ int32_t AirQualityTelemetryModule::runOnce() lastSentToPhone = millis(); } - // Send to sleep sensors that consume power - LOG_INFO("Sending sensors to sleep"); - for (TelemetrySensor *sensor : sensors) { - // TODO FIX - if (sensor->isActive()) { - if (sensor.warmup_time < Default::getConfiguredOrDefaultMsScaled( - moduleConfig.telemetry.air_quality_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes)) { - LOG_DEBUG("SEN5X: Disabling sensor until next period"); - sensor->sleep(); - } else { - LOG_DEBUG("SEN5X: Sensor stays enabled due to warm up period"); + // Send to sleep sensors that consume power + LOG_INFO("Sending sensors to sleep"); + for (TelemetrySensor *sensor : sensors) { + // TODO FIX + if (sensor->isActive()) { + if (sensor.warmup_time < Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes)) { + LOG_DEBUG("SEN5X: Disabling sensor until next period"); + sensor->sleep(); + } else { + LOG_DEBUG("SEN5X: Sensor stays enabled due to warm up period"); + } } } - } - - - - } return min(sendToPhoneIntervalMs, result); } @@ -334,8 +332,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 2c31d3f1d68..6e1ed89c9c7 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 + // 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; + } + } - // 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(); + if (vocStateStable() && vocValid) { + saveState(); + } else { + LOG_INFO("SEN5X: Not stopping measurement, vocState is not stable yet!"); + return true; } - } else { - vocValid = false; } 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"); - - // 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 + uint32_t now; + now = getValidTime(RTCQuality::RTCQualityDevice); + LOG_DEBUG("SEN5X: Waking up sensor"); - // 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 1f4694a3569..d94bfe76ca3 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; From 9ac258d6fb42b6b325bc91d943c30bdf2d2da574 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sun, 14 Sep 2025 19:35:21 +0200 Subject: [PATCH 081/108] Fixes on VOC state and mode swtiching * Adds a new RHT/Gas only mode, with 3600s stabilization time * Fixes the VOCState buffer mismatch * Fixes SEN50/54/55 model mistake --- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 187 ++++++++++--------- src/modules/Telemetry/Sensor/SEN5XSensor.h | 28 ++- 2 files changed, 120 insertions(+), 95 deletions(-) diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index 6e1ed89c9c7..cdb72bbdc85 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -216,7 +216,7 @@ bool SEN5XSensor::I2Cdetect(TwoWire *_Wire, uint8_t address) else return false; } -bool SEN5XSensor::idle() +bool SEN5XSensor::idle(bool checkState) { // From the datasheet: // By default, the VOC algorithm resets its state to initial @@ -227,26 +227,28 @@ bool SEN5XSensor::idle() // restoring the previously memorized algorithm state before // starting the measure mode - // 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 (checkState) { + // If the stabilisation period is not passed for SEN54 or SEN55, don't go to idle + if (model != SEN50) { + // 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 (vocStateStable() && vocValid) { + saveState(); + } else { + LOG_INFO("SEN5X: Not stopping measurement, vocState is not stable yet!"); + return true; + } } } @@ -255,16 +257,25 @@ bool SEN5XSensor::idle() return true; } - if (!sendCommand(SEN5X_STOP_MEASUREMENT)) { - LOG_ERROR("SEN5X: Error stoping measurement"); - return false; + // Switch to low-power based on the model + if (model == SEN50) { + if (!sendCommand(SEN5X_STOP_MEASUREMENT)) { + LOG_ERROR("SEN5X: Error stopping measurement"); + return false; + } + state = SEN5X_IDLE; + LOG_INFO("SEN5X: Stop measurement mode"); + } else { + if (!sendCommand(SEN5X_START_MEASUREMENT_RHT_GAS)) { + LOG_ERROR("SEN5X: Error switching to RHT/Gas measurement"); + return false; + } + state = SEN5X_RHTGAS_ONLY; + LOG_INFO("SEN5X: Switch to RHT/Gas only measurement mode"); } delay(200); // From Sensirion Datasheet - LOG_INFO("SEN5X: Stop measurement mode"); - - state = SEN5X_IDLE; - measureStarted = 0; + pmMeasureStarted = 0; return true; } @@ -293,7 +304,7 @@ bool SEN5XSensor::vocStateValid() { bool SEN5XSensor::vocStateToSensor() { - if (model != SEN55){ + if (model == SEN50){ return true; } @@ -325,7 +336,7 @@ bool SEN5XSensor::vocStateToSensor() bool SEN5XSensor::vocStateFromSensor() { - if (model != SEN55){ + if (model == SEN50){ return true; } @@ -340,8 +351,8 @@ bool SEN5XSensor::vocStateFromSensor() // 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)); + // uint8_t vocBuffer[SEN5X_VOC_STATE_BUFFER_SIZE + (SEN5X_VOC_STATE_BUFFER_SIZE / 2)]; + size_t receivedNumber = readBuffer(&vocState[0], SEN5X_VOC_STATE_BUFFER_SIZE + (SEN5X_VOC_STATE_BUFFER_SIZE / 2)); delay(20); // From Sensirion Datasheet if (receivedNumber == 0) { @@ -349,14 +360,14 @@ bool SEN5XSensor::vocStateFromSensor() 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]; + // 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: [%u, %u, %u, %u, %u, %u, %u, %u]", @@ -383,7 +394,7 @@ bool SEN5XSensor::loadState() lastCleaningValid = sen5xstate.last_cleaning_valid; oneShotMode = sen5xstate.one_shot_mode; - if (model == SEN55) { + if (model != SEN50) { vocTime = sen5xstate.voc_state_time; vocValid = sen5xstate.voc_state_valid; // Unpack state @@ -429,7 +440,7 @@ bool SEN5XSensor::saveState() sen5xstate.last_cleaning_valid = lastCleaningValid; sen5xstate.one_shot_mode = oneShotMode; - if (model == SEN55) { + if (model != SEN50) { sen5xstate.has_voc_state_time = true; sen5xstate.has_voc_state_valid = true; sen5xstate.has_voc_state_array = true; @@ -474,18 +485,19 @@ bool SEN5XSensor::isActive(){ } uint32_t SEN5XSensor::wakeUp(){ - uint32_t now; - now = getValidTime(RTCQuality::RTCQualityDevice); + // uint32_t now; + // now = getValidTime(RTCQuality::RTCQualityDevice); LOG_DEBUG("SEN5X: Waking up sensor"); - // 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"); - } + // NOTE - No need to send it everytime if we switch to RHT/gas only mode + // // 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"); @@ -497,7 +509,7 @@ uint32_t SEN5XSensor::wakeUp(){ // 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(); + pmMeasureStarted = getTime(); state = SEN5X_MEASUREMENT; if (state == SEN5X_MEASUREMENT) LOG_INFO("SEN5X: Started measurement mode"); @@ -508,9 +520,9 @@ bool SEN5XSensor::vocStateStable() { uint32_t now; now = getTime(); - uint32_t sinceFirstMeasureStarted = (now - firstMeasureStarted); + uint32_t sinceFirstMeasureStarted = (now - rhtGasMeasureStarted); LOG_DEBUG("sinceFirstMeasureStarted: %us", sinceFirstMeasureStarted); - return sinceFirstMeasureStarted > SEN55_VOC_STATE_WARMUP_S; + return sinceFirstMeasureStarted > SEN5X_VOC_STATE_WARMUP_S; } bool SEN5XSensor::startCleaning() @@ -622,7 +634,7 @@ int32_t SEN5XSensor::runOnce() saveState(); } - if (model == SEN55) { + if (model != SEN50) { if (!vocValid) { LOG_INFO("SEN5X: No valid VOC's state found"); } else { @@ -643,6 +655,9 @@ int32_t SEN5XSensor::runOnce() LOG_INFO("SEN5X: Not enough RTCQuality, ignoring saved state. Trying again later"); } + idle(false); + rhtGasMeasureStarted = now; + return initI2CSensor(); } @@ -659,7 +674,7 @@ bool SEN5XSensor::readValues() LOG_DEBUG("SEN5X: Reading PM Values"); delay(20); // From Sensirion Datasheet - uint8_t dataBuffer[24]; + uint8_t dataBuffer[16]; size_t receivedNumber = readBuffer(&dataBuffer[0], 24); if (receivedNumber == 0) { LOG_ERROR("SEN5X: Error getting values"); @@ -671,6 +686,7 @@ bool SEN5XSensor::readValues() 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]); @@ -690,21 +706,21 @@ 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", + if (model != SEN50) { + LOG_DEBUG("Got: humidity=%.2f, temperature=%.2f, vocIndex=%.2f", sen5xmeasurement.humidity, sen5xmeasurement.temperature, - sen5xmeasurement.noxIndex); + sen5xmeasurement.vocIndex); } if (model == SEN55) { - LOG_DEBUG("Got: vocIndex=%.2f", - sen5xmeasurement.vocIndex); + LOG_DEBUG("Got: noxIndex=%.2f", + sen5xmeasurement.noxIndex); } return true; } -bool SEN5XSensor::readPnValues(bool cumulative) +bool SEN5XSensor::readPNValues(bool cumulative) { if (!sendCommand(SEN5X_READ_PM_VALUES)){ LOG_ERROR("SEN5X: Error sending read command"); @@ -714,7 +730,7 @@ bool SEN5XSensor::readPnValues(bool cumulative) LOG_DEBUG("SEN5X: Reading PN Values"); delay(20); // From Sensirion Datasheet - uint8_t dataBuffer[30]; + uint8_t dataBuffer[20]; size_t receivedNumber = readBuffer(&dataBuffer[0], 30); if (receivedNumber == 0) { LOG_ERROR("SEN5X: Error getting PN values"); @@ -735,11 +751,11 @@ bool SEN5XSensor::readPnValues(bool cumulative) // Convert values based on Sensirion Arduino lib // 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.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: @@ -769,7 +785,7 @@ bool SEN5XSensor::readPnValues(bool cumulative) // } // delay(20); // From Sensirion Datasheet -// uint8_t dataBuffer[12]; +// uint8_t dataBuffer[8]; // size_t receivedNumber = readBuffer(&dataBuffer[0], 12); // if (receivedNumber == 0) { // LOG_ERROR("SEN5X: Error getting Raw values"); @@ -813,7 +829,7 @@ uint8_t SEN5XSensor::getMeasurements() return 2; } - if(!readPnValues(false)) { + if(!readPNValues(false)) { LOG_ERROR("SEN5X: Error getting PN readings"); return 2; } @@ -829,36 +845,37 @@ uint8_t SEN5XSensor::getMeasurements() int32_t SEN5XSensor::pendingForReady(){ uint32_t now; now = getTime(); - uint32_t sinceMeasureStarted = (now - measureStarted)*1000; - LOG_DEBUG("SEN5X: Since measure started: %ums", sinceMeasureStarted); + uint32_t sincePmMeasureStarted = (now - pmMeasureStarted)*1000; + LOG_DEBUG("SEN5X: Since measure started: %ums", sincePmMeasureStarted); + switch (state) { case SEN5X_MEASUREMENT: { - if (sinceMeasureStarted < SEN5X_WARMUP_MS_1) { + if (sincePmMeasureStarted < SEN5X_WARMUP_MS_1) { LOG_INFO("SEN5X: not enough time passed since starting measurement"); - return SEN5X_WARMUP_MS_1 - sinceMeasureStarted; + return SEN5X_WARMUP_MS_1 - sincePmMeasureStarted; } - if (!firstMeasureStarted) { - firstMeasureStarted = now; + if (!pmMeasureStarted) { + pmMeasureStarted = now; } // Get PN values to check if we are above or below threshold - readPnValues(true); + 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) { + if ((sen5xmeasurement.pN4p0 / 100) < SEN5X_PN4P0_CONC_THD && sincePmMeasureStarted < 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 SEN5X_WARMUP_MS_2 - sincePmMeasureStarted; } return 0; } case SEN5X_MEASUREMENT_2: { - if (sinceMeasureStarted < SEN5X_WARMUP_MS_2) { + if (sincePmMeasureStarted < SEN5X_WARMUP_MS_2) { // Report how many seconds are pending to cover the first warm up period - return SEN5X_WARMUP_MS_2 - sinceMeasureStarted; + return SEN5X_WARMUP_MS_2 - sincePmMeasureStarted; } return 0; } @@ -921,7 +938,7 @@ bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement) measurement->variant.air_quality_metrics.particles_tps = sen5xmeasurement.tSize; } - if (model == SEN54 || model == SEN55) { + if (model != SEN50) { if (sen5xmeasurement.humidity!= FLT_MAX) { measurement->variant.air_quality_metrics.has_pm_humidity = true; measurement->variant.air_quality_metrics.pm_humidity = sen5xmeasurement.humidity; @@ -931,15 +948,15 @@ bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement) 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; + measurement->variant.air_quality_metrics.has_pm_voc_idx = true; + measurement->variant.air_quality_metrics.pm_voc_idx = sen5xmeasurement.vocIndex; } } if (model == SEN55) { 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; + measurement->variant.air_quality_metrics.has_pm_nox_idx = true; + measurement->variant.air_quality_metrics.pm_nox_idx = sen5xmeasurement.noxIndex; } } diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.h b/src/modules/Telemetry/Sensor/SEN5XSensor.h index d94bfe76ca3..c0650bfd471 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.h +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h @@ -25,9 +25,13 @@ 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 +#ifndef SEN5X_VOC_STATE_WARMUP_S +/* Note for Testing 5' is enough +Sensirion recommends 1h +This can be bypassed completely if switching to low-power RHT/Gas mode and setting +SEN5X_VOC_STATE_WARMUP_S 0 +*/ +#define SEN5X_VOC_STATE_WARMUP_S 3600 #endif #define ONE_WEEK_IN_SECONDS 604800 @@ -82,7 +86,7 @@ class SEN5XSensor : public TelemetrySensor 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 }; + enum SEN5XState { SEN5X_OFF, SEN5X_IDLE, SEN5X_RHTGAS_ONLY, SEN5X_MEASUREMENT, SEN5X_MEASUREMENT_2, SEN5X_CLEANING, SEN5X_NOT_DETECTED }; SEN5XState state = SEN5X_OFF; // Flag to work on one-shot (read and sleep), or continuous mode bool oneShotMode = true; @@ -98,11 +102,11 @@ class SEN5XSensor : public TelemetrySensor bool startCleaning(); uint8_t getMeasurements(); // bool readRawValues(); - bool readPnValues(bool cumulative); + bool readPNValues(bool cumulative); bool readValues(); - uint32_t measureStarted = 0; - uint32_t firstMeasureStarted = 0; + uint32_t pmMeasureStarted = 0; + uint32_t rhtGasMeasureStarted = 0; _SEN5XMeasurements sen5xmeasurement; protected: @@ -135,13 +139,17 @@ class SEN5XSensor : public TelemetrySensor SEN5XSensor(); bool isActive(); uint32_t wakeUp(); - bool idle(); + bool idle(bool checkState=true); 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 + /* 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. + See: https://sensirion.com/resource/application_note/low_power_mode/sen5x + */ #define SEN5X_PN4P0_CONC_THD 100 + // TODO - Add a way to take averages of samples // 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 31da50a41d139a8f03cded7bda9ad889bfe74575 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sat, 17 Jan 2026 20:40:02 +0100 Subject: [PATCH 082/108] Adapt SEN5X to new sensor list structure. Improve reclock. * Improve reClockI2C conditions for different variants * Add sleep, wakeUp, pendingForReady, hasSleep functions to PM sensors to save battery * Add SEN5X --- src/detect/reClockI2C.cpp | 31 +++++ src/detect/reClockI2C.h | 42 +------ src/main.cpp | 2 - src/modules/Telemetry/AirQualityTelemetry.cpp | 77 ++++-------- .../Telemetry/Sensor/PMSA003ISensor.cpp | 95 +++++++++++---- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 5 + src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 112 +++++++++--------- src/modules/Telemetry/Sensor/SEN5XSensor.h | 37 +++--- .../Telemetry/Sensor/TelemetrySensor.h | 10 +- 9 files changed, 225 insertions(+), 186 deletions(-) create mode 100644 src/detect/reClockI2C.cpp diff --git a/src/detect/reClockI2C.cpp b/src/detect/reClockI2C.cpp new file mode 100644 index 00000000000..f1dfee25909 --- /dev/null +++ b/src/detect/reClockI2C.cpp @@ -0,0 +1,31 @@ +#include "reClockI2C.h" +#include "ScanI2CTwoWire.h" + +uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force) { + + uint32_t currentClock = 0; + + /* See https://github.com/arduino/Arduino/issues/11457 + Currently, only ESP32 can getClock() + While all cores can setClock() + https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50 + https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60 + https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103 + For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes) + we need to reclock I2C and set it back to the previous desired speed. + Only for cases where we can know OR predefine the speed, we can do this. + */ + +// TODO add getClock function or return a predefined clock speed per variant? +#ifdef CAN_RECLOCK_I2C + currentClock = i2cBus->getClock(); +#endif + + if (currentClock != desiredClock || force){ + LOG_DEBUG("Changing I2C clock to %u", desiredClock); + i2cBus->setClock(desiredClock); + } + + return currentClock; +} + diff --git a/src/detect/reClockI2C.h b/src/detect/reClockI2C.h index edcd0afb601..cb1bbed378f 100644 --- a/src/detect/reClockI2C.h +++ b/src/detect/reClockI2C.h @@ -1,40 +1,10 @@ -#ifdef CAN_RECLOCK_I2C -#include "ScanI2CTwoWire.h" - -uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus) { - uint32_t currentClock; +#ifndef RECLOCK_I2C_ +#define RECLOCK_I2C_ - /* See https://github.com/arduino/Arduino/issues/11457 - Currently, only ESP32 can getClock() - While all cores can setClock() - https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50 - https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60 - https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103 - For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes) - we need to reclock I2C and set it back to the previous desired speed. - Only for cases where we can know OR predefine the speed, we can do this. - */ +#include "ScanI2CTwoWire.h" +#include -#ifdef ARCH_ESP32 - currentClock = i2cBus->getClock(); -#elif defined(ARCH_NRF52) - // TODO add getClock function or return a predefined clock speed per variant? - return 0; -#elif defined(ARCH_RP2040) - // TODO add getClock function or return a predefined clock speed per variant - return 0; -#elif defined(ARCH_STM32WL) - // TODO add getClock function or return a predefined clock speed per variant - return 0; -#else - return 0; -#endif +uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force); - if (currentClock != desiredClock){ - LOG_DEBUG("Changing I2C clock to %u", desiredClock); - i2cBus->setClock(desiredClock); - } - return currentClock; -} -#endif +#endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 0e812ba1244..dee29dcce93 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -767,8 +767,6 @@ void setup() scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SEN5X, meshtastic_TelemetrySensorType_SEN5X); - i2cScanner.reset(); #endif #ifdef HAS_SDCARD diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index ea5733fa62d..20c5a10d9b0 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -17,11 +17,12 @@ #include "main.h" #include "sleep.h" #include -#include "Sensor/AddI2CSensorTemplate.h" + // Sensors +#include "Sensor/AddI2CSensorTemplate.h" #include "Sensor/PMSA003ISensor.h" - +#include "Sensor/SEN5XSensor.h" void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { @@ -43,19 +44,9 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) // order by priority of metrics/values (low top, high bottom) addSensor(i2cScanner, ScanI2C::DeviceType::PMSA003I); + addSensor(i2cScanner, ScanI2C::DeviceType::SEN5X); } -// TODO - Small hack to review -#ifndef INCLUDE_SEN5X -#define INCLUDE_SEN5X 1 -#endif - -#ifdef INCLUDE_SEN5X -#include "Sensor/SEN5XSensor.h" -SEN5XSensor sen5xSensor; - -#include "graphics/ScreenFonts.h" - int32_t AirQualityTelemetryModule::runOnce() { if (sleepOnNextExecution == true) { @@ -67,7 +58,6 @@ int32_t AirQualityTelemetryModule::runOnce() } uint32_t result = UINT32_MAX; - uint32_t sen5xPendingForReady; if (!(moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_screen_enabled || AIR_QUALITY_TELEMETRY_MODULE_ENABLE)) { @@ -98,31 +88,27 @@ int32_t AirQualityTelemetryModule::runOnce() } // Wake up the sensors that need it - LOG_INFO("Waking up sensors"); + LOG_INFO("Waking up sensors..."); for (TelemetrySensor *sensor : sensors) { - if (((lastSentToMesh == 0) || - !Throttle::isWithinTimespanMs(lastSentToMesh - sensor->warmup_time, 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()) { + if (!sensor->canSleep()) { + LOG_DEBUG("%s sensor doesn't have sleep feature. Skipping", sensor->sensorName); + } else if (((lastSentToMesh == 0) || + !Throttle::isWithinTimespanMs(lastSentToMesh - sensor->wakeUpTimeMs(), 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()) { if (!sensor->isActive()) { + LOG_DEBUG("Waking up: %s", sensor->sensorName); return sensor->wakeUp(); + } else { + int32_t pendingForReadyMs = sensor->pendingForReadyMs(); + LOG_DEBUG("%s. Pending for ready %ums", sensor->sensorName, pendingForReadyMs); + if (pendingForReadyMs) { + return pendingForReadyMs; + } } } } - - // 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 > 0) { - return sen5xPendingForReady; - } - } - LOG_DEBUG("Checking if sending telemetry"); - if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( moduleConfig.telemetry.air_quality_interval, @@ -140,17 +126,16 @@ int32_t AirQualityTelemetryModule::runOnce() } // Send to sleep sensors that consume power - LOG_INFO("Sending sensors to sleep"); + LOG_DEBUG("Sending sensors to sleep"); for (TelemetrySensor *sensor : sensors) { - // TODO FIX - if (sensor->isActive()) { - if (sensor.warmup_time < Default::getConfiguredOrDefaultMsScaled( + if (sensor->isActive() && sensor->canSleep()) { + if (sensor->wakeUpTimeMs() < Default::getConfiguredOrDefaultMsScaled( moduleConfig.telemetry.air_quality_interval, default_telemetry_broadcast_interval_secs, numOnlineNodes)) { - LOG_DEBUG("SEN5X: Disabling sensor until next period"); + LOG_DEBUG("Disabling %s until next period", sensor->sensorName); sensor->sleep(); } else { - LOG_DEBUG("SEN5X: Sensor stays enabled due to warm up period"); + LOG_DEBUG("Sensor stays enabled due to warm up period"); } } } @@ -282,16 +267,14 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) { - bool valid = false; + bool valid = true; bool hasSensor = false; m->time = getTime(); m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag; m->variant.air_quality_metrics = meshtastic_AirQualityMetrics_init_zero; - // TODO - Should we check for sensor state here? - // If a sensor is sleeping, we should know and check to wake it up for (TelemetrySensor *sensor : sensors) { - LOG_INFO("Reading AQ sensors"); + LOG_DEBUG("Reading %s", sensor->sensorName); valid = valid && sensor->getMetrics(m); hasSensor = true; } @@ -333,9 +316,6 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) 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, @@ -396,13 +376,6 @@ 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/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 467659efe6c..8d5511bec75 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -24,26 +24,33 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) _bus = bus; _address = dev->address.address; -#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) - uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus); - if (!currentClock){ - LOG_WARN("PMSA003I can't be used at this clock speed"); +#ifdef PMSA003I_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, false); + if (currentClock != PMSA003I_I2C_CLOCK_SPEED){ + LOG_WARN("%s can't be used at this clock speed (%u)", sensorName, currentClock); return false; } -#endif +#elif !HAS_SCREEN + reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* PMSA003I_I2C_CLOCK_SPEED */ _bus->beginTransmission(_address); if (_bus->endTransmission() != 0) { - LOG_WARN("PMSA003I not found on I2C at 0x12"); + LOG_WARN("%s not found on I2C at 0x12", sensorName); return false; } #if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) - reClockI2C(currentClock, _bus); + reClockI2C(currentClock, _bus, false); #endif status = 1; - LOG_INFO("PMSA003I Enabled"); + LOG_INFO("%s Enabled", sensorName); initI2CSensor(); return true; @@ -52,30 +59,41 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) { if(!isActive()){ - LOG_WARN("PMSA003I is not active"); + LOG_WARN("Can't get metrics. %s is not active", sensorName); return false; } -#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) - uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus); -#endif +#ifdef PMSA003I_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, false); + if (currentClock != PMSA003I_I2C_CLOCK_SPEED){ + LOG_WARN("%s can't be used at this clock speed (%u)", sensorName, currentClock); + return false; + } +#elif !HAS_SCREEN + reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* PMSA003I_I2C_CLOCK_SPEED */ _bus->requestFrom(_address, PMSA003I_FRAME_LENGTH); if (_bus->available() < PMSA003I_FRAME_LENGTH) { - LOG_WARN("PMSA003I read failed: incomplete data (%d bytes)", _bus->available()); + LOG_WARN("%s read failed: incomplete data (%d bytes)", sensorName, _bus->available()); return false; } -#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) - reClockI2C(currentClock, _bus); -#endif - for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) { buffer[i] = _bus->read(); } +#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + if (buffer[0] != 0x42 || buffer[1] != 0x4D) { - LOG_WARN("PMSA003I frame header invalid: 0x%02X 0x%02X", buffer[0], buffer[1]); + LOG_WARN("%s frame header invalid: 0x%02X 0x%02X", sensorName, buffer[0], buffer[1]); return false; } @@ -91,7 +109,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) receivedChecksum = read16(buffer, PMSA003I_FRAME_LENGTH - 2); if (computedChecksum != receivedChecksum) { - LOG_WARN("PMSA003I checksum failed: computed 0x%04X, received 0x%04X", computedChecksum, receivedChecksum); + LOG_WARN("%s checksum failed: computed 0x%04X, received 0x%04X", sensorName, computedChecksum, receivedChecksum); return false; } @@ -141,21 +159,58 @@ bool PMSA003ISensor::isActive() return state == State::ACTIVE; } +int32_t PMSA003ISensor::wakeUpTimeMs() +{ +#ifdef PMSA003I_ENABLE_PIN + return PMSA003I_WARMUP_MS; +#endif + return 0; +} + +int32_t PMSA003ISensor::pendingForReadyMs() +{ +#ifdef PMSA003I_ENABLE_PIN + + uint32_t now; + now = getTime(); + uint32_t sincePmMeasureStarted = (now - pmMeasureStarted)*1000; + LOG_DEBUG("%s: Since measure started: %ums", sensorName, sincePmMeasureStarted); + + if (sincePmMeasureStarted < PMSA003I_WARMUP_MS) { + LOG_INFO("%s: not enough time passed since starting measurement", sensorName); + return PMSA003I_WARMUP_MS - sincePmMeasureStarted; + } + return 0; + +#endif + return 0; +} + + +bool PMSA003ISensor::canSleep() { +#ifdef PMSA003I_ENABLE_PIN + return true; +#endif + return false; +} void PMSA003ISensor::sleep() { #ifdef PMSA003I_ENABLE_PIN digitalWrite(PMSA003I_ENABLE_PIN, LOW); state = State::IDLE; + pmMeasureStarted = 0; #endif } uint32_t PMSA003ISensor::wakeUp() { #ifdef PMSA003I_ENABLE_PIN - LOG_INFO("Waking up PMSA003I"); + LOG_INFO("Waking up %s", sensorName); digitalWrite(PMSA003I_ENABLE_PIN, HIGH); state = State::ACTIVE; + pmMeasureStarted = getTime(); + return PMSA003I_WARMUP_MS; #endif // No need to wait for warmup if already active diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index 47c8a05cc5d..6d621d0963d 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -4,6 +4,7 @@ #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" +#include "RTC.h" #define PMSA003I_I2C_CLOCK_SPEED 100000 #define PMSA003I_FRAME_LENGTH 32 @@ -19,6 +20,9 @@ class PMSA003ISensor : public TelemetrySensor virtual bool isActive() override; virtual void sleep() override; virtual uint32_t wakeUp() override; + virtual bool canSleep() override; + virtual int32_t wakeUpTimeMs() override; + virtual int32_t pendingForReadyMs() override; private: enum class State { IDLE, ACTIVE }; @@ -26,6 +30,7 @@ class PMSA003ISensor : public TelemetrySensor uint16_t computedChecksum = 0; uint16_t receivedChecksum = 0; + uint32_t pmMeasureStarted = 0; uint8_t buffer[PMSA003I_FRAME_LENGTH]{}; TwoWire * _bus{}; diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index cdb72bbdc85..04165cb7ecb 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -1,10 +1,11 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "SEN5XSensor.h" #include "TelemetrySensor.h" +#include "../detect/reClockI2C.h" #include "FSCommon.h" #include "SPILock.h" #include "SafeFile.h" @@ -12,16 +13,9 @@ #include #include // FLT_MAX -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 +SEN5XSensor::SEN5XSensor() + : TelemetrySensor(meshtastic_TelemetrySensorType_SEN5X, "SEN5X") +{ } bool SEN5XSensor::getVersion() @@ -117,24 +111,31 @@ bool SEN5XSensor::sendCommand(uint16_t command, uint8_t* buffer, uint8_t byteNum } #ifdef SEN5X_I2C_CLOCK_SPEED - uint32_t currentClock; - currentClock = bus->getClock(); +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, false); if (currentClock != SEN5X_I2C_CLOCK_SPEED){ - // LOG_DEBUG("Changing I2C clock to %u", SEN5X_I2C_CLOCK_SPEED); - bus->setClock(SEN5X_I2C_CLOCK_SPEED); + LOG_WARN("%s can't be used at this clock speed (%u)", sensorName, currentClock); + return false; } -#endif +#elif !HAS_SCREEN + reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SEN5X_I2C_CLOCK_SPEED */ + // Transmit the data // 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(); + _bus->beginTransmission(_address); + size_t writtenBytes = _bus->write(toSend, bufferSize); + uint8_t i2c_error = _bus->endTransmission(); -#ifdef SEN5X_I2C_CLOCK_SPEED - restoreClock(currentClock); +#if defined(SEN5X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); #endif if (writtenBytes != bufferSize) { @@ -152,15 +153,21 @@ bool SEN5XSensor::sendCommand(uint16_t command, uint8_t* buffer, uint8_t byteNum uint8_t SEN5XSensor::readBuffer(uint8_t* buffer, uint8_t byteNumber) { #ifdef SEN5X_I2C_CLOCK_SPEED - uint32_t currentClock; - currentClock = bus->getClock(); +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, false); if (currentClock != SEN5X_I2C_CLOCK_SPEED){ - // LOG_DEBUG("Changing I2C clock to %u", SEN5X_I2C_CLOCK_SPEED); - bus->setClock(SEN5X_I2C_CLOCK_SPEED); + LOG_WARN("%s can't be used at this clock speed (%u)", sensorName, currentClock); + return false; } -#endif +#elif !HAS_SCREEN + reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SEN5X_I2C_CLOCK_SPEED */ - size_t readBytes = bus->requestFrom(address, byteNumber); + size_t readBytes = _bus->requestFrom(_address, byteNumber); if (readBytes != byteNumber) { LOG_ERROR("SEN5X: Error reading I2C bus"); return 0; @@ -169,9 +176,9 @@ uint8_t SEN5XSensor::readBuffer(uint8_t* buffer, uint8_t byteNumber) uint8_t i = 0; uint8_t receivedBytes = 0; while (readBytes > 0) { - buffer[i++] = bus->read(); // Just as a reminder: i++ returns i and after that increments. - buffer[i++] = bus->read(); - uint8_t recvCRC = bus->read(); + buffer[i++] = _bus->read(); // Just as a reminder: i++ returns i and after that increments. + buffer[i++] = _bus->read(); + uint8_t recvCRC = _bus->read(); uint8_t calcCRC = sen5xCRC(&buffer[i - 2]); if (recvCRC != calcCRC) { LOG_ERROR("SEN5X: Checksum error while receiving msg"); @@ -180,8 +187,8 @@ uint8_t SEN5XSensor::readBuffer(uint8_t* buffer, uint8_t byteNumber) readBytes -=3; receivedBytes += 2; } -#ifdef SEN5X_I2C_CLOCK_SPEED - restoreClock(currentClock); +#if defined(SEN5X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); #endif return receivedBytes; @@ -207,13 +214,9 @@ uint8_t SEN5XSensor::sen5xCRC(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; +void SEN5XSensor::sleep(){ + // TODO Check this works + idle(true); } bool SEN5XSensor::idle(bool checkState) @@ -566,35 +569,32 @@ bool SEN5XSensor::startCleaning() return true; } -int32_t SEN5XSensor::runOnce() +bool SEN5XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { 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; + _bus = bus; + _address = dev->address.address; delay(50); // without this there is an error on the deviceReset function if (!sendCommand(SEN5X_RESET)) { LOG_ERROR("SEN5X: Error reseting device"); - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + return false; } delay(200); // From Sensirion Datasheet if (!findModel()) { LOG_ERROR("SEN5X: error finding sensor model"); - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + return false; } // Check the firmware version - if (!getVersion()) return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + if (!getVersion()) return false; 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; + return false; } delay(200); // From Sensirion Datasheet @@ -658,11 +658,8 @@ int32_t SEN5XSensor::runOnce() idle(false); rhtGasMeasureStarted = now; - return initI2CSensor(); -} - -void SEN5XSensor::setup() -{ + initI2CSensor(); + return true; } bool SEN5XSensor::readValues() @@ -842,7 +839,12 @@ uint8_t SEN5XSensor::getMeasurements() return 0; } -int32_t SEN5XSensor::pendingForReady(){ +int32_t SEN5XSensor::wakeUpTimeMs() +{ + return SEN5X_WARMUP_MS_2; +} + +int32_t SEN5XSensor::pendingForReadyMs(){ uint32_t now; now = getTime(); uint32_t sincePmMeasureStarted = (now - pmMeasureStarted)*1000; diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.h b/src/modules/Telemetry/Sensor/SEN5XSensor.h index c0650bfd471..6d6fc7ffe5c 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.h +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" @@ -56,8 +56,8 @@ struct _SEN5XMeasurements { class SEN5XSensor : public TelemetrySensor { private: - TwoWire * bus; - uint8_t address; + TwoWire * _bus{}; + uint8_t _address{}; bool getVersion(); float firmwareVer = -1; @@ -92,13 +92,17 @@ class SEN5XSensor : public TelemetrySensor bool oneShotMode = true; void setMode(bool setOneShot); bool vocStateValid(); + /* 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. + See: https://sensirion.com/resource/application_note/low_power_mode/sen5x + */ + #define SEN5X_PN4P0_CONC_THD 100 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(); @@ -109,6 +113,8 @@ class SEN5XSensor : public TelemetrySensor uint32_t rhtGasMeasureStarted = 0; _SEN5XMeasurements sen5xmeasurement; + bool idle(bool checkState=true); + protected: // Store status of the sensor in this file const char *sen5XStateFileName = "/prefs/sen5X.dat"; @@ -132,26 +138,21 @@ class SEN5XSensor : public TelemetrySensor bool vocStateStable(); bool vocStateRecent(uint32_t now); - virtual void setup() override; - public: SEN5XSensor(); - bool isActive(); - uint32_t wakeUp(); - bool idle(bool checkState=true); - virtual int32_t runOnce() override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) 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. - See: https://sensirion.com/resource/application_note/low_power_mode/sen5x - */ - #define SEN5X_PN4P0_CONC_THD 100 + virtual bool isActive() override; + virtual void sleep() override; + virtual uint32_t wakeUp() override; + virtual bool canSleep() override { return true; } + virtual int32_t wakeUpTimeMs() override; + virtual int32_t pendingForReadyMs() override; + // TODO - Add a way to take averages of samples // 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; }; diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.h b/src/modules/Telemetry/Sensor/TelemetrySensor.h index 4a325aeed79..9c745422c04 100644 --- a/src/modules/Telemetry/Sensor/TelemetrySensor.h +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.h @@ -26,7 +26,6 @@ class TelemetrySensor this->status = 0; } - const char *sensorName; meshtastic_TelemetrySensorType sensorType = meshtastic_TelemetrySensorType_SENSOR_UNSET; unsigned status; bool initialized = false; @@ -56,13 +55,18 @@ class TelemetrySensor return AdminMessageHandleResult::NOT_HANDLED; } + const char *sensorName; // TODO: delete after migration bool hasSensor() { return nodeTelemetrySensorsMap[sensorType].first > 0; } + // Functions to sleep / wakeup sensors that support it + // These functions can save power consumption in cases like AQ virtual void sleep() {}; virtual uint32_t wakeUp() { return 0; } - // Return active by default, override per sensor - virtual bool isActive() { return true; } + virtual bool isActive() { return true; } // Return true by default, override per sensor + virtual bool canSleep() { return false; } // Return false by default, override per sensor + virtual int32_t wakeUpTimeMs() { return 0; } + virtual int32_t pendingForReadyMs() { return 0; } #if WIRE_INTERFACES_COUNT > 1 // Set to true if Implementation only works first I2C port (Wire) From 457eac38b598698c921698c9572095bfa2c01870 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sat, 17 Jan 2026 21:02:26 +0100 Subject: [PATCH 083/108] Fix merge errors --- src/modules/Telemetry/AirQualityTelemetry.h | 7 +++++-- src/modules/Telemetry/EnvironmentTelemetry.cpp | 1 - src/modules/Telemetry/Sensor/TelemetrySensor.h | 5 ----- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.h b/src/modules/Telemetry/AirQualityTelemetry.h index f25af0d0e72..2b88b74bacf 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.h +++ b/src/modules/Telemetry/AirQualityTelemetry.h @@ -11,10 +11,13 @@ #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "NodeDB.h" #include "ProtobufModule.h" +#include "detect/ScanI2CConsumer.h" +#include +#include class AirQualityTelemetryModule : private concurrency::OSThread, - public ScanI2CConsumer, - public ProtobufModule + public ScanI2CConsumer, + public ProtobufModule { CallbackObserver nodeStatusObserver = CallbackObserver(this, diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index ddfb2b1839d..ec6fe479962 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -144,7 +144,6 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c #include "Sensor/AddI2CSensorTemplate.h" #include "graphics/ScreenFonts.h" #include -#include "Sensor/AddI2CSensorTemplate.h" void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.h b/src/modules/Telemetry/Sensor/TelemetrySensor.h index 848af9e8c8d..9c745422c04 100644 --- a/src/modules/Telemetry/Sensor/TelemetrySensor.h +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.h @@ -58,11 +58,6 @@ class TelemetrySensor const char *sensorName; // TODO: delete after migration bool hasSensor() { return nodeTelemetrySensorsMap[sensorType].first > 0; } - // Functions to sleep / wakeup sensors that support it - virtual void sleep(){}; - virtual uint32_t wakeUp() { return 0; } - // Return active by default, override per sensor - virtual bool isActive() { return true; } // Functions to sleep / wakeup sensors that support it // These functions can save power consumption in cases like AQ From d547c4cb0f6326e51a095dc126d1a7997094923b Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sun, 18 Jan 2026 18:41:55 +0100 Subject: [PATCH 084/108] Small reordering in PMS class for consistency --- .../Telemetry/Sensor/PMSA003ISensor.cpp | 3 --- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 23 ++++++++++--------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 8d5511bec75..38bc1b609c2 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -4,11 +4,8 @@ #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "PMSA003ISensor.h" -#include "TelemetrySensor.h" #include "../detect/reClockI2C.h" -#include - PMSA003ISensor::PMSA003ISensor() : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I") { diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index 6d621d0963d..a2a0951801d 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -12,6 +12,18 @@ class PMSA003ISensor : public TelemetrySensor { +private: + enum class State { IDLE, ACTIVE }; + State state = State::ACTIVE; + + uint16_t computedChecksum = 0; + uint16_t receivedChecksum = 0; + uint32_t pmMeasureStarted = 0; + + uint8_t buffer[PMSA003I_FRAME_LENGTH]{}; + TwoWire * _bus{}; + uint8_t _address{}; + public: PMSA003ISensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; @@ -24,17 +36,6 @@ class PMSA003ISensor : public TelemetrySensor virtual int32_t wakeUpTimeMs() override; virtual int32_t pendingForReadyMs() override; -private: - enum class State { IDLE, ACTIVE }; - State state = State::ACTIVE; - - uint16_t computedChecksum = 0; - uint16_t receivedChecksum = 0; - uint32_t pmMeasureStarted = 0; - - uint8_t buffer[PMSA003I_FRAME_LENGTH]{}; - TwoWire * _bus{}; - uint8_t _address{}; }; #endif \ No newline at end of file From 2f95db200aef9b63f9079b237e31198d8e9115a6 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sun, 18 Jan 2026 18:42:38 +0100 Subject: [PATCH 085/108] If one sensor fails, AQ telemetry still reports data --- src/modules/Telemetry/AirQualityTelemetry.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 956aaa2a859..e7fbbad327e 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -278,7 +278,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; @@ -286,7 +286,7 @@ bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) for (TelemetrySensor *sensor : sensors) { LOG_DEBUG("Reading %s", sensor->sensorName); - valid = valid && sensor->getMetrics(m); + valid = valid || sensor->getMetrics(m); hasSensor = true; } From f6cd9dbe90503b6f84d108b09a4fc6140eea86e7 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sun, 18 Jan 2026 18:42:48 +0100 Subject: [PATCH 086/108] Small formatting fix --- 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 e7fbbad327e..92f2997b486 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -337,9 +337,9 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) 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); + 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); } bool hasAnyCO2 = m.variant.air_quality_metrics.has_co2 || m.variant.air_quality_metrics.has_co2_temperature || m.variant.air_quality_metrics.has_co2_humidity; From b4a25601942f09d93d883066301104ffdbdeb983 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sun, 18 Jan 2026 18:43:15 +0100 Subject: [PATCH 087/108] Add SEN5X to AQI in ScanI2C --- src/detect/ScanI2C.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp index 4795d2abc17..bf01a03657d 100644 --- a/src/detect/ScanI2C.cpp +++ b/src/detect/ScanI2C.cpp @@ -43,7 +43,7 @@ ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const ScanI2C::FoundDevice ScanI2C::firstAQI() const { - ScanI2C::DeviceType types[] = {PMSA003I, SCD4X}; + ScanI2C::DeviceType types[] = {PMSA003I, SEN5X, SCD4X}; return firstOfOrNONE(2, types); } From 52ca090ffc0edccf2acca725896a53f199a2ad47 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sun, 18 Jan 2026 18:44:36 +0100 Subject: [PATCH 088/108] SCD4X now part of AQ module with template list * Fixes difference between idle and sleep * In LowPower, sleep is disabled * Requires testing for I2C clock comms for commands --- src/modules/Telemetry/Sensor/SCD4XSensor.cpp | 301 +++++++++++-------- src/modules/Telemetry/Sensor/SCD4XSensor.h | 36 ++- 2 files changed, 201 insertions(+), 136 deletions(-) diff --git a/src/modules/Telemetry/Sensor/SCD4XSensor.cpp b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp index 132a1233f6c..2b791bfdd92 100644 --- a/src/modules/Telemetry/Sensor/SCD4XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp @@ -1,83 +1,74 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "SCD4XSensor.h" -#include "TelemetrySensor.h" -#include +#include "../detect/reClockI2C.h" #define SCD4X_NO_ERROR 0 -SCD4XSensor::SCD4XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SCD4X, "SCD4X") {} - -#ifdef SCD4X_I2C_CLOCK_SPEED -uint32_t SCD4XSensor::setI2CClock(uint32_t desiredClock){ - uint32_t currentClock; - currentClock = bus->getClock(); - LOG_DEBUG("Current I2C clock: %uHz", currentClock); - if (currentClock != desiredClock){ - LOG_DEBUG("Setting I2C clock to: %uHz", desiredClock); - bus->setClock(desiredClock); - return currentClock; - } - return 0; +SCD4XSensor::SCD4XSensor() + : TelemetrySensor(meshtastic_TelemetrySensorType_SCD4X, "SCD4X") +{ } -#endif -int32_t SCD4XSensor::runOnce() +bool SCD4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { 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; + _bus = bus; + _address = dev->address.address; #ifdef SCD4X_I2C_CLOCK_SPEED - uint32_t currentClock; - currentClock = setI2CClock(SCD4X_I2C_CLOCK_SPEED); -#endif +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); + if (currentClock != SCD4X_I2C_CLOCK_SPEED){ + LOG_WARN("%s can't be used at this clock speed (%u)", sensorName, currentClock); + return false; + } +#elif !HAS_SCREEN + reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD4X_I2C_CLOCK_SPEED */ - // FIXME - This should be based on bus and address from above - scd4x.begin(*nodeTelemetrySensorsMap[sensorType].second, - address); + scd4x.begin(*_bus, _address); - // SCD4X library + // From SCD4X library delay(30); // Stop periodic measurement if (!stopMeasurement()) { - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + return false; } // Get sensor variant scd4x.getSensorVariant(sensorVariant); if (sensorVariant == SCD4X_SENSOR_VARIANT_SCD41){ - LOG_INFO("SCD4X: Found SCD41"); - if (!wakeUp()) { - LOG_ERROR("SCD4X: Error trying to execute wakeUp()"); - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + LOG_INFO("%s: Found SCD41", sensorName); + if (!powerUp()) { + LOG_ERROR("%s: Error trying to execute powerUp()", sensorName); + return false; } } if (!getASC(ascActive)){ - LOG_ERROR("SCD4X: Unable to check if ASC is enabled"); - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName); + return false; } // Start measurement in selected power mode (low power by default) if (!startMeasurement()){ - LOG_ERROR("SCD4X: Couldn't start measurement"); - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + LOG_ERROR("%s: Couldn't start measurement", sensorName); + return false; } -#ifdef SCD4X_I2C_CLOCK_SPEED - if (currentClock){ - setI2CClock(currentClock); - } +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); #endif if (state == SCD4X_MEASUREMENT){ @@ -86,16 +77,16 @@ int32_t SCD4XSensor::runOnce() status = 0; } - return initI2CSensor(); -} + initI2CSensor(); -void SCD4XSensor::setup() {} + return true; +} bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) { if (state != SCD4X_MEASUREMENT) { - LOG_ERROR("SCD4X: Not in measurement mode"); + LOG_ERROR("%s: Not in measurement mode", sensorName); return false; } @@ -103,9 +94,19 @@ bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) float temperature, humidity; #ifdef SCD4X_I2C_CLOCK_SPEED - uint32_t currentClock; - currentClock = setI2CClock(SCD4X_I2C_CLOCK_SPEED); -#endif +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); + if (currentClock != SCD4X_I2C_CLOCK_SPEED){ + LOG_WARN("%s can't be used at this clock speed (%u)", sensorName, currentClock); + return false; + } +#elif !HAS_SCREEN + reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD4X_I2C_CLOCK_SPEED */ bool dataReady; error = scd4x.getDataReadyStatus(dataReady); @@ -116,17 +117,15 @@ bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) error = scd4x.readMeasurement(co2, temperature, humidity); -#ifdef SCD4X_I2C_CLOCK_SPEED - if (currentClock){ - setI2CClock(currentClock); - } +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); #endif - LOG_DEBUG("SCD4X readings: %u ppm, %.2f degC, %.2f %rh", co2, temperature, humidity); + LOG_DEBUG("%s readings: %u ppm, %.2f degC, %.2f %rh", sensorName, co2, temperature, humidity); if (error != SCD4X_NO_ERROR) { - LOG_DEBUG("SCD4X: Error while getting measurements: %u", error); + LOG_DEBUG("%s: Error while getting measurements: %u", sensorName, error); if (co2 == 0) { - LOG_ERROR("SCD4X: Skipping invalid measurement."); + LOG_ERROR("%s: Skipping invalid measurement.", sensorName); } return false; } else { @@ -140,6 +139,9 @@ bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) } } +// TODO +// Make all functions change I2C clock + /** * @brief Perform a forced recalibration (FRC) of the CO₂ concentration. * @@ -155,29 +157,29 @@ bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) bool SCD4XSensor::performFRC(uint32_t targetCO2) { uint16_t error, frcCorr; - LOG_INFO("SCD4X: Issuing FRC. Ensure device has been working at least 3 minutes in stable target environment"); + LOG_INFO("%s: Issuing FRC. Ensure device has been working at least 3 minutes in stable target environment", sensorName); if (!stopMeasurement()) { return false; } - LOG_INFO("SCD4X: Target CO2: %u ppm", targetCO2); + LOG_INFO("%s: Target CO2: %u ppm", sensorName, targetCO2); error = scd4x.performForcedRecalibration((uint16_t)targetCO2, frcCorr); // SCD4X Sensirion datasheet delay(400); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to perform forced recalibration."); + LOG_ERROR("%s: Unable to perform forced recalibration.", sensorName); return false; } if (frcCorr == 0xFFFF) { - LOG_ERROR("SCD4X: Error while performing forced recalibration."); + LOG_ERROR("%s: Error while performing forced recalibration.", sensorName); return false; } - LOG_INFO("SCD4X: FRC Correction successful. Correction output: %u", (uint16_t)(frcCorr-0x8000)); + LOG_INFO("%s: FRC Correction successful. Correction output: %u", sensorName, (uint16_t)(frcCorr-0x8000)); return true; } @@ -186,7 +188,7 @@ bool SCD4XSensor::startMeasurement() { uint16_t error; if (state == SCD4X_MEASUREMENT){ - LOG_DEBUG("SCD4X: Already in measurement mode"); + LOG_DEBUG("%s: Already in measurement mode", sensorName); return true; } @@ -197,17 +199,17 @@ bool SCD4XSensor::startMeasurement() { } if (error == SCD4X_NO_ERROR) { - LOG_INFO("SCD4X: Started measurement mode"); + LOG_INFO("%s: Started measurement mode", sensorName); if (lowPower) { - LOG_INFO("SCD4X: Low power mode"); + LOG_INFO("%s: Low power mode", sensorName); } else { - LOG_INFO("SCD4X: Normal power mode"); + LOG_INFO("%s: Normal power mode", sensorName); } state = SCD4X_MEASUREMENT; return true; } else { - LOG_ERROR("SCD4X: Couldn't start measurement mode"); + LOG_ERROR("%s: Couldn't start measurement mode", sensorName); return false; } } @@ -217,11 +219,12 @@ bool SCD4XSensor::stopMeasurement(){ error = scd4x.stopPeriodicMeasurement(); if (error != SCD4X_NO_ERROR) { - LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X."); + LOG_ERROR("%s: Unable to set idle mode on SCD4X.", sensorName); return false; } state = SCD4X_IDLE; + co2MeasureStarted = 0; return true; } @@ -233,9 +236,9 @@ bool SCD4XSensor::setPowerMode(bool _lowPower) { } if (lowPower) { - LOG_DEBUG("SCD4X: Set low power mode"); + LOG_DEBUG("%s: Set low power mode", sensorName); } else { - LOG_DEBUG("SCD4X: Set normal power mode"); + LOG_DEBUG("%s: Set normal power mode", sensorName); } return true; @@ -248,7 +251,7 @@ bool SCD4XSensor::setPowerMode(bool _lowPower) { */ bool SCD4XSensor::getASC(uint16_t &_ascActive) { uint16_t error; - LOG_INFO("SCD4X: Getting ASC"); + LOG_INFO("%s: Getting ASC", sensorName); if (!stopMeasurement()) { return false; @@ -256,14 +259,14 @@ bool SCD4XSensor::getASC(uint16_t &_ascActive) { error = scd4x.getAutomaticSelfCalibrationEnabled(_ascActive); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to send command."); + LOG_ERROR("%s: Unable to send command.", sensorName); return false; } if (_ascActive){ - LOG_INFO("SCD4X: ASC is enabled"); + LOG_INFO("%s: ASC is enabled", sensorName); } else { - LOG_INFO("SCD4X: FRC is enabled"); + LOG_INFO("%s: FRC is enabled", sensorName); } return true; @@ -281,9 +284,9 @@ bool SCD4XSensor::setASC(bool ascEnabled){ uint16_t error; if (ascEnabled){ - LOG_INFO("SCD4X: Enabling ASC"); + LOG_INFO("%s: Enabling ASC", sensorName); } else { - LOG_INFO("SCD4X: Disabling ASC"); + LOG_INFO("%s: Disabling ASC", sensorName); } if (!stopMeasurement()) { @@ -293,25 +296,25 @@ bool SCD4XSensor::setASC(bool ascEnabled){ error = scd4x.setAutomaticSelfCalibrationEnabled((uint16_t)ascEnabled); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to send command."); + LOG_ERROR("%s: Unable to send command.", sensorName); return false; } error = scd4x.persistSettings(); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to make settings persistent."); + LOG_ERROR("%s: Unable to make settings persistent.", sensorName); return false; } if (!getASC(ascActive)){ - LOG_ERROR("SCD4X: Unable to check if ASC is enabled"); + LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName); return false; } if (ascActive){ - LOG_INFO("SCD4X: ASC is enabled"); + LOG_INFO("%s: ASC is enabled", sensorName); } else { - LOG_INFO("SCD4X: ASC is disabled"); + LOG_INFO("%s: ASC is disabled", sensorName); } return true; @@ -333,11 +336,11 @@ bool SCD4XSensor::setASCBaseline(uint32_t targetCO2){ // TODO - Remove? // Available in library, but not described in datasheet. uint16_t error; - LOG_INFO("SCD4X: Setting ASC baseline to: %u", targetCO2); + LOG_INFO("%s: Setting ASC baseline to: %u", sensorName, targetCO2); getASC(ascActive); if (!ascActive){ - LOG_ERROR("SCD4X: Can't set ASC baseline. ASC is not active"); + LOG_ERROR("%s: Can't set ASC baseline. ASC is not active", sensorName); return false; } @@ -348,17 +351,17 @@ bool SCD4XSensor::setASCBaseline(uint32_t targetCO2){ error = scd4x.setAutomaticSelfCalibrationTarget((uint16_t)targetCO2); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to send command."); + LOG_ERROR("%s: Unable to send command.", sensorName); return false; } error = scd4x.persistSettings(); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to make settings persistent."); + LOG_ERROR("%s: Unable to make settings persistent.", sensorName); return false; } - LOG_INFO("SCD4X: Setting ASC baseline successful"); + LOG_INFO("%s: Setting ASC baseline successful", sensorName); return true; } @@ -394,21 +397,21 @@ bool SCD4XSensor::setTemperature(float tempReference){ float temperature; float humidity; - LOG_INFO("SCD4X: Setting reference temperature at: %.2f", tempReference); + LOG_INFO("%s: Setting reference temperature at: %.2f", sensorName, tempReference); error = scd4x.getDataReadyStatus(dataReady); if (!dataReady) { - LOG_ERROR("SCD4X: Data is not ready"); + LOG_ERROR("%s: Data is not ready", sensorName); return false; } error = scd4x.readMeasurement(co2, temperature, humidity); if (error != SCD4X_NO_ERROR) { - LOG_ERROR("SCD4X: Unable to read current temperature. Error code: %u", error); + LOG_ERROR("%s: Unable to read current temperature. Error code: %u", sensorName, error); return false; } - LOG_INFO("SCD4X: Current sensor temperature: %.2f", temperature); + LOG_INFO("%s: Current sensor temperature: %.2f", sensorName, temperature); if (!stopMeasurement()) { return false; @@ -417,28 +420,28 @@ bool SCD4XSensor::setTemperature(float tempReference){ error = scd4x.getTemperatureOffset(prevTempOffset); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to get temperature offset. Error code: %u", error); + LOG_ERROR("%s: Unable to get temperature offset. Error code: %u", sensorName, error); return false; } - LOG_INFO("SCD4X: Current sensor temperature offset: %.2f", prevTempOffset); + LOG_INFO("%s: Current sensor temperature offset: %.2f", sensorName, prevTempOffset); tempOffset = temperature - tempReference + prevTempOffset; - LOG_INFO("SCD4X: Setting temperature offset: %.2f", tempOffset); + LOG_INFO("%s: Setting temperature offset: %.2f", sensorName, tempOffset); error = scd4x.setTemperatureOffset(tempOffset); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to set temperature offset. Error code: %u", error); + LOG_ERROR("%s: Unable to set temperature offset. Error code: %u", sensorName, error); return false; } error = scd4x.persistSettings(); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to make settings persistent. Error code: %u", error); + LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error); return false; } scd4x.getTemperatureOffset(updatedTempOffset); - LOG_INFO("SCD4X: Updated sensor temperature offset: %.2f", updatedTempOffset); + LOG_INFO("%s: Updated sensor temperature offset: %.2f", sensorName, updatedTempOffset); return true; } @@ -453,7 +456,7 @@ bool SCD4XSensor::setTemperature(float tempReference){ */ bool SCD4XSensor::getAltitude(uint16_t &altitude){ uint16_t error; - LOG_INFO("SCD4X: Requesting sensor altitude"); + LOG_INFO("%s: Requesting sensor altitude", sensorName); if (!stopMeasurement()) { return false; @@ -462,10 +465,10 @@ bool SCD4XSensor::getAltitude(uint16_t &altitude){ error = scd4x.getSensorAltitude(altitude); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to get altitude. Error code: %u", error); + LOG_ERROR("%s: Unable to get altitude. Error code: %u", sensorName, error); return false; } - LOG_INFO("SCD4X: Sensor altitude: %u", altitude); + LOG_INFO("%s: Sensor altitude: %u", sensorName, altitude); return true; } @@ -479,15 +482,15 @@ bool SCD4XSensor::getAltitude(uint16_t &altitude){ */ bool SCD4XSensor::getAmbientPressure(uint32_t &ambientPressure){ uint16_t error; - LOG_INFO("SCD4X: Requesting sensor ambient pressure"); + LOG_INFO("%s: Requesting sensor ambient pressure", sensorName); error = scd4x.getAmbientPressure(ambientPressure); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to get altitude. Error code: %u", error); + LOG_ERROR("%s: Unable to get altitude. Error code: %u", sensorName, error); return false; } - LOG_INFO("SCD4X: Sensor ambient pressure: %u", ambientPressure); + LOG_INFO("%s: Sensor ambient pressure: %u", sensorName, ambientPressure); return true; } @@ -510,13 +513,13 @@ bool SCD4XSensor::setAltitude(uint32_t altitude){ error = scd4x.setSensorAltitude(altitude); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to set altitude. Error code: %u", error); + LOG_ERROR("%s: Unable to set altitude. Error code: %u", sensorName, error); return false; } error = scd4x.persistSettings(); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to make settings persistent. Error code: %u", error); + LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error); return false; } @@ -542,14 +545,14 @@ bool SCD4XSensor::setAmbientPressure(uint32_t ambientPressure) { error = scd4x.setAmbientPressure(ambientPressure); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to set altitude. Error code: %u", error); + LOG_ERROR("%s: Unable to set altitude. Error code: %u", sensorName, error); return false; } // Sensirion doesn't indicate if this is necessary. We send it anyway error = scd4x.persistSettings(); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to make settings persistent. Error code: %u", error); + LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error); return false; } @@ -567,7 +570,7 @@ bool SCD4XSensor::setAmbientPressure(uint32_t ambientPressure) { bool SCD4XSensor::factoryReset() { uint16_t error; - LOG_INFO("SCD4X: Requesting factory reset"); + LOG_INFO("%s: Requesting factory reset", sensorName); if (!stopMeasurement()) { return false; @@ -576,11 +579,11 @@ bool SCD4XSensor::factoryReset() { error = scd4x.performFactoryReset(); if (error != SCD4X_NO_ERROR){ - LOG_ERROR("SCD4X: Unable to do factory reset. Error code: %u", error); + LOG_ERROR("%s: Unable to do factory reset. Error code: %u", sensorName, error); return false; } - LOG_INFO("SCD4X: Factory reset successful"); + LOG_INFO("%s: Factory reset successful", sensorName); return true; } @@ -596,8 +599,8 @@ bool SCD4XSensor::factoryReset() { * * @note This command is only available in idle mode. Only for SCD41. */ -bool SCD4XSensor::sleep() { - LOG_INFO("SCD4X: Powering down"); +bool SCD4XSensor::powerDown() { + LOG_INFO("%s: Trying to send sensor to sleep", sensorName); if (sensorVariant != SCD4X_SENSOR_VARIANT_SCD41) { LOG_WARN("SCD4X: Can't send sensor to sleep. Incorrect variant. Ignoring"); @@ -609,7 +612,7 @@ bool SCD4XSensor::sleep() { } if (scd4x.powerDown() != SCD4X_NO_ERROR) { - LOG_ERROR("SCD4X: Error trying to execute wakeUp()"); + LOG_ERROR("%s: Error trying to execute sleep()", sensorName); return false; } state = SCD4X_OFF; @@ -617,7 +620,7 @@ bool SCD4XSensor::sleep() { } /** -* @brief Wake up sensor from sleep mode to idle mode. +* @brief Wake up sensor from sleep mode to idle mode (powerUp) * * From Sensirion SCD4X I2C Library. * @@ -627,17 +630,71 @@ bool SCD4XSensor::sleep() { * * @note This command is only available for SCD41. */ -bool SCD4XSensor::wakeUp(){ - LOG_INFO("SCD4X: Waking up"); +bool SCD4XSensor::powerUp(){ + LOG_INFO("%s: Waking up", sensorName); if (scd4x.wakeUp() != SCD4X_NO_ERROR) { - LOG_ERROR("SCD4X: Error trying to execute wakeUp()"); + LOG_ERROR("%s: Error trying to execute wakeUp()", sensorName); return false; } state = SCD4X_IDLE; return true; } +/** +* @brief Check if sensor is in measurement mode +*/ +bool SCD4XSensor::isActive(){ + return state == SCD4X_MEASUREMENT; +} + +/** +* @brief Start measurement mode +*/ +uint32_t SCD4XSensor::wakeUp(){ + if (startMeasurement()) { + co2MeasureStarted = getTime(); + return SCD4X_WARMUP_MS; + } + return 0; +} + +/** +* @brief Stop measurement mode +*/ +void SCD4XSensor::sleep(){ + stopMeasurement(); +} + +/** +* @brief Can sleep function +* +* Power consumption is very low on lowPower mode, modify this function if +* you still want to override this behaviour. Otherwise, sleep is disabled +* routinely in low power mode +*/ +bool SCD4XSensor::canSleep(){ + return lowPower ? false : true; +} + +int32_t SCD4XSensor::wakeUpTimeMs(){ + return SCD4X_WARMUP_MS; +} + +int32_t SCD4XSensor::pendingForReadyMs() +{ + uint32_t now; + now = getTime(); + uint32_t sinceCO2MeasureStarted = (now - co2MeasureStarted)*1000; + LOG_DEBUG("%s: Since measure started: %ums", sensorName, sinceCO2MeasureStarted); + + if (sinceCO2MeasureStarted < SCD4X_WARMUP_MS) { + LOG_INFO("%s: not enough time passed since starting measurement", sensorName); + return SCD4X_WARMUP_MS - sinceCO2MeasureStarted; + } + return 0; +} + AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) { @@ -654,30 +711,30 @@ AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPa } if (request->sensor_config.scd4x_config.has_factory_reset) { - LOG_DEBUG("SCD4X: Requested factory reset"); + LOG_DEBUG("%s: Requested factory reset", sensorName); this->factoryReset(); } else { if (request->sensor_config.scd4x_config.has_set_asc) { this->setASC(request->sensor_config.scd4x_config.set_asc); if (request->sensor_config.scd4x_config.set_asc == false) { - LOG_DEBUG("SCD4X: Request for FRC"); + LOG_DEBUG("%s: Request for FRC", sensorName); if (request->sensor_config.scd4x_config.has_set_target_co2_conc) { this->performFRC(request->sensor_config.scd4x_config.set_target_co2_conc); } else { // FRC requested but no target CO2 provided - LOG_ERROR("SCD4X: target CO2 not provided"); + LOG_ERROR("%s: target CO2 not provided", sensorName); result = AdminMessageHandleResult::NOT_HANDLED; break; } } else { - LOG_DEBUG("SCD4X: Request for ASC"); + LOG_DEBUG("%s: Request for ASC", sensorName); if (request->sensor_config.scd4x_config.has_set_target_co2_conc) { - LOG_DEBUG("SCD4X: Request has target CO2"); + LOG_DEBUG("%s: Request has target CO2", sensorName); // TODO - Remove? see setASCBaseline function this->setASCBaseline(request->sensor_config.scd4x_config.set_target_co2_conc); } else { - LOG_DEBUG("SCD4X: Request doesn't have target CO2"); + LOG_DEBUG("%s: Request doesn't have target CO2", sensorName); } } } diff --git a/src/modules/Telemetry/Sensor/SCD4XSensor.h b/src/modules/Telemetry/Sensor/SCD4XSensor.h index 52a0b7046f2..4f12398b39d 100644 --- a/src/modules/Telemetry/Sensor/SCD4XSensor.h +++ b/src/modules/Telemetry/Sensor/SCD4XSensor.h @@ -1,19 +1,22 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include +#include "RTC.h" +// Max speed 400kHz #define SCD4X_I2C_CLOCK_SPEED 100000 +#define SCD4X_WARMUP_MS 5000 class SCD4XSensor : public TelemetrySensor { private: SensirionI2cScd4x scd4x; - TwoWire* bus; - uint8_t address; + TwoWire * _bus{}; + uint8_t _address{}; bool performFRC(uint32_t targetCO2); bool setASCBaseline(uint32_t targetCO2); @@ -24,30 +27,35 @@ class SCD4XSensor : public TelemetrySensor bool setAltitude(uint32_t altitude); bool getAmbientPressure(uint32_t &ambientPressure); bool setAmbientPressure(uint32_t ambientPressure); -#ifdef SCD4X_I2C_CLOCK_SPEED - uint32_t setI2CClock(uint32_t currentClock); -#endif bool factoryReset(); bool setPowerMode(bool _lowPower); bool startMeasurement(); bool stopMeasurement(); - // Parameters uint16_t ascActive; + // low power measurement mode (on sensirion side). Disables sleep mode + // Improvement and testing needed for timings bool lowPower = true; - - protected: - virtual void setup() override; + uint32_t co2MeasureStarted = 0; public: SCD4XSensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + enum SCD4XState { SCD4X_OFF, SCD4X_IDLE, SCD4X_MEASUREMENT }; SCD4XState state = SCD4X_OFF; SCD4xSensorVariant sensorVariant; - bool sleep(); - bool wakeUp(); - virtual int32_t runOnce() override; - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + + virtual bool isActive() override; + + virtual void sleep() override; // Stops measurement (measurement -> idle) + virtual uint32_t wakeUp() override; // Starts measurement (idle -> measurement) + bool powerDown(); // Powers down sensor (idle -> power-off) + bool powerUp(); // Powers the sensor (power-off -> idle) + virtual bool canSleep() override; + virtual int32_t wakeUpTimeMs() override; + virtual int32_t pendingForReadyMs() override; AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) override; }; From c6c97f1e389671dfef71ee0c93f78c729296ac1c Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sun, 18 Jan 2026 18:45:02 +0100 Subject: [PATCH 089/108] Remove unnecessary import --- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index 04165cb7ecb..c22feeecad4 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -4,7 +4,6 @@ #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "SEN5XSensor.h" -#include "TelemetrySensor.h" #include "../detect/reClockI2C.h" #include "FSCommon.h" #include "SPILock.h" From 26a05c31e8aa046e9aed59abaa4e52c6b2b1ee54 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 20 Jan 2026 09:47:01 +0100 Subject: [PATCH 090/108] Add co2 to serialized AQ metrics --- src/serialization/MeshPacketSerializer.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/serialization/MeshPacketSerializer.cpp b/src/serialization/MeshPacketSerializer.cpp index 92e70036811..633db6314a5 100644 --- a/src/serialization/MeshPacketSerializer.cpp +++ b/src/serialization/MeshPacketSerializer.cpp @@ -161,6 +161,15 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, // msgPayload["pm100_e"] = // new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental); // } + if (decoded->variant.air_quality_metrics.has_co2) { + msgPayload["co2"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.co2); + } + if (decoded->variant.air_quality_metrics.has_co2_temperature) { + msgPayload["co2_temperature"] = new JSONValue(decoded->variant.air_quality_metrics.co2_temperature); + } + if (decoded->variant.air_quality_metrics.has_co2_humidity) { + msgPayload["co2_humidity"] = new JSONValue(decoded->variant.air_quality_metrics.co2_humidity); + } } 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); From 958923d447f9b212d075faa2b53c401970fadf87 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 20 Jan 2026 22:00:14 +0100 Subject: [PATCH 091/108] Update library dependencies in platformio.ini --- platformio.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 7ba2c4166ff..e5ba5ce930b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -212,6 +212,7 @@ lib_deps = sensirion/Sensirion Core@0.7.2 # renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x sensirion/Sensirion I2C SCD4x@1.1.0 + ; Same as environmental_extra but without BSEC (saves ~3.5KB DRAM for original ESP32 targets) [environmental_extra_no_bsec] lib_deps = @@ -238,4 +239,4 @@ lib_deps = # renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core sensirion/Sensirion Core@0.7.2 # renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x - sensirion/Sensirion I2C SCD4x@1.1.0 \ No newline at end of file + sensirion/Sensirion I2C SCD4x@1.1.0 From d1f1cf4d2989b1ebc8b2ee7a0905c66a4a4fab97 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 28 Jan 2026 14:16:50 +0100 Subject: [PATCH 092/108] Fix unitialized variables in SEN5X constructor --- src/modules/Telemetry/Sensor/SEN5XSensor.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.h b/src/modules/Telemetry/Sensor/SEN5XSensor.h index 6d6fc7ffe5c..91d7724ca23 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.h +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h @@ -111,7 +111,7 @@ class SEN5XSensor : public TelemetrySensor uint32_t pmMeasureStarted = 0; uint32_t rhtGasMeasureStarted = 0; - _SEN5XMeasurements sen5xmeasurement; + _SEN5XMeasurements sen5xmeasurement {}; bool idle(bool checkState=true); @@ -129,7 +129,7 @@ class SEN5XSensor : public TelemetrySensor // VOC State #define SEN5X_VOC_STATE_BUFFER_SIZE 8 - uint8_t vocState[SEN5X_VOC_STATE_BUFFER_SIZE]; + uint8_t vocState[SEN5X_VOC_STATE_BUFFER_SIZE] {}; uint32_t vocTime = 0; bool vocValid = false; From cdf6183975f9b9c9519448c3ff1a05413c2ec5e7 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 28 Jan 2026 14:25:53 +0100 Subject: [PATCH 093/108] Fix missing import --- src/detect/reClockI2C.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/detect/reClockI2C.h b/src/detect/reClockI2C.h index d7d88a6302a..c74ccffc557 100644 --- a/src/detect/reClockI2C.h +++ b/src/detect/reClockI2C.h @@ -4,6 +4,7 @@ #include "ScanI2CTwoWire.h" #include +#include uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force); From 105206cb27d1bd65a24a7008c9a77d261d6191f5 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 28 Jan 2026 17:42:44 +0100 Subject: [PATCH 094/108] Fix uninitMemberVars --- src/modules/Telemetry/Sensor/SCD4XSensor.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/Telemetry/Sensor/SCD4XSensor.h b/src/modules/Telemetry/Sensor/SCD4XSensor.h index 4f12398b39d..2542e408bdd 100644 --- a/src/modules/Telemetry/Sensor/SCD4XSensor.h +++ b/src/modules/Telemetry/Sensor/SCD4XSensor.h @@ -32,7 +32,7 @@ class SCD4XSensor : public TelemetrySensor bool startMeasurement(); bool stopMeasurement(); - uint16_t ascActive; + uint16_t ascActive = 1; // low power measurement mode (on sensirion side). Disables sleep mode // Improvement and testing needed for timings bool lowPower = true; @@ -45,7 +45,7 @@ class SCD4XSensor : public TelemetrySensor enum SCD4XState { SCD4X_OFF, SCD4X_IDLE, SCD4X_MEASUREMENT }; SCD4XState state = SCD4X_OFF; - SCD4xSensorVariant sensorVariant; + SCD4xSensorVariant sensorVariant{}; virtual bool isActive() override; From 637e0c61232c9b6902178f18bb9d4c03e2e06bcb Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 28 Jan 2026 19:47:47 +0100 Subject: [PATCH 095/108] Fix import error for SCD4X --- src/modules/Telemetry/AirQualityTelemetry.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 92f2997b486..1f027bb3dbe 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -48,7 +48,9 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) // order by priority of metrics/values (low top, high bottom) addSensor(i2cScanner, ScanI2C::DeviceType::PMSA003I); addSensor(i2cScanner, ScanI2C::DeviceType::SEN5X); +#if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::SCD4X); +#endif } int32_t AirQualityTelemetryModule::runOnce() From 66a1fc05f8f3d311f0cd50aa73eab3dba9db276c Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 28 Jan 2026 20:30:34 +0100 Subject: [PATCH 096/108] Fix I2CClock logic * Fix not reclocking back to 700000Hz --- src/detect/reClockI2C.cpp | 2 +- src/modules/Telemetry/Sensor/SCD4XSensor.cpp | 8 -------- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 9 --------- 3 files changed, 1 insertion(+), 18 deletions(-) diff --git a/src/detect/reClockI2C.cpp b/src/detect/reClockI2C.cpp index f1dfee25909..5315bf3eca1 100644 --- a/src/detect/reClockI2C.cpp +++ b/src/detect/reClockI2C.cpp @@ -21,7 +21,7 @@ uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force) { currentClock = i2cBus->getClock(); #endif - if (currentClock != desiredClock || force){ + if ((currentClock != desiredClock) || force){ LOG_DEBUG("Changing I2C clock to %u", desiredClock); i2cBus->setClock(desiredClock); } diff --git a/src/modules/Telemetry/Sensor/SCD4XSensor.cpp b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp index 2b791bfdd92..9a2a59850a7 100644 --- a/src/modules/Telemetry/Sensor/SCD4XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp @@ -23,10 +23,6 @@ bool SCD4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) #ifdef SCD4X_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); - if (currentClock != SCD4X_I2C_CLOCK_SPEED){ - LOG_WARN("%s can't be used at this clock speed (%u)", sensorName, currentClock); - return false; - } #elif !HAS_SCREEN reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); #else @@ -96,10 +92,6 @@ bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) #ifdef SCD4X_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); - if (currentClock != SCD4X_I2C_CLOCK_SPEED){ - LOG_WARN("%s can't be used at this clock speed (%u)", sensorName, currentClock); - return false; - } #elif !HAS_SCREEN reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); #else diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index c22feeecad4..967c0771917 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -112,10 +112,6 @@ bool SEN5XSensor::sendCommand(uint16_t command, uint8_t* buffer, uint8_t byteNum #ifdef SEN5X_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, false); - if (currentClock != SEN5X_I2C_CLOCK_SPEED){ - LOG_WARN("%s can't be used at this clock speed (%u)", sensorName, currentClock); - return false; - } #elif !HAS_SCREEN reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, true); #else @@ -124,7 +120,6 @@ bool SEN5XSensor::sendCommand(uint16_t command, uint8_t* buffer, uint8_t byteNum #endif /* CAN_RECLOCK_I2C */ #endif /* SEN5X_I2C_CLOCK_SPEED */ - // Transmit the data // LOG_DEBUG("Beginning connection to SEN5X: 0x%x. Size: %u", address, bufferSize); // Note: this is necessary to allow for long-buffers @@ -154,10 +149,6 @@ uint8_t SEN5XSensor::readBuffer(uint8_t* buffer, uint8_t byteNumber) #ifdef SEN5X_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, false); - if (currentClock != SEN5X_I2C_CLOCK_SPEED){ - LOG_WARN("%s can't be used at this clock speed (%u)", sensorName, currentClock); - return false; - } #elif !HAS_SCREEN reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, true); #else From 5cfbc651dc455d695980fdbbdddcde92a0c6a997 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 28 Jan 2026 20:32:15 +0100 Subject: [PATCH 097/108] Fix multiple sensors being read simultaneously * The logic in AQ module is different to the one in EnvironmentTelemetryModule. In Env module, if any sensor fails to getMetrics, no valid flag for the module. This prevents other sensors to report data. --- src/modules/Telemetry/AirQualityTelemetry.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 1f027bb3dbe..2d46990f30c 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -280,15 +280,20 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) { + // Note: this is different to the case in EnvironmentTelemetryModule + // There, if any sensor fails to read - valid = false. bool valid = false; bool hasSensor = false; m->time = getTime(); m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag; m->variant.air_quality_metrics = meshtastic_AirQualityMetrics_init_zero; + bool sensor_get = false; for (TelemetrySensor *sensor : sensors) { LOG_DEBUG("Reading %s", sensor->sensorName); - valid = valid || sensor->getMetrics(m); + // Note - this function doesn't get properly called if within a conditional + sensor_get = sensor->getMetrics(m); + valid = valid || sensor_get; hasSensor = true; } From 7f4f078452daf8db8714253143771f438886ff52 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Fri, 30 Jan 2026 09:43:45 +0100 Subject: [PATCH 098/108] Fix pending clock change in PMSA003 --- src/modules/Telemetry/Sensor/PMSA003ISensor.cpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 38bc1b609c2..a29175aaecc 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -24,10 +24,6 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) #ifdef PMSA003I_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, false); - if (currentClock != PMSA003I_I2C_CLOCK_SPEED){ - LOG_WARN("%s can't be used at this clock speed (%u)", sensorName, currentClock); - return false; - } #elif !HAS_SCREEN reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, true); #else @@ -63,10 +59,6 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) #ifdef PMSA003I_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, false); - if (currentClock != PMSA003I_I2C_CLOCK_SPEED){ - LOG_WARN("%s can't be used at this clock speed (%u)", sensorName, currentClock); - return false; - } #elif !HAS_SCREEN reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, true); #else From 7b125aab98a68b318173e08a03624940342b93d4 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Fri, 6 Feb 2026 19:47:54 +0100 Subject: [PATCH 099/108] Cleanup of SEN5X class --- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 61 +------------------- src/modules/Telemetry/Sensor/SEN5XSensor.h | 2 - 2 files changed, 3 insertions(+), 60 deletions(-) diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index 04165cb7ecb..b937c2bfb72 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -128,7 +128,7 @@ bool SEN5XSensor::sendCommand(uint16_t command, uint8_t* buffer, uint8_t byteNum // Transmit the data // LOG_DEBUG("Beginning connection to SEN5X: 0x%x. Size: %u", address, bufferSize); - // Note: this is necessary to allow for long-buffers + // Note: this delay is necessary to allow for long-buffers delay(20); _bus->beginTransmission(_address); size_t writtenBytes = _bus->write(toSend, bufferSize); @@ -354,7 +354,6 @@ bool SEN5XSensor::vocStateFromSensor() // 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(&vocState[0], SEN5X_VOC_STATE_BUFFER_SIZE + (SEN5X_VOC_STATE_BUFFER_SIZE / 2)); delay(20); // From Sensirion Datasheet @@ -363,15 +362,6 @@ bool SEN5XSensor::vocStateFromSensor() 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: [%u, %u, %u, %u, %u, %u, %u, %u]", vocState[0],vocState[1], vocState[2], vocState[3], @@ -434,8 +424,6 @@ bool SEN5XSensor::loadState() bool SEN5XSensor::saveState() { - // 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); @@ -488,23 +476,12 @@ bool SEN5XSensor::isActive(){ } uint32_t SEN5XSensor::wakeUp(){ - // uint32_t now; - // now = getValidTime(RTCQuality::RTCQualityDevice); - LOG_DEBUG("SEN5X: Waking up sensor"); - // NOTE - No need to send it everytime if we switch to RHT/gas only mode - // // 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"); - // } + LOG_DEBUG("SEN5X: Waking up sensor"); if (!sendCommand(SEN5X_START_MEASUREMENT)) { LOG_ERROR("SEN5X: Error starting measurement"); - // TODO - what should this return?? Something actually on the default interval + // TODO - what should this return?? Something actually on the default interval? return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } delay(50); // From Sensirion Datasheet @@ -552,7 +529,6 @@ bool SEN5XSensor::startCleaning() uint16_t started = millis(); while (millis() - started < 10500) { - // Serial.print("."); delay(500); } LOG_INFO("SEN5X: Cleaning done!!"); @@ -773,31 +749,6 @@ bool SEN5XSensor::readPNValues(bool cumulative) 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 Datasheet - -// uint8_t dataBuffer[8]; -// size_t receivedNumber = readBuffer(&dataBuffer[0], 12); -// if (receivedNumber == 0) { -// LOG_ERROR("SEN5X: Error getting Raw values"); -// return false; -// } - -// // Get values -// rawHumidity = static_cast((dataBuffer[0] << 8) | dataBuffer[1]); -// rawTemperature = static_cast((dataBuffer[2] << 8) | dataBuffer[3]); -// rawVoc = static_cast((dataBuffer[4] << 8) | dataBuffer[5]); -// rawNox = static_cast((dataBuffer[6] << 8) | dataBuffer[7]); - -// return true; -// } - uint8_t SEN5XSensor::getMeasurements() { // Try to get new data @@ -831,11 +782,6 @@ uint8_t SEN5XSensor::getMeasurements() return 2; } - // if(!readRawValues()) { - // LOG_ERROR("SEN5X: Error getting Raw readings"); - // return 2; - // } - return 0; } @@ -1016,5 +962,4 @@ AdminMessageHandleResult SEN5XSensor::handleAdminMessage(const meshtastic_MeshPa 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 91d7724ca23..14f40ad4d4c 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.h +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h @@ -151,8 +151,6 @@ class SEN5XSensor : public TelemetrySensor virtual int32_t wakeUpTimeMs() override; virtual int32_t pendingForReadyMs() override; - // TODO - Add a way to take averages of samples - // This value represents the time needed for pending data AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) override; }; From b5681dc1e8b11813f6bd7ff0c6e57fc7c93fa000 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Fri, 6 Feb 2026 19:48:14 +0100 Subject: [PATCH 100/108] Exclude AQ sensor from wio-e5 due to flash limitations --- variants/stm32/wio-e5/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/stm32/wio-e5/platformio.ini b/variants/stm32/wio-e5/platformio.ini index 311cade581e..c8dbb2b7236 100644 --- a/variants/stm32/wio-e5/platformio.ini +++ b/variants/stm32/wio-e5/platformio.ini @@ -17,5 +17,6 @@ build_flags = -DPIN_SERIAL2_RX=PA3 -DHAS_GPS=1 -DGPS_SERIAL_PORT=Serial2 + -DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1 upload_port = stlink From 8faeeb5f62fcdf2aa4712132b5dc7e435cf46f2b Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Fri, 6 Feb 2026 20:12:50 +0100 Subject: [PATCH 101/108] Fix I2C clock change logic --- src/modules/Telemetry/Sensor/PMSA003ISensor.cpp | 8 -------- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 8 -------- 2 files changed, 16 deletions(-) diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 8d5511bec75..cc672db856a 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -27,10 +27,6 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) #ifdef PMSA003I_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, false); - if (currentClock != PMSA003I_I2C_CLOCK_SPEED){ - LOG_WARN("%s can't be used at this clock speed (%u)", sensorName, currentClock); - return false; - } #elif !HAS_SCREEN reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, true); #else @@ -66,10 +62,6 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) #ifdef PMSA003I_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, false); - if (currentClock != PMSA003I_I2C_CLOCK_SPEED){ - LOG_WARN("%s can't be used at this clock speed (%u)", sensorName, currentClock); - return false; - } #elif !HAS_SCREEN reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, true); #else diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index b937c2bfb72..48b189078bb 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -113,10 +113,6 @@ bool SEN5XSensor::sendCommand(uint16_t command, uint8_t* buffer, uint8_t byteNum #ifdef SEN5X_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, false); - if (currentClock != SEN5X_I2C_CLOCK_SPEED){ - LOG_WARN("%s can't be used at this clock speed (%u)", sensorName, currentClock); - return false; - } #elif !HAS_SCREEN reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, true); #else @@ -155,10 +151,6 @@ uint8_t SEN5XSensor::readBuffer(uint8_t* buffer, uint8_t byteNumber) #ifdef SEN5X_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, false); - if (currentClock != SEN5X_I2C_CLOCK_SPEED){ - LOG_WARN("%s can't be used at this clock speed (%u)", sensorName, currentClock); - return false; - } #elif !HAS_SCREEN reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, true); #else From 4e1d4052e9c6c10f4da28d28cabdbd676e4edf44 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Fri, 6 Feb 2026 21:12:41 +0100 Subject: [PATCH 102/108] Make sure clock is always set to needed value --- src/modules/Telemetry/Sensor/SCD4XSensor.cpp | 157 +++++++++++++++---- 1 file changed, 130 insertions(+), 27 deletions(-) diff --git a/src/modules/Telemetry/Sensor/SCD4XSensor.cpp b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp index 9a2a59850a7..6b38a04672f 100644 --- a/src/modules/Telemetry/Sensor/SCD4XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp @@ -13,8 +13,7 @@ SCD4XSensor::SCD4XSensor() { } -bool SCD4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ +bool SCD4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); _bus = bus; @@ -38,6 +37,9 @@ bool SCD4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) // Stop periodic measurement if (!stopMeasurement()) { +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif return false; } @@ -48,18 +50,27 @@ bool SCD4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) LOG_INFO("%s: Found SCD41", sensorName); if (!powerUp()) { LOG_ERROR("%s: Error trying to execute powerUp()", sensorName); +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif return false; } } if (!getASC(ascActive)){ LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName); +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif return false; } // Start measurement in selected power mode (low power by default) if (!startMeasurement()){ LOG_ERROR("%s: Couldn't start measurement", sensorName); +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif return false; } @@ -78,8 +89,7 @@ bool SCD4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) return true; } -bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) -{ +bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) { if (state != SCD4X_MEASUREMENT) { LOG_ERROR("%s: Not in measurement mode", sensorName); @@ -103,6 +113,9 @@ bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) bool dataReady; error = scd4x.getDataReadyStatus(dataReady); if (!dataReady) { +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif LOG_ERROR("SCD4X: Data is not ready"); return false; } @@ -131,9 +144,6 @@ bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) } } -// TODO -// Make all functions change I2C clock - /** * @brief Perform a forced recalibration (FRC) of the CO₂ concentration. * @@ -145,6 +155,7 @@ bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) * must be operated at the voltage desired for the application when * performing the FRC sequence. 2. Issue the stop_periodic_measurement * command. 3. Issue the perform_forced_recalibration command. +* @note This function should not change the clock */ bool SCD4XSensor::performFRC(uint32_t targetCO2) { uint16_t error, frcCorr; @@ -176,6 +187,10 @@ bool SCD4XSensor::performFRC(uint32_t targetCO2) { return true; } +/** +* @brief Start measurement mode +* @note This function should not change the clock +*/ bool SCD4XSensor::startMeasurement() { uint16_t error; @@ -206,7 +221,11 @@ bool SCD4XSensor::startMeasurement() { } } -bool SCD4XSensor::stopMeasurement(){ +/** +* @brief Stop measurement mode +* @note This function should not change the clock +*/ +bool SCD4XSensor::stopMeasurement() { uint16_t error; error = scd4x.stopPeriodicMeasurement(); @@ -220,6 +239,11 @@ bool SCD4XSensor::stopMeasurement(){ return true; } +/** +* @brief Set power mode +* Pass true to set low power mode +* @note This function should not change the clock +*/ bool SCD4XSensor::setPowerMode(bool _lowPower) { lowPower = _lowPower; @@ -238,8 +262,8 @@ bool SCD4XSensor::setPowerMode(bool _lowPower) { /** * @brief Check the current mode (ASC or FRC) - * From Sensirion SCD4X I2C Library +* @note This function should not change the clock */ bool SCD4XSensor::getASC(uint16_t &_ascActive) { uint16_t error; @@ -271,8 +295,9 @@ bool SCD4XSensor::getASC(uint16_t &_ascActive) { * * Sets the current state (enabled / disabled) of the ASC. By default, ASC * is enabled. +* @note This function should not change the clock */ -bool SCD4XSensor::setASC(bool ascEnabled){ +bool SCD4XSensor::setASC(bool ascEnabled) { uint16_t error; if (ascEnabled){ @@ -323,8 +348,9 @@ bool SCD4XSensor::setASC(bool ascEnabled){ * operation. To save the setting to the EEPROM, the persist_settings * command must be issued subsequently. The factory default value is 400 * ppm. +* @note This function should not change the clock */ -bool SCD4XSensor::setASCBaseline(uint32_t targetCO2){ +bool SCD4XSensor::setASCBaseline(uint32_t targetCO2) { // TODO - Remove? // Available in library, but not described in datasheet. uint16_t error; @@ -358,7 +384,6 @@ bool SCD4XSensor::setASCBaseline(uint32_t targetCO2){ return true; } - /** * @brief Set the temperature compensation reference. * @@ -378,8 +403,9 @@ bool SCD4XSensor::setASCBaseline(uint32_t targetCO2){ * * Recommended temperature offset values are between 0 °C and 20 °C. The * temperature offset does not impact the accuracy of the CO2 output. +* @note This function should not change the clock */ -bool SCD4XSensor::setTemperature(float tempReference){ +bool SCD4XSensor::setTemperature(float tempReference) { uint16_t error; float prevTempOffset; float updatedTempOffset; @@ -445,8 +471,9 @@ bool SCD4XSensor::setTemperature(float tempReference){ * * Altitude in meters above sea level can be set after device installation. * Valid value between 0 and 3000m. This overrides pressure offset. +* @note This function should not change the clock */ -bool SCD4XSensor::getAltitude(uint16_t &altitude){ +bool SCD4XSensor::getAltitude(uint16_t &altitude) { uint16_t error; LOG_INFO("%s: Requesting sensor altitude", sensorName); @@ -471,8 +498,9 @@ bool SCD4XSensor::getAltitude(uint16_t &altitude){ * From Sensirion SCD4X I2C Library. * * Gets the ambient pressure in Pa. +* @note This function should not change the clock */ -bool SCD4XSensor::getAmbientPressure(uint32_t &ambientPressure){ +bool SCD4XSensor::getAmbientPressure(uint32_t &ambientPressure) { uint16_t error; LOG_INFO("%s: Requesting sensor ambient pressure", sensorName); @@ -494,8 +522,9 @@ bool SCD4XSensor::getAmbientPressure(uint32_t &ambientPressure){ * * Altitude in meters above sea level can be set after device installation. * Valid value between 0 and 3000m. This overrides pressure offset. +* @note This function should not change the clock */ -bool SCD4XSensor::setAltitude(uint32_t altitude){ +bool SCD4XSensor::setAltitude(uint32_t altitude) { uint16_t error; if (!stopMeasurement()) { @@ -530,6 +559,7 @@ bool SCD4XSensor::setAltitude(uint32_t altitude){ * applications experiencing significant ambient pressure changes to ensure * sensor accuracy. Valid input values are between 70000 - 120000 Pa. The * default value is 101300 Pa. +* @note This function should not change the clock */ bool SCD4XSensor::setAmbientPressure(uint32_t ambientPressure) { uint16_t error; @@ -558,6 +588,7 @@ bool SCD4XSensor::setAmbientPressure(uint32_t ambientPressure) { * * The perform_factory_reset command resets all configuration settings * stored in the EEPROM and erases the FRC and ASC algorithm history. +* @note This function should not change the clock */ bool SCD4XSensor::factoryReset() { uint16_t error; @@ -588,7 +619,6 @@ bool SCD4XSensor::factoryReset() { * Put the sensor from idle to sleep to reduce power consumption. Can be * used to power down when operating the sensor in power-cycled single shot * mode. -* * @note This command is only available in idle mode. Only for SCD41. */ bool SCD4XSensor::powerDown() { @@ -599,14 +629,36 @@ bool SCD4XSensor::powerDown() { return true; } +#ifdef SCD4X_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD4X_I2C_CLOCK_SPEED */ + if (!stopMeasurement()) { +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif return false; } if (scd4x.powerDown() != SCD4X_NO_ERROR) { LOG_ERROR("%s: Error trying to execute sleep()", sensorName); +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif return false; } + +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + state = SCD4X_OFF; return true; } @@ -619,17 +671,19 @@ bool SCD4XSensor::powerDown() { * Wake up the sensor from sleep mode into idle mode. Note that the SCD4x * does not acknowledge the wake_up command. The sensor's idle state after * wake up can be verified by reading out the serial number. -* * @note This command is only available for SCD41. +* @note This function can't change clock (used in init) */ -bool SCD4XSensor::powerUp(){ +bool SCD4XSensor::powerUp() { LOG_INFO("%s: Waking up", sensorName); if (scd4x.wakeUp() != SCD4X_NO_ERROR) { LOG_ERROR("%s: Error trying to execute wakeUp()", sensorName); return false; } + state = SCD4X_IDLE; + return true; } @@ -642,20 +696,57 @@ bool SCD4XSensor::isActive(){ /** * @brief Start measurement mode +* @note Not used in admin comands, getMetrics or init, can change clock. */ -uint32_t SCD4XSensor::wakeUp(){ +uint32_t SCD4XSensor::wakeUp() { + +#ifdef SCD4X_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD4X_I2C_CLOCK_SPEED */ + if (startMeasurement()) { co2MeasureStarted = getTime(); +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif return SCD4X_WARMUP_MS; } + +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return 0; } /** * @brief Stop measurement mode +* @note Not used in admin comands, getMetrics or init, can change clock. */ -void SCD4XSensor::sleep(){ +void SCD4XSensor::sleep() { +#ifdef SCD4X_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD4X_I2C_CLOCK_SPEED */ + stopMeasurement(); + +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif } /** @@ -665,16 +756,15 @@ void SCD4XSensor::sleep(){ * you still want to override this behaviour. Otherwise, sleep is disabled * routinely in low power mode */ -bool SCD4XSensor::canSleep(){ +bool SCD4XSensor::canSleep() { return lowPower ? false : true; } -int32_t SCD4XSensor::wakeUpTimeMs(){ +int32_t SCD4XSensor::wakeUpTimeMs() { return SCD4X_WARMUP_MS; } -int32_t SCD4XSensor::pendingForReadyMs() -{ +int32_t SCD4XSensor::pendingForReadyMs() { uint32_t now; now = getTime(); uint32_t sinceCO2MeasureStarted = (now - co2MeasureStarted)*1000; @@ -692,8 +782,18 @@ AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPa { AdminMessageHandleResult result; - // TODO: potentially add selftest command? +#ifdef SCD4X_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return AdminMessageHandleResult::NOT_HANDLED; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD4X_I2C_CLOCK_SPEED */ + // TODO: potentially add selftest command? switch (request->which_payload_variant) { case meshtastic_AdminMessage_sensor_config_tag: // Check for ASC-FRC request first @@ -751,7 +851,6 @@ AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPa if (request->sensor_config.scd4x_config.has_set_power_mode) { this->setPowerMode(request->sensor_config.scd4x_config.set_power_mode); } - } // Start measurement mode @@ -764,6 +863,10 @@ AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPa result = AdminMessageHandleResult::NOT_HANDLED; } +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return result; } From 1f91a6203eb3a6f5a58c1300d2c4018f4961f5a1 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Fri, 6 Feb 2026 21:19:05 +0100 Subject: [PATCH 103/108] Fix returns --- src/modules/Telemetry/Sensor/SCD4XSensor.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/Telemetry/Sensor/SCD4XSensor.cpp b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp index 6b38a04672f..e2f7ae0c7c0 100644 --- a/src/modules/Telemetry/Sensor/SCD4XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp @@ -707,7 +707,7 @@ uint32_t SCD4XSensor::wakeUp() { reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); #else LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); - return false; + return 0; #endif /* CAN_RECLOCK_I2C */ #endif /* SCD4X_I2C_CLOCK_SPEED */ @@ -738,7 +738,7 @@ void SCD4XSensor::sleep() { reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); #else LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); - return false; + return; #endif /* CAN_RECLOCK_I2C */ #endif /* SCD4X_I2C_CLOCK_SPEED */ From 2d022738342336796a94cf4ae51511e3a037ce45 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Fri, 6 Feb 2026 21:39:48 +0100 Subject: [PATCH 104/108] Fix trunk --- src/detect/ScanI2CTwoWire.cpp | 11 +- src/detect/reClockI2C.cpp | 6 +- src/detect/reClockI2C.h | 2 +- src/modules/Telemetry/AirQualityTelemetry.cpp | 47 +-- .../Telemetry/Sensor/AddI2CSensorTemplate.h | 2 +- .../Telemetry/Sensor/PMSA003ISensor.cpp | 19 +- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 10 +- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 323 +++++++++--------- src/modules/Telemetry/Sensor/SEN5XSensor.h | 78 +++-- .../Telemetry/Sensor/TelemetrySensor.h | 6 +- 10 files changed, 251 insertions(+), 253 deletions(-) diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index f89f2eabcd0..6655addf17b 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -119,16 +119,19 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation // 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 }; +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 ""; + if (i2cBus->endTransmission() != 0) + return ""; delay(20); - if (i2cBus->requestFrom(address, (uint8_t)48) != 48) return ""; + if (i2cBus->requestFrom(address, (uint8_t)48) != 48) + return ""; for (int i = 0; i < 48 && i2cBus->available(); ++i) { response[i] = i2cBus->read(); diff --git a/src/detect/reClockI2C.cpp b/src/detect/reClockI2C.cpp index f1dfee25909..2dfc6d84a0f 100644 --- a/src/detect/reClockI2C.cpp +++ b/src/detect/reClockI2C.cpp @@ -1,7 +1,8 @@ #include "reClockI2C.h" #include "ScanI2CTwoWire.h" -uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force) { +uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force) +{ uint32_t currentClock = 0; @@ -21,11 +22,10 @@ uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force) { currentClock = i2cBus->getClock(); #endif - if (currentClock != desiredClock || force){ + if (currentClock != desiredClock || force) { LOG_DEBUG("Changing I2C clock to %u", desiredClock); i2cBus->setClock(desiredClock); } return currentClock; } - diff --git a/src/detect/reClockI2C.h b/src/detect/reClockI2C.h index c74ccffc557..9c53efc4fe5 100644 --- a/src/detect/reClockI2C.h +++ b/src/detect/reClockI2C.h @@ -3,8 +3,8 @@ #define RECLOCK_I2C_ #include "ScanI2CTwoWire.h" -#include #include +#include uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force); diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index e5bdfde7b59..2ccb147c39d 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -3,22 +3,21 @@ #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "Default.h" #include "AirQualityTelemetry.h" +#include "Default.h" #include "MeshService.h" #include "NodeDB.h" #include "PowerFSM.h" #include "RTC.h" #include "Router.h" #include "UnitConversions.h" +#include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "graphics/images.h" -#include "graphics/ScreenFonts.h" #include "main.h" #include "sleep.h" #include - // Sensors #include "Sensor/AddI2CSensorTemplate.h" #include "Sensor/PMSA003ISensor.h" @@ -59,7 +58,7 @@ int32_t AirQualityTelemetryModule::runOnce() uint32_t result = UINT32_MAX; - if (!(moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_screen_enabled || + if (!(moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_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(); @@ -93,9 +92,12 @@ int32_t AirQualityTelemetryModule::runOnce() if (!sensor->canSleep()) { LOG_DEBUG("%s sensor doesn't have sleep feature. Skipping", sensor->sensorName); } else if (((lastSentToMesh == 0) || - !Throttle::isWithinTimespanMs(lastSentToMesh - sensor->wakeUpTimeMs(), 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()) { + !Throttle::isWithinTimespanMs(lastSentToMesh - sensor->wakeUpTimeMs(), + 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()) { if (!sensor->isActive()) { LOG_DEBUG("Waking up: %s", sensor->sensorName); return sensor->wakeUp(); @@ -110,15 +112,15 @@ int32_t AirQualityTelemetryModule::runOnce() } if (((lastSentToMesh == 0) || - !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( - moduleConfig.telemetry.air_quality_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + !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())) { + (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); @@ -129,11 +131,11 @@ int32_t AirQualityTelemetryModule::runOnce() LOG_DEBUG("Sending sensors to sleep"); for (TelemetrySensor *sensor : sensors) { if (sensor->isActive() && sensor->canSleep()) { - if (sensor->wakeUpTimeMs() < Default::getConfiguredOrDefaultMsScaled( - moduleConfig.telemetry.air_quality_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes)) { - LOG_DEBUG("Disabling %s until next period", sensor->sensorName); - sensor->sleep(); + if (sensor->wakeUpTimeMs() < Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs, + numOnlineNodes)) { + LOG_DEBUG("Disabling %s until next period", sensor->sensorName); + sensor->sleep(); } else { LOG_DEBUG("Sensor stays enabled due to warm up period"); } @@ -317,13 +319,12 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) m.time = getTime(); 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, - m.variant.air_quality_metrics.pm100_standard); + 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); + 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; @@ -379,8 +380,8 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) } AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, - meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) { AdminMessageHandleResult result = AdminMessageHandleResult::NOT_HANDLED; diff --git a/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h b/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h index 01aacc6741b..37d909d71f1 100644 --- a/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h +++ b/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h @@ -1,10 +1,10 @@ #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR -#include #include "TelemetrySensor.h" #include "detect/ScanI2C.h" #include "detect/ScanI2CTwoWire.h" #include +#include static std::forward_list sensors; diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index cc672db856a..bc067c04c5b 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -2,17 +2,14 @@ #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR +#include "../detect/reClockI2C.h" #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "PMSA003ISensor.h" #include "TelemetrySensor.h" -#include "../detect/reClockI2C.h" #include -PMSA003ISensor::PMSA003ISensor() - : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I") -{ -} +PMSA003ISensor::PMSA003ISensor() : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I") {} bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { @@ -54,7 +51,7 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) { - if(!isActive()){ + if (!isActive()) { LOG_WARN("Can't get metrics. %s is not active", sensorName); return false; } @@ -89,9 +86,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) return false; } - auto read16 = [](uint8_t *data, uint8_t idx) -> uint16_t { - return (data[idx] << 8) | data[idx + 1]; - }; + auto read16 = [](uint8_t *data, uint8_t idx) -> uint16_t { return (data[idx] << 8) | data[idx + 1]; }; computedChecksum = 0; @@ -165,7 +160,7 @@ int32_t PMSA003ISensor::pendingForReadyMs() uint32_t now; now = getTime(); - uint32_t sincePmMeasureStarted = (now - pmMeasureStarted)*1000; + uint32_t sincePmMeasureStarted = (now - pmMeasureStarted) * 1000; LOG_DEBUG("%s: Since measure started: %ums", sensorName, sincePmMeasureStarted); if (sincePmMeasureStarted < PMSA003I_WARMUP_MS) { @@ -178,8 +173,8 @@ int32_t PMSA003ISensor::pendingForReadyMs() return 0; } - -bool PMSA003ISensor::canSleep() { +bool PMSA003ISensor::canSleep() +{ #ifdef PMSA003I_ENABLE_PIN return true; #endif diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index 6d621d0963d..3fe96888ddf 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -3,16 +3,16 @@ #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "TelemetrySensor.h" #include "RTC.h" +#include "TelemetrySensor.h" #define PMSA003I_I2C_CLOCK_SPEED 100000 -#define PMSA003I_FRAME_LENGTH 32 +#define PMSA003I_FRAME_LENGTH 32 #define PMSA003I_WARMUP_MS 30000 class PMSA003ISensor : public TelemetrySensor { -public: + public: PMSA003ISensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; @@ -24,7 +24,7 @@ class PMSA003ISensor : public TelemetrySensor virtual int32_t wakeUpTimeMs() override; virtual int32_t pendingForReadyMs() override; -private: + private: enum class State { IDLE, ACTIVE }; State state = State::ACTIVE; @@ -33,7 +33,7 @@ class PMSA003ISensor : public TelemetrySensor uint32_t pmMeasureStarted = 0; uint8_t buffer[PMSA003I_FRAME_LENGTH]{}; - TwoWire * _bus{}; + TwoWire *_bus{}; uint8_t _address{}; }; diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index 48b189078bb..ee4de53fc67 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -2,25 +2,22 @@ #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR -#include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "SEN5XSensor.h" -#include "TelemetrySensor.h" #include "../detect/reClockI2C.h" +#include "../mesh/generated/meshtastic/telemetry.pb.h" #include "FSCommon.h" +#include "SEN5XSensor.h" #include "SPILock.h" #include "SafeFile.h" +#include "TelemetrySensor.h" +#include // FLT_MAX #include #include -#include // FLT_MAX -SEN5XSensor::SEN5XSensor() - : TelemetrySensor(meshtastic_TelemetrySensorType_SEN5X, "SEN5X") -{ -} +SEN5XSensor::SEN5XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SEN5X, "SEN5X") {} bool SEN5XSensor::getVersion() { - if (!sendCommand(SEN5X_GET_FIRMWARE_VERSION)){ + if (!sendCommand(SEN5X_GET_FIRMWARE_VERSION)) { LOG_ERROR("SEN5X: Error sending version command"); return false; } @@ -61,20 +58,19 @@ bool SEN5XSensor::findModel() } // 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; + 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; @@ -86,13 +82,14 @@ bool SEN5XSensor::sendCommand(uint16_t command) return sendCommand(command, ¬hing, 0); } -bool SEN5XSensor::sendCommand(uint16_t command, uint8_t* buffer, uint8_t byteNumber) +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); + if (byteNumber > 0) + bufferSize += byteNumber + (byteNumber / 2); uint8_t toSend[bufferSize]; uint8_t i = 0; @@ -121,7 +118,6 @@ bool SEN5XSensor::sendCommand(uint16_t command, uint8_t* buffer, uint8_t byteNum #endif /* CAN_RECLOCK_I2C */ #endif /* SEN5X_I2C_CLOCK_SPEED */ - // Transmit the data // LOG_DEBUG("Beginning connection to SEN5X: 0x%x. Size: %u", address, bufferSize); // Note: this delay is necessary to allow for long-buffers @@ -146,7 +142,7 @@ bool SEN5XSensor::sendCommand(uint16_t command, uint8_t* buffer, uint8_t byteNum return true; } -uint8_t SEN5XSensor::readBuffer(uint8_t* buffer, uint8_t byteNumber) +uint8_t SEN5XSensor::readBuffer(uint8_t *buffer, uint8_t byteNumber) { #ifdef SEN5X_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C @@ -176,7 +172,7 @@ uint8_t SEN5XSensor::readBuffer(uint8_t* buffer, uint8_t byteNumber) LOG_ERROR("SEN5X: Checksum error while receiving msg"); return 0; } - readBytes -=3; + readBytes -= 3; receivedBytes += 2; } #if defined(SEN5X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) @@ -186,16 +182,17 @@ uint8_t SEN5XSensor::readBuffer(uint8_t* buffer, uint8_t byteNumber) return receivedBytes; } -uint8_t SEN5XSensor::sen5xCRC(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 + // 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++){ + for (uint8_t i = 0; i < 2; i++) { crc ^= buffer[i]; - for (uint8_t bit=8; bit>0; bit--) { + for (uint8_t bit = 8; bit > 0; bit--) { if (crc & 0x80) crc = (crc << 1) ^ 0x31; else @@ -206,7 +203,8 @@ uint8_t SEN5XSensor::sen5xCRC(uint8_t* buffer) return crc; } -void SEN5XSensor::sleep(){ +void SEN5XSensor::sleep() +{ // TODO Check this works idle(true); } @@ -230,7 +228,7 @@ bool SEN5XSensor::idle(bool checkState) if (vocStateFromSensor()) { vocValid = vocStateValid(); // Check if we have time, and store it - uint32_t now; // If time is RTCQualityNone, it will return zero + uint32_t now; // If time is RTCQualityNone, it will return zero now = getValidTime(RTCQuality::RTCQualityDevice); if (now) { // Check if state is valid (non-zero) @@ -274,9 +272,10 @@ bool SEN5XSensor::idle(bool checkState) return true; } -bool SEN5XSensor::vocStateRecent(uint32_t now){ +bool SEN5XSensor::vocStateRecent(uint32_t now) +{ if (now) { - uint32_t passed = now - vocTime; //in seconds + 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)) { @@ -286,9 +285,10 @@ bool SEN5XSensor::vocStateRecent(uint32_t now){ return false; } -bool SEN5XSensor::vocStateValid() { - if (!vocState[0] && !vocState[1] && !vocState[2] && !vocState[3] && - !vocState[4] && !vocState[5] && !vocState[6] && !vocState[7]) { +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 { @@ -299,7 +299,7 @@ bool SEN5XSensor::vocStateValid() { bool SEN5XSensor::vocStateToSensor() { - if (model == SEN50){ + if (model == SEN50) { return true; } @@ -315,13 +315,12 @@ bool SEN5XSensor::vocStateToSensor() 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]); + 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)){ + if (!sendCommand(SEN5X_RW_VOCS_STATE, vocState, SEN5X_VOC_STATE_BUFFER_SIZE)) { LOG_ERROR("SEN5X: Error sending VOC's state command'"); return false; } @@ -331,13 +330,13 @@ bool SEN5XSensor::vocStateToSensor() bool SEN5XSensor::vocStateFromSensor() { - if (model == SEN50){ + if (model == SEN50) { return true; } LOG_INFO("SEN5X: Getting VOC state from sensor"); // Ask VOCs state from the sensor - if (!sendCommand(SEN5X_RW_VOCS_STATE)){ + if (!sendCommand(SEN5X_RW_VOCS_STATE)) { LOG_ERROR("SEN5X: Error sending VOC's state command'"); return false; } @@ -355,9 +354,8 @@ bool SEN5XSensor::vocStateFromSensor() } // Print the state (if debug is on) - 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]); + 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]); return true; } @@ -390,7 +388,7 @@ bool SEN5XSensor::loadState() 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; + vocState[0] = (uint8_t)sen5xstate.voc_state_array; } // LOG_DEBUG("Loaded lastCleaning %u", lastCleaning); @@ -431,14 +429,10 @@ bool SEN5XSensor::saveState() 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]); + 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; @@ -463,11 +457,13 @@ bool SEN5XSensor::saveState() #endif } -bool SEN5XSensor::isActive(){ +bool SEN5XSensor::isActive() +{ return state == SEN5X_MEASUREMENT || state == SEN5X_MEASUREMENT_2; } -uint32_t SEN5XSensor::wakeUp(){ +uint32_t SEN5XSensor::wakeUp() +{ LOG_DEBUG("SEN5X: Waking up sensor"); @@ -559,7 +555,8 @@ bool SEN5XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) } // Check the firmware version - if (!getVersion()) return false; + if (!getVersion()) + return false; if (firmwareVer < 2) { LOG_ERROR("SEN5X: error firmware is too old and will not work with this implementation"); return false; @@ -586,7 +583,8 @@ bool SEN5XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) 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); + 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); @@ -632,7 +630,7 @@ bool SEN5XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) bool SEN5XSensor::readValues() { - if (!sendCommand(SEN5X_READ_VALUES)){ + if (!sendCommand(SEN5X_READ_VALUES)) { LOG_ERROR("SEN5X: Error sending read command"); return false; } @@ -647,39 +645,36 @@ bool SEN5XSensor::readValues() } // 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_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]); + 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]); // Convert values based on Sensirion Arduino lib - 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, - sen5xmeasurement.pM4p0, sen5xmeasurement.pM10p0); + 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, + sen5xmeasurement.pM4p0, sen5xmeasurement.pM10p0); if (model != SEN50) { - LOG_DEBUG("Got: humidity=%.2f, temperature=%.2f, vocIndex=%.2f", - sen5xmeasurement.humidity, sen5xmeasurement.temperature, - sen5xmeasurement.vocIndex); + LOG_DEBUG("Got: humidity=%.2f, temperature=%.2f, vocIndex=%.2f", sen5xmeasurement.humidity, sen5xmeasurement.temperature, + sen5xmeasurement.vocIndex); } if (model == SEN55) { - LOG_DEBUG("Got: noxIndex=%.2f", - sen5xmeasurement.noxIndex); + LOG_DEBUG("Got: noxIndex=%.2f", sen5xmeasurement.noxIndex); } return true; @@ -687,7 +682,7 @@ bool SEN5XSensor::readValues() bool SEN5XSensor::readPNValues(bool cumulative) { - if (!sendCommand(SEN5X_READ_PM_VALUES)){ + if (!sendCommand(SEN5X_READ_PM_VALUES)) { LOG_ERROR("SEN5X: Error sending read command"); return false; } @@ -707,21 +702,21 @@ bool SEN5XSensor::readPNValues(bool cumulative) // 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]); + 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 values based on Sensirion Arduino lib // 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; + 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 @@ -732,11 +727,9 @@ bool SEN5XSensor::readPNValues(bool cumulative) 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, - sen5xmeasurement.pN10p0, sen5xmeasurement.tSize - ); + LOG_DEBUG("Got: pN0p5=%u, pN1p0=%u, pN2p5=%u, pN4p0=%u, pN10p0=%u, tSize=%.2f", sen5xmeasurement.pN0p5, + sen5xmeasurement.pN1p0, sen5xmeasurement.pN2p5, sen5xmeasurement.pN4p0, sen5xmeasurement.pN10p0, + sen5xmeasurement.tSize); return true; } @@ -744,7 +737,7 @@ bool SEN5XSensor::readPNValues(bool cumulative) uint8_t SEN5XSensor::getMeasurements() { // Try to get new data - if (!sendCommand(SEN5X_READ_DATA_READY)){ + if (!sendCommand(SEN5X_READ_DATA_READY)) { LOG_ERROR("SEN5X: Error sending command data ready flag"); return 2; } @@ -764,12 +757,12 @@ uint8_t SEN5XSensor::getMeasurements() return 1; } - if(!readValues()) { + if (!readValues()) { LOG_ERROR("SEN5X: Error getting readings"); return 2; } - if(!readPNValues(false)) { + if (!readPNValues(false)) { LOG_ERROR("SEN5X: Error getting PN readings"); return 2; } @@ -782,53 +775,54 @@ int32_t SEN5XSensor::wakeUpTimeMs() return SEN5X_WARMUP_MS_2; } -int32_t SEN5XSensor::pendingForReadyMs(){ +int32_t SEN5XSensor::pendingForReadyMs() +{ uint32_t now; now = getTime(); - uint32_t sincePmMeasureStarted = (now - pmMeasureStarted)*1000; + uint32_t sincePmMeasureStarted = (now - pmMeasureStarted) * 1000; LOG_DEBUG("SEN5X: Since measure started: %ums", sincePmMeasureStarted); switch (state) { - case SEN5X_MEASUREMENT: { + case SEN5X_MEASUREMENT: { - if (sincePmMeasureStarted < SEN5X_WARMUP_MS_1) { - LOG_INFO("SEN5X: not enough time passed since starting measurement"); - return SEN5X_WARMUP_MS_1 - sincePmMeasureStarted; - } + if (sincePmMeasureStarted < SEN5X_WARMUP_MS_1) { + LOG_INFO("SEN5X: not enough time passed since starting measurement"); + return SEN5X_WARMUP_MS_1 - sincePmMeasureStarted; + } - if (!pmMeasureStarted) { - pmMeasureStarted = now; - } + if (!pmMeasureStarted) { + pmMeasureStarted = now; + } - // Get PN values to check if we are above or below threshold - readPNValues(true); + // Get PN values to check if we are above or below threshold + 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 && sincePmMeasureStarted < 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 - sincePmMeasureStarted; - } - return 0; - } - case SEN5X_MEASUREMENT_2: { - if (sincePmMeasureStarted < SEN5X_WARMUP_MS_2) { - // Report how many seconds are pending to cover the first warm up period - return SEN5X_WARMUP_MS_2 - sincePmMeasureStarted; - } - return 0; + // 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 && sincePmMeasureStarted < 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 - sincePmMeasureStarted; } - default: { - return -1; + return 0; + } + case SEN5X_MEASUREMENT_2: { + if (sincePmMeasureStarted < SEN5X_WARMUP_MS_2) { + // Report how many seconds are pending to cover the first warm up period + return SEN5X_WARMUP_MS_2 - sincePmMeasureStarted; } + return 0; + } + default: { + return -1; + } } } bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement) { LOG_INFO("SEN5X: Attempting to get metrics"); - if (!isActive()){ + if (!isActive()) { LOG_INFO("SEN5X: not in measurement mode"); return false; } @@ -879,28 +873,27 @@ bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement) } if (model != SEN50) { - if (sen5xmeasurement.humidity!= FLT_MAX) { + 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) { + 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) { + 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; } } if (model == SEN55) { - if (sen5xmeasurement.noxIndex!= FLT_MAX) { + 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; } } - return true; } else if (response == 1) { // TODO return because data was not ready yet @@ -916,40 +909,40 @@ bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement) return true; } -void SEN5XSensor::setMode(bool setOneShot) { +void SEN5XSensor::setMode(bool setOneShot) +{ oneShotMode = setOneShot; } AdminMessageHandleResult SEN5XSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) + meshtastic_AdminMessage *response) { AdminMessageHandleResult result; result = AdminMessageHandleResult::NOT_HANDLED; - switch (request->which_payload_variant) { - case meshtastic_AdminMessage_sensor_config_tag: - if (!request->sensor_config.has_sen5x_config) { - result = AdminMessageHandleResult::NOT_HANDLED; - break; - } + case meshtastic_AdminMessage_sensor_config_tag: + if (!request->sensor_config.has_sen5x_config) { + 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); - // } + // 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); + // } - // 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); - } + // 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; + result = AdminMessageHandleResult::HANDLED; + break; - default: - result = AdminMessageHandleResult::NOT_HANDLED; + default: + result = AdminMessageHandleResult::NOT_HANDLED; } return result; diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.h b/src/modules/Telemetry/Sensor/SEN5XSensor.h index 14f40ad4d4c..c31f46d11c6 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.h +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h @@ -3,9 +3,9 @@ #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "RTC.h" #include "TelemetrySensor.h" #include "Wire.h" -#include "RTC.h" // Warm up times for SEN5X from the datasheet #ifndef SEN5X_WARMUP_MS_1 @@ -56,7 +56,7 @@ struct _SEN5XMeasurements { class SEN5XSensor : public TelemetrySensor { private: - TwoWire * _bus{}; + TwoWire *_bus{}; uint8_t _address{}; bool getVersion(); @@ -65,44 +65,52 @@ class SEN5XSensor : public TelemetrySensor 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 +// 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 +#define SEN5X_READ_VALUES 0x03C4 +#define SEN5X_READ_RAW_VALUES 0x03D2 +#define SEN5X_READ_PM_VALUES 0x0413 - #define SEN5X_VOC_VALID_TIME 600 - #define SEN5X_VOC_VALID_DATE 1514764800 +#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; - enum SEN5XState { SEN5X_OFF, SEN5X_IDLE, SEN5X_RHTGAS_ONLY, SEN5X_MEASUREMENT, SEN5X_MEASUREMENT_2, SEN5X_CLEANING, SEN5X_NOT_DETECTED }; + enum SEN5XState { + SEN5X_OFF, + SEN5X_IDLE, + SEN5X_RHTGAS_ONLY, + SEN5X_MEASUREMENT, + SEN5X_MEASUREMENT_2, + SEN5X_CLEANING, + SEN5X_NOT_DETECTED + }; SEN5XState state = SEN5X_OFF; // Flag to work on one-shot (read and sleep), or continuous mode bool oneShotMode = true; void setMode(bool setOneShot); bool vocStateValid(); - /* 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. - See: https://sensirion.com/resource/application_note/low_power_mode/sen5x - */ - #define SEN5X_PN4P0_CONC_THD 100 +/* 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. +See: https://sensirion.com/resource/application_note/low_power_mode/sen5x +*/ +#define SEN5X_PN4P0_CONC_THD 100 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 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 startCleaning(); uint8_t getMeasurements(); // bool readRawValues(); @@ -111,9 +119,9 @@ class SEN5XSensor : public TelemetrySensor uint32_t pmMeasureStarted = 0; uint32_t rhtGasMeasureStarted = 0; - _SEN5XMeasurements sen5xmeasurement {}; + _SEN5XMeasurements sen5xmeasurement{}; - bool idle(bool checkState=true); + bool idle(bool checkState = true); protected: // Store status of the sensor in this file @@ -127,9 +135,9 @@ class SEN5XSensor : public TelemetrySensor uint32_t lastCleaning = 0; bool lastCleaningValid = false; - // VOC State - #define SEN5X_VOC_STATE_BUFFER_SIZE 8 - uint8_t vocState[SEN5X_VOC_STATE_BUFFER_SIZE] {}; +// VOC State +#define SEN5X_VOC_STATE_BUFFER_SIZE 8 + uint8_t vocState[SEN5X_VOC_STATE_BUFFER_SIZE]{}; uint32_t vocTime = 0; bool vocValid = false; @@ -139,7 +147,6 @@ class SEN5XSensor : public TelemetrySensor bool vocStateRecent(uint32_t now); public: - SEN5XSensor(); virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; virtual bool getMetrics(meshtastic_Telemetry *measurement) override; @@ -151,9 +158,8 @@ class SEN5XSensor : public TelemetrySensor virtual int32_t wakeUpTimeMs() override; virtual int32_t pendingForReadyMs() override; - AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) override; + AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; }; - - #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.h b/src/modules/Telemetry/Sensor/TelemetrySensor.h index 9c745422c04..6c53bbd72b7 100644 --- a/src/modules/Telemetry/Sensor/TelemetrySensor.h +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.h @@ -61,10 +61,10 @@ class TelemetrySensor // Functions to sleep / wakeup sensors that support it // These functions can save power consumption in cases like AQ - virtual void sleep() {}; + virtual void sleep(){}; virtual uint32_t wakeUp() { return 0; } - virtual bool isActive() { return true; } // Return true by default, override per sensor - virtual bool canSleep() { return false; } // Return false by default, override per sensor + virtual bool isActive() { return true; } // Return true by default, override per sensor + virtual bool canSleep() { return false; } // Return false by default, override per sensor virtual int32_t wakeUpTimeMs() { return 0; } virtual int32_t pendingForReadyMs() { return 0; } From 960c1c65a0b5d4352546da2c0e47f65cb296d4d4 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Fri, 6 Feb 2026 21:45:20 +0100 Subject: [PATCH 105/108] Fix on condition in reclock --- src/detect/reClockI2C.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/detect/reClockI2C.cpp b/src/detect/reClockI2C.cpp index 2dfc6d84a0f..60cd3c8084c 100644 --- a/src/detect/reClockI2C.cpp +++ b/src/detect/reClockI2C.cpp @@ -22,7 +22,7 @@ uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force) currentClock = i2cBus->getClock(); #endif - if (currentClock != desiredClock || force) { + if ((currentClock != desiredClock) || force) { LOG_DEBUG("Changing I2C clock to %u", desiredClock); i2cBus->setClock(desiredClock); } From 2cc1682782e7a7560e16b521bca6f9e98aaf4e71 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Fri, 6 Feb 2026 21:53:14 +0100 Subject: [PATCH 106/108] Fix trunk --- src/modules/Telemetry/AirQualityTelemetry.cpp | 29 +- src/modules/Telemetry/Sensor/SCD4XSensor.cpp | 530 +++++++++--------- src/modules/Telemetry/Sensor/SCD4XSensor.h | 14 +- 3 files changed, 296 insertions(+), 277 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 946b1521660..aac398eb9c5 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -222,7 +222,6 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta if (m.has_co2) entries.push_back("CO2: " + String(m.co2) + "ppm"); - // === Show first available metric on top-right of first line === if (!entries.empty()) { String valueStr = entries.front(); @@ -266,9 +265,8 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack // t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental, // t->variant.air_quality_metrics.pm100_environmental); - LOG_INFO(" | CO2=%i, CO2_T=%f, CO2_H=%f", - t->variant.air_quality_metrics.co2, t->variant.air_quality_metrics.co2_temperature, - t->variant.air_quality_metrics.co2_humidity); + LOG_INFO(" | CO2=%i, CO2_T=%f, CO2_H=%f", t->variant.air_quality_metrics.co2, + t->variant.air_quality_metrics.co2_temperature, t->variant.air_quality_metrics.co2_humidity); #endif // release previous packet before occupying a new spot if (lastMeasurementPacket != nullptr) @@ -338,25 +336,26 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) if (getAirQualityTelemetry(&m)) { - bool hasAnyPM = m.variant.air_quality_metrics.has_pm10_standard || m.variant.air_quality_metrics.has_pm25_standard || m.variant.air_quality_metrics.has_pm100_standard || m.variant.air_quality_metrics.has_pm10_environmental || m.variant.air_quality_metrics.has_pm25_environmental || - m.variant.air_quality_metrics.has_pm100_environmental; + bool hasAnyPM = + m.variant.air_quality_metrics.has_pm10_standard || m.variant.air_quality_metrics.has_pm25_standard || + m.variant.air_quality_metrics.has_pm100_standard || m.variant.air_quality_metrics.has_pm10_environmental || + m.variant.air_quality_metrics.has_pm25_environmental || m.variant.air_quality_metrics.has_pm100_environmental; if (hasAnyPM) { - 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); + 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); + m.variant.air_quality_metrics.pm10_environmental, m.variant.air_quality_metrics.pm25_environmental, + m.variant.air_quality_metrics.pm100_environmental); } - bool hasAnyCO2 = m.variant.air_quality_metrics.has_co2 || m.variant.air_quality_metrics.has_co2_temperature || m.variant.air_quality_metrics.has_co2_humidity; + bool hasAnyCO2 = m.variant.air_quality_metrics.has_co2 || m.variant.air_quality_metrics.has_co2_temperature || + m.variant.air_quality_metrics.has_co2_humidity; if (hasAnyCO2) { - LOG_INFO("Send: co2=%i, co2_t=%f, co2_rh=%f", - m.variant.air_quality_metrics.co2, m.variant.air_quality_metrics.co2_temperature, - m.variant.air_quality_metrics.co2_humidity); + LOG_INFO("Send: co2=%i, co2_t=%f, co2_rh=%f", m.variant.air_quality_metrics.co2, + m.variant.air_quality_metrics.co2_temperature, m.variant.air_quality_metrics.co2_humidity); } meshtastic_MeshPacket *p = allocDataProtobuf(m); diff --git a/src/modules/Telemetry/Sensor/SCD4XSensor.cpp b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp index e2f7ae0c7c0..4f6e28b4b26 100644 --- a/src/modules/Telemetry/Sensor/SCD4XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp @@ -2,18 +2,16 @@ #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() +#include "../detect/reClockI2C.h" #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "SCD4XSensor.h" -#include "../detect/reClockI2C.h" #define SCD4X_NO_ERROR 0 -SCD4XSensor::SCD4XSensor() - : TelemetrySensor(meshtastic_TelemetrySensorType_SCD4X, "SCD4X") -{ -} +SCD4XSensor::SCD4XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SCD4X, "SCD4X") {} -bool SCD4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { +bool SCD4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ LOG_INFO("Init sensor: %s", sensorName); _bus = bus; @@ -46,7 +44,7 @@ bool SCD4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { // Get sensor variant scd4x.getSensorVariant(sensorVariant); - if (sensorVariant == SCD4X_SENSOR_VARIANT_SCD41){ + if (sensorVariant == SCD4X_SENSOR_VARIANT_SCD41) { LOG_INFO("%s: Found SCD41", sensorName); if (!powerUp()) { LOG_ERROR("%s: Error trying to execute powerUp()", sensorName); @@ -57,7 +55,7 @@ bool SCD4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { } } - if (!getASC(ascActive)){ + if (!getASC(ascActive)) { LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName); #if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); @@ -66,7 +64,7 @@ bool SCD4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { } // Start measurement in selected power mode (low power by default) - if (!startMeasurement()){ + if (!startMeasurement()) { LOG_ERROR("%s: Couldn't start measurement", sensorName); #if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); @@ -78,7 +76,7 @@ bool SCD4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { reClockI2C(currentClock, _bus, false); #endif - if (state == SCD4X_MEASUREMENT){ + if (state == SCD4X_MEASUREMENT) { status = 1; } else { status = 0; @@ -89,7 +87,8 @@ bool SCD4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { return true; } -bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) { +bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) +{ if (state != SCD4X_MEASUREMENT) { LOG_ERROR("%s: Not in measurement mode", sensorName); @@ -145,19 +144,20 @@ bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) { } /** -* @brief Perform a forced recalibration (FRC) of the CO₂ concentration. -* -* From Sensirion SCD4X I2C Library -* -* 1. Operate the SCD4x in the operation mode later used for normal sensor -* operation (e.g. periodic measurement) for at least 3 minutes in an -* environment with a homogenous and constant CO2 concentration. The sensor -* must be operated at the voltage desired for the application when -* performing the FRC sequence. 2. Issue the stop_periodic_measurement -* command. 3. Issue the perform_forced_recalibration command. -* @note This function should not change the clock -*/ -bool SCD4XSensor::performFRC(uint32_t targetCO2) { + * @brief Perform a forced recalibration (FRC) of the CO₂ concentration. + * + * From Sensirion SCD4X I2C Library + * + * 1. Operate the SCD4x in the operation mode later used for normal sensor + * operation (e.g. periodic measurement) for at least 3 minutes in an + * environment with a homogenous and constant CO2 concentration. The sensor + * must be operated at the voltage desired for the application when + * performing the FRC sequence. 2. Issue the stop_periodic_measurement + * command. 3. Issue the perform_forced_recalibration command. + * @note This function should not change the clock + */ +bool SCD4XSensor::performFRC(uint32_t targetCO2) +{ uint16_t error, frcCorr; LOG_INFO("%s: Issuing FRC. Ensure device has been working at least 3 minutes in stable target environment", sensorName); @@ -172,7 +172,7 @@ bool SCD4XSensor::performFRC(uint32_t targetCO2) { // SCD4X Sensirion datasheet delay(400); - if (error != SCD4X_NO_ERROR){ + if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to perform forced recalibration.", sensorName); return false; } @@ -182,19 +182,20 @@ bool SCD4XSensor::performFRC(uint32_t targetCO2) { return false; } - LOG_INFO("%s: FRC Correction successful. Correction output: %u", sensorName, (uint16_t)(frcCorr-0x8000)); + LOG_INFO("%s: FRC Correction successful. Correction output: %u", sensorName, (uint16_t)(frcCorr - 0x8000)); return true; } /** -* @brief Start measurement mode -* @note This function should not change the clock -*/ -bool SCD4XSensor::startMeasurement() { + * @brief Start measurement mode + * @note This function should not change the clock + */ +bool SCD4XSensor::startMeasurement() +{ uint16_t error; - if (state == SCD4X_MEASUREMENT){ + if (state == SCD4X_MEASUREMENT) { LOG_DEBUG("%s: Already in measurement mode", sensorName); return true; } @@ -222,10 +223,11 @@ bool SCD4XSensor::startMeasurement() { } /** -* @brief Stop measurement mode -* @note This function should not change the clock -*/ -bool SCD4XSensor::stopMeasurement() { + * @brief Stop measurement mode + * @note This function should not change the clock + */ +bool SCD4XSensor::stopMeasurement() +{ uint16_t error; error = scd4x.stopPeriodicMeasurement(); @@ -240,11 +242,12 @@ bool SCD4XSensor::stopMeasurement() { } /** -* @brief Set power mode -* Pass true to set low power mode -* @note This function should not change the clock -*/ -bool SCD4XSensor::setPowerMode(bool _lowPower) { + * @brief Set power mode + * Pass true to set low power mode + * @note This function should not change the clock + */ +bool SCD4XSensor::setPowerMode(bool _lowPower) +{ lowPower = _lowPower; if (!stopMeasurement()) { @@ -261,11 +264,12 @@ bool SCD4XSensor::setPowerMode(bool _lowPower) { } /** -* @brief Check the current mode (ASC or FRC) -* From Sensirion SCD4X I2C Library -* @note This function should not change the clock -*/ -bool SCD4XSensor::getASC(uint16_t &_ascActive) { + * @brief Check the current mode (ASC or FRC) + * From Sensirion SCD4X I2C Library + * @note This function should not change the clock + */ +bool SCD4XSensor::getASC(uint16_t &_ascActive) +{ uint16_t error; LOG_INFO("%s: Getting ASC", sensorName); @@ -274,12 +278,12 @@ bool SCD4XSensor::getASC(uint16_t &_ascActive) { } error = scd4x.getAutomaticSelfCalibrationEnabled(_ascActive); - if (error != SCD4X_NO_ERROR){ + if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to send command.", sensorName); return false; } - if (_ascActive){ + if (_ascActive) { LOG_INFO("%s: ASC is enabled", sensorName); } else { LOG_INFO("%s: FRC is enabled", sensorName); @@ -289,18 +293,19 @@ bool SCD4XSensor::getASC(uint16_t &_ascActive) { } /** -* @brief Enable or disable automatic self calibration (ASC). -* -* From Sensirion SCD4X I2C Library -* -* Sets the current state (enabled / disabled) of the ASC. By default, ASC -* is enabled. -* @note This function should not change the clock -*/ -bool SCD4XSensor::setASC(bool ascEnabled) { + * @brief Enable or disable automatic self calibration (ASC). + * + * From Sensirion SCD4X I2C Library + * + * Sets the current state (enabled / disabled) of the ASC. By default, ASC + * is enabled. + * @note This function should not change the clock + */ +bool SCD4XSensor::setASC(bool ascEnabled) +{ uint16_t error; - if (ascEnabled){ + if (ascEnabled) { LOG_INFO("%s: Enabling ASC", sensorName); } else { LOG_INFO("%s: Disabling ASC", sensorName); @@ -312,23 +317,23 @@ bool SCD4XSensor::setASC(bool ascEnabled) { error = scd4x.setAutomaticSelfCalibrationEnabled((uint16_t)ascEnabled); - if (error != SCD4X_NO_ERROR){ + if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to send command.", sensorName); return false; } error = scd4x.persistSettings(); - if (error != SCD4X_NO_ERROR){ + if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to make settings persistent.", sensorName); return false; } - if (!getASC(ascActive)){ + if (!getASC(ascActive)) { LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName); return false; } - if (ascActive){ + if (ascActive) { LOG_INFO("%s: ASC is enabled", sensorName); } else { LOG_INFO("%s: ASC is disabled", sensorName); @@ -338,26 +343,27 @@ bool SCD4XSensor::setASC(bool ascEnabled) { } /** -* @brief Set the value of ASC baseline target in ppm. -* -* From Sensirion SCD4X I2C Library. -* -* Sets the value of the ASC baseline target, i.e. the CO₂ concentration in -* ppm which the ASC algorithm will assume as lower-bound background to -* which the SCD4x is exposed to regularly within one ASC period of -* operation. To save the setting to the EEPROM, the persist_settings -* command must be issued subsequently. The factory default value is 400 -* ppm. -* @note This function should not change the clock -*/ -bool SCD4XSensor::setASCBaseline(uint32_t targetCO2) { + * @brief Set the value of ASC baseline target in ppm. + * + * From Sensirion SCD4X I2C Library. + * + * Sets the value of the ASC baseline target, i.e. the CO₂ concentration in + * ppm which the ASC algorithm will assume as lower-bound background to + * which the SCD4x is exposed to regularly within one ASC period of + * operation. To save the setting to the EEPROM, the persist_settings + * command must be issued subsequently. The factory default value is 400 + * ppm. + * @note This function should not change the clock + */ +bool SCD4XSensor::setASCBaseline(uint32_t targetCO2) +{ // TODO - Remove? // Available in library, but not described in datasheet. uint16_t error; LOG_INFO("%s: Setting ASC baseline to: %u", sensorName, targetCO2); getASC(ascActive); - if (!ascActive){ + if (!ascActive) { LOG_ERROR("%s: Can't set ASC baseline. ASC is not active", sensorName); return false; } @@ -368,13 +374,13 @@ bool SCD4XSensor::setASCBaseline(uint32_t targetCO2) { error = scd4x.setAutomaticSelfCalibrationTarget((uint16_t)targetCO2); - if (error != SCD4X_NO_ERROR){ + if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to send command.", sensorName); return false; } error = scd4x.persistSettings(); - if (error != SCD4X_NO_ERROR){ + if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to make settings persistent.", sensorName); return false; } @@ -385,27 +391,28 @@ bool SCD4XSensor::setASCBaseline(uint32_t targetCO2) { } /** -* @brief Set the temperature compensation reference. -* -* From Sensirion SCD4X I2C Library. -* -* Setting the temperature offset of the SCD4x inside the customer device -* allows the user to optimize the RH and T output signal. -* By default, the temperature offset is set to 4 °C. To save -* the setting to the EEPROM, the persist_settings command may be issued. -* Equation (1) details how the characteristic temperature offset can be -* calculated using the current temperature output of the sensor (TSCD4x), a -* reference temperature value (TReference), and the previous temperature -* offset (Toffset_pervious) obtained using the get_temperature_offset_raw -* command: -* -* Toffset_actual = TSCD4x - TReference + Toffset_pervious. -* -* Recommended temperature offset values are between 0 °C and 20 °C. The -* temperature offset does not impact the accuracy of the CO2 output. -* @note This function should not change the clock -*/ -bool SCD4XSensor::setTemperature(float tempReference) { + * @brief Set the temperature compensation reference. + * + * From Sensirion SCD4X I2C Library. + * + * Setting the temperature offset of the SCD4x inside the customer device + * allows the user to optimize the RH and T output signal. + * By default, the temperature offset is set to 4 °C. To save + * the setting to the EEPROM, the persist_settings command may be issued. + * Equation (1) details how the characteristic temperature offset can be + * calculated using the current temperature output of the sensor (TSCD4x), a + * reference temperature value (TReference), and the previous temperature + * offset (Toffset_pervious) obtained using the get_temperature_offset_raw + * command: + * + * Toffset_actual = TSCD4x - TReference + Toffset_pervious. + * + * Recommended temperature offset values are between 0 °C and 20 °C. The + * temperature offset does not impact the accuracy of the CO2 output. + * @note This function should not change the clock + */ +bool SCD4XSensor::setTemperature(float tempReference) +{ uint16_t error; float prevTempOffset; float updatedTempOffset; @@ -437,7 +444,7 @@ bool SCD4XSensor::setTemperature(float tempReference) { error = scd4x.getTemperatureOffset(prevTempOffset); - if (error != SCD4X_NO_ERROR){ + if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to get temperature offset. Error code: %u", sensorName, error); return false; } @@ -447,13 +454,13 @@ bool SCD4XSensor::setTemperature(float tempReference) { LOG_INFO("%s: Setting temperature offset: %.2f", sensorName, tempOffset); error = scd4x.setTemperatureOffset(tempOffset); - if (error != SCD4X_NO_ERROR){ + if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to set temperature offset. Error code: %u", sensorName, error); return false; } error = scd4x.persistSettings(); - if (error != SCD4X_NO_ERROR){ + if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error); return false; } @@ -465,15 +472,16 @@ bool SCD4XSensor::setTemperature(float tempReference) { } /** -* @brief Get the sensor altitude. -* -* From Sensirion SCD4X I2C Library. -* -* Altitude in meters above sea level can be set after device installation. -* Valid value between 0 and 3000m. This overrides pressure offset. -* @note This function should not change the clock -*/ -bool SCD4XSensor::getAltitude(uint16_t &altitude) { + * @brief Get the sensor altitude. + * + * From Sensirion SCD4X I2C Library. + * + * Altitude in meters above sea level can be set after device installation. + * Valid value between 0 and 3000m. This overrides pressure offset. + * @note This function should not change the clock + */ +bool SCD4XSensor::getAltitude(uint16_t &altitude) +{ uint16_t error; LOG_INFO("%s: Requesting sensor altitude", sensorName); @@ -483,7 +491,7 @@ bool SCD4XSensor::getAltitude(uint16_t &altitude) { error = scd4x.getSensorAltitude(altitude); - if (error != SCD4X_NO_ERROR){ + if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to get altitude. Error code: %u", sensorName, error); return false; } @@ -493,20 +501,21 @@ bool SCD4XSensor::getAltitude(uint16_t &altitude) { } /** -* @brief Get the ambient pressure around the sensor. -* -* From Sensirion SCD4X I2C Library. -* -* Gets the ambient pressure in Pa. -* @note This function should not change the clock -*/ -bool SCD4XSensor::getAmbientPressure(uint32_t &ambientPressure) { + * @brief Get the ambient pressure around the sensor. + * + * From Sensirion SCD4X I2C Library. + * + * Gets the ambient pressure in Pa. + * @note This function should not change the clock + */ +bool SCD4XSensor::getAmbientPressure(uint32_t &ambientPressure) +{ uint16_t error; LOG_INFO("%s: Requesting sensor ambient pressure", sensorName); error = scd4x.getAmbientPressure(ambientPressure); - if (error != SCD4X_NO_ERROR){ + if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to get altitude. Error code: %u", sensorName, error); return false; } @@ -516,15 +525,16 @@ bool SCD4XSensor::getAmbientPressure(uint32_t &ambientPressure) { } /** -* @brief Set the sensor altitude. -* -* From Sensirion SCD4X I2C Library. -* -* Altitude in meters above sea level can be set after device installation. -* Valid value between 0 and 3000m. This overrides pressure offset. -* @note This function should not change the clock -*/ -bool SCD4XSensor::setAltitude(uint32_t altitude) { + * @brief Set the sensor altitude. + * + * From Sensirion SCD4X I2C Library. + * + * Altitude in meters above sea level can be set after device installation. + * Valid value between 0 and 3000m. This overrides pressure offset. + * @note This function should not change the clock + */ +bool SCD4XSensor::setAltitude(uint32_t altitude) +{ uint16_t error; if (!stopMeasurement()) { @@ -533,13 +543,13 @@ bool SCD4XSensor::setAltitude(uint32_t altitude) { error = scd4x.setSensorAltitude(altitude); - if (error != SCD4X_NO_ERROR){ + if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to set altitude. Error code: %u", sensorName, error); return false; } error = scd4x.persistSettings(); - if (error != SCD4X_NO_ERROR){ + if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error); return false; } @@ -548,32 +558,33 @@ bool SCD4XSensor::setAltitude(uint32_t altitude) { } /** -* @brief Set the ambient pressure around the sensor. -* -* From Sensirion SCD4X I2C Library. -* -* The set_ambient_pressure command can be sent during periodic measurements -* to enable continuous pressure compensation. Note that setting an ambient -* pressure overrides any pressure compensation based on a previously set -* sensor altitude. Use of this command is highly recommended for -* applications experiencing significant ambient pressure changes to ensure -* sensor accuracy. Valid input values are between 70000 - 120000 Pa. The -* default value is 101300 Pa. -* @note This function should not change the clock -*/ -bool SCD4XSensor::setAmbientPressure(uint32_t ambientPressure) { + * @brief Set the ambient pressure around the sensor. + * + * From Sensirion SCD4X I2C Library. + * + * The set_ambient_pressure command can be sent during periodic measurements + * to enable continuous pressure compensation. Note that setting an ambient + * pressure overrides any pressure compensation based on a previously set + * sensor altitude. Use of this command is highly recommended for + * applications experiencing significant ambient pressure changes to ensure + * sensor accuracy. Valid input values are between 70000 - 120000 Pa. The + * default value is 101300 Pa. + * @note This function should not change the clock + */ +bool SCD4XSensor::setAmbientPressure(uint32_t ambientPressure) +{ uint16_t error; error = scd4x.setAmbientPressure(ambientPressure); - if (error != SCD4X_NO_ERROR){ + if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to set altitude. Error code: %u", sensorName, error); return false; } // Sensirion doesn't indicate if this is necessary. We send it anyway error = scd4x.persistSettings(); - if (error != SCD4X_NO_ERROR){ + if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error); return false; } @@ -582,15 +593,16 @@ bool SCD4XSensor::setAmbientPressure(uint32_t ambientPressure) { } /** -* @brief Perform factory reset to erase the settings stored in the EEPROM. -* -* From Sensirion SCD4X I2C Library. -* -* The perform_factory_reset command resets all configuration settings -* stored in the EEPROM and erases the FRC and ASC algorithm history. -* @note This function should not change the clock -*/ -bool SCD4XSensor::factoryReset() { + * @brief Perform factory reset to erase the settings stored in the EEPROM. + * + * From Sensirion SCD4X I2C Library. + * + * The perform_factory_reset command resets all configuration settings + * stored in the EEPROM and erases the FRC and ASC algorithm history. + * @note This function should not change the clock + */ +bool SCD4XSensor::factoryReset() +{ uint16_t error; LOG_INFO("%s: Requesting factory reset", sensorName); @@ -601,7 +613,7 @@ bool SCD4XSensor::factoryReset() { error = scd4x.performFactoryReset(); - if (error != SCD4X_NO_ERROR){ + if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to do factory reset. Error code: %u", sensorName, error); return false; } @@ -612,16 +624,17 @@ bool SCD4XSensor::factoryReset() { } /** -* @brief Put the sensor into sleep mode from idle mode. -* -* From Sensirion SCD4X I2C Library. -* -* Put the sensor from idle to sleep to reduce power consumption. Can be -* used to power down when operating the sensor in power-cycled single shot -* mode. -* @note This command is only available in idle mode. Only for SCD41. -*/ -bool SCD4XSensor::powerDown() { + * @brief Put the sensor into sleep mode from idle mode. + * + * From Sensirion SCD4X I2C Library. + * + * Put the sensor from idle to sleep to reduce power consumption. Can be + * used to power down when operating the sensor in power-cycled single shot + * mode. + * @note This command is only available in idle mode. Only for SCD41. + */ +bool SCD4XSensor::powerDown() +{ LOG_INFO("%s: Trying to send sensor to sleep", sensorName); if (sensorVariant != SCD4X_SENSOR_VARIANT_SCD41) { @@ -664,17 +677,18 @@ bool SCD4XSensor::powerDown() { } /** -* @brief Wake up sensor from sleep mode to idle mode (powerUp) -* -* From Sensirion SCD4X I2C Library. -* -* Wake up the sensor from sleep mode into idle mode. Note that the SCD4x -* does not acknowledge the wake_up command. The sensor's idle state after -* wake up can be verified by reading out the serial number. -* @note This command is only available for SCD41. -* @note This function can't change clock (used in init) -*/ -bool SCD4XSensor::powerUp() { + * @brief Wake up sensor from sleep mode to idle mode (powerUp) + * + * From Sensirion SCD4X I2C Library. + * + * Wake up the sensor from sleep mode into idle mode. Note that the SCD4x + * does not acknowledge the wake_up command. The sensor's idle state after + * wake up can be verified by reading out the serial number. + * @note This command is only available for SCD41. + * @note This function can't change clock (used in init) + */ +bool SCD4XSensor::powerUp() +{ LOG_INFO("%s: Waking up", sensorName); if (scd4x.wakeUp() != SCD4X_NO_ERROR) { @@ -688,17 +702,19 @@ bool SCD4XSensor::powerUp() { } /** -* @brief Check if sensor is in measurement mode -*/ -bool SCD4XSensor::isActive(){ + * @brief Check if sensor is in measurement mode + */ +bool SCD4XSensor::isActive() +{ return state == SCD4X_MEASUREMENT; } /** -* @brief Start measurement mode -* @note Not used in admin comands, getMetrics or init, can change clock. -*/ -uint32_t SCD4XSensor::wakeUp() { + * @brief Start measurement mode + * @note Not used in admin comands, getMetrics or init, can change clock. + */ +uint32_t SCD4XSensor::wakeUp() +{ #ifdef SCD4X_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C @@ -727,10 +743,11 @@ uint32_t SCD4XSensor::wakeUp() { } /** -* @brief Stop measurement mode -* @note Not used in admin comands, getMetrics or init, can change clock. -*/ -void SCD4XSensor::sleep() { + * @brief Stop measurement mode + * @note Not used in admin comands, getMetrics or init, can change clock. + */ +void SCD4XSensor::sleep() +{ #ifdef SCD4X_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); @@ -750,24 +767,27 @@ void SCD4XSensor::sleep() { } /** -* @brief Can sleep function -* -* Power consumption is very low on lowPower mode, modify this function if -* you still want to override this behaviour. Otherwise, sleep is disabled -* routinely in low power mode -*/ -bool SCD4XSensor::canSleep() { + * @brief Can sleep function + * + * Power consumption is very low on lowPower mode, modify this function if + * you still want to override this behaviour. Otherwise, sleep is disabled + * routinely in low power mode + */ +bool SCD4XSensor::canSleep() +{ return lowPower ? false : true; } -int32_t SCD4XSensor::wakeUpTimeMs() { +int32_t SCD4XSensor::wakeUpTimeMs() +{ return SCD4X_WARMUP_MS; } -int32_t SCD4XSensor::pendingForReadyMs() { +int32_t SCD4XSensor::pendingForReadyMs() +{ uint32_t now; now = getTime(); - uint32_t sinceCO2MeasureStarted = (now - co2MeasureStarted)*1000; + uint32_t sinceCO2MeasureStarted = (now - co2MeasureStarted) * 1000; LOG_DEBUG("%s: Since measure started: %ums", sensorName, sinceCO2MeasureStarted); if (sinceCO2MeasureStarted < SCD4X_WARMUP_MS) { @@ -778,7 +798,7 @@ int32_t SCD4XSensor::pendingForReadyMs() { } AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) + meshtastic_AdminMessage *response) { AdminMessageHandleResult result; @@ -795,72 +815,72 @@ AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPa // TODO: potentially add selftest command? switch (request->which_payload_variant) { - case meshtastic_AdminMessage_sensor_config_tag: - // Check for ASC-FRC request first - if (!request->sensor_config.has_scd4x_config) { - result = AdminMessageHandleResult::NOT_HANDLED; - break; - } + case meshtastic_AdminMessage_sensor_config_tag: + // Check for ASC-FRC request first + if (!request->sensor_config.has_scd4x_config) { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + + if (request->sensor_config.scd4x_config.has_factory_reset) { + LOG_DEBUG("%s: Requested factory reset", sensorName); + this->factoryReset(); + } else { - if (request->sensor_config.scd4x_config.has_factory_reset) { - LOG_DEBUG("%s: Requested factory reset", sensorName); - this->factoryReset(); - } else { - - if (request->sensor_config.scd4x_config.has_set_asc) { - this->setASC(request->sensor_config.scd4x_config.set_asc); - if (request->sensor_config.scd4x_config.set_asc == false) { - LOG_DEBUG("%s: Request for FRC", sensorName); - if (request->sensor_config.scd4x_config.has_set_target_co2_conc) { - this->performFRC(request->sensor_config.scd4x_config.set_target_co2_conc); - } else { - // FRC requested but no target CO2 provided - LOG_ERROR("%s: target CO2 not provided", sensorName); - result = AdminMessageHandleResult::NOT_HANDLED; - break; - } + if (request->sensor_config.scd4x_config.has_set_asc) { + this->setASC(request->sensor_config.scd4x_config.set_asc); + if (request->sensor_config.scd4x_config.set_asc == false) { + LOG_DEBUG("%s: Request for FRC", sensorName); + if (request->sensor_config.scd4x_config.has_set_target_co2_conc) { + this->performFRC(request->sensor_config.scd4x_config.set_target_co2_conc); } else { - LOG_DEBUG("%s: Request for ASC", sensorName); - if (request->sensor_config.scd4x_config.has_set_target_co2_conc) { - LOG_DEBUG("%s: Request has target CO2", sensorName); - // TODO - Remove? see setASCBaseline function - this->setASCBaseline(request->sensor_config.scd4x_config.set_target_co2_conc); - } else { - LOG_DEBUG("%s: Request doesn't have target CO2", sensorName); - } + // FRC requested but no target CO2 provided + LOG_ERROR("%s: target CO2 not provided", sensorName); + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + } else { + LOG_DEBUG("%s: Request for ASC", sensorName); + if (request->sensor_config.scd4x_config.has_set_target_co2_conc) { + LOG_DEBUG("%s: Request has target CO2", sensorName); + // TODO - Remove? see setASCBaseline function + this->setASCBaseline(request->sensor_config.scd4x_config.set_target_co2_conc); + } else { + LOG_DEBUG("%s: Request doesn't have target CO2", sensorName); } } + } - // Check for temperature offset - // NOTE: this requires to have a sensor working on stable environment - // And to make it between readings - if (request->sensor_config.scd4x_config.has_set_temperature) { - this->setTemperature(request->sensor_config.scd4x_config.set_temperature); - } + // Check for temperature offset + // NOTE: this requires to have a sensor working on stable environment + // And to make it between readings + if (request->sensor_config.scd4x_config.has_set_temperature) { + this->setTemperature(request->sensor_config.scd4x_config.set_temperature); + } - // Check for altitude or pressure offset - if (request->sensor_config.scd4x_config.has_set_altitude) { - this->setAltitude(request->sensor_config.scd4x_config.set_altitude); - } else if (request->sensor_config.scd4x_config.has_set_ambient_pressure){ - this->setAmbientPressure(request->sensor_config.scd4x_config.set_ambient_pressure); - } + // Check for altitude or pressure offset + if (request->sensor_config.scd4x_config.has_set_altitude) { + this->setAltitude(request->sensor_config.scd4x_config.set_altitude); + } else if (request->sensor_config.scd4x_config.has_set_ambient_pressure) { + this->setAmbientPressure(request->sensor_config.scd4x_config.set_ambient_pressure); + } - // Check for low power mode - // NOTE: to switch from one mode to another do: - // setPowerMode -> startMeasurement - if (request->sensor_config.scd4x_config.has_set_power_mode) { - this->setPowerMode(request->sensor_config.scd4x_config.set_power_mode); - } + // Check for low power mode + // NOTE: to switch from one mode to another do: + // setPowerMode -> startMeasurement + if (request->sensor_config.scd4x_config.has_set_power_mode) { + this->setPowerMode(request->sensor_config.scd4x_config.set_power_mode); } + } - // Start measurement mode - this->startMeasurement(); + // Start measurement mode + this->startMeasurement(); - result = AdminMessageHandleResult::HANDLED; - break; + result = AdminMessageHandleResult::HANDLED; + break; - default: - result = AdminMessageHandleResult::NOT_HANDLED; + default: + result = AdminMessageHandleResult::NOT_HANDLED; } #if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) diff --git a/src/modules/Telemetry/Sensor/SCD4XSensor.h b/src/modules/Telemetry/Sensor/SCD4XSensor.h index 2542e408bdd..1ed86a183b9 100644 --- a/src/modules/Telemetry/Sensor/SCD4XSensor.h +++ b/src/modules/Telemetry/Sensor/SCD4XSensor.h @@ -3,9 +3,9 @@ #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "RTC.h" #include "TelemetrySensor.h" #include -#include "RTC.h" // Max speed 400kHz #define SCD4X_I2C_CLOCK_SPEED 100000 @@ -15,7 +15,7 @@ class SCD4XSensor : public TelemetrySensor { private: SensirionI2cScd4x scd4x; - TwoWire * _bus{}; + TwoWire *_bus{}; uint8_t _address{}; bool performFRC(uint32_t targetCO2); @@ -33,8 +33,8 @@ class SCD4XSensor : public TelemetrySensor bool stopMeasurement(); uint16_t ascActive = 1; - // low power measurement mode (on sensirion side). Disables sleep mode - // Improvement and testing needed for timings + // low power measurement mode (on sensirion side). Disables sleep mode + // Improvement and testing needed for timings bool lowPower = true; uint32_t co2MeasureStarted = 0; @@ -49,10 +49,10 @@ class SCD4XSensor : public TelemetrySensor virtual bool isActive() override; - virtual void sleep() override; // Stops measurement (measurement -> idle) + virtual void sleep() override; // Stops measurement (measurement -> idle) virtual uint32_t wakeUp() override; // Starts measurement (idle -> measurement) - bool powerDown(); // Powers down sensor (idle -> power-off) - bool powerUp(); // Powers the sensor (power-off -> idle) + bool powerDown(); // Powers down sensor (idle -> power-off) + bool powerUp(); // Powers the sensor (power-off -> idle) virtual bool canSleep() override; virtual int32_t wakeUpTimeMs() override; virtual int32_t pendingForReadyMs() override; From a4697d89d4ff33165c9352467edc29f2c13b264b Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sun, 8 Feb 2026 10:42:41 +0100 Subject: [PATCH 107/108] Add check on polling interval of sen5x --- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 13 ++++++++++--- src/modules/Telemetry/Sensor/SEN5XSensor.h | 5 +++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index ee4de53fc67..fc20ce1f26a 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -736,6 +736,9 @@ bool SEN5XSensor::readPNValues(bool cumulative) uint8_t SEN5XSensor::getMeasurements() { + uint32_t now; + now = getTime(); + // Try to get new data if (!sendCommand(SEN5X_READ_DATA_READY)) { LOG_ERROR("SEN5X: Error sending command data ready flag"); @@ -750,9 +753,10 @@ uint8_t SEN5XSensor::getMeasurements() return 2; } - bool data_ready = dataReadyBuffer[1]; - - if (!data_ready) { + bool dataReady = dataReadyBuffer[1]; + uint32_t sinceLastDataPollMs = (now - lastDataPoll) * 1000; + // Check if data is ready, and if since last time we requested is less than SEN5X_POLL_INTERVAL + if (!dataReady && (sinceLastDataPollMs > SEN5X_POLL_INTERVAL)) { LOG_INFO("SEN5X: Data is not ready"); return 1; } @@ -767,6 +771,8 @@ uint8_t SEN5XSensor::getMeasurements() return 2; } + lastDataPoll = now; + return 0; } @@ -796,6 +802,7 @@ int32_t SEN5XSensor::pendingForReadyMs() // Get PN values to check if we are above or below threshold readPNValues(true); + lastDataPoll = now; // 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 && sincePmMeasureStarted < SEN5X_WARMUP_MS_2) { diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.h b/src/modules/Telemetry/Sensor/SEN5XSensor.h index c31f46d11c6..46f8c70e98d 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.h +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h @@ -16,6 +16,10 @@ #define SEN5X_WARMUP_MS_2 30000 #endif +#ifndef SEN5X_POLL_INTERVAL +#define SEN5X_POLL_INTERVAL 1000 +#endif + #ifndef SEN5X_I2C_CLOCK_SPEED #define SEN5X_I2C_CLOCK_SPEED 100000 #endif @@ -119,6 +123,7 @@ See: https://sensirion.com/resource/application_note/low_power_mode/sen5x uint32_t pmMeasureStarted = 0; uint32_t rhtGasMeasureStarted = 0; + uint32_t lastDataPoll = 0; _SEN5XMeasurements sen5xmeasurement{}; bool idle(bool checkState = true); From 71507dffdda6d2ffd930805bf4f0ba0e1d7ed7a9 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Mon, 9 Feb 2026 15:01:51 +0100 Subject: [PATCH 108/108] Add missing serializer --- src/serialization/MeshPacketSerializer.cpp | 12 ------------ .../MeshPacketSerializer_nRF52.cpp | 18 +++++++++--------- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/src/serialization/MeshPacketSerializer.cpp b/src/serialization/MeshPacketSerializer.cpp index 633db6314a5..042bc376396 100644 --- a/src/serialization/MeshPacketSerializer.cpp +++ b/src/serialization/MeshPacketSerializer.cpp @@ -149,18 +149,6 @@ 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_co2) { msgPayload["co2"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.co2); } diff --git a/src/serialization/MeshPacketSerializer_nRF52.cpp b/src/serialization/MeshPacketSerializer_nRF52.cpp index 80aef0e9477..a0ad4e4b9d7 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_co2) { + jsonObj["payload"]["co2"] = (unsigned int)decoded->variant.air_quality_metrics.co2; + } + if (decoded->variant.air_quality_metrics.has_co2_temperature) { + jsonObj["payload"]["co2_temperature"] = decoded->variant.air_quality_metrics.co2_temperature; + } + if (decoded->variant.air_quality_metrics.has_co2_humidity) { + jsonObj["payload"]["co2_humidity"] = decoded->variant.air_quality_metrics.co2_humidity; + } } 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;