This commit is contained in:
oscgonfer 2025-07-21 14:09:58 +02:00 committed by GitHub
commit 686748a92b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 749 additions and 137 deletions

View File

@ -202,3 +202,5 @@ lib_deps =
sensirion/Sensirion Core@0.7.1 sensirion/Sensirion Core@0.7.1
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x # renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
sensirion/Sensirion I2C SCD4x@1.1.0 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

View File

@ -195,6 +195,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define LTR390UV_ADDR 0x53 #define LTR390UV_ADDR 0x53
#define XPOWERS_AXP192_AXP2101_ADDRESS 0x34 // same adress as TCA8418 #define XPOWERS_AXP192_AXP2101_ADDRESS 0x34 // same adress as TCA8418
#define PCT2075_ADDR 0x37 #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 // ACCELEROMETER

View File

@ -75,6 +75,8 @@ class ScanI2C
TCA8418KB, TCA8418KB,
PCT2075, PCT2075,
BMM150, BMM150,
ADS1X15,
ADS1X15_ALT,
} DeviceType; } DeviceType;
// typedef uint8_t DeviceAddress; // typedef uint8_t DeviceAddress;

View File

@ -109,6 +109,75 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation
return value; 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, ...) \ #define SCAN_SIMPLE_CASE(ADDR, T, ...) \
case ADDR: \ case ADDR: \
logFoundDevice(__VA_ARGS__); \ logFoundDevice(__VA_ARGS__); \
@ -495,7 +564,16 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
} }
break; 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); i2cBus->beginTransmission(addr.address);
uint8_t getInfo[] = {0x5A, 0xC0, 0x00, 0xFF, 0xFC}; uint8_t getInfo[] = {0x5A, 0xC0, 0x00, 0xFF, 0xFC};
uint8_t expectedInfo[] = {0xa5, 0xE0, 0x00, 0x3F, 0x19}; uint8_t expectedInfo[] = {0xa5, 0xE0, 0x00, 0x3F, 0x19};
@ -515,6 +593,17 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
break; 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: default:
LOG_INFO("Device found at address 0x%x was not able to be enumerated", (uint8_t)addr.address); LOG_INFO("Device found at address 0x%x was not able to be enumerated", (uint8_t)addr.address);
} }

View File

@ -29,6 +29,9 @@ class ScanI2CTwoWire : public ScanI2C
size_t countDevices() const override; size_t countDevices() const override;
bool setClockSpeed(ScanI2C::I2CPort, uint32_t);
uint32_t getClockSpeed(ScanI2C::I2CPort);
protected: protected:
FoundDevice firstOfOrNONE(size_t, DeviceType[]) const override; FoundDevice firstOfOrNONE(size_t, DeviceType[]) const override;

View File

@ -472,6 +472,7 @@ void setup()
Wire.setSCL(I2C_SCL); Wire.setSCL(I2C_SCL);
Wire.begin(); Wire.begin();
#elif defined(I2C_SDA) && !defined(ARCH_RP2040) #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); Wire.begin(I2C_SDA, I2C_SCL);
#elif defined(ARCH_PORTDUINO) #elif defined(ARCH_PORTDUINO)
if (settingsStrings[i2cdev] != "") { if (settingsStrings[i2cdev] != "") {
@ -530,6 +531,26 @@ void setup()
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE); i2cScanner->scanPort(ScanI2C::I2CPort::WIRE);
#endif #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(); auto i2cCount = i2cScanner->countDevices();
if (i2cCount == 0) { if (i2cCount == 0) {
LOG_INFO("No I2C devices found"); LOG_INFO("No I2C devices found");
@ -682,7 +703,8 @@ void setup()
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::RAK12035, meshtastic_TelemetrySensorType_RAK12035); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::RAK12035, meshtastic_TelemetrySensorType_RAK12035);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::PCT2075, meshtastic_TelemetrySensorType_PCT2075); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::PCT2075, meshtastic_TelemetrySensorType_PCT2075);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X); 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(); i2cScanner.reset();
#endif #endif

View File

