This commit is contained in:
oscgonfer 2025-07-29 12:34:21 +00:00 committed by GitHub
commit 09a9c67257
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1168 additions and 133 deletions

View File

@ -133,8 +133,6 @@ lib_deps =
adafruit/Adafruit INA260 Library@1.5.3 adafruit/Adafruit INA260 Library@1.5.3
# renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219 # renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219
adafruit/Adafruit INA219@1.2.3 adafruit/Adafruit INA219@1.2.3
# renovate: datasource=custom.pio depName=Adafruit PM25 AQI Sensor packageName=adafruit/library/Adafruit PM25 AQI Sensor
adafruit/Adafruit PM25 AQI Sensor@2.0.0
# renovate: datasource=custom.pio depName=Adafruit MPU6050 packageName=adafruit/library/Adafruit MPU6050 # renovate: datasource=custom.pio depName=Adafruit MPU6050 packageName=adafruit/library/Adafruit MPU6050
adafruit/Adafruit MPU6050@2.2.6 adafruit/Adafruit MPU6050@2.2.6
# renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH # renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH

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__); \

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

@ -480,6 +480,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] != "") {
@ -523,6 +524,39 @@ void setup()
LOG_INFO("Scan for i2c devices"); LOG_INFO("Scan for i2c devices");
#endif #endif
// Scan I2C port at desired speed
#ifdef SCAN_I2C_CLOCK_SPEED
uint32_t currentClock;
currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE);
LOG_INFO("Clock speed: %uHz on WIRE", currentClock);
LOG_DEBUG("Setting Wire with defined clock speed, %uHz...", SCAN_I2C_CLOCK_SPEED);
if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE, SCAN_I2C_CLOCK_SPEED)) {
LOG_ERROR("Unable to set clock speed on WIRE");
} else {
currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE);
LOG_INFO("Set clock speed: %uHz on WIRE", currentClock);
}
// TODO Check if necessary
// LOG_DEBUG("Starting Wire with defined clock speed, %d...", SCAN_I2C_CLOCK_SPEED);
// if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE1, SCAN_I2C_CLOCK_SPEED)) {
// LOG_ERROR("Unable to set clock speed on WIRE1");
// } else {
// LOG_INFO("Set clock speed: %d on WIRE1", SCAN_I2C_CLOCK_SPEED);
// }
// Restore clock speed
if (currentClock != SCAN_I2C_CLOCK_SPEED) {
if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE, currentClock)) {
LOG_ERROR("Unable to restore clock speed on WIRE");
} else {
currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE);
LOG_INFO("Set clock speed restored to: %uHz on WIRE", currentClock);
}
}
#endif
#if defined(I2C_SDA1) || (defined(NRF52840_XXAA) && (WIRE_INTERFACES_COUNT == 2)) #if defined(I2C_SDA1) || (defined(NRF52840_XXAA) && (WIRE_INTERFACES_COUNT == 2))
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1); i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1);
#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")
if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) {
new AirQualityTelemetryModule(); 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,61 @@
#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>
// Sensor includes
#include "Sensor/PMSA003ISensor.h"
#ifndef PMSA003I_WARMUP_MS // Sensors
// from the PMSA003I datasheet: PMSA003ISensor pmsa003iSensor;
// "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 #if __has_include(<SensirionI2cScd4x.h>)
#include "Sensor/SCD4XSensor.h"
SCD4XSensor scd4xSensor;
#else
NullSensor scd4xSensor;
#endif #endif
#include "graphics/ScreenFonts.h"
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,54 +67,27 @@ 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()) { if (scd4xSensor.hasSensor())
#ifndef I2C_NO_RESCAN result = scd4xSensor.runOnce();
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(); // 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 setStartDelay(); return result == UINT32_MAX ? disable() : setStartDelay();
}
return disable();
} 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(); return disable();
}
switch (state) { // Wake up the sensors that need it
#ifdef PMSA003I_ENABLE_PIN #ifdef PMSA003I_ENABLE_PIN
case State::IDLE: if (pmsa003iSensor.hasSensor() && !pmsa003iSensor.isActive())
// sensor is in standby; fire it up and sleep return pmsa003iSensor.wakeUp();
LOG_DEBUG("runOnce(): state = idle");
digitalWrite(PMSA003I_ENABLE_PIN, HIGH);
state = State::ACTIVE;
return PMSA003I_WARMUP_MS;
#endif /* PMSA003I_ENABLE_PIN */ #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) || if (((lastSentToMesh == 0) ||
!Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled(
@ -99,22 +97,117 @@ int32_t AirQualityTelemetryModule::runOnce()
airTime->isTxAllowedAirUtil()) { airTime->isTxAllowedAirUtil()) {
sendTelemetry(); sendTelemetry();
lastSentToMesh = millis(); lastSentToMesh = millis();
} else if (service->isToPhoneQueueEmpty()) { } 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 // Just send to phone when it's not our time to send to mesh yet
// Only send while queue is empty (phone assumed connected) // Only send while queue is empty (phone assumed connected)
sendTelemetry(NODENUM_BROADCAST, true); sendTelemetry(NODENUM_BROADCAST, true);
lastSentToPhone = millis();
} }
#ifdef PMSA003I_ENABLE_PIN #ifdef PMSA003I_ENABLE_PIN
// put sensor back to sleep pmsa003iSensor.sleep();
digitalWrite(PMSA003I_ENABLE_PIN, LOW);
state = State::IDLE;
#endif /* PMSA003I_ENABLE_PIN */ #endif /* PMSA003I_ENABLE_PIN */
return sendToPhoneIntervalMs;
default:
return disable();
} }
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 || m.has_co2;
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) + "ug/m3");
if (m.has_pm25_standard)
entries.push_back("PM2.5: " + String(m.pm25_standard) + "ug/m3");
if (m.has_pm100_standard)
entries.push_back("PM10.0: " + String(m.pm100_standard) + "ug/m3");
if (m.has_co2)
entries.push_back("CO2: " + String(m.co2) + "ppm");
// === 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;
} }
} }
@ -131,6 +224,10 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack
LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i", LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i",
t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental, t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental,
t->variant.air_quality_metrics.pm100_environmental); t->variant.air_quality_metrics.pm100_environmental);
LOG_INFO(" | CO2=%i, CO2_T=%f, CO2_H=%f",
t->variant.air_quality_metrics.co2, t->variant.air_quality_metrics.co2_temperature,
t->variant.air_quality_metrics.co2_humidity);
#endif #endif
// release previous packet before occupying a new spot // release previous packet before occupying a new spot
if (lastMeasurementPacket != nullptr) if (lastMeasurementPacket != nullptr)
@ -144,35 +241,25 @@ 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, if (scd4xSensor.hasSensor()) {
m->variant.air_quality_metrics.pm25_standard, m->variant.air_quality_metrics.pm100_standard); valid = valid && scd4xSensor.getMetrics(m);
hasSensor = true;
}
LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i", return valid && hasSensor;
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 +293,29 @@ 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)) {
bool hasAnyPM = m.variant.air_quality_metrics.has_pm10_standard || m.variant.air_quality_metrics.has_pm25_standard || m.variant.air_quality_metrics.has_pm100_standard || m.variant.air_quality_metrics.has_pm10_environmental || m.variant.air_quality_metrics.has_pm25_environmental ||
m.variant.air_quality_metrics.has_pm100_environmental;
if (hasAnyPM) {
LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u, \
pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u", \
m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard, \
m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.pm10_environmental, \
m.variant.air_quality_metrics.pm25_environmental, m.variant.air_quality_metrics.pm100_environmental);
}
bool hasAnyCO2 = m.variant.air_quality_metrics.has_co2 || m.variant.air_quality_metrics.has_co2_temperature || m.variant.air_quality_metrics.has_co2_humidity;
if (hasAnyCO2) {
LOG_INFO("Send: co2=%i, co2_t=%f, co2_rh=%f",
m.variant.air_quality_metrics.co2, m.variant.air_quality_metrics.co2_temperature,
m.variant.air_quality_metrics.co2_humidity);
}
meshtastic_MeshPacket *p = allocDataProtobuf(m); meshtastic_MeshPacket *p = allocDataProtobuf(m);
p->to = dest; p->to = dest;
p->decoded.want_response = false; p->decoded.want_response = false;
@ -221,16 +330,54 @@ 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()) {
// TODO - Potentially implement an admin message to choose between pm_standard
// and pm_environmental. This could be configurable as it doesn't make sense so
// have both
result = pmsa003iSensor.handleAdminMessage(mp, request, response);
if (result != AdminMessageHandleResult::NOT_HANDLED)
return result;
}
if (scd4xSensor.hasSensor()) {
result = scd4xSensor.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

@ -0,0 +1,175 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "PMSA003ISensor.h"
#include "TelemetrySensor.h"
#include <Wire.h>
PMSA003ISensor::PMSA003ISensor()
: TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I")
{
}
void PMSA003ISensor::setup()
{
#ifdef PMSA003I_ENABLE_PIN
pinMode(PMSA003I_ENABLE_PIN, OUTPUT);
#endif
}
bool PMSA003ISensor::restoreClock(uint32_t currentClock){
#ifdef PMSA003I_I2C_CLOCK_SPEED
if (currentClock != PMSA003I_I2C_CLOCK_SPEED){
// LOG_DEBUG("Restoring I2C clock to %uHz", currentClock);
return bus->setClock(currentClock);
}
return true;
#endif
}
int32_t PMSA003ISensor::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 PMSA003I_I2C_CLOCK_SPEED
uint32_t currentClock;
currentClock = bus->getClock();
if (currentClock != PMSA003I_I2C_CLOCK_SPEED){
// LOG_DEBUG("Changing I2C clock to %u", PMSA003I_I2C_CLOCK_SPEED);
bus->setClock(PMSA003I_I2C_CLOCK_SPEED);
}
#endif
bus->beginTransmission(address);
if (bus->endTransmission() != 0) {
LOG_WARN("PMSA003I not found on I2C at 0x12");
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
}
restoreClock(currentClock);
status = 1;
LOG_INFO("PMSA003I Enabled");
return initI2CSensor();
}
bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement)
{
if(!isActive()){
LOG_WARN("PMSA003I is not active");
return false;
}
#ifdef PMSA003I_I2C_CLOCK_SPEED
uint32_t currentClock;
currentClock = bus->getClock();
if (currentClock != PMSA003I_I2C_CLOCK_SPEED){
// LOG_DEBUG("Changing I2C clock to %u", PMSA003I_I2C_CLOCK_SPEED);
bus->setClock(PMSA003I_I2C_CLOCK_SPEED);
}
#endif
bus->requestFrom(address, PMSA003I_FRAME_LENGTH);
if (bus->available() < PMSA003I_FRAME_LENGTH) {
LOG_WARN("PMSA003I read failed: incomplete data (%d bytes)", bus->available());
return false;
}
restoreClock(currentClock);
for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) {
buffer[i] = bus->read();
}
if (buffer[0] != 0x42 || buffer[1] != 0x4D) {
LOG_WARN("PMSA003I frame header invalid: 0x%02X 0x%02X", buffer[0], buffer[1]);
return false;
}
auto read16 = [](uint8_t *data, uint8_t idx) -> uint16_t {
return (data[idx] << 8) | data[idx + 1];
};
computedChecksum = 0;
for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH - 2; i++) {
computedChecksum += buffer[i];
}
receivedChecksum = read16(buffer, PMSA003I_FRAME_LENGTH - 2);
if (computedChecksum != receivedChecksum) {
LOG_WARN("PMSA003I checksum failed: computed 0x%04X, received 0x%04X", computedChecksum, receivedChecksum);
return false;
}
measurement->variant.air_quality_metrics.has_pm10_standard = true;
measurement->variant.air_quality_metrics.pm10_standard = read16(buffer, 4);
measurement->variant.air_quality_metrics.has_pm25_standard = true;
measurement->variant.air_quality_metrics.pm25_standard = read16(buffer, 6);
measurement->variant.air_quality_metrics.has_pm100_standard = true;
measurement->variant.air_quality_metrics.pm100_standard = read16(buffer, 8);
measurement->variant.air_quality_metrics.has_pm10_environmental = true;
measurement->variant.air_quality_metrics.pm10_environmental = read16(buffer, 10);
measurement->variant.air_quality_metrics.has_pm25_environmental = true;
measurement->variant.air_quality_metrics.pm25_environmental = read16(buffer, 12);
measurement->variant.air_quality_metrics.has_pm100_environmental = true;
measurement->variant.air_quality_metrics.pm100_environmental = read16(buffer, 14);
measurement->variant.air_quality_metrics.has_particles_03um = true;
measurement->variant.air_quality_metrics.particles_03um = read16(buffer, 16);
measurement->variant.air_quality_metrics.has_particles_05um = true;
measurement->variant.air_quality_metrics.particles_05um = read16(buffer, 18);
measurement->variant.air_quality_metrics.has_particles_10um = true;
measurement->variant.air_quality_metrics.particles_10um = read16(buffer, 20);
measurement->variant.air_quality_metrics.has_particles_25um = true;
measurement->variant.air_quality_metrics.particles_25um = read16(buffer, 22);
measurement->variant.air_quality_metrics.has_particles_50um = true;
measurement->variant.air_quality_metrics.particles_50um = read16(buffer, 24);
measurement->variant.air_quality_metrics.has_particles_100um = true;
measurement->variant.air_quality_metrics.particles_100um = read16(buffer, 26);
return true;
}
bool PMSA003ISensor::isActive()
{
return state == State::ACTIVE;
}
#ifdef PMSA003I_ENABLE_PIN
void PMSA003ISensor::sleep()
{
digitalWrite(PMSA003I_ENABLE_PIN, LOW);
state = State::IDLE;
}
uint32_t PMSA003ISensor::wakeUp()
{
digitalWrite(PMSA003I_ENABLE_PIN, HIGH);
state = State::ACTIVE;
return PMSA003I_WARMUP_MS;
}
#endif
#endif

