Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8fa513f
add CO2 sensing with SCD4X
Coloradohusky Aug 31, 2024
30532d4
Merge branch 'master' into scd4x
caveman99 Sep 2, 2024
e73ff62
Merge remote-tracking branch 'upstream/master' into scd4x
Coloradohusky Sep 26, 2024
f599984
update from upstream
Coloradohusky Sep 26, 2024
c9233fe
Merge remote-tracking branch 'upstream/master' into scd4x
Coloradohusky Oct 11, 2024
8a4427a
Merge remote-tracking branch 'upstream/master' into scd4x
Coloradohusky Oct 11, 2024
a28e83c
Merge branch 'master' into scd4x
fifieldt Oct 18, 2024
318da22
Abstract AirQualityTelemetry module
fifieldt Oct 18, 2024
8faf466
Re-add I2C scan, fix frame.
fifieldt Oct 19, 2024
81f9b38
Merge branch 'master' into scd4x
fifieldt Oct 19, 2024
e34aada
Merge branch 'master' into scd4x
thebentern Oct 20, 2024
16e2540
Merge branch 'master' into scd4x
caveman99 Nov 7, 2024
a1cbe8e
Merge branch 'master' into scd4x
fifieldt Nov 19, 2024
3cef1e8
Merge branch 'master' into scd4x
caveman99 Mar 31, 2025
ad7647e
Merge branch 'master' into scd4x
caveman99 Apr 7, 2025
7fe9efe
Wupps
caveman99 Apr 7, 2025
bac816d
Merge branch 'master' into scd4x
caveman99 Apr 11, 2025
726cf86
Merge branch 'master' into scd4x
fifieldt Jul 1, 2025
02a8414
Merge branch 'master' into scd4x
fifieldt Jul 1, 2025
58d916d
Fix merge
fifieldt Jul 1, 2025
ef203b5
Merge branch 'master' into scd4x
fifieldt Jul 1, 2025
cfc355b
Update main.cpp
fifieldt Jul 1, 2025
1eeadde
Merge branch 'master' into scd4x
fifieldt Jul 1, 2025
80175b1
Merge branch 'master' into scd4x
caveman99 Jul 13, 2025
f0775c5
Merge branch 'master' into scd4x
vidplace7 Jul 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 132 additions & 18 deletions src/modules/Telemetry/AirQualityTelemetry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Throttle.h>

// 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
Expand All @@ -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)) {
Expand All @@ -41,33 +59,64 @@ 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<ScanI2CTwoWire>(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;
nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].second =
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();
}
Expand Down Expand Up @@ -115,6 +164,7 @@ int32_t AirQualityTelemetryModule::runOnce()
default:
return disable();
}
return min(sendToPhoneIntervalMs, result);
}
}

Expand All @@ -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,
Expand All @@ -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()
{
return moduleConfig.telemetry.environment_screen_enabled;
}

void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
if (!aqi.read(&data)) {
LOG_WARN("Skip send measurements. Could not read AQIn");
return false;
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;
Expand All @@ -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");
}
}

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 true;
return valid && hasSensor;
}

meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply()
Expand Down Expand Up @@ -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;
Expand Down
14 changes: 11 additions & 3 deletions src/modules/Telemetry/AirQualityTelemetry.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@

#pragma once
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "Adafruit_PM25AQI.h"
#include "NodeDB.h"
#include "ProtobufModule.h"
#include <OLEDDisplay.h>
#include <OLEDDisplayUi.h>

class AirQualityTelemetryModule : private concurrency::OSThread, public ProtobufModule<meshtastic_Telemetry>
{
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions src/modules/Telemetry/EnvironmentTelemetry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
47 changes: 47 additions & 0 deletions src/modules/Telemetry/Sensor/PMSA0031Sensor.cpp
Original file line number Diff line number Diff line change
@@ -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 <Adafruit_PM25AQI.h>

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
24 changes: 24 additions & 0 deletions src/modules/Telemetry/Sensor/PMSA0031Sensor.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#include "configuration.h"

#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR

#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "TelemetrySensor.h"
#include <Adafruit_PM25AQI.h>

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
Loading
Loading