From 7f042a011af7d4cbb9e66451fb46d20d239ab0c3 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 1 Jul 2025 17:23:52 +0200 Subject: [PATCH 01/14] Move PMSA003I to separate class and update AQ telemetry --- src/modules/Telemetry/AirQualityTelemetry.cpp | 313 ++++++++++++------ src/modules/Telemetry/AirQualityTelemetry.h | 39 ++- .../Telemetry/EnvironmentTelemetry.cpp | 2 - src/modules/Telemetry/EnvironmentTelemetry.h | 1 - .../Telemetry/Sensor/PMSA003ISensor.cpp | 89 +++++ src/modules/Telemetry/Sensor/PMSA003ISensor.h | 52 +++ 6 files changed, 368 insertions(+), 128 deletions(-) create mode 100644 src/modules/Telemetry/Sensor/PMSA003ISensor.cpp create mode 100644 src/modules/Telemetry/Sensor/PMSA003ISensor.h diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 2472b95b1..8b7ab1b24 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -1,36 +1,54 @@ #include "configuration.h" -#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h") +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "AirQualityTelemetry.h" #include "Default.h" +#include "AirQualityTelemetry.h" #include "MeshService.h" #include "NodeDB.h" #include "PowerFSM.h" #include "RTC.h" #include "Router.h" -#include "detect/ScanI2CTwoWire.h" +#include "UnitConversions.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/images.h" #include "main.h" +#include "sleep.h" #include -#ifndef PMSA003I_WARMUP_MS -// from the PMSA003I datasheet: -// "Stable data should be got at least 30 seconds after the sensor wakeup -// from the sleep mode because of the fan’s performance." -#define PMSA003I_WARMUP_MS 30000 +#if __has_include() +#include "Sensor/PMSA003ISensor.h" +PMSA003ISensor pmsa003iSensor; +#else +NullSensor pmsa003iSensor; #endif int32_t AirQualityTelemetryModule::runOnce() { + if (sleepOnNextExecution == true) { + sleepOnNextExecution = false; + // uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.environment_update_interval, + uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs); + LOG_DEBUG("Sleeping for %ims, then awaking to send metrics again.", nightyNightMs); + doDeepSleep(nightyNightMs, true, false); + } + + uint32_t result = UINT32_MAX; + /* Uncomment the preferences below if you want to use the module without having to configure it from the PythonAPI or WebUI. */ // moduleConfig.telemetry.air_quality_enabled = 1; + // TODO there is no config in module_config.proto for air_quality_screen_enabled. Reusing environment one, although it should have its own + // moduleConfig.telemetry.environment_screen_enabled = 1; + // moduleConfig.telemetry.air_quality_interval = 15; - if (!(moduleConfig.telemetry.air_quality_enabled)) { + if (!(moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.environment_screen_enabled || + AIR_QUALITY_TELEMETRY_MODULE_ENABLE)) { // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it return disable(); } @@ -42,79 +60,141 @@ int32_t AirQualityTelemetryModule::runOnce() if (moduleConfig.telemetry.air_quality_enabled) { LOG_INFO("Air quality Telemetry: init"); -#ifdef PMSA003I_ENABLE_PIN - // put the sensor to sleep on startup - pinMode(PMSA003I_ENABLE_PIN, OUTPUT); - digitalWrite(PMSA003I_ENABLE_PIN, LOW); -#endif /* PMSA003I_ENABLE_PIN */ - - if (!aqi.begin_I2C()) { -#ifndef I2C_NO_RESCAN - LOG_WARN("Could not establish i2c connection to AQI sensor. Rescan"); - // rescan for late arriving sensors. AQI Module starts about 10 seconds into the boot so this is plenty. - uint8_t i2caddr_scan[] = {PMSA0031_ADDR}; - uint8_t i2caddr_asize = 1; - auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); -#if defined(I2C_SDA1) - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1, i2caddr_scan, i2caddr_asize); -#endif - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE, i2caddr_scan, i2caddr_asize); - auto found = i2cScanner->find(ScanI2C::DeviceType::PMSA0031); - if (found.type != ScanI2C::DeviceType::NONE) { - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first = found.address.address; - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].second = - i2cScanner->fetchI2CBus(found.address); - return setStartDelay(); - } -#endif - return disable(); - } - return setStartDelay(); + if (pmsa003iSensor.hasSensor()) + result = pmsa003iSensor.runOnce(); } - return disable(); + + // it's possible to have this module enabled, only for displaying values on the screen. + // therefore, we should only enable the sensor loop if measurement is also enabled + return result == UINT32_MAX ? disable() : setStartDelay(); } else { // if we somehow got to a second run of this module with measurement disabled, then just wait forever - if (!moduleConfig.telemetry.air_quality_enabled) - return disable(); - - switch (state) { -#ifdef PMSA003I_ENABLE_PIN - case State::IDLE: - // sensor is in standby; fire it up and sleep - LOG_DEBUG("runOnce(): state = idle"); - digitalWrite(PMSA003I_ENABLE_PIN, HIGH); - state = State::ACTIVE; - - return PMSA003I_WARMUP_MS; -#endif /* PMSA003I_ENABLE_PIN */ - case State::ACTIVE: - // sensor is already warmed up; grab telemetry and send it - LOG_DEBUG("runOnce(): state = active"); - - if (((lastSentToMesh == 0) || - !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( - moduleConfig.telemetry.air_quality_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) && - airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && - airTime->isTxAllowedAirUtil()) { - sendTelemetry(); - lastSentToMesh = millis(); - } else if (service->isToPhoneQueueEmpty()) { - // Just send to phone when it's not our time to send to mesh yet - // Only send while queue is empty (phone assumed connected) - sendTelemetry(NODENUM_BROADCAST, true); - } - -#ifdef PMSA003I_ENABLE_PIN - // put sensor back to sleep - digitalWrite(PMSA003I_ENABLE_PIN, LOW); - state = State::IDLE; -#endif /* PMSA003I_ENABLE_PIN */ - - return sendToPhoneIntervalMs; - default: + if (!moduleConfig.telemetry.air_quality_enabled && !AIR_QUALITY_TELEMETRY_MODULE_ENABLE) { return disable(); } + + // Wake up the sensors that need it +#ifdef PMSA003I_ENABLE_PIN + if (pmsa003iSensor.hasSensor() && pmsa003iSensor.state == pmsa003iSensor::State::IDLE) + return pmsa003iSensor.wakeUp(); +#endif /* PMSA003I_ENABLE_PIN */ + + if (((lastSentToMesh == 0) || + !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && + airTime->isTxAllowedAirUtil()) { + sendTelemetry(); + lastSentToMesh = millis(); + } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && + (service->isToPhoneQueueEmpty())) { + // Just send to phone when it's not our time to send to mesh yet + // Only send while queue is empty (phone assumed connected) + sendTelemetry(NODENUM_BROADCAST, true); + lastSentToPhone = millis(); + } + +#ifdef PMSA003I_ENABLE_PIN + pmsa003iSensor.sleep(); +#endif /* PMSA003I_ENABLE_PIN */ + + } + return min(sendToPhoneIntervalMs, result); +} + +bool AirQualityTelemetryModule::wantUIFrame() +{ + return moduleConfig.telemetry.environment_screen_enabled; +} + +void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // === Setup display === + display->clear(); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + int line = 1; + + // === Set Title + const char *titleStr = (graphics::isHighResolution) ? "Environment" : "Env."; + + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); + + // === Row spacing setup === + const int rowHeight = FONT_HEIGHT_SMALL - 4; + int currentY = graphics::getTextPositions(display)[line++]; + + // === Show "No Telemetry" if no data available === + if (!lastMeasurementPacket) { + display->drawString(x, currentY, "No Telemetry"); + return; + } + + // Decode the telemetry message from the latest received packet + const meshtastic_Data &p = lastMeasurementPacket->decoded; + meshtastic_Telemetry telemetry; + if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &telemetry)) { + display->drawString(x, currentY, "No Telemetry"); + return; + } + + const auto &m = telemetry.variant.air_quality_metrics; + + // Check if any telemetry field has valid data + bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard || m.has_pm10_environmental || m.has_pm25_environmental || + m.has_pm100_environmental; + + if (!hasAny) { + display->drawString(x, currentY, "No Telemetry"); + return; + } + + // === First line: Show sender name + time since received (left), and first metric (right) === + const char *sender = getSenderShortName(*lastMeasurementPacket); + uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); + String agoStr = (agoSecs > 864000) ? "?" + : (agoSecs > 3600) ? String(agoSecs / 3600) + "h" + : (agoSecs > 60) ? String(agoSecs / 60) + "m" + : String(agoSecs) + "s"; + + String leftStr = String(sender) + " (" + agoStr + ")"; + display->drawString(x, currentY, leftStr); // Left side: who and when + + // === Collect sensor readings as label strings (no icons) === + std::vector entries; + + if (m.has_pm10_standard) + entries.push_back("PM1.0: " + String(m.pm10_standard, 0) + "ug/m3"); + if (m.has_pm25_standard) + entries.push_back("PM2.5: " + String(m.pm25_standard, 0) + "ug/m3"); + if (m.has_pm100_standard) + entries.push_back("PM10.0: " + String(m.pm100_standard, 0) + "ug/m3"); + + // === Show first available metric on top-right of first line === + if (!entries.empty()) { + String valueStr = entries.front(); + int rightX = SCREEN_WIDTH - display->getStringWidth(valueStr); + display->drawString(rightX, currentY, valueStr); + entries.erase(entries.begin()); // Remove from queue + } + + // === Advance to next line for remaining telemetry entries === + currentY += rowHeight; + + // === Draw remaining entries in 2-column format (left and right) === + for (size_t i = 0; i < entries.size(); i += 2) { + // Left column + display->drawString(x, currentY, entries[i]); + + // Right column if it exists + if (i + 1 < entries.size()) { + int rightX = SCREEN_WIDTH / 2; + display->drawString(rightX, currentY, entries[i + 1]); + } + + currentY += rowHeight; } } @@ -142,37 +222,23 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack return false; // Let others look at this message also if they want } +// CHECKED bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) { - if (!aqi.read(&data)) { - LOG_WARN("Skip send measurements. Could not read AQIn"); - return false; - } - + bool valid = true; + bool hasSensor = false; m->time = getTime(); m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag; - m->variant.air_quality_metrics.has_pm10_standard = true; - m->variant.air_quality_metrics.pm10_standard = data.pm10_standard; - m->variant.air_quality_metrics.has_pm25_standard = true; - m->variant.air_quality_metrics.pm25_standard = data.pm25_standard; - m->variant.air_quality_metrics.has_pm100_standard = true; - m->variant.air_quality_metrics.pm100_standard = data.pm100_standard; + m->variant.air_quality_metrics = meshtastic_AirQualityMetrics_init_zero; - m->variant.air_quality_metrics.has_pm10_environmental = true; - m->variant.air_quality_metrics.pm10_environmental = data.pm10_env; - m->variant.air_quality_metrics.has_pm25_environmental = true; - m->variant.air_quality_metrics.pm25_environmental = data.pm25_env; - m->variant.air_quality_metrics.has_pm100_environmental = true; - m->variant.air_quality_metrics.pm100_environmental = data.pm100_env; + if (pmsa003iSensor.hasSensor()) { + // TODO - Should we check for sensor state here? + // If a sensor is sleeping, we should know and check to wake it up + valid = valid && pmsa003iSensor.getMetrics(m); + hasSensor = true; + } - LOG_INFO("Send: PM1.0(Standard)=%i, PM2.5(Standard)=%i, PM10.0(Standard)=%i", m->variant.air_quality_metrics.pm10_standard, - m->variant.air_quality_metrics.pm25_standard, m->variant.air_quality_metrics.pm100_standard); - - LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i", - m->variant.air_quality_metrics.pm10_environmental, m->variant.air_quality_metrics.pm25_environmental, - m->variant.air_quality_metrics.pm100_environmental); - - return true; + return valid && hasSensor; } meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply() @@ -206,7 +272,14 @@ meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply() bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) { meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; + m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag; + m.time = getTime(); if (getAirQualityTelemetry(&m)) { + LOG_INFO("Send: pm10_standard=%f, pm25_standard=%f, pm100_standard=%f, pm10_environmental=%f, pm100_environmental=%f", + m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard, + m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.pm10_environmental, + m.variant.air_quality_metrics.pm100_environmental); + meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; p->decoded.want_response = false; @@ -221,16 +294,46 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) lastMeasurementPacket = packetPool.allocCopy(*p); if (phoneOnly) { - LOG_INFO("Send packet to phone"); + LOG_INFO("Sending packet to phone"); service->sendToPhone(p); } else { - LOG_INFO("Send packet to mesh"); + LOG_INFO("Sending packet to mesh"); service->sendToMesh(p, RX_SRC_LOCAL, true); + + if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { + meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed(); + notification->level = meshtastic_LogRecord_Level_INFO; + notification->time = getValidTime(RTCQualityFromNet); + sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment", + Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs) / + 1000U); + service->sendClientNotification(notification); + sleepOnNextExecution = true; + LOG_DEBUG("Start next execution in 5s, then sleep"); + setIntervalFromNow(FIVE_SECONDS_MS); + } } return true; } - return false; } +AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) +{ + AdminMessageHandleResult result = AdminMessageHandleResult::NOT_HANDLED; +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL + if (pmsa003iSensor.hasSensor()) { + result = pmsa003iSensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + + +#endif + return result; +} + #endif \ No newline at end of file diff --git a/src/modules/Telemetry/AirQualityTelemetry.h b/src/modules/Telemetry/AirQualityTelemetry.h index 0142ee686..8314c54bc 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.h +++ b/src/modules/Telemetry/AirQualityTelemetry.h @@ -1,12 +1,18 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h") +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #pragma once + +#ifndef AIR_QUALITY_TELEMETRY_MODULE_ENABLE +#define AIR_QUALITY_TELEMETRY_MODULE_ENABLE 0 +#endif + #include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "Adafruit_PM25AQI.h" #include "NodeDB.h" #include "ProtobufModule.h" +#include +#include class AirQualityTelemetryModule : private concurrency::OSThread, public ProtobufModule { @@ -20,18 +26,15 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public Protobuf ProtobufModule("AirQualityTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { lastMeasurementPacket = nullptr; - setIntervalFromNow(10 * 1000); - aqi = Adafruit_PM25AQI(); nodeStatusObserver.observe(&nodeStatus->onNewStatus); - -#ifdef PMSA003I_ENABLE_PIN - // the PMSA003I sensor uses about 300mW on its own; support powering it off when it's not actively taking - // a reading - state = State::IDLE; -#else - state = State::ACTIVE; -#endif + setIntervalFromNow(10 * 1000); } + virtual bool wantUIFrame() override; +#if !HAS_SCREEN + void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +#else + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; +#endif protected: /** Called to handle a particular incoming message @@ -49,19 +52,15 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public Protobuf */ bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); + virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; private: - enum State { - IDLE = 0, - ACTIVE = 1, - }; - - State state; - Adafruit_PM25AQI aqi; - PM25_AQI_Data data = {0}; bool firstTime = true; meshtastic_MeshPacket *lastMeasurementPacket; uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute uint32_t lastSentToMesh = 0; + uint32_t lastSentToPhone = 0; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index d1b10fa82..2d6a8a0cb 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -743,8 +743,6 @@ bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) LOG_INFO("Send: soil_temperature=%f, soil_moisture=%u", m.variant.environment_metrics.soil_temperature, m.variant.environment_metrics.soil_moisture); - sensor_read_error_count = 0; - meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; p->decoded.want_response = false; diff --git a/src/modules/Telemetry/EnvironmentTelemetry.h b/src/modules/Telemetry/EnvironmentTelemetry.h index d70c063fc..ffbb229f0 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.h +++ b/src/modules/Telemetry/EnvironmentTelemetry.h @@ -62,7 +62,6 @@ class EnvironmentTelemetryModule : private concurrency::OSThread, public Protobu uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute uint32_t lastSentToMesh = 0; uint32_t lastSentToPhone = 0; - uint32_t sensor_read_error_count = 0; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp new file mode 100644 index 000000000..dacdf5ff4 --- /dev/null +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -0,0 +1,89 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "PMSA003ISensor.h" +#include "TelemetrySensor.h" +#include "detect/ScanI2CTwoWire.h" +#include + +PMSA003ISensor::PMSA003ISensor() : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I") {} + +int32_t PMSA003ISensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + +#ifdef PMSA003I_ENABLE_PIN +// TODO not sure why this was like this + sleep(); +#endif /* PMSA003I_ENABLE_PIN */ + + if (!pmsa003i.begin_I2C()){ +#ifndef I2C_NO_RESCAN + LOG_WARN("Could not establish i2c connection to AQI sensor. Rescan"); + // rescan for late arriving sensors. AQI Module starts about 10 seconds into the boot so this is plenty. + uint8_t i2caddr_scan[] = {PMSA0031_ADDR}; + uint8_t i2caddr_asize = 1; + auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); +#if defined(I2C_SDA1) + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1, i2caddr_scan, i2caddr_asize); +#endif + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE, i2caddr_scan, i2caddr_asize); + auto found = i2cScanner->find(ScanI2C::DeviceType::PMSA0031); + if (found.type != ScanI2C::DeviceType::NONE) { + nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first = found.address.address; + nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].second = + i2cScanner->fetchI2CBus(found.address); + return initI2CSensor(); + } +#endif + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + return initI2CSensor(); +} + +void PMSA003ISensor::setup() +{ +} + +#ifdef PMSA003I_ENABLE_PIN +void sleep() { + digitalWrite(PMSA003I_ENABLE_PIN, LOW); + state = State::IDLE; +} + +uint32_t wakeUp() { + digitalWrite(PMSA003I_ENABLE_PIN, HIGH); + state = State::ACTIVE; +} +#endif /* PMSA003I_ENABLE_PIN */ + +bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) +{ + if (!pmsa003i.read(&pmsa003iData)) { + LOG_WARN("Skip send measurements. Could not read AQIn"); + return false; + } + + measurement->variant.air_quality_metrics.has_pm10_standard = true; + measurement->variant.air_quality_metrics.pm10_standard = pmsa003iData.pm10_standard; + measurement->variant.air_quality_metrics.has_pm25_standard = true; + measurement->variant.air_quality_metrics.pm25_standard = pmsa003iData.pm25_standard; + measurement->variant.air_quality_metrics.has_pm100_standard = true; + measurement->variant.air_quality_metrics.pm100_standard = pmsa003iData.pm100_standard; + + measurement->variant.air_quality_metrics.has_pm10_environmental = true; + measurement->variant.air_quality_metrics.pm10_environmental = pmsa003iData.pm10_env; + measurement->variant.air_quality_metrics.has_pm25_environmental = true; + measurement->variant.air_quality_metrics.pm25_environmental = pmsa003iData.pm25_env; + measurement->variant.air_quality_metrics.has_pm100_environmental = true; + measurement->variant.air_quality_metrics.pm100_environmental = pmsa003iData.pm100_env; + + return true; +} + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h new file mode 100644 index 000000000..01b04368e --- /dev/null +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -0,0 +1,52 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include "detect/ScanI2CTwoWire.h" +#include + +#ifndef PMSA003I_WARMUP_MS +// from the PMSA003I datasheet: +// "Stable data should be got at least 30 seconds after the sensor wakeup +// from the sleep mode because of the fan’s performance." +#define PMSA003I_WARMUP_MS 30000 +#endif + +class PMSA003ISensor : public TelemetrySensor +{ + private: + Adafruit_PM25AQI pmsa003i = Adafruit_PM25AQI(); + PM25_AQI_Data pmsa003iData = {0}; + +#ifdef PMSA003I_ENABLE_PIN + void sleep(); + uint32_t wakeUp(); +#endif + + protected: + virtual void setup() override; + + public: + enum State { + IDLE = 0, + ACTIVE = 1, + }; + +#ifdef PMSA003I_ENABLE_PIN + // the PMSA003I sensor uses about 300mW on its own; support powering it off when it's not actively taking + // a reading + // put the sensor to sleep on startup + pinMode(PMSA003I_ENABLE_PIN, OUTPUT); + State state = State::IDLE; +#else + State state = State::ACTIVE; +#endif + + PMSA003ISensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; +}; + +#endif \ No newline at end of file From 835adb2eac8de9de76a03f875439e2490a6f6625 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Tue, 1 Jul 2025 22:17:38 +0200 Subject: [PATCH 02/14] AirQualityTelemetry module not depend on PM sensor presence --- src/modules/Modules.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 3528f57f5..2ff5a345a 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -221,11 +221,7 @@ void setupModules() // TODO: How to improve this? #if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR new EnvironmentTelemetryModule(); -#if __has_include("Adafruit_PM25AQI.h") - if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) { - new AirQualityTelemetryModule(); - } -#endif + new AirQualityTelemetryModule(); #if !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX30102].first > 0 || nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MLX90614].first > 0) { From 3b470b7f3b38bdd4f25fa911f9f8daefc3ef066e Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 2 Jul 2025 11:29:02 +0200 Subject: [PATCH 03/14] Remove commented line --- src/modules/Telemetry/AirQualityTelemetry.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 8b7ab1b24..75058f849 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -28,7 +28,6 @@ int32_t AirQualityTelemetryModule::runOnce() { if (sleepOnNextExecution == true) { sleepOnNextExecution = false; - // uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.environment_update_interval, uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval, default_telemetry_broadcast_interval_secs); LOG_DEBUG("Sleeping for %ims, then awaking to send metrics again.", nightyNightMs); @@ -222,7 +221,6 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack return false; // Let others look at this message also if they want } -// CHECKED bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) { bool valid = true; From 2f68458a83076f84909fc03a20dd279c0f913745 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 2 Jul 2025 13:12:07 +0200 Subject: [PATCH 04/14] Fixes on PMS class --- src/modules/Telemetry/AirQualityTelemetry.cpp | 2 +- src/modules/Telemetry/Sensor/PMSA003ISensor.cpp | 13 ++++++++++--- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 75058f849..7689802ea 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -74,7 +74,7 @@ int32_t AirQualityTelemetryModule::runOnce() // Wake up the sensors that need it #ifdef PMSA003I_ENABLE_PIN - if (pmsa003iSensor.hasSensor() && pmsa003iSensor.state == pmsa003iSensor::State::IDLE) + if (pmsa003iSensor.hasSensor() && !pmsa003iSensor.isActive()) return pmsa003iSensor.wakeUp(); #endif /* PMSA003I_ENABLE_PIN */ diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index dacdf5ff4..8567d7e70 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -48,24 +48,31 @@ int32_t PMSA003ISensor::runOnce() void PMSA003ISensor::setup() { +#ifdef PMSA003I_ENABLE_PIN + pinMode(PMSA003I_ENABLE_PIN, OUTPUT); +#endif /* PMSA003I_ENABLE_PIN */ } #ifdef PMSA003I_ENABLE_PIN -void sleep() { +void PMSA003ISensor::sleep() { digitalWrite(PMSA003I_ENABLE_PIN, LOW); state = State::IDLE; } -uint32_t wakeUp() { +uint32_t PMSA003ISensor::wakeUp() { digitalWrite(PMSA003I_ENABLE_PIN, HIGH); state = State::ACTIVE; } #endif /* PMSA003I_ENABLE_PIN */ +bool PMSA003ISensor::isActive() { + return state == State::ACTIVE; +} + bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) { if (!pmsa003i.read(&pmsa003iData)) { - LOG_WARN("Skip send measurements. Could not read AQIn"); + LOG_WARN("Skip send measurements. Could not read AQI"); return false; } diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index 01b04368e..db7c9aaa9 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -38,13 +38,13 @@ class PMSA003ISensor : public TelemetrySensor // the PMSA003I sensor uses about 300mW on its own; support powering it off when it's not actively taking // a reading // put the sensor to sleep on startup - pinMode(PMSA003I_ENABLE_PIN, OUTPUT); State state = State::IDLE; #else State state = State::ACTIVE; #endif PMSA003ISensor(); + bool isActive(); virtual int32_t runOnce() override; virtual bool getMetrics(meshtastic_Telemetry *measurement) override; }; From e4903eb43070a554caa866c7937ba0380a3208fc Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 2 Jul 2025 14:41:44 +0200 Subject: [PATCH 05/14] Add missing warmup period to wakeUp function --- src/modules/Telemetry/Sensor/PMSA003ISensor.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 8567d7e70..67f00574e 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -62,6 +62,7 @@ void PMSA003ISensor::sleep() { uint32_t PMSA003ISensor::wakeUp() { digitalWrite(PMSA003I_ENABLE_PIN, HIGH); state = State::ACTIVE; + return PMSA003I_WARMUP_MS } #endif /* PMSA003I_ENABLE_PIN */ From 9111f88f02489bd9446615f39c4b2f86bc91d0c5 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sun, 6 Jul 2025 08:31:57 +0200 Subject: [PATCH 06/14] Fixes on compilation for different variants --- src/modules/Telemetry/Sensor/PMSA003ISensor.cpp | 2 +- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 67f00574e..2b165cd6d 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -62,7 +62,7 @@ void PMSA003ISensor::sleep() { uint32_t PMSA003ISensor::wakeUp() { digitalWrite(PMSA003I_ENABLE_PIN, HIGH); state = State::ACTIVE; - return PMSA003I_WARMUP_MS + return PMSA003I_WARMUP_MS; } #endif /* PMSA003I_ENABLE_PIN */ diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index db7c9aaa9..7e460ce33 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -20,11 +20,6 @@ class PMSA003ISensor : public TelemetrySensor Adafruit_PM25AQI pmsa003i = Adafruit_PM25AQI(); PM25_AQI_Data pmsa003iData = {0}; -#ifdef PMSA003I_ENABLE_PIN - void sleep(); - uint32_t wakeUp(); -#endif - protected: virtual void setup() override; @@ -35,6 +30,8 @@ class PMSA003ISensor : public TelemetrySensor }; #ifdef PMSA003I_ENABLE_PIN + void sleep(); + uint32_t wakeUp(); // the PMSA003I sensor uses about 300mW on its own; support powering it off when it's not actively taking // a reading // put the sensor to sleep on startup From 40af7b82c4bc63a2e63eda9355a5677e39212611 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Fri, 11 Jul 2025 14:39:26 +0200 Subject: [PATCH 07/14] Add functions to check for I2C bus speed and set it --- src/detect/ScanI2CTwoWire.cpp | 69 +++++++++++++++++++++++++++++++++++ src/detect/ScanI2CTwoWire.h | 3 ++ src/main.cpp | 21 +++++++++++ 3 files changed, 93 insertions(+) diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 652d50d51..215efc1d0 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -109,6 +109,75 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation return value; } +bool ScanI2CTwoWire::setClockSpeed(I2CPort port, uint32_t speed) { + + DeviceAddress addr(port, 0x00); + TwoWire *i2cBus; + +#if WIRE_INTERFACES_COUNT == 2 + if (port == I2CPort::WIRE1) { + i2cBus = &Wire1; + } else { +#endif + i2cBus = &Wire; +#if WIRE_INTERFACES_COUNT == 2 + } +#endif + + return i2cBus->setClock(speed); +} + +uint32_t ScanI2CTwoWire::getClockSpeed(I2CPort port) { + + DeviceAddress addr(port, 0x00); + TwoWire *i2cBus; + +#if WIRE_INTERFACES_COUNT == 2 + if (port == I2CPort::WIRE1) { + i2cBus = &Wire1; + } else { +#endif + i2cBus = &Wire; +#if WIRE_INTERFACES_COUNT == 2 + } +#endif + + return i2cBus->getClock(); +} + +/// for SEN5X detection +String readSEN5xProductName(TwoWire* i2cBus, uint8_t address) { + uint8_t cmd[] = { 0xD0, 0x14 }; + uint8_t response[48] = {0}; + + i2cBus->beginTransmission(address); + i2cBus->write(cmd, 2); + if (i2cBus->endTransmission() != 0) return ""; + + delay(20); + if (i2cBus->requestFrom(address, (uint8_t)48) != 48) return ""; + + for (int i = 0; i < 48 && i2cBus->available(); ++i) { + response[i] = i2cBus->read(); + } + + char productName[33] = {0}; + int j = 0; + for (int i = 0; i < 48 && j < 32; i += 3) { + if (response[i] >= 32 && response[i] <= 126) + productName[j++] = response[i]; + else + break; + + if (response[i + 1] >= 32 && response[i + 1] <= 126) + productName[j++] = response[i + 1]; + else + break; + } + + return String(productName); +} + #define SCAN_SIMPLE_CASE(ADDR, T, ...) \ case ADDR: \ logFoundDevice(__VA_ARGS__); \ diff --git a/src/detect/ScanI2CTwoWire.h b/src/detect/ScanI2CTwoWire.h index 6988091ad..28b073a17 100644 --- a/src/detect/ScanI2CTwoWire.h +++ b/src/detect/ScanI2CTwoWire.h @@ -29,6 +29,9 @@ class ScanI2CTwoWire : public ScanI2C size_t countDevices() const override; + bool setClockSpeed(ScanI2C::I2CPort, uint32_t); + uint32_t getClockSpeed(ScanI2C::I2CPort); + protected: FoundDevice firstOfOrNONE(size_t, DeviceType[]) const override; diff --git a/src/main.cpp b/src/main.cpp index 1868d98c7..132dab9e2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -480,6 +480,7 @@ void setup() Wire.setSCL(I2C_SCL); Wire.begin(); #elif defined(I2C_SDA) && !defined(ARCH_RP2040) + LOG_INFO("Starting Bus with (SDA) %d and (SCL) %d: ", I2C_SDA, I2C_SCL); Wire.begin(I2C_SDA, I2C_SCL); #elif defined(ARCH_PORTDUINO) if (settingsStrings[i2cdev] != "") { @@ -538,6 +539,26 @@ void setup() i2cScanner->scanPort(ScanI2C::I2CPort::WIRE); #endif +#ifdef I2C_CLOCK_SPEED + uint32_t currentClock; + currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE); + LOG_INFO("Clock speed: %uHz on WIRE", currentClock); + LOG_DEBUG("Setting Wire with defined clock speed, %uHz...", I2C_CLOCK_SPEED); + if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE, I2C_CLOCK_SPEED)) { + LOG_ERROR("Unable to set clock speed on WIRE"); + } else { + + currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE); + LOG_INFO("Set clock speed: %uHz on WIRE", currentClock); + } + // LOG_DEBUG("Starting Wire with defined clock speed, %d...", I2C_CLOCK_SPEED); + // if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE1, I2C_CLOCK_SPEED)) { + // LOG_ERROR("Unable to set clock speed on WIRE1"); + // } else { + // LOG_INFO("Set clock speed: %d on WIRE1", I2C_CLOCK_SPEED); + // } +#endif + auto i2cCount = i2cScanner->countDevices(); if (i2cCount == 0) { LOG_INFO("No I2C devices found"); From ff8691dc136555c997db5b6c23c69b1e99ea1d90 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Sat, 12 Jul 2025 22:12:31 +0200 Subject: [PATCH 08/14] Add ScreenFonts.h Co-authored-by: Hannes Fuchs --- src/modules/Telemetry/AirQualityTelemetry.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 7689802ea..1c8e95a82 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -23,6 +23,7 @@ PMSA003ISensor pmsa003iSensor; #else NullSensor pmsa003iSensor; #endif +#include "graphics/ScreenFonts.h" int32_t AirQualityTelemetryModule::runOnce() { From 0dda175d97e8ea004c7d26c9af27abaf2e90ea83 Mon Sep 17 00:00:00 2001 From: Nashui-Yan Date: Tue, 22 Jul 2025 16:55:09 +0100 Subject: [PATCH 09/14] PMSA003I 1st round test --- src/modules/Telemetry/AirQualityTelemetry.cpp | 8 +- .../Telemetry/Sensor/PMSA003ISensor.cpp | 200 ++++++++++++------ src/modules/Telemetry/Sensor/PMSA003ISensor.h | 57 ++--- 3 files changed, 166 insertions(+), 99 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 1c8e95a82..9bc41bfa5 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -166,11 +166,11 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta std::vector entries; if (m.has_pm10_standard) - entries.push_back("PM1.0: " + String(m.pm10_standard, 0) + "ug/m3"); + entries.push_back("PM1.0: " + String(m.pm10_standard) + "ug/m3"); if (m.has_pm25_standard) - entries.push_back("PM2.5: " + String(m.pm25_standard, 0) + "ug/m3"); + entries.push_back("PM2.5: " + String(m.pm25_standard) + "ug/m3"); if (m.has_pm100_standard) - entries.push_back("PM10.0: " + String(m.pm100_standard, 0) + "ug/m3"); + entries.push_back("PM10.0: " + String(m.pm100_standard) + "ug/m3"); // === Show first available metric on top-right of first line === if (!entries.empty()) { @@ -274,7 +274,7 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag; m.time = getTime(); if (getAirQualityTelemetry(&m)) { - LOG_INFO("Send: pm10_standard=%f, pm25_standard=%f, pm100_standard=%f, pm10_environmental=%f, pm100_environmental=%f", + LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u, pm10_environmental=%u, pm100_environmental=%u", m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard, m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.pm10_environmental, m.variant.air_quality_metrics.pm100_environmental); diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 2b165cd6d..39f8269a2 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -1,97 +1,175 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "PMSA003ISensor.h" #include "TelemetrySensor.h" -#include "detect/ScanI2CTwoWire.h" -#include -PMSA003ISensor::PMSA003ISensor() : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I") {} +#include -int32_t PMSA003ISensor::runOnce() +PMSA003ISensor::PMSA003ISensor() + : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I") { - LOG_INFO("Init sensor: %s", sensorName); - if (!hasSensor()) { - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - } - -#ifdef PMSA003I_ENABLE_PIN -// TODO not sure why this was like this - sleep(); -#endif /* PMSA003I_ENABLE_PIN */ - - if (!pmsa003i.begin_I2C()){ -#ifndef I2C_NO_RESCAN - LOG_WARN("Could not establish i2c connection to AQI sensor. Rescan"); - // rescan for late arriving sensors. AQI Module starts about 10 seconds into the boot so this is plenty. - uint8_t i2caddr_scan[] = {PMSA0031_ADDR}; - uint8_t i2caddr_asize = 1; - auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); -#if defined(I2C_SDA1) - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1, i2caddr_scan, i2caddr_asize); -#endif - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE, i2caddr_scan, i2caddr_asize); - auto found = i2cScanner->find(ScanI2C::DeviceType::PMSA0031); - if (found.type != ScanI2C::DeviceType::NONE) { - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first = found.address.address; - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].second = - i2cScanner->fetchI2CBus(found.address); - return initI2CSensor(); - } -#endif - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - } - return initI2CSensor(); } void PMSA003ISensor::setup() { #ifdef PMSA003I_ENABLE_PIN pinMode(PMSA003I_ENABLE_PIN, OUTPUT); -#endif /* PMSA003I_ENABLE_PIN */ +#endif } -#ifdef PMSA003I_ENABLE_PIN -void PMSA003ISensor::sleep() { - digitalWrite(PMSA003I_ENABLE_PIN, LOW); - state = State::IDLE; +bool PMSA003ISensor::restoreClock(uint32_t currentClock){ +#ifdef PMSA003I_I2C_CLOCK_SPEED + if (currentClock != PMSA003I_I2C_CLOCK_SPEED){ + // LOG_DEBUG("Restoring I2C clock to %uHz", currentClock); + return bus->setClock(currentClock); + } + return true; +#endif } -uint32_t PMSA003ISensor::wakeUp() { - digitalWrite(PMSA003I_ENABLE_PIN, HIGH); - state = State::ACTIVE; - return PMSA003I_WARMUP_MS; -} -#endif /* PMSA003I_ENABLE_PIN */ +int32_t PMSA003ISensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); -bool PMSA003ISensor::isActive() { - return state == State::ACTIVE; + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + + bus = nodeTelemetrySensorsMap[sensorType].second; + address = (uint8_t)nodeTelemetrySensorsMap[sensorType].first; + +#ifdef PMSA003I_I2C_CLOCK_SPEED + uint32_t currentClock; + currentClock = bus->getClock(); + if (currentClock != PMSA003I_I2C_CLOCK_SPEED){ + // LOG_DEBUG("Changing I2C clock to %u", PMSA003I_I2C_CLOCK_SPEED); + bus->setClock(PMSA003I_I2C_CLOCK_SPEED); + } +#endif + + bus->beginTransmission(address); + if (bus->endTransmission() != 0) { + LOG_WARN("PMSA003I not found on I2C at 0x12"); + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + + restoreClock(currentClock); + + status = 1; + LOG_INFO("PMSA003I Enabled"); + + return initI2CSensor(); } bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) { - if (!pmsa003i.read(&pmsa003iData)) { - LOG_WARN("Skip send measurements. Could not read AQI"); + if(!isActive()){ + LOG_WARN("PMSA003I is not active"); + return false; + } + +#ifdef PMSA003I_I2C_CLOCK_SPEED + uint32_t currentClock; + currentClock = bus->getClock(); + if (currentClock != PMSA003I_I2C_CLOCK_SPEED){ + // LOG_DEBUG("Changing I2C clock to %u", PMSA003I_I2C_CLOCK_SPEED); + bus->setClock(PMSA003I_I2C_CLOCK_SPEED); + } +#endif + + bus->requestFrom(address, PMSA003I_FRAME_LENGTH); + if (bus->available() < PMSA003I_FRAME_LENGTH) { + LOG_WARN("PMSA003I read failed: incomplete data (%d bytes)", bus->available()); + return false; + } + + restoreClock(currentClock); + + for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) { + buffer[i] = bus->read(); + } + + if (buffer[0] != 0x42 || buffer[1] != 0x4D) { + LOG_WARN("PMSA003I frame header invalid: 0x%02X 0x%02X", buffer[0], buffer[1]); + return false; + } + + auto read16 = [](uint8_t *data, uint8_t idx) -> uint16_t { + return (data[idx] << 8) | data[idx + 1]; + }; + + computedChecksum = 0; + + for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH - 2; i++) { + computedChecksum += buffer[i]; + } + receivedChecksum = read16(buffer, PMSA003I_FRAME_LENGTH - 2); + + if (computedChecksum != receivedChecksum) { + LOG_WARN("PMSA003I checksum failed: computed 0x%04X, received 0x%04X", computedChecksum, receivedChecksum); return false; } measurement->variant.air_quality_metrics.has_pm10_standard = true; - measurement->variant.air_quality_metrics.pm10_standard = pmsa003iData.pm10_standard; + measurement->variant.air_quality_metrics.pm10_standard = read16(buffer, 4); + measurement->variant.air_quality_metrics.has_pm25_standard = true; - measurement->variant.air_quality_metrics.pm25_standard = pmsa003iData.pm25_standard; + measurement->variant.air_quality_metrics.pm25_standard = read16(buffer, 6); + measurement->variant.air_quality_metrics.has_pm100_standard = true; - measurement->variant.air_quality_metrics.pm100_standard = pmsa003iData.pm100_standard; + measurement->variant.air_quality_metrics.pm100_standard = read16(buffer, 8); measurement->variant.air_quality_metrics.has_pm10_environmental = true; - measurement->variant.air_quality_metrics.pm10_environmental = pmsa003iData.pm10_env; - measurement->variant.air_quality_metrics.has_pm25_environmental = true; - measurement->variant.air_quality_metrics.pm25_environmental = pmsa003iData.pm25_env; - measurement->variant.air_quality_metrics.has_pm100_environmental = true; - measurement->variant.air_quality_metrics.pm100_environmental = pmsa003iData.pm100_env; + measurement->variant.air_quality_metrics.pm10_environmental = read16(buffer, 10); + measurement->variant.air_quality_metrics.has_pm25_environmental = true; + measurement->variant.air_quality_metrics.pm25_environmental = read16(buffer, 12); + + measurement->variant.air_quality_metrics.has_pm100_environmental = true; + measurement->variant.air_quality_metrics.pm100_environmental = read16(buffer, 14); + + measurement->variant.air_quality_metrics.has_particles_03um = true; + measurement->variant.air_quality_metrics.particles_03um = read16(buffer, 16); + + measurement->variant.air_quality_metrics.has_particles_05um = true; + measurement->variant.air_quality_metrics.particles_05um = read16(buffer, 18); + + measurement->variant.air_quality_metrics.has_particles_10um = true; + measurement->variant.air_quality_metrics.particles_10um = read16(buffer, 20); + + measurement->variant.air_quality_metrics.has_particles_25um = true; + measurement->variant.air_quality_metrics.particles_25um = read16(buffer, 22); + + measurement->variant.air_quality_metrics.has_particles_50um = true; + measurement->variant.air_quality_metrics.particles_50um = read16(buffer, 24); + + measurement->variant.air_quality_metrics.has_particles_100um = true; + measurement->variant.air_quality_metrics.particles_100um = read16(buffer, 26); + return true; } -#endif \ No newline at end of file +bool PMSA003ISensor::isActive() +{ + return state == State::ACTIVE; +} + +#ifdef PMSA003I_ENABLE_PIN +void PMSA003ISensor::sleep() +{ + digitalWrite(PMSA003I_ENABLE_PIN, LOW); + state = State::IDLE; +} + +uint32_t PMSA003ISensor::wakeUp() +{ + digitalWrite(PMSA003I_ENABLE_PIN, HIGH); + state = State::ACTIVE; + return PMSA003I_WARMUP_MS; +} +#endif + +#endif diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index 7e460ce33..d6f12dfbb 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -1,49 +1,38 @@ -#include "configuration.h" +#pragma once -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() - -#include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" -#include "detect/ScanI2CTwoWire.h" -#include -#ifndef PMSA003I_WARMUP_MS -// from the PMSA003I datasheet: -// "Stable data should be got at least 30 seconds after the sensor wakeup -// from the sleep mode because of the fan’s performance." -#define PMSA003I_WARMUP_MS 30000 +#ifndef PMSA003I_I2C_CLOCK_SPEED +#define PMSA003I_I2C_CLOCK_SPEED 100000 +#endif + +#ifndef PMSA003I_ENABLE_PIN +#define PMSA003I_FRAME_LENGTH 32 #endif class PMSA003ISensor : public TelemetrySensor { - private: - Adafruit_PM25AQI pmsa003i = Adafruit_PM25AQI(); - PM25_AQI_Data pmsa003iData = {0}; - - protected: +public: + PMSA003ISensor(); virtual void setup() override; - - public: - enum State { - IDLE = 0, - ACTIVE = 1, - }; + virtual int32_t runOnce() override; + virtual bool restoreClock(uint32_t currentClock); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool isActive(); #ifdef PMSA003I_ENABLE_PIN void sleep(); uint32_t wakeUp(); - // the PMSA003I sensor uses about 300mW on its own; support powering it off when it's not actively taking - // a reading - // put the sensor to sleep on startup - State state = State::IDLE; -#else - State state = State::ACTIVE; #endif - PMSA003ISensor(); - bool isActive(); - virtual int32_t runOnce() override; - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; -}; +private: + enum class State { IDLE, ACTIVE }; + State state = State::ACTIVE; + TwoWire * bus; + uint8_t address; -#endif \ No newline at end of file + uint16_t computedChecksum = 0; + uint16_t receivedChecksum = 0; + + uint8_t buffer[PMSA003I_FRAME_LENGTH]; +}; From 14eaa3e097e33fa11c6739470249045fbfd0ff2a Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 23 Jul 2025 13:42:34 +0200 Subject: [PATCH 10/14] Fix I2C scan speed --- src/main.cpp | 53 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 132dab9e2..deda7f107 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -524,6 +524,39 @@ void setup() LOG_INFO("Scan for i2c devices"); #endif +// Scan I2C port at desired speed +#ifdef SCAN_I2C_CLOCK_SPEED + uint32_t currentClock; + currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE); + LOG_INFO("Clock speed: %uHz on WIRE", currentClock); + LOG_DEBUG("Setting Wire with defined clock speed, %uHz...", SCAN_I2C_CLOCK_SPEED); + + if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE, SCAN_I2C_CLOCK_SPEED)) { + LOG_ERROR("Unable to set clock speed on WIRE"); + } else { + currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE); + LOG_INFO("Set clock speed: %uHz on WIRE", currentClock); + } + + // TODO Check if necessary + // LOG_DEBUG("Starting Wire with defined clock speed, %d...", SCAN_I2C_CLOCK_SPEED); + // if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE1, SCAN_I2C_CLOCK_SPEED)) { + // LOG_ERROR("Unable to set clock speed on WIRE1"); + // } else { + // LOG_INFO("Set clock speed: %d on WIRE1", SCAN_I2C_CLOCK_SPEED); + // } + + // Restore clock speed + if (currentClock != SCAN_I2C_CLOCK_SPEED) { + if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE, currentClock)) { + LOG_ERROR("Unable to restore clock speed on WIRE"); + } else { + currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE); + LOG_INFO("Set clock speed restored to: %uHz on WIRE", currentClock); + } + } +#endif + #if defined(I2C_SDA1) || (defined(NRF52840_XXAA) && (WIRE_INTERFACES_COUNT == 2)) i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1); #endif @@ -539,26 +572,6 @@ void setup() i2cScanner->scanPort(ScanI2C::I2CPort::WIRE); #endif -#ifdef I2C_CLOCK_SPEED - uint32_t currentClock; - currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE); - LOG_INFO("Clock speed: %uHz on WIRE", currentClock); - LOG_DEBUG("Setting Wire with defined clock speed, %uHz...", I2C_CLOCK_SPEED); - if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE, I2C_CLOCK_SPEED)) { - LOG_ERROR("Unable to set clock speed on WIRE"); - } else { - - currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE); - LOG_INFO("Set clock speed: %uHz on WIRE", currentClock); - } - // LOG_DEBUG("Starting Wire with defined clock speed, %d...", I2C_CLOCK_SPEED); - // if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE1, I2C_CLOCK_SPEED)) { - // LOG_ERROR("Unable to set clock speed on WIRE1"); - // } else { - // LOG_INFO("Set clock speed: %d on WIRE1", I2C_CLOCK_SPEED); - // } -#endif - auto i2cCount = i2cScanner->countDevices(); if (i2cCount == 0) { LOG_INFO("No I2C devices found"); From 8a811b209db77bab3c9ff6b31b3009a86dadf89f Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 23 Jul 2025 17:23:04 +0200 Subject: [PATCH 11/14] Fix minor issues and bring back I2C SPEED def --- src/modules/Telemetry/AirQualityTelemetry.cpp | 9 +++++---- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 7 ++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 9bc41bfa5..431473e05 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -274,10 +274,11 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag; m.time = getTime(); if (getAirQualityTelemetry(&m)) { - LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u, pm10_environmental=%u, pm100_environmental=%u", - m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard, - m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.pm10_environmental, - m.variant.air_quality_metrics.pm100_environmental); + LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u, \ + pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u", \ + m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard, \ + m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.pm10_environmental, \ + m.variant.air_quality_metrics.pm25_environmental, m.variant.air_quality_metrics.pm100_environmental); meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index d6f12dfbb..ee3258ab1 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -2,13 +2,10 @@ #include "TelemetrySensor.h" -#ifndef PMSA003I_I2C_CLOCK_SPEED #define PMSA003I_I2C_CLOCK_SPEED 100000 -#endif - -#ifndef PMSA003I_ENABLE_PIN #define PMSA003I_FRAME_LENGTH 32 -#endif +#define PMSA003I_WARMUP_MS 30000 + class PMSA003ISensor : public TelemetrySensor { From ec5a752078c6e651ab171bfcf226b19bb438f281 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Fri, 25 Jul 2025 12:29:40 +0200 Subject: [PATCH 12/14] Remove PMSA003I library as its no longer needed --- platformio.ini | 2 -- src/modules/Telemetry/AirQualityTelemetry.cpp | 12 +++++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/platformio.ini b/platformio.ini index 8bf56cf5b..b5f08dd72 100644 --- a/platformio.ini +++ b/platformio.ini @@ -133,8 +133,6 @@ lib_deps = adafruit/Adafruit INA260 Library@1.5.3 # renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219 adafruit/Adafruit INA219@1.2.3 - # renovate: datasource=custom.pio depName=Adafruit PM25 AQI Sensor packageName=adafruit/library/Adafruit PM25 AQI Sensor - adafruit/Adafruit PM25 AQI Sensor@2.0.0 # renovate: datasource=custom.pio depName=Adafruit MPU6050 packageName=adafruit/library/Adafruit MPU6050 adafruit/Adafruit MPU6050@2.2.6 # renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 431473e05..ff5bd22ef 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -16,13 +16,12 @@ #include "main.h" #include "sleep.h" #include - -#if __has_include() +// Sensor includes #include "Sensor/PMSA003ISensor.h" + +// Sensors PMSA003ISensor pmsa003iSensor; -#else -NullSensor pmsa003iSensor; -#endif + #include "graphics/ScreenFonts.h" int32_t AirQualityTelemetryModule::runOnce() @@ -326,6 +325,9 @@ AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule( AdminMessageHandleResult result = AdminMessageHandleResult::NOT_HANDLED; #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL if (pmsa003iSensor.hasSensor()) { + // TODO - Potentially implement an admin message to choose between pm_standard + // and pm_environmental. This could be configurable as it doesn't make sense so + // have both result = pmsa003iSensor.handleAdminMessage(mp, request, response); if (result != AdminMessageHandleResult::NOT_HANDLED) return result; From 7305f1ccccde923ec3bfba3bfc4155f778c7ae2c Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 2 Jul 2025 11:57:34 +0200 Subject: [PATCH 13/14] 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 ff5bd22ef..c908717d3 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 000000000..c3ccb902e --- /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 000000000..981723edf --- /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 228b315f714a353ac9535b162fe5359dde4d5697 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Fri, 18 Jul 2025 01:18:23 +0200 Subject: [PATCH 14/14] 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 c908717d3..13ed4cad6 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 ===