diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 2472b95b1..9abf83887 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -10,10 +10,19 @@ #include "PowerFSM.h" #include "RTC.h" #include "Router.h" +#include "UnitConversions.h" #include "detect/ScanI2CTwoWire.h" +#include "graphics/ScreenFonts.h" #include "main.h" +#include "sleep.h" #include +// Sensors +#include "Sensor/PMSA0031Sensor.h" +#include "Sensor/SCD4XSensor.h" + +SCD4XSensor scd4xSensor; +PMSA0031Sensor pmsa0031Sensor; #ifndef PMSA003I_WARMUP_MS // from the PMSA003I datasheet: // "Stable data should be got at least 30 seconds after the sensor wakeup @@ -23,11 +32,20 @@ int32_t AirQualityTelemetryModule::runOnce() { + if (sleepOnNextExecution == true) { + sleepOnNextExecution = false; + uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.environment_update_interval, + default_telemetry_broadcast_interval_secs); + LOG_DEBUG("Sleeping for %ims, then awaking to send metrics again.", nightyNightMs); + doDeepSleep(nightyNightMs, true); + } + + 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; if (!(moduleConfig.telemetry.air_quality_enabled)) { @@ -41,24 +59,27 @@ 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_found.address == 0x00) { + - if (!aqi.begin_I2C()) { #ifndef I2C_NO_RESCAN - LOG_WARN("Could not establish i2c connection to AQI sensor. Rescan"); + LOG_WARN("Rescan for I2C AQI Sensor"); // 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; + uint8_t i2caddr_scan[] = {PMSA0031_ADDR, SCD4X_ADDR}; + uint8_t i2caddr_asize = 2; auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); -#if defined(I2C_SDA1) + +#if WIRE_INTERFACES_COUNT == 2 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; @@ -66,8 +87,36 @@ int32_t AirQualityTelemetryModule::runOnce() i2cScanner->fetchI2CBus(found.address); return setStartDelay(); } -#endif + if (aqi_found.address == 0x00) { + return disable(); + } + #endif + } + if (scd4xSensor.hasSensor()) + result = scd4xSensor.runOnce(); + if (pmsa0031Sensor.hasSensor()) + result = pmsa0031Sensor.runOnce(); + return result; + + } 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(); + + 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(); } return setStartDelay(); } @@ -115,6 +164,7 @@ int32_t AirQualityTelemetryModule::runOnce() default: return disable(); } + return min(sendToPhoneIntervalMs, result); } } @@ -124,9 +174,9 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack #ifdef DEBUG_PORT const char *sender = getSenderShortName(mp); - LOG_INFO("(Received from %s): pm10_standard=%i, pm25_standard=%i, pm100_standard=%i", sender, + LOG_INFO("(Received from %s): pm10_standard=%i, pm25_standard=%i, pm100_standard=%i, co2=%i ppm", sender, t->variant.air_quality_metrics.pm10_standard, t->variant.air_quality_metrics.pm25_standard, - t->variant.air_quality_metrics.pm100_standard); + t->variant.air_quality_metrics.pm100_standard, t->variant.air_quality_metrics.co2); 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, @@ -142,13 +192,38 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack return false; // Let others look at this message also if they want } -bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) +bool AirQualityTelemetryModule::wantUIFrame() { - if (!aqi.read(&data)) { - LOG_WARN("Skip send measurements. Could not read AQIn"); - return false; + return moduleConfig.telemetry.environment_screen_enabled; +} + +void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + if (lastMeasurementPacket == nullptr) { + // If there's no valid packet, display "Environment" + display->drawString(x, y, "Air Quality"); + display->drawString(x, y += _fontHeight(FONT_SMALL), "No measurement"); + return; } + // Decode the last measurement packet + meshtastic_Telemetry lastMeasurement; + uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); + const char *lastSender = getSenderShortName(*lastMeasurementPacket); + + const meshtastic_Data &p = lastMeasurementPacket->decoded; + if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) { + display->drawString(x, y, "Measurement Error"); + LOG_ERROR("Unable to decode last packet"); + return; + } + + // Display "Env. From: ..." on its own + display->drawString(x, y, "AQ. From: " + String(lastSender) + "(" + String(agoSecs) + "s)"); + m->time = getTime(); m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag; m->variant.air_quality_metrics.has_pm10_standard = true; @@ -168,11 +243,42 @@ bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) 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); + if (lastMeasurement.variant.air_quality_metrics.has_pm10_standard) { + display->drawString(x, y += _fontHeight(FONT_SMALL), + "PM1.0(Standard): " + String(lastMeasurement.variant.air_quality_metrics.pm10_standard, 0)); + } + if (lastMeasurement.variant.air_quality_metrics.has_pm25_standard) { + display->drawString(x, y += _fontHeight(FONT_SMALL), + "PM2.5(Standard): " + String(lastMeasurement.variant.air_quality_metrics.pm25_standard, 0)); + } + if (lastMeasurement.variant.air_quality_metrics.has_pm10_environmental) { + display->drawString(x, y += _fontHeight(FONT_SMALL), + "PM10.0(Standard): " + String(lastMeasurement.variant.air_quality_metrics.pm100_standard, 0)); + } + if (lastMeasurement.variant.air_quality_metrics.has_co2) { + display->drawString(x, y += _fontHeight(FONT_SMALL), + "CO2: " + String(lastMeasurement.variant.air_quality_metrics.co2, 0) + " ppm"); + } +} - return true; +bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) +{ + 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; + + if (scd4xSensor.hasSensor()) { + valid = valid && scd4xSensor.getMetrics(m); + hasSensor = true; + } + if (pmsa0031Sensor.hasSensor()) { + valid = valid && pmsa0031Sensor.getMetrics(m); + hasSensor = true; + } + + return valid && hasSensor; } meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply() @@ -207,6 +313,14 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) { meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; if (getAirQualityTelemetry(&m)) { + LOG_INFO("(Sending): PM1.0(Standard)=%i, PM2.5(Standard)=%i, PM10.0(Standard)=%i, cO2=%i ppm", + 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.co2); + + 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); + meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; p->decoded.want_response = false; diff --git a/src/modules/Telemetry/AirQualityTelemetry.h b/src/modules/Telemetry/AirQualityTelemetry.h index 0142ee686..52b7ebbaf 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.h +++ b/src/modules/Telemetry/AirQualityTelemetry.h @@ -4,9 +4,10 @@ #pragma once #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,9 +21,8 @@ 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); + setIntervalFromNow(10 * 1000); #ifdef PMSA003I_ENABLE_PIN // the PMSA003I sensor uses about 300mW on its own; support powering it off when it's not actively taking @@ -32,6 +32,12 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public Protobuf state = State::ACTIVE; #endif } + 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 @@ -62,6 +68,8 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public Protobuf meshtastic_MeshPacket *lastMeasurementPacket; 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/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index d1b10fa82..236417cf7 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -510,6 +510,7 @@ bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPac sender, t->variant.environment_metrics.barometric_pressure, t->variant.environment_metrics.current, t->variant.environment_metrics.gas_resistance, t->variant.environment_metrics.relative_humidity, t->variant.environment_metrics.temperature); + LOG_INFO("(Received from %s): voltage=%f, IAQ=%d, distance=%f, lux=%f, white_lux=%f", sender, t->variant.environment_metrics.voltage, t->variant.environment_metrics.iaq, t->variant.environment_metrics.distance, t->variant.environment_metrics.lux, diff --git a/src/modules/Telemetry/Sensor/PMSA0031Sensor.cpp b/src/modules/Telemetry/Sensor/PMSA0031Sensor.cpp new file mode 100644 index 000000000..d2e032f60 --- /dev/null +++ b/src/modules/Telemetry/Sensor/PMSA0031Sensor.cpp @@ -0,0 +1,47 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "PMSA0031Sensor.h" +#include "TelemetrySensor.h" +#include + +PMSA0031Sensor::PMSA0031Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA0031") {} + +int32_t PMSA0031Sensor::runOnce() +{ + LOG_INFO("Init sensor: %s\n", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + aqi = Adafruit_PM25AQI(); + delay(10000); + aqi.begin_I2C(); + /* nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first = found.address.address; + nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].second = + i2cScanner->fetchI2CBus(found.address););*/ + return initI2CSensor(); +} + +void PMSA0031Sensor::setup() {} + +bool PMSA0031Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + uint16_t co2, error; + float temperature, humidity; + if (!aqi.read(&data)) { + LOG_WARN("Skipping send measurements. Could not read AQIn"); + return false; + } + measurement->variant.air_quality_metrics.pm10_standard = data.pm10_standard; + measurement->variant.air_quality_metrics.pm25_standard = data.pm25_standard; + measurement->variant.air_quality_metrics.pm100_standard = data.pm100_standard; + + measurement->variant.air_quality_metrics.pm10_environmental = data.pm10_env; + measurement->variant.air_quality_metrics.pm25_environmental = data.pm25_env; + measurement->variant.air_quality_metrics.pm100_environmental = data.pm100_env; + return true; +} + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/PMSA0031Sensor.h b/src/modules/Telemetry/Sensor/PMSA0031Sensor.h new file mode 100644 index 000000000..880766007 --- /dev/null +++ b/src/modules/Telemetry/Sensor/PMSA0031Sensor.h @@ -0,0 +1,24 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +class PMSA0031Sensor : public TelemetrySensor +{ + private: + Adafruit_PM25AQI aqi; + PM25_AQI_Data data = {0}; + + protected: + virtual void setup() override; + + public: + PMSA0031Sensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; +}; + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SCD4XSensor.cpp b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp new file mode 100644 index 000000000..1fdfacecf --- /dev/null +++ b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp @@ -0,0 +1,52 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "SCD4XSensor.h" +#include "TelemetrySensor.h" +#include + +SCD4XSensor::SCD4XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SCD4X, "SCD4X") {} + +int32_t SCD4XSensor::runOnce() +{ + LOG_INFO("Init sensor: %s\n", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + // scd4x = SensirionI2CScd4x(nodeTelemetrySensorsMap[sensorType].second); + // status = scd4x.begin(nodeTelemetrySensorsMap[sensorType].first); + scd4x.begin(*nodeTelemetrySensorsMap[sensorType].second); + scd4x.stopPeriodicMeasurement(); + status = scd4x.startLowPowerPeriodicMeasurement(); + if (status == 0) { + status = 1; + } else { + status = 0; + } + return initI2CSensor(); +} + +void SCD4XSensor::setup() {} + +bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) +{ + uint16_t co2, error; + float temperature, humidity; + error = scd4x.readMeasurement(co2, temperature, humidity); + if (error || co2 == 0) { + LOG_DEBUG("Skipping invalid SCD4X measurement.\n"); + return false; + } else { + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_relative_humidity = true; + measurement->variant.air_quality_metrics.has_co2 = true; + measurement->variant.environment_metrics.temperature = temperature; + measurement->variant.environment_metrics.relative_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..26141f57e --- /dev/null +++ b/src/modules/Telemetry/Sensor/SCD4XSensor.h @@ -0,0 +1,23 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +class SCD4XSensor : public TelemetrySensor +{ + private: + SensirionI2CScd4x scd4x = SensirionI2CScd4x(); + + 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