View File

@ -0,0 +1,35 @@
#pragma once
#include "TelemetrySensor.h"
#define PMSA003I_I2C_CLOCK_SPEED 100000
#define PMSA003I_FRAME_LENGTH 32
#define PMSA003I_WARMUP_MS 30000
class PMSA003ISensor : public TelemetrySensor
{
public:
PMSA003ISensor();
virtual void setup() override;
virtual int32_t runOnce() override;
virtual bool restoreClock(uint32_t currentClock);
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
virtual bool isActive();
#ifdef PMSA003I_ENABLE_PIN
void sleep();
uint32_t wakeUp();
#endif
private:
enum class State { IDLE, ACTIVE };
State state = State::ACTIVE;
TwoWire * bus;
uint8_t address;
uint16_t computedChecksum = 0;
uint16_t receivedChecksum = 0;
uint8_t buffer[PMSA003I_FRAME_LENGTH];
};

View File

@ -0,0 +1,539 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<SensirionI2cScd4x.h>)
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "SCD4XSensor.h"
#include "TelemetrySensor.h"
#include <SensirionI2cScd4x.h>
#define SCD4X_NO_ERROR 0
SCD4XSensor::SCD4XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SCD4X, "SCD4X") {}
bool SCD4XSensor::restoreClock(uint32_t currentClock){
#ifdef SCD4X_I2C_CLOCK_SPEED
if (currentClock != SCD4X_I2C_CLOCK_SPEED){
// LOG_DEBUG("Restoring I2C clock to %uHz", currentClock);
return bus->setClock(currentClock);
}
return true;
#endif
}
int32_t SCD4XSensor::runOnce()
{
LOG_INFO("Init sensor: %s", sensorName);
if (!hasSensor()) {
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
}
uint16_t error;
bus = nodeTelemetrySensorsMap[sensorType].second;
address = (uint8_t)nodeTelemetrySensorsMap[sensorType].first;
#ifdef SCD4X_I2C_CLOCK_SPEED
uint32_t currentClock;
currentClock = bus->getClock();
if (currentClock != SCD4X_I2C_CLOCK_SPEED){
// LOG_DEBUG("Changing I2C clock to %u", SCD4X_I2C_CLOCK_SPEED);
bus->setClock(SCD4X_I2C_CLOCK_SPEED);
}
#endif
// FIXME - This should be based on bus and address from above
scd4x.begin(*nodeTelemetrySensorsMap[sensorType].second,
(uint8_t)nodeTelemetrySensorsMap[sensorType].first);
delay(30);
// Ensure sensor is in clean state
error = scd4x.wakeUp();
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("SCD4X: Error trying to execute wakeUp()");
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
}
// Stop periodic measurement
error = scd4x.stopPeriodicMeasurement();
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("SCD4X: Error trying to stopPeriodicMeasurement()");
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
}
if (!getASC(ascActive)){
LOG_ERROR("SCD4X: Unable to check if ASC is enabled");
return false;
}
if (!ascActive){
LOG_INFO("SCD4X: ASC is not active");
} else {
LOG_INFO("SCD4X: ASC is active");
}
if (!scd4x.startLowPowerPeriodicMeasurement()) {
status = 1;
} else {
status = 0;
}
restoreClock(currentClock);
return initI2CSensor();
}
void SCD4XSensor::setup() {}
bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement)
{
uint16_t co2, error;
float temperature;
float humidity;
#ifdef SCD4X_I2C_CLOCK_SPEED
uint32_t currentClock;
currentClock = bus->getClock();
if (currentClock != SCD4X_I2C_CLOCK_SPEED){
// LOG_DEBUG("Changing I2C clock to %u", SCD4X_I2C_CLOCK_SPEED);
bus->setClock(SCD4X_I2C_CLOCK_SPEED);
}
#endif
error = scd4x.readMeasurement(co2, temperature, humidity);
restoreClock(currentClock);
LOG_DEBUG("SCD4X: Error while getting measurements: %u", error);
LOG_DEBUG("SCD4X readings: %u, %.2f, %.2f", co2, temperature, humidity);
if (error != SCD4X_NO_ERROR || co2 == 0) {
LOG_ERROR("SCD4X: Skipping invalid measurement.");
return false;
} else {
measurement->variant.air_quality_metrics.has_co2_temperature = true;
measurement->variant.air_quality_metrics.has_co2_humidity = true;
measurement->variant.air_quality_metrics.has_co2 = true;
measurement->variant.air_quality_metrics.co2_temperature = temperature;
measurement->variant.air_quality_metrics.co2_humidity = humidity;
measurement->variant.air_quality_metrics.co2 = co2;
return true;
}
}
/**
* @brief Perform a forced recalibration (FRC) of the CO concentration.
*
* From Sensirion SCD4X I2C Library
*
* 1. Operate the SCD4x in the operation mode later used for normal sensor
* operation (e.g. periodic measurement) for at least 3 minutes in an
* environment with a homogenous and constant CO2 concentration. The sensor
* must be operated at the voltage desired for the application when
* performing the FRC sequence. 2. Issue the stop_periodic_measurement
* command. 3. Issue the perform_forced_recalibration command.
*/
bool SCD4XSensor::performFRC(uint32_t targetCO2) {
uint16_t error;
uint16_t frcCorr;
LOG_INFO("SCD4X: Issuing FRC. Ensure device has been working at least 3 minutes in stable target environment");
error = scd4x.stopPeriodicMeasurement();
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X.");
return false;
}
error = scd4x.performForcedRecalibration((uint16_t)targetCO2, frcCorr);
if (error != SCD4X_NO_ERROR){
LOG_ERROR("SCD4X: Unable to perform forced recalibration.");
return false;
}
if (frcCorr == 0xFFFF) {
LOG_ERROR("SCD4X: Error while performing forced recalibration.");
return false;
}
return true;
}
/**
* @brief Check the current mode (ASC or FRC)
* From Sensirion SCD4X I2C Library
*/
bool SCD4XSensor::getASC(uint16_t &ascEnabled) {
uint16_t error;
LOG_INFO("SCD4X: Getting ASC");
error = scd4x.stopPeriodicMeasurement();
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X.");
return false;
}
error = scd4x.getAutomaticSelfCalibrationEnabled(ascEnabled);
if (error != SCD4X_NO_ERROR){
LOG_ERROR("SCD4X: Unable to send command.");
return false;
}
return true;
}
/**
* @brief Enable or disable automatic self calibration (ASC).
*
* From Sensirion SCD4X I2C Library
*
* Sets the current state (enabled / disabled) of the ASC. By default, ASC
* is enabled.
*/
bool SCD4XSensor::setASC(bool ascEnabled){
uint16_t error;
if (ascEnabled){
LOG_INFO("SCD4X: Enabling ASC");
} else {
LOG_INFO("SCD4X: Disabling ASC");
}
error = scd4x.stopPeriodicMeasurement();
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X.");
return false;
}
error = scd4x.setAutomaticSelfCalibrationEnabled((uint16_t)ascEnabled);
if (error != SCD4X_NO_ERROR){
LOG_ERROR("SCD4X: Unable to send command.");
return false;
}
error = scd4x.persistSettings();
if (error != SCD4X_NO_ERROR){
LOG_ERROR("SCD4X: Unable to make settings persistent.");
return false;
}
if (!getASC(ascActive)){
LOG_ERROR("SCD4X: Unable to check if ASC is enabled");
return false;
}
if (ascActive){
LOG_INFO("SCD4X: ASC is enabled");
} else {
LOG_INFO("SCD4X: ASC is disabled");
}
return true;
}
/**
* @brief Set the value of ASC baseline target in ppm.
*
* From Sensirion SCD4X I2C Library.
*
* Sets the value of the ASC baseline target, i.e. the CO concentration in
* ppm which the ASC algorithm will assume as lower-bound background to
* which the SCD4x is exposed to regularly within one ASC period of
* operation. To save the setting to the EEPROM, the persist_settings
* command must be issued subsequently. The factory default value is 400
* ppm.
*/
bool SCD4XSensor::setASCBaseline(uint32_t targetCO2){
uint16_t error;
LOG_INFO("SCD4X: Setting ASC baseline");
getASC(ascActive);
if (!ascActive){
LOG_ERROR("SCD4X: ASC is not active");
return false;
}
error = scd4x.stopPeriodicMeasurement();
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X.");
return false;
}
error = scd4x.setAutomaticSelfCalibrationTarget((uint16_t)targetCO2);
if (error != SCD4X_NO_ERROR){
LOG_ERROR("SCD4X: Unable to send command.");
return false;
}
error = scd4x.persistSettings();
if (error != SCD4X_NO_ERROR){
LOG_ERROR("SCD4X: Unable to make settings persistent.");
return false;
}
return true;
}
/**
* @brief Set the temperature compensation reference.
*
* From Sensirion SCD4X I2C Library.
*
* Setting the temperature offset of the SCD4x inside the customer device
* allows the user to optimize the RH and T output signal. By default, the temperature offset is set to 4 °C. To save
* the setting to the EEPROM, the persist_settings command may be issued.
* Equation (1) details how the characteristic temperature offset can be
* calculated using the current temperature output of the sensor (TSCD4x), a
* reference temperature value (TReference), and the previous temperature
* offset (Toffset_pervious) obtained using the get_temperature_offset_raw
* command:
*
* Toffset_actual = TSCD4x - TReference + Toffset_pervious.
*
* Recommended temperature offset values are between 0 °C and 20 °C. The
* temperature offset does not impact the accuracy of the CO2 output.
*/
bool SCD4XSensor::setTemperature(float tempReference){
uint16_t error;
float prevTempOffset;
float tempOffset;
uint16_t co2;
float temperature;
float humidity;
LOG_INFO("SCD4X: Setting reference temperature at: %.2f". temperature);
error = scd4x.readMeasurement(co2, temperature, humidity);
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("SCD4X: Unable read current temperature.");
return false;
}
LOG_INFO("SCD4X: Current sensor temperature: %.2f", temperature);
error = scd4x.stopPeriodicMeasurement();
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X.");
return false;
}
error = scd4x.getTemperatureOffset(prevTempOffset);
if (error != SCD4X_NO_ERROR){
LOG_ERROR("SCD4X: Unable to get temperature offset.");
return false;
}
LOG_INFO("SCD4X: Sensor temperature offset: %.2f", prevTempOffset);
tempOffset = temperature - tempReference + prevTempOffset;
LOG_INFO("SCD4X: Setting temperature offset: %.2f", tempOffset);
error = scd4x.setTemperatureOffset(tempOffset);
if (error != SCD4X_NO_ERROR){
LOG_ERROR("SCD4X: Unable to set temperature offset.");
return false;
}
error = scd4x.persistSettings();
if (error != SCD4X_NO_ERROR){
LOG_ERROR("SCD4X: Unable to make settings persistent.");
return false;
}
return true;
}
/**
* @brief Get the sensor altitude.
*
* From Sensirion SCD4X I2C Library.
*
* Altitude in meters above sea level can be set after device installation.
* Valid value between 0 and 3000m. This overrides pressure offset.
*/
bool SCD4XSensor::getAltitude(uint16_t &altitude){
uint16_t error;
LOG_INFO("SCD4X: Requesting sensor altitude");
error = scd4x.stopPeriodicMeasurement();
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X.");
return false;
}
error = scd4x.getSensorAltitude(altitude);
if (error != SCD4X_NO_ERROR){
LOG_ERROR("SCD4X: Unable to get altitude.");
return false;
}
LOG_INFO("SCD4X: Sensor altitude: %u", altitude);
return true;
}
/**
* @brief Set the sensor altitude.
*
* From Sensirion SCD4X I2C Library.
*
* Altitude in meters above sea level can be set after device installation.
* Valid value between 0 and 3000m. This overrides pressure offset.
*/
bool SCD4XSensor::setAltitude(uint32_t altitude){
uint16_t error;
error = scd4x.stopPeriodicMeasurement();
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X.");
return false;
}
error = scd4x.setSensorAltitude(altitude);
if (error != SCD4X_NO_ERROR){
LOG_ERROR("SCD4X: Unable to set altitude.");
return false;
}
error = scd4x.persistSettings();
if (error != SCD4X_NO_ERROR){
LOG_ERROR("SCD4X: Unable to make settings persistent.");
return false;
}
return true;
}
/**
* @brief Set the ambient pressure around the sensor.
*
* From Sensirion SCD4X I2C Library.
*
* The set_ambient_pressure command can be sent during periodic measurements
* to enable continuous pressure compensation. Note that setting an ambient
* pressure overrides any pressure compensation based on a previously set
* sensor altitude. Use of this command is highly recommended for
* applications experiencing significant ambient pressure changes to ensure
* sensor accuracy. Valid input values are between 70000 - 120000 Pa. The
* default value is 101300 Pa.
*/
bool SCD4XSensor::setAmbientPressure(uint32_t ambientPressure) {
uint16_t error;
error = scd4x.setAmbientPressure(ambientPressure);
if (error != SCD4X_NO_ERROR){
LOG_ERROR("SCD4X: Unable to set altitude.");
return false;
}
// Sensirion doesn't indicate if this is necessary. We send it anyway
error = scd4x.persistSettings();
if (error != SCD4X_NO_ERROR){
LOG_ERROR("SCD4X: Unable to make settings persistent.");
return false;
}
return true;
}
/**
* @brief Perform factory reset to erase the settings stored in the EEPROM.
*
* From Sensirion SCD4X I2C Library.
*
* The perform_factory_reset command resets all configuration settings
* stored in the EEPROM and erases the FRC and ASC algorithm history.
*/
bool SCD4XSensor::factoryReset() {
uint16_t error;
error = scd4x.stopPeriodicMeasurement();
if (error != SCD4X_NO_ERROR) {
LOG_ERROR("SCD4X: Unable to set idle mode on SCD4X.");
return false;
}
error = scd4x.performFactoryReset();
if (error != SCD4X_NO_ERROR){
LOG_ERROR("SCD4X: Unable to do factory reset.");
return false;
}
return true;
}
AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
meshtastic_AdminMessage *response)
{
AdminMessageHandleResult result;
switch (request->which_payload_variant) {
case meshtastic_AdminMessage_sensor_config_tag:
// Check for ASC-FRC request first
if (!request->sensor_config.has_scdxx_config) {
result = AdminMessageHandleResult::NOT_HANDLED;
break;
}
if (request->sensor_config.scdxx_config.has_factory_reset) {
LOG_DEBUG("SCD4X: Requested factory reset");
this->factoryReset();
} else {
if (request->sensor_config.scdxx_config.has_set_asc) {
this->setASC(request->sensor_config.scdxx_config.set_asc);
if (request->sensor_config.scdxx_config.set_asc == false) {
LOG_DEBUG("SCD4X: Request for FRC");
if (request->sensor_config.scdxx_config.has_target_co2_conc) {
this->performFRC(request->sensor_config.scdxx_config.target_co2_conc);
} else {
// FRC requested but no target CO2 provided
LOG_ERROR("SCD4X: target CO2 not provided");
result = AdminMessageHandleResult::NOT_HANDLED;
break;
}
} else {
LOG_DEBUG("SCD4X: Request for ASC");
if (request->sensor_config.scdxx_config.has_target_co2_conc) {
LOG_DEBUG("SCD4X: Request has target CO2");
this->setASCBaseline(request->sensor_config.scdxx_config.target_co2_conc);
} else {
LOG_DEBUG("SCD4X: Request doesn't have target CO2");
}
}
}
// Check for temperature offset
if (request->sensor_config.scdxx_config.has_temperature) {
this->setTemperature(request->sensor_config.scdxx_config.temperature);
}
// Check for altitude or pressure offset
if (request->sensor_config.scdxx_config.has_altitude) {
this->setAltitude(request->sensor_config.scdxx_config.altitude);
} else if (request->sensor_config.scdxx_config.has_ambient_pressure){
this->setAmbientPressure(request->sensor_config.scdxx_config.ambient_pressure);
}
}
result = AdminMessageHandleResult::HANDLED;
break;
default:
result = AdminMessageHandleResult::NOT_HANDLED;
}
return result;
}
#endif

View File

@ -0,0 +1,43 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<SensirionI2cScd4x.h>)
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "TelemetrySensor.h"
#include <SensirionI2cScd4x.h>
#define SCD4X_I2C_CLOCK_SPEED 100000
class SCD4XSensor : public TelemetrySensor
{
private:
SensirionI2cScd4x scd4x;
TwoWire* bus;
uint8_t address;
bool performFRC(uint32_t targetCO2);
bool setASCBaseline(uint32_t targetCO2);
bool getASC(uint16_t &ascEnabled);
bool setASC(bool ascEnabled);
bool setTemperature(float tempReference);
bool getAltitude(uint16_t &altitude);
bool setAltitude(uint32_t altitude);
bool setAmbientPressure(uint32_t ambientPressure);
bool restoreClock(uint32_t currentClock);
bool factoryReset();
// Parameters
uint16_t ascActive;
protected:
virtual void setup() override;
public:
SCD4XSensor();
virtual int32_t runOnce() override;
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
meshtastic_AdminMessage *response) override;
};
#endif