@ -221,11 +221,7 @@ void setupModules()
// TODO: How to improve this? // TODO: How to improve this?
#if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
new EnvironmentTelemetryModule(); new EnvironmentTelemetryModule();
#if __has_include("Adafruit_PM25AQI.h") new AirQualityTelemetryModule();
if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) {
new AirQualityTelemetryModule();
}
#endif
#if !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY #if !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY
if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX30102].first > 0 || if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX30102].first > 0 ||
nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MLX90614].first > 0) { nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MLX90614].first > 0) {

View File

@ -1,36 +1,53 @@
#include "configuration.h" #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 "../mesh/generated/meshtastic/telemetry.pb.h"
#include "AirQualityTelemetry.h"
#include "Default.h" #include "Default.h"
#include "AirQualityTelemetry.h"
#include "MeshService.h" #include "MeshService.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "PowerFSM.h" #include "PowerFSM.h"
#include "RTC.h" #include "RTC.h"
#include "Router.h" #include "Router.h"
#include "detect/ScanI2CTwoWire.h" #include "UnitConversions.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/images.h"
#include "main.h" #include "main.h"
#include "sleep.h"
#include <Throttle.h> #include <Throttle.h>
#ifndef PMSA003I_WARMUP_MS #if __has_include(<Adafruit_PM25AQI.h>)
// from the PMSA003I datasheet: #include "Sensor/PMSA003ISensor.h"
// "Stable data should be got at least 30 seconds after the sensor wakeup PMSA003ISensor pmsa003iSensor;
// from the sleep mode because of the fans performance." #else
#define PMSA003I_WARMUP_MS 30000 NullSensor pmsa003iSensor;
#endif #endif
int32_t AirQualityTelemetryModule::runOnce() 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 Uncomment the preferences below if you want to use the module
without having to configure it from the PythonAPI or WebUI. without having to configure it from the PythonAPI or WebUI.
*/ */
// moduleConfig.telemetry.air_quality_enabled = 1; // 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 // 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(); return disable();
} }
@ -42,79 +59,141 @@ int32_t AirQualityTelemetryModule::runOnce()
if (moduleConfig.telemetry.air_quality_enabled) { if (moduleConfig.telemetry.air_quality_enabled) {
LOG_INFO("Air quality Telemetry: init"); LOG_INFO("Air quality Telemetry: init");
#ifdef PMSA003I_ENABLE_PIN if (pmsa003iSensor.hasSensor())
// put the sensor to sleep on startup result = pmsa003iSensor.runOnce();
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<ScanI2CTwoWire>(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();
} }
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 { } else {
// if we somehow got to a second run of this module with measurement disabled, then just wait forever // if we somehow got to a second run of this module with measurement disabled, then just wait forever
if (!moduleConfig.telemetry.air_quality_enabled) if (!moduleConfig.telemetry.air_quality_enabled && !AIR_QUALITY_TELEMETRY_MODULE_ENABLE) {
return disable();
switch (state) {
#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:
return disable(); 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<String> 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) bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m)
{ {
if (!aqi.read(&data)) { bool valid = true;
LOG_WARN("Skip send measurements. Could not read AQIn"); bool hasSensor = false;
return false;
}
m->time = getTime(); m->time = getTime();
m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag; m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag;
m->variant.air_quality_metrics.has_pm10_standard = true; m->variant.air_quality_metrics = meshtastic_AirQualityMetrics_init_zero;
m->variant.air_quality_metrics.pm10_standard = data.pm10_standard;
m->variant.air_quality_metrics.has_pm25_standard = true;
m->variant.air_quality_metrics.pm25_standard = data.pm25_standard;
m->variant.air_quality_metrics.has_pm100_standard = true;
m->variant.air_quality_metrics.pm100_standard = data.pm100_standard;
m->variant.air_quality_metrics.has_pm10_environmental = true; if (pmsa003iSensor.hasSensor()) {
m->variant.air_quality_metrics.pm10_environmental = data.pm10_env; // TODO - Should we check for sensor state here?
m->variant.air_quality_metrics.has_pm25_environmental = true; // If a sensor is sleeping, we should know and check to wake it up
m->variant.air_quality_metrics.pm25_environmental = data.pm25_env; valid = valid && pmsa003iSensor.getMetrics(m);
m->variant.air_quality_metrics.has_pm100_environmental = true; hasSensor = true;
m->variant.air_quality_metrics.pm100_environmental = data.pm100_env; }
LOG_INFO("Send: PM1.0(Standard)=%i, PM2.5(Standard)=%i, PM10.0(Standard)=%i", m->variant.air_quality_metrics.pm10_standard, return valid && hasSensor;
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;
} }
meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply() meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply()
@ -206,7 +270,14 @@ meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply()
bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
{ {
meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag;
m.time = getTime();
if (getAirQualityTelemetry(&m)) { 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); meshtastic_MeshPacket *p = allocDataProtobuf(m);
p->to = dest; p->to = dest;
p->decoded.want_response = false; p->decoded.want_response = false;
@ -221,16 +292,46 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
lastMeasurementPacket = packetPool.allocCopy(*p); lastMeasurementPacket = packetPool.allocCopy(*p);
if (phoneOnly) { if (phoneOnly) {
LOG_INFO("Send packet to phone"); LOG_INFO("Sending packet to phone");
service->sendToPhone(p); service->sendToPhone(p);
} else { } else {
LOG_INFO("Send packet to mesh"); LOG_INFO("Sending packet to mesh");
service->sendToMesh(p, RX_SRC_LOCAL, true); 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 true;
} }
return false; 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 #endif

View File

@ -1,12 +1,18 @@
#include "configuration.h" #include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h") #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#pragma once #pragma once
#ifndef AIR_QUALITY_TELEMETRY_MODULE_ENABLE
#define AIR_QUALITY_TELEMETRY_MODULE_ENABLE 0
#endif
#include "../mesh/generated/meshtastic/telemetry.pb.h" #include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "Adafruit_PM25AQI.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "ProtobufModule.h" #include "ProtobufModule.h"
#include <OLEDDisplay.h>
#include <OLEDDisplayUi.h>
class AirQualityTelemetryModule : private concurrency::OSThread, public ProtobufModule<meshtastic_Telemetry> class AirQualityTelemetryModule : private concurrency::OSThread, public ProtobufModule<meshtastic_Telemetry>
{ {
@ -20,18 +26,15 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public Protobuf
ProtobufModule("AirQualityTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) ProtobufModule("AirQualityTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg)
{ {
lastMeasurementPacket = nullptr; lastMeasurementPacket = nullptr;
setIntervalFromNow(10 * 1000);
aqi = Adafruit_PM25AQI();
nodeStatusObserver.observe(&nodeStatus->onNewStatus); 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
// a reading
state = State::IDLE;
#else
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: protected:
/** Called to handle a particular incoming message /** 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); bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false);
virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp,
meshtastic_AdminMessage *request,
meshtastic_AdminMessage *response) override;
private: private:
enum State {
IDLE = 0,
ACTIVE = 1,
};
State state;
Adafruit_PM25AQI aqi;
PM25_AQI_Data data = {0};
bool firstTime = true; bool firstTime = true;
meshtastic_MeshPacket *lastMeasurementPacket; meshtastic_MeshPacket *lastMeasurementPacket;
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
uint32_t lastSentToMesh = 0; uint32_t lastSentToMesh = 0;
uint32_t lastSentToPhone = 0;
}; };
#endif #endif

View File

@ -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, LOG_INFO("Send: soil_temperature=%f, soil_moisture=%u", m.variant.environment_metrics.soil_temperature,
m.variant.environment_metrics.soil_moisture); m.variant.environment_metrics.soil_moisture);
sensor_read_error_count = 0;
meshtastic_MeshPacket *p = allocDataProtobuf(m); meshtastic_MeshPacket *p = allocDataProtobuf(m);
p->to = dest; p->to = dest;
p->decoded.want_response = false; p->decoded.want_response = false;

View File

@ -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 sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
uint32_t lastSentToMesh = 0; uint32_t lastSentToMesh = 0;
uint32_t lastSentToPhone = 0; uint32_t lastSentToPhone = 0;
uint32_t sensor_read_error_count = 0;
}; };
#endif #endif

View File

@ -22,6 +22,23 @@
#include "graphics/ScreenFonts.h" #include "graphics/ScreenFonts.h"
#include <Throttle.h> #include <Throttle.h>
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(<Adafruit_ADS1X15.h>)
#include "Sensor/ADS1X15Sensor.h"
ADS1X15Sensor ads1x15Sensor;
ADS1X15Sensor ads1x15Sensor_alt(meshtastic_TelemetrySensorType_ADS1X15_ALT);
#else
NullSensor ads1x15Sensor;
NullSensor ads1x15Sensor_alt;
#endif
namespace graphics namespace graphics
{ {
extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only); 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(); result = ina3221Sensor.isInitialized() ? 0 : ina3221Sensor.runOnce();
if (max17048Sensor.hasSensor()) if (max17048Sensor.hasSensor())
result = max17048Sensor.isInitialized() ? 0 : max17048Sensor.runOnce(); 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. // 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); valid = ina3221Sensor.getMetrics(m);
if (max17048Sensor.hasSensor()) if (max17048Sensor.hasSensor())
valid = max17048Sensor.getMetrics(m); valid = max17048Sensor.getMetrics(m);
if (ads1x15Sensor.hasSensor())
valid = ads1x15Sensor.getMetrics(m);
if (ads1x15Sensor_alt.hasSensor())
valid = ads1x15Sensor_alt.getMetrics(m);
#endif #endif
return valid; return valid;
@ -245,10 +270,17 @@ bool PowerTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
m.which_variant = meshtastic_Telemetry_power_metrics_tag; m.which_variant = meshtastic_Telemetry_power_metrics_tag;
m.time = getTime(); m.time = getTime();
if (getPowerTelemetry(&m)) { 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, " 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.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; sensor_read_error_count = 0;

View File

@ -0,0 +1,177 @@
#include "configuration.h"
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_ADS1X15.h>)
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "ADS1X15Sensor.h"
#include "TelemetrySensor.h"
#include <Adafruit_ADS1X15.h>
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

View File

@ -0,0 +1,42 @@
#include "configuration.h"
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_ADS1X15.h>)
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "TelemetrySensor.h"
#include <Adafruit_ADS1X15.h>
#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

View File

@ -0,0 +1,97 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_PM25AQI.h>)
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "PMSA003ISensor.h"
#include "TelemetrySensor.h"
#include "detect/ScanI2CTwoWire.h"
#include <Adafruit_PM25AQI.h>
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<ScanI2CTwoWire>(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

View File

@ -0,0 +1,49 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_PM25AQI.h>)
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "TelemetrySensor.h"
#include "detect/ScanI2CTwoWire.h"
#include <Adafruit_PM25AQI.h>
#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 fans 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