diff --git a/platformio.ini b/platformio.ini
index c0eb6fedb..c01a41b76 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -202,3 +202,5 @@ lib_deps =
sensirion/Sensirion Core@0.7.1
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
sensirion/Sensirion I2C SCD4x@1.1.0
+ # renovate: datasource=custom.pio depName=Adafruit ADS1X15 packageName=adafruit/library/Adafruit ADS1X15 Library
+ adafruit/Adafruit ADS1X15@2.5.0
diff --git a/src/configuration.h b/src/configuration.h
index cddc7ba7a..92df118d7 100644
--- a/src/configuration.h
+++ b/src/configuration.h
@@ -195,6 +195,10 @@ along with this program. If not, see .
#define LTR390UV_ADDR 0x53
#define XPOWERS_AXP192_AXP2101_ADDRESS 0x34 // same adress as TCA8418
#define PCT2075_ADDR 0x37
+#define ADS1X15_ADDR 0x48 // same address as FT6336U
+#define ADS1X15_ADDR_ALT1 0x49
+#define ADS1X15_ADDR_ALT2 0x4A
+#define ADS1X15_ADDR_ALT3 0x4B
// -----------------------------------------------------------------------------
// ACCELEROMETER
diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h
index dd290db98..16383bbd1 100644
--- a/src/detect/ScanI2C.h
+++ b/src/detect/ScanI2C.h
@@ -75,6 +75,8 @@ class ScanI2C
TCA8418KB,
PCT2075,
BMM150,
+ ADS1X15,
+ ADS1X15_ALT,
} DeviceType;
// typedef uint8_t DeviceAddress;
diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp
index 9e9441123..63f50b9a1 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__); \
@@ -495,7 +564,16 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
}
break;
- case 0x48: {
+ case 0x48: { // same as ADS1X15 main address
+
+ // ADS1X15 default config register is 8583h
+ registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x01), 2);
+ if (registerValue == 0x8583) {
+ type = ADS1X15;
+ logFoundDevice("ADS1X15", (uint8_t)addr.address);
+ break;
+ }
+
i2cBus->beginTransmission(addr.address);
uint8_t getInfo[] = {0x5A, 0xC0, 0x00, 0xFF, 0xFC};
uint8_t expectedInfo[] = {0xa5, 0xE0, 0x00, 0x3F, 0x19};
@@ -515,6 +593,17 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
break;
}
+ case ADS1X15_ADDR_ALT1:
+ case ADS1X15_ADDR_ALT2:
+ case ADS1X15_ADDR_ALT3:
+ // ADS1X15 default config register is 8583h
+ registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x01), 2);
+ if (registerValue == 0x8583) {
+ type = ADS1X15_ALT;
+ logFoundDevice("ADS1X15_ALT", (uint8_t)addr.address);
+ break;
+ }
+
default:
LOG_INFO("Device found at address 0x%x was not able to be enumerated", (uint8_t)addr.address);
}
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 2e2adfd46..54baae0db 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -472,6 +472,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] != "") {
@@ -530,6 +531,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");
@@ -682,7 +703,8 @@ void setup()
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::RAK12035, meshtastic_TelemetrySensorType_RAK12035);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::PCT2075, meshtastic_TelemetrySensorType_PCT2075);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X);
-
+ scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ADS1X15, meshtastic_TelemetrySensorType_ADS1X15);
+ scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ADS1X15_ALT, meshtastic_TelemetrySensorType_ADS1X15_ALT);
i2cScanner.reset();
#endif
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) {
diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp
index 2472b95b1..7689802ea 100644
--- a/src/modules/Telemetry/AirQualityTelemetry.cpp
+++ b/src/modules/Telemetry/AirQualityTelemetry.cpp
@@ -1,36 +1,53 @@
#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.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 +59,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.isActive())
+ 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;
}
}
@@ -144,35 +223,20 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack
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 +270,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 +292,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/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp
index a92013d01..9200cf2c7 100644
--- a/src/modules/Telemetry/PowerTelemetry.cpp
+++ b/src/modules/Telemetry/PowerTelemetry.cpp
@@ -22,6 +22,23 @@
#include "graphics/ScreenFonts.h"
#include
+namespace graphics
+{
+extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only);
+}
+
+
+#include "Sensor/nullSensor.h"
+
+#if __has_include()
+#include "Sensor/ADS1X15Sensor.h"
+ADS1X15Sensor ads1x15Sensor;
+ADS1X15Sensor ads1x15Sensor_alt(meshtastic_TelemetrySensorType_ADS1X15_ALT);
+#else
+NullSensor ads1x15Sensor;
+NullSensor ads1x15Sensor_alt;
+#endif
+
namespace graphics
{
extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only);
@@ -74,6 +91,10 @@ int32_t PowerTelemetryModule::runOnce()
result = ina3221Sensor.isInitialized() ? 0 : ina3221Sensor.runOnce();
if (max17048Sensor.hasSensor())
result = max17048Sensor.isInitialized() ? 0 : max17048Sensor.runOnce();
+ if (ads1x15Sensor.hasSensor())
+ result = ads1x15Sensor.isInitialized() ? 0 : ads1x15Sensor.runOnce();
+ if (ads1x15Sensor_alt.hasSensor())
+ result = ads1x15Sensor_alt.isInitialized() ? 0 : ads1x15Sensor_alt.runOnce();
}
// it's possible to have this module enabled, only for displaying values on the screen.
@@ -205,6 +226,10 @@ bool PowerTelemetryModule::getPowerTelemetry(meshtastic_Telemetry *m)
valid = ina3221Sensor.getMetrics(m);
if (max17048Sensor.hasSensor())
valid = max17048Sensor.getMetrics(m);
+ if (ads1x15Sensor.hasSensor())
+ valid = ads1x15Sensor.getMetrics(m);
+ if (ads1x15Sensor_alt.hasSensor())
+ valid = ads1x15Sensor_alt.getMetrics(m);
#endif
return valid;
@@ -245,10 +270,17 @@ bool PowerTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
m.which_variant = meshtastic_Telemetry_power_metrics_tag;
m.time = getTime();
if (getPowerTelemetry(&m)) {
+ // TODO - Consider adding all 8 channels here - seems a bit much?
LOG_INFO("Send: ch1_voltage=%f, ch1_current=%f, ch2_voltage=%f, ch2_current=%f, "
- "ch3_voltage=%f, ch3_current=%f",
+ "ch3_voltage=%f, ch3_current=%f, ch4_voltage=%f",
m.variant.power_metrics.ch1_voltage, m.variant.power_metrics.ch1_current, m.variant.power_metrics.ch2_voltage,
- m.variant.power_metrics.ch2_current, m.variant.power_metrics.ch3_voltage, m.variant.power_metrics.ch3_current);
+ m.variant.power_metrics.ch2_current, m.variant.power_metrics.ch3_voltage, m.variant.power_metrics.ch3_current,
+ m.variant.power_metrics.ch4_voltage);
+ LOG_INFO("Send: ch5_voltage=%f, ch5_current=%f, ch6_voltage=%f, ch6_current=%f, "
+ "ch7_voltage=%f, ch7_current=%f, ch8_voltage=%f",
+ m.variant.power_metrics.ch5_voltage, m.variant.power_metrics.ch5_current, m.variant.power_metrics.ch6_voltage,
+ m.variant.power_metrics.ch6_current, m.variant.power_metrics.ch7_voltage, m.variant.power_metrics.ch7_current,
+ m.variant.power_metrics.ch8_voltage, m.variant.power_metrics.ch8_current);
sensor_read_error_count = 0;
diff --git a/src/modules/Telemetry/Sensor/ADS1X15Sensor.cpp b/src/modules/Telemetry/Sensor/ADS1X15Sensor.cpp
new file mode 100644
index 000000000..3715b2f9b
--- /dev/null
+++ b/src/modules/Telemetry/Sensor/ADS1X15Sensor.cpp
@@ -0,0 +1,177 @@
+#include "configuration.h"
+
+#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include()
+
+#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "ADS1X15Sensor.h"
+#include "TelemetrySensor.h"
+#include
+
+ADS1X15Sensor::ADS1X15Sensor(meshtastic_TelemetrySensorType sensorType) : TelemetrySensor(sensorType, "ADS1X15") {}
+
+int32_t ADS1X15Sensor::runOnce()
+{
+ LOG_INFO("Init sensor: %s", sensorName);
+ if (!hasSensor()) {
+ return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
+ }
+
+ bus = nodeTelemetrySensorsMap[sensorType].second;
+ address = (uint8_t)nodeTelemetrySensorsMap[sensorType].first;
+
+#ifdef ADS1X15_I2C_CLOCK_SPEED
+ uint32_t currentClock;
+ currentClock = bus->getClock();
+ if (currentClock != ADS1X15_I2C_CLOCK_SPEED){
+ // LOG_DEBUG("Changing I2C clock to %u", ADS1X15_I2C_CLOCK_SPEED);
+ bus->setClock(ADS1X15_I2C_CLOCK_SPEED);
+ }
+#endif
+
+ status = ads1x15.begin(address);
+
+#ifdef ADS1X15_I2C_CLOCK_SPEED
+ if (currentClock != ADS1X15_I2C_CLOCK_SPEED){
+ // LOG_DEBUG("Restoring I2C clock to %uHz", currentClock);
+ bus->setClock(currentClock);
+ }
+#endif
+
+ return initI2CSensor();
+}
+
+void ADS1X15Sensor::setup() {}
+
+struct _ADS1X15Measurement ADS1X15Sensor::getMeasurement(uint8_t ch)
+{
+ struct _ADS1X15Measurement measurement;
+
+#ifdef ADS1X15_I2C_CLOCK_SPEED
+ uint32_t currentClock;
+ currentClock = bus->getClock();
+ if (currentClock != ADS1X15_I2C_CLOCK_SPEED){
+ // LOG_DEBUG("Changing I2C clock to %u", ADS1X15_I2C_CLOCK_SPEED);
+ bus->setClock(ADS1X15_I2C_CLOCK_SPEED);
+ }
+#endif
+
+ // Reset gain
+ ads1x15.setGain(GAIN_TWOTHIRDS);
+ double voltage_range = 6.144;
+
+ // Get value with full range
+ uint16_t value = ads1x15.readADC_SingleEnded(ch);
+
+ // Dynamic gain, to increase resolution of low voltage values
+ // If value is under 4.096v increase the gain depending on voltage
+ if (value < 21845) {
+ if (value > 10922) {
+
+ // 1x gain, 4.096V
+ ads1x15.setGain(GAIN_ONE);
+ voltage_range = 4.096;
+
+ } else if (value > 5461) {
+
+ // 2x gain, 2.048V
+ ads1x15.setGain(GAIN_TWO);
+ voltage_range = 2.048;
+
+ } else if (value > 2730) {
+
+ // 4x gain, 1.024V
+ ads1x15.setGain(GAIN_FOUR);
+ voltage_range = 1.024;
+
+ } else if (value > 1365) {
+
+ // 8x gain, 0.25V
+ ads1x15.setGain(GAIN_EIGHT);
+ voltage_range = 0.512;
+
+ } else {
+
+ // 16x gain, 0.125V
+ ads1x15.setGain(GAIN_SIXTEEN);
+ voltage_range = 0.256;
+ }
+
+ // Get the value again
+ value = ads1x15.readADC_SingleEnded(ch);
+ }
+
+#ifdef ADS1X15_I2C_CLOCK_SPEED
+ if (currentClock != ADS1X15_I2C_CLOCK_SPEED){
+ // LOG_DEBUG("Restoring I2C clock to %uHz", currentClock);
+ bus->setClock(currentClock);
+ }
+#endif
+
+ measurement.voltage = (float)value / 32768 * voltage_range;
+
+ return measurement;
+}
+
+struct _ADS1X15Measurements ADS1X15Sensor::getMeasurements()
+{
+ struct _ADS1X15Measurements measurements;
+
+ // ADS1X15 has 4 channels starting from 0
+ for (int i = 0; i < 4; i++) {
+ measurements.measurements[i] = getMeasurement(i);
+ }
+
+ return measurements;
+}
+
+bool ADS1X15Sensor::getMetrics(meshtastic_Telemetry *measurement)
+{
+
+ struct _ADS1X15Measurements m = getMeasurements();
+
+ switch (sensorType)
+ {
+ case meshtastic_TelemetrySensorType_ADS1X15:
+ {
+ measurement->variant.power_metrics.has_ch1_voltage = true;
+ measurement->variant.power_metrics.has_ch2_voltage = true;
+ measurement->variant.power_metrics.has_ch3_voltage = true;
+ measurement->variant.power_metrics.has_ch4_voltage = true;
+
+ measurement->variant.power_metrics.ch1_voltage = m.measurements[0].voltage;
+ measurement->variant.power_metrics.ch2_voltage = m.measurements[1].voltage;
+ measurement->variant.power_metrics.ch3_voltage = m.measurements[2].voltage;
+ measurement->variant.power_metrics.ch4_voltage = m.measurements[3].voltage;
+ break;
+ }
+ case meshtastic_TelemetrySensorType_ADS1X15_ALT:
+ {
+ measurement->variant.power_metrics.has_ch5_voltage = true;
+ measurement->variant.power_metrics.has_ch6_voltage = true;
+ measurement->variant.power_metrics.has_ch7_voltage = true;
+ measurement->variant.power_metrics.has_ch8_voltage = true;
+
+ measurement->variant.power_metrics.ch5_voltage = m.measurements[0].voltage;
+ measurement->variant.power_metrics.ch6_voltage = m.measurements[1].voltage;
+ measurement->variant.power_metrics.ch7_voltage = m.measurements[2].voltage;
+ measurement->variant.power_metrics.ch8_voltage = m.measurements[3].voltage;
+ break;
+ }
+ default:
+ {
+ measurement->variant.power_metrics.has_ch1_voltage = true;
+ measurement->variant.power_metrics.has_ch2_voltage = true;
+ measurement->variant.power_metrics.has_ch3_voltage = true;
+ measurement->variant.power_metrics.has_ch4_voltage = true;
+
+ measurement->variant.power_metrics.ch1_voltage = m.measurements[0].voltage;
+ measurement->variant.power_metrics.ch2_voltage = m.measurements[1].voltage;
+ measurement->variant.power_metrics.ch3_voltage = m.measurements[2].voltage;
+ measurement->variant.power_metrics.ch4_voltage = m.measurements[3].voltage;
+ break;
+ }
+ }
+ return true;
+}
+
+#endif
\ No newline at end of file
diff --git a/src/modules/Telemetry/Sensor/ADS1X15Sensor.h b/src/modules/Telemetry/Sensor/ADS1X15Sensor.h
new file mode 100644
index 000000000..309beb922
--- /dev/null
+++ b/src/modules/Telemetry/Sensor/ADS1X15Sensor.h
@@ -0,0 +1,42 @@
+#include "configuration.h"
+
+#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include()
+
+#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "TelemetrySensor.h"
+#include
+
+#define ADS1X15_I2C_CLOCK_SPEED 100000
+
+class ADS1X15Sensor : public TelemetrySensor
+{
+ private:
+ Adafruit_ADS1X15 ads1x15;
+ TwoWire * bus;
+ uint8_t address;
+
+ // get a single measurement for a channel
+ struct _ADS1X15Measurement getMeasurement(uint8_t ch);
+
+ // get all measurements for all channels
+ struct _ADS1X15Measurements getMeasurements();
+
+ protected:
+ virtual void setup() override;
+
+ public:
+ ADS1X15Sensor(meshtastic_TelemetrySensorType sensorType = meshtastic_TelemetrySensorType_ADS1X15);
+ virtual int32_t runOnce() override;
+ virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
+};
+
+struct _ADS1X15Measurement {
+ float voltage;
+};
+
+struct _ADS1X15Measurements {
+ // ADS1X15 has 4 channels
+ struct _ADS1X15Measurement measurements[4];
+};
+
+#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..2b165cd6d
--- /dev/null
+++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp
@@ -0,0 +1,97 @@
+#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
+ pinMode(PMSA003I_ENABLE_PIN, OUTPUT);
+#endif /* PMSA003I_ENABLE_PIN */
+}
+
+#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 /* 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 AQI");
+ 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..7e460ce33
--- /dev/null
+++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h
@@ -0,0 +1,49 @@
+#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};
+
+ protected:
+ virtual void setup() override;
+
+ public:
+ enum State {
+ IDLE = 0,
+ ACTIVE = 1,
+ };
+
+#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;
+};
+
+#endif
\ No newline at end of file