diff --git a/.github/workflows/models_pr_triage.yml b/.github/workflows/models_pr_triage.yml
index ef303c02a4f..d4c8509d26b 100644
--- a/.github/workflows/models_pr_triage.yml
+++ b/.github/workflows/models_pr_triage.yml
@@ -88,9 +88,10 @@ jobs:
# ─────────────────────────────────────────────────────────────────────────
# Step 3: Auto-label PR type (bugfix/hardware-support/enhancement)
+ # Only skip for spam/ai-generated; still classify needs-review PRs
# ─────────────────────────────────────────────────────────────────────────
- name: Classify PR for labeling
- if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '')
+ if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.quality.outputs.response != 'spam' && steps.quality.outputs.response != 'ai-generated'
uses: actions/ai-inference@v2
id: classify
continue-on-error: true
diff --git a/platformio.ini b/platformio.ini
index 25fc8829fdd..f9a7351c3f1 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -213,6 +213,7 @@ lib_deps =
sensirion/Sensirion Core@0.7.2
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
sensirion/Sensirion I2C SCD4x@1.1.0
+
; Same as environmental_extra but without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
[environmental_extra_no_bsec]
lib_deps =
@@ -239,4 +240,4 @@ lib_deps =
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
sensirion/Sensirion Core@0.7.2
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
- sensirion/Sensirion I2C SCD4x@1.1.0
\ No newline at end of file
+ sensirion/Sensirion I2C SCD4x@1.1.0
diff --git a/src/configuration.h b/src/configuration.h
index f7b438272a4..1a92078e491 100644
--- a/src/configuration.h
+++ b/src/configuration.h
@@ -241,6 +241,7 @@ along with this program. If not, see .
#define BQ27220_ADDR 0x55 // same address as TDECK_KB
#define BQ25896_ADDR 0x6B
#define LTR553ALS_ADDR 0x23
+#define SEN5X_ADDR 0x69
// -----------------------------------------------------------------------------
// ACCELEROMETER
diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp
index 4795d2abc17..bf01a03657d 100644
--- a/src/detect/ScanI2C.cpp
+++ b/src/detect/ScanI2C.cpp
@@ -43,7 +43,7 @@ ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
ScanI2C::FoundDevice ScanI2C::firstAQI() const
{
- ScanI2C::DeviceType types[] = {PMSA003I, SCD4X};
+ ScanI2C::DeviceType types[] = {PMSA003I, SEN5X, SCD4X};
return firstOfOrNONE(2, types);
}
diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h
index dffcd8fb65a..3910ddf64c2 100644
--- a/src/detect/ScanI2C.h
+++ b/src/detect/ScanI2C.h
@@ -88,7 +88,8 @@ class ScanI2C
BH1750,
DA217,
CHSC6X,
- CST226SE
+ CST226SE,
+ SEN5X
} DeviceType;
// typedef uint8_t DeviceAddress;
diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp
index c6ef3484679..f89f2eabcd0 100644
--- a/src/detect/ScanI2CTwoWire.cpp
+++ b/src/detect/ScanI2CTwoWire.cpp
@@ -8,6 +8,7 @@
#endif
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL)
#include "meshUtils.h" // vformat
+
#endif
bool in_array(uint8_t *array, int size, uint8_t lookfor)
@@ -114,6 +115,42 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation
return value;
}
+/// for SEN5X detection
+// Note, this code needs to be called before setting the I2C bus speed
+// for the screen at high speed. The speed needs to be at 100kHz, otherwise
+// detection will not work
+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__); \
@@ -568,8 +605,9 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
}
break;
- case ICM20948_ADDR: // same as BMX160_ADDR
+ case ICM20948_ADDR: // same as BMX160_ADDR and SEN5X_ADDR
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR
+ // ICM20948 Register check
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
#ifdef HAS_ICM20948
type = ICM20948;
@@ -580,14 +618,31 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
type = ICM20948;
logFoundDevice("ICM20948", (uint8_t)addr.address);
break;
- } else if (addr.address == BMX160_ADDR) {
- type = BMX160;
- logFoundDevice("BMX160", (uint8_t)addr.address);
- break;
} else {
- type = MPU6050;
- logFoundDevice("MPU6050", (uint8_t)addr.address);
- break;
+ String prod = "";
+ prod = readSEN5xProductName(i2cBus, addr.address);
+ if (prod.startsWith("SEN55")) {
+ type = SEN5X;
+ logFoundDevice("Sensirion SEN55", addr.address);
+ break;
+ } else if (prod.startsWith("SEN54")) {
+ type = SEN5X;
+ logFoundDevice("Sensirion SEN54", addr.address);
+ break;
+ } else if (prod.startsWith("SEN50")) {
+ type = SEN5X;
+ logFoundDevice("Sensirion SEN50", addr.address);
+ break;
+ }
+ if (addr.address == BMX160_ADDR) {
+ type = BMX160;
+ logFoundDevice("BMX160", (uint8_t)addr.address);
+ break;
+ } else {
+ type = MPU6050;
+ logFoundDevice("MPU6050", (uint8_t)addr.address);
+ break;
+ }
}
break;
diff --git a/src/detect/reClockI2C.cpp b/src/detect/reClockI2C.cpp
new file mode 100644
index 00000000000..5315bf3eca1
--- /dev/null
+++ b/src/detect/reClockI2C.cpp
@@ -0,0 +1,31 @@
+#include "reClockI2C.h"
+#include "ScanI2CTwoWire.h"
+
+uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force) {
+
+ uint32_t currentClock = 0;
+
+ /* See https://github.com/arduino/Arduino/issues/11457
+ Currently, only ESP32 can getClock()
+ While all cores can setClock()
+ https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50
+ https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60
+ https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103
+ For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes)
+ we need to reclock I2C and set it back to the previous desired speed.
+ Only for cases where we can know OR predefine the speed, we can do this.
+ */
+
+// TODO add getClock function or return a predefined clock speed per variant?
+#ifdef CAN_RECLOCK_I2C
+ currentClock = i2cBus->getClock();
+#endif
+
+ if ((currentClock != desiredClock) || force){
+ LOG_DEBUG("Changing I2C clock to %u", desiredClock);
+ i2cBus->setClock(desiredClock);
+ }
+
+ return currentClock;
+}
+
diff --git a/src/detect/reClockI2C.h b/src/detect/reClockI2C.h
index 689e88d6f19..c74ccffc557 100644
--- a/src/detect/reClockI2C.h
+++ b/src/detect/reClockI2C.h
@@ -1,41 +1,11 @@
-#ifdef CAN_RECLOCK_I2C
-#include "ScanI2CTwoWire.h"
-
-uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus)
-{
- uint32_t currentClock;
+#ifndef RECLOCK_I2C_
+#define RECLOCK_I2C_
- /* See https://github.com/arduino/Arduino/issues/11457
- Currently, only ESP32 can getClock()
- While all cores can setClock()
- https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50
- https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60
- https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103
- For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes)
- we need to reclock I2C and set it back to the previous desired speed.
- Only for cases where we can know OR predefine the speed, we can do this.
- */
+#include "ScanI2CTwoWire.h"
+#include
+#include
-#ifdef ARCH_ESP32
- currentClock = i2cBus->getClock();
-#elif defined(ARCH_NRF52)
- // TODO add getClock function or return a predefined clock speed per variant?
- return 0;
-#elif defined(ARCH_RP2040)
- // TODO add getClock function or return a predefined clock speed per variant
- return 0;
-#elif defined(ARCH_STM32WL)
- // TODO add getClock function or return a predefined clock speed per variant
- return 0;
-#else
- return 0;
-#endif
+uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force);
- if (currentClock != desiredClock) {
- LOG_DEBUG("Changing I2C clock to %u", desiredClock);
- i2cBus->setClock(desiredClock);
- }
- return currentClock;
-}
#endif
diff --git a/src/main.cpp b/src/main.cpp
index 68eda2d0ddd..dd75f41387b 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -713,7 +713,6 @@ void setup()
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X);
-
#endif
#ifdef HAS_SDCARD
diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp
index 01f5da2c6c8..d34ff593c51 100644
--- a/src/modules/Telemetry/AirQualityTelemetry.cpp
+++ b/src/modules/Telemetry/AirQualityTelemetry.cpp
@@ -3,24 +3,29 @@
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_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 "Sensor/AddI2CSensorTemplate.h"
#include "UnitConversions.h"
-#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/images.h"
+#include "graphics/ScreenFonts.h"
#include "main.h"
#include "sleep.h"
#include
+
// Sensors
+#include "Sensor/AddI2CSensorTemplate.h"
#include "Sensor/PMSA003ISensor.h"
+#include "Sensor/SEN5XSensor.h"
+#if __has_include()
+#include "Sensor/SCD4XSensor.h"
+#endif
void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
{
@@ -42,6 +47,10 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
// order by priority of metrics/values (low top, high bottom)
addSensor(i2cScanner, ScanI2C::DeviceType::PMSA003I);
+ addSensor(i2cScanner, ScanI2C::DeviceType::SEN5X);
+#if __has_include()
+ addSensor(i2cScanner, ScanI2C::DeviceType::SCD4X);
+#endif
}
int32_t AirQualityTelemetryModule::runOnce()
@@ -56,7 +65,7 @@ int32_t AirQualityTelemetryModule::runOnce()
uint32_t result = UINT32_MAX;
- if (!(moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_screen_enabled ||
+ if (!(moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_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();
@@ -85,23 +94,37 @@ int32_t AirQualityTelemetryModule::runOnce()
}
// Wake up the sensors that need it
- LOG_INFO("Waking up sensors");
+ LOG_INFO("Waking up sensors...");
for (TelemetrySensor *sensor : sensors) {
- if (!sensor->isActive()) {
- return sensor->wakeUp();
+ if (!sensor->canSleep()) {
+ LOG_DEBUG("%s sensor doesn't have sleep feature. Skipping", sensor->sensorName);
+ } else if (((lastSentToMesh == 0) ||
+ !Throttle::isWithinTimespanMs(lastSentToMesh - sensor->wakeUpTimeMs(), 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()) {
+ if (!sensor->isActive()) {
+ LOG_DEBUG("Waking up: %s", sensor->sensorName);
+ return sensor->wakeUp();
+ } else {
+ int32_t pendingForReadyMs = sensor->pendingForReadyMs();
+ LOG_DEBUG("%s. Pending for ready %ums", sensor->sensorName, pendingForReadyMs);
+ if (pendingForReadyMs) {
+ return pendingForReadyMs;
+ }
+ }
}
}
if (((lastSentToMesh == 0) ||
- !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled(
- moduleConfig.telemetry.air_quality_interval,
- default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
+ !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())) {
+ (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);
@@ -109,9 +132,18 @@ int32_t AirQualityTelemetryModule::runOnce()
}
// Send to sleep sensors that consume power
- LOG_INFO("Sending sensors to sleep");
+ LOG_DEBUG("Sending sensors to sleep");
for (TelemetrySensor *sensor : sensors) {
- sensor->sleep();
+ if (sensor->isActive() && sensor->canSleep()) {
+ if (sensor->wakeUpTimeMs() < Default::getConfiguredOrDefaultMsScaled(
+ moduleConfig.telemetry.air_quality_interval,
+ default_telemetry_broadcast_interval_secs, numOnlineNodes)) {
+ LOG_DEBUG("Disabling %s until next period", sensor->sensorName);
+ sensor->sleep();
+ } else {
+ LOG_DEBUG("Sensor stays enabled due to warm up period");
+ }
+ }
}
}
return min(sendToPhoneIntervalMs, result);
@@ -158,8 +190,7 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta
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;
+ bool hasAny = m.has_sen5xdata || m.has_pmsa003idata || m.has_scd4xdata;
if (!hasAny) {
display->drawString(x, currentY, "No Telemetry");
@@ -180,12 +211,25 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta
// === Collect sensor readings as label strings (no icons) ===
std::vector entries;
- if (m.has_pm10_standard)
- entries.push_back("PM1: " + 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: " + String(m.pm100_standard) + "ug/m3");
+ if (m.has_pmsa003idata) {
+ if (m.pmsa003idata.has_pm10_standard)
+ entries.push_back("PM1: " + String(m.pmsa003idata.pm10_standard) + "ug/m3");
+ if (m.pmsa003idata.has_pm25_standard)
+ entries.push_back("PM2.5: " + String(m.pmsa003idata.pm25_standard) + "ug/m3");
+ if (m.pmsa003idata.has_pm100_standard)
+ entries.push_back("PM10: " + String(m.pmsa003idata.pm100_standard) + "ug/m3");
+ }
+ if (m.has_sen5xdata) {
+ if (m.sen5xdata.has_pm10_standard)
+ entries.push_back("PM1: " + String(m.sen5xdata.pm10_standard) + "ug/m3");
+ if (m.sen5xdata.has_pm25_standard)
+ entries.push_back("PM2.5: " + String(m.sen5xdata.pm25_standard) + "ug/m3");
+ if (m.sen5xdata.has_pm100_standard)
+ entries.push_back("PM10: " + String(m.sen5xdata.pm100_standard) + "ug/m3");
+ }
+ if (m.has_scd4xdata)
+ entries.push_back("CO2: " + String(m.scd4xdata.co2) + "ppm");
+
// === Show first available metric on top-right of first line ===
if (!entries.empty()) {
@@ -221,13 +265,13 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
const char *sender = getSenderShortName(mp);
- LOG_INFO("(Received from %s): pm10_standard=%i, pm25_standard=%i, pm100_standard=%i", sender,
- t->variant.air_quality_metrics.pm10_standard, t->variant.air_quality_metrics.pm25_standard,
- t->variant.air_quality_metrics.pm100_standard);
+ // LOG_INFO("(Received from %s): pm10_standard=%i, pm25_standard=%i, pm100_standard=%i", sender,
+ // t->variant.air_quality_metrics.pm10_standard, t->variant.air_quality_metrics.pm25_standard,
+ // t->variant.air_quality_metrics.pm100_standard);
- 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.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
// release previous packet before occupying a new spot
if (lastMeasurementPacket != nullptr)
@@ -241,17 +285,20 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack
bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m)
{
- bool valid = true;
+ // Note: this is different to the case in EnvironmentTelemetryModule
+ // There, if any sensor fails to read - valid = false.
+ bool valid = false;
bool hasSensor = false;
m->time = getTime();
m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag;
m->variant.air_quality_metrics = meshtastic_AirQualityMetrics_init_zero;
- // TODO - Should we check for sensor state here?
- // If a sensor is sleeping, we should know and check to wake it up
+ bool sensor_get = false;
for (TelemetrySensor *sensor : sensors) {
- LOG_INFO("Reading AQ sensors");
- valid = valid && sensor->getMetrics(m);
+ LOG_DEBUG("Reading %s", sensor->sensorName);
+ // Note - this function doesn't get properly called if within a conditional
+ sensor_get = sensor->getMetrics(m);
+ valid = valid || sensor_get;
hasSensor = true;
}
@@ -291,12 +338,24 @@ 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=%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 hasAnyPM = m.variant.air_quality_metrics.has_sen5xdata || m.variant.air_quality_metrics.has_pmsa003idata;
+
+ // if (hasAnyPM) {
+ // LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u", \
+ // m.variant.air_quality_metrics.sen5xdata.pm10_standard, m.variant.air_quality_metrics.sen5xdata.pm25_standard, \
+ // m.variant.air_quality_metrics.sen5xdata.pm100_standard);
+ // }
+
+ // bool hasAnyCO2 = m.variant.air_quality_metrics.has_scd4xdata;
+
+ // if (hasAnyCO2) {
+ // LOG_INFO("Send: co2=%i, co2_t=%f, co2_rh=%f",
+ // m.variant.air_quality_metrics.scd4xdata.co2, m.variant.air_quality_metrics.scd4xdata.co2_temperature,
+ // m.variant.air_quality_metrics.scd4xdata.co2_humidity);
+ // }
meshtastic_MeshPacket *p = allocDataProtobuf(m);
p->to = dest;
@@ -331,6 +390,20 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
LOG_DEBUG("Start next execution in 5s, then sleep");
setIntervalFromNow(FIVE_SECONDS_MS);
}
+
+ 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;
}
@@ -338,8 +411,8 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
}
AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp,
- meshtastic_AdminMessage *request,
- meshtastic_AdminMessage *response)
+ meshtastic_AdminMessage *request,
+ meshtastic_AdminMessage *response)
{
AdminMessageHandleResult result = AdminMessageHandleResult::NOT_HANDLED;
diff --git a/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h b/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h
index 37d909d71f1..01aacc6741b 100644
--- a/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h
+++ b/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h
@@ -1,10 +1,10 @@
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
+#include
#include "TelemetrySensor.h"
#include "detect/ScanI2C.h"
#include "detect/ScanI2CTwoWire.h"
#include
-#include
static std::forward_list sensors;
diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp
index 2225a4d8728..7aae3f2661d 100644
--- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp
+++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp
@@ -2,14 +2,14 @@
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
-#include "../detect/reClockI2C.h"
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "PMSA003ISensor.h"
-#include "TelemetrySensor.h"
-
-#include
+#include "../detect/reClockI2C.h"
-PMSA003ISensor::PMSA003ISensor() : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I") {}
+PMSA003ISensor::PMSA003ISensor()
+ : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I")
+{
+}
bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
{
@@ -21,26 +21,29 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
_bus = bus;
_address = dev->address.address;
-#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
- uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus);
- if (!currentClock) {
- LOG_WARN("PMSA003I can't be used at this clock speed");
- return false;
- }
-#endif
+#ifdef PMSA003I_I2C_CLOCK_SPEED
+#ifdef CAN_RECLOCK_I2C
+ uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, false);
+#elif !HAS_SCREEN
+ reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, true);
+#else
+ LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
+ return false;
+#endif /* CAN_RECLOCK_I2C */
+#endif /* PMSA003I_I2C_CLOCK_SPEED */
_bus->beginTransmission(_address);
if (_bus->endTransmission() != 0) {
- LOG_WARN("PMSA003I not found on I2C at 0x12");
+ LOG_WARN("%s not found on I2C at 0x12", sensorName);
return false;
}
#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
- reClockI2C(currentClock, _bus);
+ reClockI2C(currentClock, _bus, false);
#endif
status = 1;
- LOG_INFO("PMSA003I Enabled");
+ LOG_INFO("%s Enabled", sensorName);
initI2CSensor();
return true;
@@ -48,35 +51,44 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement)
{
- if (!isActive()) {
- LOG_WARN("PMSA003I is not active");
+ if(!isActive()){
+ LOG_WARN("Can't get metrics. %s is not active", sensorName);
return false;
}
-#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
- uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus);
-#endif
+#ifdef PMSA003I_I2C_CLOCK_SPEED
+#ifdef CAN_RECLOCK_I2C
+ uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, false);
+#elif !HAS_SCREEN
+ reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, true);
+#else
+ LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
+ return false;
+#endif /* CAN_RECLOCK_I2C */
+#endif /* PMSA003I_I2C_CLOCK_SPEED */
_bus->requestFrom(_address, PMSA003I_FRAME_LENGTH);
if (_bus->available() < PMSA003I_FRAME_LENGTH) {
- LOG_WARN("PMSA003I read failed: incomplete data (%d bytes)", _bus->available());
+ LOG_WARN("%s read failed: incomplete data (%d bytes)", sensorName, _bus->available());
return false;
}
-#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
- reClockI2C(currentClock, _bus);
-#endif
-
for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) {
buffer[i] = _bus->read();
}
+#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
+ reClockI2C(currentClock, _bus, false);
+#endif
+
if (buffer[0] != 0x42 || buffer[1] != 0x4D) {
- LOG_WARN("PMSA003I frame header invalid: 0x%02X 0x%02X", buffer[0], buffer[1]);
+ LOG_WARN("%s frame header invalid: 0x%02X 0x%02X", sensorName, buffer[0], buffer[1]);
return false;
}
- auto read16 = [](uint8_t *data, uint8_t idx) -> uint16_t { return (data[idx] << 8) | data[idx + 1]; };
+ auto read16 = [](uint8_t *data, uint8_t idx) -> uint16_t {
+ return (data[idx] << 8) | data[idx + 1];
+ };
computedChecksum = 0;
@@ -86,47 +98,48 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement)
receivedChecksum = read16(buffer, PMSA003I_FRAME_LENGTH - 2);
if (computedChecksum != receivedChecksum) {
- LOG_WARN("PMSA003I checksum failed: computed 0x%04X, received 0x%04X", computedChecksum, receivedChecksum);
+ LOG_WARN("%s checksum failed: computed 0x%04X, received 0x%04X", sensorName, 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_pmsa003idata = true;
+ measurement->variant.air_quality_metrics.pmsa003idata.has_pm10_standard = true;
+ measurement->variant.air_quality_metrics.pmsa003idata.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.pmsa003idata.has_pm25_standard = true;
+ measurement->variant.air_quality_metrics.pmsa003idata.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.pmsa003idata.has_pm100_standard = true;
+ measurement->variant.air_quality_metrics.pmsa003idata.pm100_standard = read16(buffer, 8);
// TODO - Add admin command to remove environmental metrics to save protobuf space
- measurement->variant.air_quality_metrics.has_pm10_environmental = true;
- measurement->variant.air_quality_metrics.pm10_environmental = read16(buffer, 10);
+ measurement->variant.air_quality_metrics.pmsa003idata.has_pm10_environmental = true;
+ measurement->variant.air_quality_metrics.pmsa003idata.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.pmsa003idata.has_pm25_environmental = true;
+ measurement->variant.air_quality_metrics.pmsa003idata.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.pmsa003idata.has_pm100_environmental = true;
+ measurement->variant.air_quality_metrics.pmsa003idata.pm100_environmental = read16(buffer, 14);
// TODO - Add admin command to remove PN to save protobuf space
- measurement->variant.air_quality_metrics.has_particles_03um = true;
- measurement->variant.air_quality_metrics.particles_03um = read16(buffer, 16);
+ measurement->variant.air_quality_metrics.pmsa003idata.has_particles_03um = true;
+ measurement->variant.air_quality_metrics.pmsa003idata.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.pmsa003idata.has_particles_05um = true;
+ measurement->variant.air_quality_metrics.pmsa003idata.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.pmsa003idata.has_particles_10um = true;
+ measurement->variant.air_quality_metrics.pmsa003idata.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.pmsa003idata.has_particles_25um = true;
+ measurement->variant.air_quality_metrics.pmsa003idata.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.pmsa003idata.has_particles_50um = true;
+ measurement->variant.air_quality_metrics.pmsa003idata.particles_50um = read16(buffer, 24);
- measurement->variant.air_quality_metrics.has_particles_100um = true;
- measurement->variant.air_quality_metrics.particles_100um = read16(buffer, 26);
+ measurement->variant.air_quality_metrics.pmsa003idata.has_particles_100um = true;
+ measurement->variant.air_quality_metrics.pmsa003idata.particles_100um = read16(buffer, 26);
return true;
}
@@ -136,20 +149,58 @@ bool PMSA003ISensor::isActive()
return state == State::ACTIVE;
}
+int32_t PMSA003ISensor::wakeUpTimeMs()
+{
+#ifdef PMSA003I_ENABLE_PIN
+ return PMSA003I_WARMUP_MS;
+#endif
+ return 0;
+}
+
+int32_t PMSA003ISensor::pendingForReadyMs()
+{
+#ifdef PMSA003I_ENABLE_PIN
+
+ uint32_t now;
+ now = getTime();
+ uint32_t sincePmMeasureStarted = (now - pmMeasureStarted)*1000;
+ LOG_DEBUG("%s: Since measure started: %ums", sensorName, sincePmMeasureStarted);
+
+ if (sincePmMeasureStarted < PMSA003I_WARMUP_MS) {
+ LOG_INFO("%s: not enough time passed since starting measurement", sensorName);
+ return PMSA003I_WARMUP_MS - sincePmMeasureStarted;
+ }
+ return 0;
+
+#endif
+ return 0;
+}
+
+
+bool PMSA003ISensor::canSleep() {
+#ifdef PMSA003I_ENABLE_PIN
+ return true;
+#endif
+ return false;
+}
+
void PMSA003ISensor::sleep()
{
#ifdef PMSA003I_ENABLE_PIN
digitalWrite(PMSA003I_ENABLE_PIN, LOW);
state = State::IDLE;
+ pmMeasureStarted = 0;
#endif
}
uint32_t PMSA003ISensor::wakeUp()
{
#ifdef PMSA003I_ENABLE_PIN
- LOG_INFO("Waking up PMSA003I");
+ LOG_INFO("Waking up %s", sensorName);
digitalWrite(PMSA003I_ENABLE_PIN, HIGH);
state = State::ACTIVE;
+ pmMeasureStarted = getTime();
+
return PMSA003I_WARMUP_MS;
#endif
// No need to wait for warmup if already active
diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h
index 09b43d620c0..a2a0951801d 100644
--- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h
+++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h
@@ -4,32 +4,38 @@
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "TelemetrySensor.h"
+#include "RTC.h"
#define PMSA003I_I2C_CLOCK_SPEED 100000
-#define PMSA003I_FRAME_LENGTH 32
+#define PMSA003I_FRAME_LENGTH 32
#define PMSA003I_WARMUP_MS 30000
class PMSA003ISensor : public TelemetrySensor
{
- public:
- PMSA003ISensor();
- virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
- virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
-
- virtual bool isActive() override;
- virtual void sleep() override;
- virtual uint32_t wakeUp() override;
-
- private:
+private:
enum class State { IDLE, ACTIVE };
State state = State::ACTIVE;
uint16_t computedChecksum = 0;
uint16_t receivedChecksum = 0;
+ uint32_t pmMeasureStarted = 0;
uint8_t buffer[PMSA003I_FRAME_LENGTH]{};
- TwoWire *_bus{};
+ TwoWire * _bus{};
uint8_t _address{};
+
+public:
+ PMSA003ISensor();
+ virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
+ virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
+
+ virtual bool isActive() override;
+ virtual void sleep() override;
+ virtual uint32_t wakeUp() override;
+ virtual bool canSleep() override;
+ virtual int32_t wakeUpTimeMs() override;
+ virtual int32_t pendingForReadyMs() override;
+
};
#endif
\ No newline at end of file
diff --git a/src/modules/Telemetry/Sensor/SCD4XSensor.cpp b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp
new file mode 100644
index 00000000000..7e5b50cc22d
--- /dev/null
+++ b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp
@@ -0,0 +1,771 @@
+#include "configuration.h"
+
+#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include()
+
+#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "SCD4XSensor.h"
+#include "../detect/reClockI2C.h"
+
+#define SCD4X_NO_ERROR 0
+
+SCD4XSensor::SCD4XSensor()
+ : TelemetrySensor(meshtastic_TelemetrySensorType_SCD4X, "SCD4X")
+{
+}
+
+bool SCD4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
+{
+ LOG_INFO("Init sensor: %s", sensorName);
+
+ _bus = bus;
+ _address = dev->address.address;
+
+#ifdef SCD4X_I2C_CLOCK_SPEED
+#ifdef CAN_RECLOCK_I2C
+ uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false);
+#elif !HAS_SCREEN
+ reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true);
+#else
+ LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
+ return false;
+#endif /* CAN_RECLOCK_I2C */
+#endif /* SCD4X_I2C_CLOCK_SPEED */
+
+ scd4x.begin(*_bus, _address);
+
+ // From SCD4X library
+ delay(30);
+
+ // Stop periodic measurement
+ if (!stopMeasurement()) {
+ return false;
+ }
+
+ // Get sensor variant
+ scd4x.getSensorVariant(sensorVariant);
+
+ if (sensorVariant == SCD4X_SENSOR_VARIANT_SCD41){
+ LOG_INFO("%s: Found SCD41", sensorName);
+ if (!powerUp()) {
+ LOG_ERROR("%s: Error trying to execute powerUp()", sensorName);
+ return false;
+ }
+ }
+
+ if (!getASC(ascActive)){
+ LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName);
+ return false;
+ }
+
+ // Start measurement in selected power mode (low power by default)
+ if (!startMeasurement()){
+ LOG_ERROR("%s: Couldn't start measurement", sensorName);
+ return false;
+ }
+
+#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
+ reClockI2C(currentClock, _bus, false);
+#endif
+
+ if (state == SCD4X_MEASUREMENT){
+ status = 1;
+ } else {
+ status = 0;
+ }
+
+ initI2CSensor();
+
+ return true;
+}
+
+bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement)
+{
+
+ if (state != SCD4X_MEASUREMENT) {
+ LOG_ERROR("%s: Not in measurement mode", sensorName);
+ return false;
+ }
+
+ uint16_t co2, error;
+ float temperature, humidity;
+
+#ifdef SCD4X_I2C_CLOCK_SPEED
+#ifdef CAN_RECLOCK_I2C
+ uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false);
+#elif !HAS_SCREEN
+ reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true);
+#else
+ LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
+ return false;
+#endif /* CAN_RECLOCK_I2C */
+#endif /* SCD4X_I2C_CLOCK_SPEED */
+
+ bool dataReady;
+ error = scd4x.getDataReadyStatus(dataReady);
+ if (!dataReady) {
+ LOG_ERROR("SCD4X: Data is not ready");
+ return false;
+ }
+
+ error = scd4x.readMeasurement(co2, temperature, humidity);
+
+#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
+ reClockI2C(currentClock, _bus, false);
+#endif
+
+ LOG_DEBUG("%s readings: %u ppm, %.2f degC, %.2f %rh", sensorName, co2, temperature, humidity);
+ if (error != SCD4X_NO_ERROR) {
+ LOG_DEBUG("%s: Error while getting measurements: %u", sensorName, error);
+ if (co2 == 0) {
+ LOG_ERROR("%s: Skipping invalid measurement.", sensorName);
+ }
+ return false;
+ } else {
+ measurement->variant.air_quality_metrics.has_scd4xdata = true;
+ measurement->variant.air_quality_metrics.scd4xdata.has_co2_temperature = true;
+ measurement->variant.air_quality_metrics.scd4xdata.has_co2_humidity = true;
+ measurement->variant.air_quality_metrics.scd4xdata.has_co2 = true;
+ measurement->variant.air_quality_metrics.scd4xdata.co2_temperature = temperature;
+ measurement->variant.air_quality_metrics.scd4xdata.co2_humidity = humidity;
+ measurement->variant.air_quality_metrics.scd4xdata.co2 = co2;
+ return true;
+ }
+}
+
+// TODO
+// Make all functions change I2C clock
+
+/**
+* @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, frcCorr;
+
+ LOG_INFO("%s: Issuing FRC. Ensure device has been working at least 3 minutes in stable target environment", sensorName);
+
+ if (!stopMeasurement()) {
+ return false;
+ }
+
+ LOG_INFO("%s: Target CO2: %u ppm", sensorName, targetCO2);
+ error = scd4x.performForcedRecalibration((uint16_t)targetCO2, frcCorr);
+
+ // SCD4X Sensirion datasheet
+ delay(400);
+
+ if (error != SCD4X_NO_ERROR){
+ LOG_ERROR("%s: Unable to perform forced recalibration.", sensorName);
+ return false;
+ }
+
+ if (frcCorr == 0xFFFF) {
+ LOG_ERROR("%s: Error while performing forced recalibration.", sensorName);
+ return false;
+ }
+
+ LOG_INFO("%s: FRC Correction successful. Correction output: %u", sensorName, (uint16_t)(frcCorr-0x8000));
+
+ return true;
+}
+
+bool SCD4XSensor::startMeasurement() {
+ uint16_t error;
+
+ if (state == SCD4X_MEASUREMENT){
+ LOG_DEBUG("%s: Already in measurement mode", sensorName);
+ return true;
+ }
+
+ if (lowPower) {
+ error = scd4x.startLowPowerPeriodicMeasurement();
+ } else {
+ error = scd4x.startPeriodicMeasurement();
+ }
+
+ if (error == SCD4X_NO_ERROR) {
+ LOG_INFO("%s: Started measurement mode", sensorName);
+ if (lowPower) {
+ LOG_INFO("%s: Low power mode", sensorName);
+ } else {
+ LOG_INFO("%s: Normal power mode", sensorName);
+ }
+
+ state = SCD4X_MEASUREMENT;
+ return true;
+ } else {
+ LOG_ERROR("%s: Couldn't start measurement mode", sensorName);
+ return false;
+ }
+}
+
+bool SCD4XSensor::stopMeasurement(){
+ uint16_t error;
+
+ error = scd4x.stopPeriodicMeasurement();
+ if (error != SCD4X_NO_ERROR) {
+ LOG_ERROR("%s: Unable to set idle mode on SCD4X.", sensorName);
+ return false;
+ }
+
+ state = SCD4X_IDLE;
+ co2MeasureStarted = 0;
+ return true;
+}
+
+bool SCD4XSensor::setPowerMode(bool _lowPower) {
+ lowPower = _lowPower;
+
+ if (!stopMeasurement()) {
+ return false;
+ }
+
+ if (lowPower) {
+ LOG_DEBUG("%s: Set low power mode", sensorName);
+ } else {
+ LOG_DEBUG("%s: Set normal power mode", sensorName);
+ }
+
+ return true;
+}
+
+/**
+* @brief Check the current mode (ASC or FRC)
+
+* From Sensirion SCD4X I2C Library
+*/
+bool SCD4XSensor::getASC(uint16_t &_ascActive) {
+ uint16_t error;
+ LOG_INFO("%s: Getting ASC", sensorName);
+
+ if (!stopMeasurement()) {
+ return false;
+ }
+ error = scd4x.getAutomaticSelfCalibrationEnabled(_ascActive);
+
+ if (error != SCD4X_NO_ERROR){
+ LOG_ERROR("%s: Unable to send command.", sensorName);
+ return false;
+ }
+
+ if (_ascActive){
+ LOG_INFO("%s: ASC is enabled", sensorName);
+ } else {
+ LOG_INFO("%s: FRC is enabled", sensorName);
+ }
+
+ 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("%s: Enabling ASC", sensorName);
+ } else {
+ LOG_INFO("%s: Disabling ASC", sensorName);
+ }
+
+ if (!stopMeasurement()) {
+ return false;
+ }
+
+ error = scd4x.setAutomaticSelfCalibrationEnabled((uint16_t)ascEnabled);
+
+ if (error != SCD4X_NO_ERROR){
+ LOG_ERROR("%s: Unable to send command.", sensorName);
+ return false;
+ }
+
+ error = scd4x.persistSettings();
+ if (error != SCD4X_NO_ERROR){
+ LOG_ERROR("%s: Unable to make settings persistent.", sensorName);
+ return false;
+ }
+
+ if (!getASC(ascActive)){
+ LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName);
+ return false;
+ }
+
+ if (ascActive){
+ LOG_INFO("%s: ASC is enabled", sensorName);
+ } else {
+ LOG_INFO("%s: ASC is disabled", sensorName);
+ }
+
+ 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){
+ // TODO - Remove?
+ // Available in library, but not described in datasheet.
+ uint16_t error;
+ LOG_INFO("%s: Setting ASC baseline to: %u", sensorName, targetCO2);
+
+ getASC(ascActive);
+ if (!ascActive){
+ LOG_ERROR("%s: Can't set ASC baseline. ASC is not active", sensorName);
+ return false;
+ }
+
+ if (!stopMeasurement()) {
+ return false;
+ }
+
+ error = scd4x.setAutomaticSelfCalibrationTarget((uint16_t)targetCO2);
+
+ if (error != SCD4X_NO_ERROR){
+ LOG_ERROR("%s: Unable to send command.", sensorName);
+ return false;
+ }
+
+ error = scd4x.persistSettings();
+ if (error != SCD4X_NO_ERROR){
+ LOG_ERROR("%s: Unable to make settings persistent.", sensorName);
+ return false;
+ }
+
+ LOG_INFO("%s: Setting ASC baseline successful", sensorName);
+
+ 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 updatedTempOffset;
+ float tempOffset;
+ bool dataReady;
+ uint16_t co2;
+ float temperature;
+ float humidity;
+
+ LOG_INFO("%s: Setting reference temperature at: %.2f", sensorName, tempReference);
+
+ error = scd4x.getDataReadyStatus(dataReady);
+ if (!dataReady) {
+ LOG_ERROR("%s: Data is not ready", sensorName);
+ return false;
+ }
+
+ error = scd4x.readMeasurement(co2, temperature, humidity);
+ if (error != SCD4X_NO_ERROR) {
+ LOG_ERROR("%s: Unable to read current temperature. Error code: %u", sensorName, error);
+ return false;
+ }
+
+ LOG_INFO("%s: Current sensor temperature: %.2f", sensorName, temperature);
+
+ if (!stopMeasurement()) {
+ return false;
+ }
+
+ error = scd4x.getTemperatureOffset(prevTempOffset);
+
+ if (error != SCD4X_NO_ERROR){
+ LOG_ERROR("%s: Unable to get temperature offset. Error code: %u", sensorName, error);
+ return false;
+ }
+ LOG_INFO("%s: Current sensor temperature offset: %.2f", sensorName, prevTempOffset);
+
+ tempOffset = temperature - tempReference + prevTempOffset;
+
+ LOG_INFO("%s: Setting temperature offset: %.2f", sensorName, tempOffset);
+ error = scd4x.setTemperatureOffset(tempOffset);
+ if (error != SCD4X_NO_ERROR){
+ LOG_ERROR("%s: Unable to set temperature offset. Error code: %u", sensorName, error);
+ return false;
+ }
+
+ error = scd4x.persistSettings();
+ if (error != SCD4X_NO_ERROR){
+ LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error);
+ return false;
+ }
+
+ scd4x.getTemperatureOffset(updatedTempOffset);
+ LOG_INFO("%s: Updated sensor temperature offset: %.2f", sensorName, updatedTempOffset);
+
+ 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("%s: Requesting sensor altitude", sensorName);
+
+ if (!stopMeasurement()) {
+ return false;
+ }
+
+ error = scd4x.getSensorAltitude(altitude);
+
+ if (error != SCD4X_NO_ERROR){
+ LOG_ERROR("%s: Unable to get altitude. Error code: %u", sensorName, error);
+ return false;
+ }
+ LOG_INFO("%s: Sensor altitude: %u", sensorName, altitude);
+
+ return true;
+}
+
+/**
+* @brief Get the ambient pressure around the sensor.
+*
+* From Sensirion SCD4X I2C Library.
+*
+* Gets the ambient pressure in Pa.
+*/
+bool SCD4XSensor::getAmbientPressure(uint32_t &ambientPressure){
+ uint16_t error;
+ LOG_INFO("%s: Requesting sensor ambient pressure", sensorName);
+
+ error = scd4x.getAmbientPressure(ambientPressure);
+
+ if (error != SCD4X_NO_ERROR){
+ LOG_ERROR("%s: Unable to get altitude. Error code: %u", sensorName, error);
+ return false;
+ }
+ LOG_INFO("%s: Sensor ambient pressure: %u", sensorName, ambientPressure);
+
+ 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;
+
+ if (!stopMeasurement()) {
+ return false;
+ }
+
+ error = scd4x.setSensorAltitude(altitude);
+
+ if (error != SCD4X_NO_ERROR){
+ LOG_ERROR("%s: Unable to set altitude. Error code: %u", sensorName, error);
+ return false;
+ }
+
+ error = scd4x.persistSettings();
+ if (error != SCD4X_NO_ERROR){
+ LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error);
+ 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("%s: Unable to set altitude. Error code: %u", sensorName, error);
+ return false;
+ }
+
+ // Sensirion doesn't indicate if this is necessary. We send it anyway
+ error = scd4x.persistSettings();
+ if (error != SCD4X_NO_ERROR){
+ LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error);
+ 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;
+
+ LOG_INFO("%s: Requesting factory reset", sensorName);
+
+ if (!stopMeasurement()) {
+ return false;
+ }
+
+ error = scd4x.performFactoryReset();
+
+ if (error != SCD4X_NO_ERROR){
+ LOG_ERROR("%s: Unable to do factory reset. Error code: %u", sensorName, error);
+ return false;
+ }
+
+ LOG_INFO("%s: Factory reset successful", sensorName);
+
+ return true;
+}
+
+/**
+* @brief Put the sensor into sleep mode from idle mode.
+*
+* From Sensirion SCD4X I2C Library.
+*
+* Put the sensor from idle to sleep to reduce power consumption. Can be
+* used to power down when operating the sensor in power-cycled single shot
+* mode.
+*
+* @note This command is only available in idle mode. Only for SCD41.
+*/
+bool SCD4XSensor::powerDown() {
+ LOG_INFO("%s: Trying to send sensor to sleep", sensorName);
+
+ if (sensorVariant != SCD4X_SENSOR_VARIANT_SCD41) {
+ LOG_WARN("SCD4X: Can't send sensor to sleep. Incorrect variant. Ignoring");
+ return true;
+ }
+
+ if (!stopMeasurement()) {
+ return false;
+ }
+
+ if (scd4x.powerDown() != SCD4X_NO_ERROR) {
+ LOG_ERROR("%s: Error trying to execute sleep()", sensorName);
+ return false;
+ }
+ state = SCD4X_OFF;
+ return true;
+}
+
+/**
+* @brief Wake up sensor from sleep mode to idle mode (powerUp)
+*
+* From Sensirion SCD4X I2C Library.
+*
+* Wake up the sensor from sleep mode into idle mode. Note that the SCD4x
+* does not acknowledge the wake_up command. The sensor's idle state after
+* wake up can be verified by reading out the serial number.
+*
+* @note This command is only available for SCD41.
+*/
+bool SCD4XSensor::powerUp(){
+ LOG_INFO("%s: Waking up", sensorName);
+
+ if (scd4x.wakeUp() != SCD4X_NO_ERROR) {
+ LOG_ERROR("%s: Error trying to execute wakeUp()", sensorName);
+ return false;
+ }
+ state = SCD4X_IDLE;
+ return true;
+}
+
+/**
+* @brief Check if sensor is in measurement mode
+*/
+bool SCD4XSensor::isActive(){
+ return state == SCD4X_MEASUREMENT;
+}
+
+/**
+* @brief Start measurement mode
+*/
+uint32_t SCD4XSensor::wakeUp(){
+ if (startMeasurement()) {
+ co2MeasureStarted = getTime();
+ return SCD4X_WARMUP_MS;
+ }
+ return 0;
+}
+
+/**
+* @brief Stop measurement mode
+*/
+void SCD4XSensor::sleep(){
+ stopMeasurement();
+}
+
+/**
+* @brief Can sleep function
+*
+* Power consumption is very low on lowPower mode, modify this function if
+* you still want to override this behaviour. Otherwise, sleep is disabled
+* routinely in low power mode
+*/
+bool SCD4XSensor::canSleep(){
+ return lowPower ? false : true;
+}
+
+int32_t SCD4XSensor::wakeUpTimeMs(){
+ return SCD4X_WARMUP_MS;
+}
+
+int32_t SCD4XSensor::pendingForReadyMs()
+{
+ uint32_t now;
+ now = getTime();
+ uint32_t sinceCO2MeasureStarted = (now - co2MeasureStarted)*1000;
+ LOG_DEBUG("%s: Since measure started: %ums", sensorName, sinceCO2MeasureStarted);
+
+ if (sinceCO2MeasureStarted < SCD4X_WARMUP_MS) {
+ LOG_INFO("%s: not enough time passed since starting measurement", sensorName);
+ return SCD4X_WARMUP_MS - sinceCO2MeasureStarted;
+ }
+ return 0;
+}
+
+AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
+ meshtastic_AdminMessage *response)
+{
+ AdminMessageHandleResult result;
+
+ // TODO: potentially add selftest command?
+
+ switch (request->which_payload_variant) {
+ case meshtastic_AdminMessage_sensor_config_tag:
+ // Check for ASC-FRC request first
+ if (!request->sensor_config.has_scd4x_config) {
+ result = AdminMessageHandleResult::NOT_HANDLED;
+ break;
+ }
+
+ if (request->sensor_config.scd4x_config.has_factory_reset) {
+ LOG_DEBUG("%s: Requested factory reset", sensorName);
+ this->factoryReset();
+ } else {
+
+ if (request->sensor_config.scd4x_config.has_set_asc) {
+ this->setASC(request->sensor_config.scd4x_config.set_asc);
+ if (request->sensor_config.scd4x_config.set_asc == false) {
+ LOG_DEBUG("%s: Request for FRC", sensorName);
+ if (request->sensor_config.scd4x_config.has_set_target_co2_conc) {
+ this->performFRC(request->sensor_config.scd4x_config.set_target_co2_conc);
+ } else {
+ // FRC requested but no target CO2 provided
+ LOG_ERROR("%s: target CO2 not provided", sensorName);
+ result = AdminMessageHandleResult::NOT_HANDLED;
+ break;
+ }
+ } else {
+ LOG_DEBUG("%s: Request for ASC", sensorName);
+ if (request->sensor_config.scd4x_config.has_set_target_co2_conc) {
+ LOG_DEBUG("%s: Request has target CO2", sensorName);
+ // TODO - Remove? see setASCBaseline function
+ this->setASCBaseline(request->sensor_config.scd4x_config.set_target_co2_conc);
+ } else {
+ LOG_DEBUG("%s: Request doesn't have target CO2", sensorName);
+ }
+ }
+ }
+
+ // Check for temperature offset
+ // NOTE: this requires to have a sensor working on stable environment
+ // And to make it between readings
+ if (request->sensor_config.scd4x_config.has_set_temperature) {
+ this->setTemperature(request->sensor_config.scd4x_config.set_temperature);
+ }
+
+ // Check for altitude or pressure offset
+ if (request->sensor_config.scd4x_config.has_set_altitude) {
+ this->setAltitude(request->sensor_config.scd4x_config.set_altitude);
+ } else if (request->sensor_config.scd4x_config.has_set_ambient_pressure){
+ this->setAmbientPressure(request->sensor_config.scd4x_config.set_ambient_pressure);
+ }
+
+ // Check for low power mode
+ // NOTE: to switch from one mode to another do:
+ // setPowerMode -> startMeasurement
+ if (request->sensor_config.scd4x_config.has_set_power_mode) {
+ this->setPowerMode(request->sensor_config.scd4x_config.set_power_mode);
+ }
+
+ }
+
+ // Start measurement mode
+ this->startMeasurement();
+
+ result = AdminMessageHandleResult::HANDLED;
+ break;
+
+ default:
+ result = AdminMessageHandleResult::NOT_HANDLED;
+ }
+
+ return result;
+}
+
+#endif
\ No newline at end of file
diff --git a/src/modules/Telemetry/Sensor/SCD4XSensor.h b/src/modules/Telemetry/Sensor/SCD4XSensor.h
new file mode 100644
index 00000000000..2542e408bdd
--- /dev/null
+++ b/src/modules/Telemetry/Sensor/SCD4XSensor.h
@@ -0,0 +1,63 @@
+#include "configuration.h"
+
+#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include()
+
+#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "TelemetrySensor.h"
+#include
+#include "RTC.h"
+
+// Max speed 400kHz
+#define SCD4X_I2C_CLOCK_SPEED 100000
+#define SCD4X_WARMUP_MS 5000
+
+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 getAmbientPressure(uint32_t &ambientPressure);
+ bool setAmbientPressure(uint32_t ambientPressure);
+ bool factoryReset();
+ bool setPowerMode(bool _lowPower);
+ bool startMeasurement();
+ bool stopMeasurement();
+
+ uint16_t ascActive = 1;
+ // low power measurement mode (on sensirion side). Disables sleep mode
+ // Improvement and testing needed for timings
+ bool lowPower = true;
+ uint32_t co2MeasureStarted = 0;
+
+ public:
+ SCD4XSensor();
+ virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
+ virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
+
+ enum SCD4XState { SCD4X_OFF, SCD4X_IDLE, SCD4X_MEASUREMENT };
+ SCD4XState state = SCD4X_OFF;
+ SCD4xSensorVariant sensorVariant{};
+
+ virtual bool isActive() override;
+
+ virtual void sleep() override; // Stops measurement (measurement -> idle)
+ virtual uint32_t wakeUp() override; // Starts measurement (idle -> measurement)
+ bool powerDown(); // Powers down sensor (idle -> power-off)
+ bool powerUp(); // Powers the sensor (power-off -> idle)
+ virtual bool canSleep() override;
+ virtual int32_t wakeUpTimeMs() override;
+ virtual int32_t pendingForReadyMs() override;
+ AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
+ meshtastic_AdminMessage *response) override;
+};
+
+#endif
\ No newline at end of file
diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp
new file mode 100644
index 00000000000..d5d23a98a3a
--- /dev/null
+++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp
@@ -0,0 +1,1011 @@
+#include "configuration.h"
+
+#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
+
+#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "SEN5XSensor.h"
+#include "../detect/reClockI2C.h"
+#include "FSCommon.h"
+#include "SPILock.h"
+#include "SafeFile.h"
+#include
+#include
+#include // FLT_MAX
+
+SEN5XSensor::SEN5XSensor()
+ : TelemetrySensor(meshtastic_TelemetrySensorType_SEN5X, "SEN5X")
+{
+}
+
+bool SEN5XSensor::getVersion()
+{
+ if (!sendCommand(SEN5X_GET_FIRMWARE_VERSION)){
+ LOG_ERROR("SEN5X: Error sending version command");
+ return false;
+ }
+ delay(20); // From Sensirion Datasheet
+
+ uint8_t versionBuffer[12];
+ size_t charNumber = readBuffer(&versionBuffer[0], 3);
+ if (charNumber == 0) {
+ LOG_ERROR("SEN5X: Error getting data ready flag value");
+ return false;
+ }
+
+ firmwareVer = versionBuffer[0] + (versionBuffer[1] / 10);
+ hardwareVer = versionBuffer[3] + (versionBuffer[4] / 10);
+ protocolVer = versionBuffer[5] + (versionBuffer[6] / 10);
+
+ LOG_INFO("SEN5X Firmware Version: %0.2f", firmwareVer);
+ LOG_INFO("SEN5X Hardware Version: %0.2f", hardwareVer);
+ LOG_INFO("SEN5X Protocol Version: %0.2f", protocolVer);
+
+ return true;
+}
+
+bool SEN5XSensor::findModel()
+{
+ if (!sendCommand(SEN5X_GET_PRODUCT_NAME)) {
+ LOG_ERROR("SEN5X: Error asking for product name");
+ return false;
+ }
+ delay(50); // From Sensirion Datasheet
+
+ const uint8_t nameSize = 48;
+ uint8_t name[nameSize];
+ size_t charNumber = readBuffer(&name[0], nameSize);
+ if (charNumber == 0) {
+ LOG_ERROR("SEN5X: Error getting device name");
+ return false;
+ }
+
+ // We only check the last character that defines the model SEN5X
+ switch(name[4])
+ {
+ case 48:
+ model = SEN50;
+ LOG_INFO("SEN5X: found sensor model SEN50");
+ break;
+ case 52:
+ model = SEN54;
+ LOG_INFO("SEN5X: found sensor model SEN54");
+ break;
+ case 53:
+ model = SEN55;
+ LOG_INFO("SEN5X: found sensor model SEN55");
+ break;
+ }
+
+ return true;
+}
+
+bool SEN5XSensor::sendCommand(uint16_t command)
+{
+ uint8_t nothing;
+ return sendCommand(command, ¬hing, 0);
+}
+
+bool SEN5XSensor::sendCommand(uint16_t command, uint8_t* buffer, uint8_t byteNumber)
+{
+ // At least we need two bytes for the command
+ uint8_t bufferSize = 2;
+
+ // Add space for CRC bytes (one every two bytes)
+ if (byteNumber > 0) bufferSize += byteNumber + (byteNumber / 2);
+
+ uint8_t toSend[bufferSize];
+ uint8_t i = 0;
+ toSend[i++] = static_cast((command & 0xFF00) >> 8);
+ toSend[i++] = static_cast((command & 0x00FF) >> 0);
+
+ // Prepare buffer with CRC every third byte
+ uint8_t bi = 0;
+ if (byteNumber > 0) {
+ while (bi < byteNumber) {
+ toSend[i++] = buffer[bi++];
+ toSend[i++] = buffer[bi++];
+ uint8_t calcCRC = sen5xCRC(&buffer[bi - 2]);
+ toSend[i++] = calcCRC;
+ }
+ }
+
+#ifdef SEN5X_I2C_CLOCK_SPEED
+#ifdef CAN_RECLOCK_I2C
+ uint32_t currentClock = reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, false);
+#elif !HAS_SCREEN
+ reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, true);
+#else
+ LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
+ return false;
+#endif /* CAN_RECLOCK_I2C */
+#endif /* SEN5X_I2C_CLOCK_SPEED */
+
+ // Transmit the data
+ // LOG_DEBUG("Beginning connection to SEN5X: 0x%x. Size: %u", address, bufferSize);
+ // Note: this is necessary to allow for long-buffers
+ delay(20);
+ _bus->beginTransmission(_address);
+ size_t writtenBytes = _bus->write(toSend, bufferSize);
+ uint8_t i2c_error = _bus->endTransmission();
+
+#if defined(SEN5X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
+ reClockI2C(currentClock, _bus, false);
+#endif
+
+ if (writtenBytes != bufferSize) {
+ LOG_ERROR("SEN5X: Error writting on I2C bus");
+ return false;
+ }
+
+ if (i2c_error != 0) {
+ LOG_ERROR("SEN5X: Error on I2C communication: %x", i2c_error);
+ return false;
+ }
+ return true;
+}
+
+uint8_t SEN5XSensor::readBuffer(uint8_t* buffer, uint8_t byteNumber)
+{
+#ifdef SEN5X_I2C_CLOCK_SPEED
+#ifdef CAN_RECLOCK_I2C
+ uint32_t currentClock = reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, false);
+#elif !HAS_SCREEN
+ reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, true);
+#else
+ LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
+ return false;
+#endif /* CAN_RECLOCK_I2C */
+#endif /* SEN5X_I2C_CLOCK_SPEED */
+
+ size_t readBytes = _bus->requestFrom(_address, byteNumber);
+ if (readBytes != byteNumber) {
+ LOG_ERROR("SEN5X: Error reading I2C bus");
+ return 0;
+ }
+
+ uint8_t i = 0;
+ uint8_t receivedBytes = 0;
+ while (readBytes > 0) {
+ buffer[i++] = _bus->read(); // Just as a reminder: i++ returns i and after that increments.
+ buffer[i++] = _bus->read();
+ uint8_t recvCRC = _bus->read();
+ uint8_t calcCRC = sen5xCRC(&buffer[i - 2]);
+ if (recvCRC != calcCRC) {
+ LOG_ERROR("SEN5X: Checksum error while receiving msg");
+ return 0;
+ }
+ readBytes -=3;
+ receivedBytes += 2;
+ }
+#if defined(SEN5X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
+ reClockI2C(currentClock, _bus, false);
+#endif
+
+ return receivedBytes;
+}
+
+uint8_t SEN5XSensor::sen5xCRC(uint8_t* buffer)
+{
+ // This code is based on Sensirion's own implementation https://github.com/Sensirion/arduino-core/blob/41fd02cacf307ec4945955c58ae495e56809b96c/src/SensirionCrc.cpp
+ uint8_t crc = 0xff;
+
+ for (uint8_t i=0; i<2; i++){
+
+ crc ^= buffer[i];
+
+ for (uint8_t bit=8; bit>0; bit--) {
+ if (crc & 0x80)
+ crc = (crc << 1) ^ 0x31;
+ else
+ crc = (crc << 1);
+ }
+ }
+
+ return crc;
+}
+
+void SEN5XSensor::sleep(){
+ // TODO Check this works
+ idle(true);
+}
+
+bool SEN5XSensor::idle(bool checkState)
+{
+ // From the datasheet:
+ // By default, the VOC algorithm resets its state to initial
+ // values each time a measurement is started,
+ // even if the measurement was stopped only for a short
+ // time. So, the VOC index output value needs a long time
+ // until it is stable again. This can be avoided by
+ // restoring the previously memorized algorithm state before
+ // starting the measure mode
+
+ if (checkState) {
+ // If the stabilisation period is not passed for SEN54 or SEN55, don't go to idle
+ if (model != SEN50) {
+ // Get VOC state before going to idle mode
+ vocValid = false;
+ if (vocStateFromSensor()) {
+ vocValid = vocStateValid();
+ // Check if we have time, and store it
+ uint32_t now; // If time is RTCQualityNone, it will return zero
+ now = getValidTime(RTCQuality::RTCQualityDevice);
+ if (now) {
+ // Check if state is valid (non-zero)
+ vocTime = now;
+ }
+ }
+
+ if (vocStateStable() && vocValid) {
+ saveState();
+ } else {
+ LOG_INFO("SEN5X: Not stopping measurement, vocState is not stable yet!");
+ return true;
+ }
+ }
+ }
+
+ if (!oneShotMode) {
+ LOG_INFO("SEN5X: Not stopping measurement, continuous mode!");
+ return true;
+ }
+
+ // Switch to low-power based on the model
+ if (model == SEN50) {
+ if (!sendCommand(SEN5X_STOP_MEASUREMENT)) {
+ LOG_ERROR("SEN5X: Error stopping measurement");
+ return false;
+ }
+ state = SEN5X_IDLE;
+ LOG_INFO("SEN5X: Stop measurement mode");
+ } else {
+ if (!sendCommand(SEN5X_START_MEASUREMENT_RHT_GAS)) {
+ LOG_ERROR("SEN5X: Error switching to RHT/Gas measurement");
+ return false;
+ }
+ state = SEN5X_RHTGAS_ONLY;
+ LOG_INFO("SEN5X: Switch to RHT/Gas only measurement mode");
+ }
+
+ delay(200); // From Sensirion Datasheet
+ pmMeasureStarted = 0;
+ return true;
+}
+
+bool SEN5XSensor::vocStateRecent(uint32_t now){
+ if (now) {
+ uint32_t passed = now - vocTime; //in seconds
+
+ // Check if state is recent, less than 10 minutes (600 seconds)
+ if (passed < SEN5X_VOC_VALID_TIME && (now > SEN5X_VOC_VALID_DATE)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool SEN5XSensor::vocStateValid() {
+ if (!vocState[0] && !vocState[1] && !vocState[2] && !vocState[3] &&
+ !vocState[4] && !vocState[5] && !vocState[6] && !vocState[7]) {
+ LOG_DEBUG("SEN5X: VOC state is all 0, invalid");
+ return false;
+ } else {
+ LOG_DEBUG("SEN5X: VOC state is valid");
+ return true;
+ }
+}
+
+bool SEN5XSensor::vocStateToSensor()
+{
+ if (model == SEN50){
+ return true;
+ }
+
+ if (!vocStateValid()) {
+ LOG_INFO("SEN5X: VOC state is invalid, not sending");
+ return true;
+ }
+
+ if (!sendCommand(SEN5X_STOP_MEASUREMENT)) {
+ LOG_ERROR("SEN5X: Error stoping measurement");
+ return false;
+ }
+ delay(200); // From Sensirion Datasheet
+
+ LOG_DEBUG("SEN5X: Sending VOC state to sensor");
+ LOG_DEBUG("[%u, %u, %u, %u, %u, %u, %u, %u]",
+ vocState[0],vocState[1], vocState[2], vocState[3],
+ vocState[4],vocState[5], vocState[6], vocState[7]);
+
+ // Note: send command already takes into account the CRC
+ // buffer size increment needed
+ if (!sendCommand(SEN5X_RW_VOCS_STATE, vocState, SEN5X_VOC_STATE_BUFFER_SIZE)){
+ LOG_ERROR("SEN5X: Error sending VOC's state command'");
+ return false;
+ }
+
+ return true;
+}
+
+bool SEN5XSensor::vocStateFromSensor()
+{
+ if (model == SEN50){
+ return true;
+ }
+
+ LOG_INFO("SEN5X: Getting VOC state from sensor");
+ // Ask VOCs state from the sensor
+ if (!sendCommand(SEN5X_RW_VOCS_STATE)){
+ LOG_ERROR("SEN5X: Error sending VOC's state command'");
+ return false;
+ }
+
+ delay(20); // From Sensirion Datasheet
+
+ // Retrieve the data
+ // Allocate buffer to account for CRC
+ // uint8_t vocBuffer[SEN5X_VOC_STATE_BUFFER_SIZE + (SEN5X_VOC_STATE_BUFFER_SIZE / 2)];
+ size_t receivedNumber = readBuffer(&vocState[0], SEN5X_VOC_STATE_BUFFER_SIZE + (SEN5X_VOC_STATE_BUFFER_SIZE / 2));
+ delay(20); // From Sensirion Datasheet
+
+ if (receivedNumber == 0) {
+ LOG_DEBUG("SEN5X: Error getting VOC's state");
+ return false;
+ }
+
+ // vocState[0] = vocBuffer[0];
+ // vocState[1] = vocBuffer[1];
+ // vocState[2] = vocBuffer[3];
+ // vocState[3] = vocBuffer[4];
+ // vocState[4] = vocBuffer[6];
+ // vocState[5] = vocBuffer[7];
+ // vocState[6] = vocBuffer[9];
+ // vocState[7] = vocBuffer[10];
+
+ // Print the state (if debug is on)
+ LOG_DEBUG("SEN5X: VOC state retrieved from sensor: [%u, %u, %u, %u, %u, %u, %u, %u]",
+ vocState[0],vocState[1], vocState[2], vocState[3],
+ vocState[4],vocState[5], vocState[6], vocState[7]);
+
+ return true;
+}
+
+bool SEN5XSensor::loadState()
+{
+#ifdef FSCom
+ spiLock->lock();
+ auto file = FSCom.open(sen5XStateFileName, FILE_O_READ);
+ bool okay = false;
+ if (file) {
+ LOG_INFO("%s state read from %s", sensorName, sen5XStateFileName);
+ pb_istream_t stream = {&readcb, &file, meshtastic_SEN5XState_size};
+
+ if (!pb_decode(&stream, &meshtastic_SEN5XState_msg, &sen5xstate)) {
+ LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream));
+ } else {
+ lastCleaning = sen5xstate.last_cleaning_time;
+ lastCleaningValid = sen5xstate.last_cleaning_valid;
+ oneShotMode = sen5xstate.one_shot_mode;
+
+ if (model != SEN50) {
+ vocTime = sen5xstate.voc_state_time;
+ vocValid = sen5xstate.voc_state_valid;
+ // Unpack state
+ vocState[7] = (uint8_t)(sen5xstate.voc_state_array >> 56);
+ vocState[6] = (uint8_t)(sen5xstate.voc_state_array >> 48);
+ vocState[5] = (uint8_t)(sen5xstate.voc_state_array >> 40);
+ vocState[4] = (uint8_t)(sen5xstate.voc_state_array >> 32);
+ vocState[3] = (uint8_t)(sen5xstate.voc_state_array >> 24);
+ vocState[2] = (uint8_t)(sen5xstate.voc_state_array >> 16);
+ vocState[1] = (uint8_t)(sen5xstate.voc_state_array >> 8);
+ vocState[0] = (uint8_t) sen5xstate.voc_state_array;
+ }
+
+ // LOG_DEBUG("Loaded lastCleaning %u", lastCleaning);
+ // LOG_DEBUG("Loaded lastCleaningValid %u", lastCleaningValid);
+ // LOG_DEBUG("Loaded oneShotMode %s", oneShotMode ? "true" : "false");
+ // LOG_DEBUG("Loaded vocTime %u", vocTime);
+ // LOG_DEBUG("Loaded [%u, %u, %u, %u, %u, %u, %u, %u]",
+ // vocState[7], vocState[6], vocState[5], vocState[4], vocState[3], vocState[2], vocState[1], vocState[0]);
+ // LOG_DEBUG("Loaded %svalid VOC state", vocValid ? "" : "in");
+
+ okay = true;
+ }
+ file.close();
+ } else {
+ LOG_INFO("No %s state found (File: %s)", sensorName, sen5XStateFileName);
+ }
+ spiLock->unlock();
+ return okay;
+#else
+ LOG_ERROR("SEN5X: ERROR - Filesystem not implemented");
+#endif
+}
+
+bool SEN5XSensor::saveState()
+{
+ // TODO - This should be called before a reboot for VOC index storage
+ // is there a way to get notified?
+#ifdef FSCom
+ auto file = SafeFile(sen5XStateFileName);
+
+ sen5xstate.last_cleaning_time = lastCleaning;
+ sen5xstate.last_cleaning_valid = lastCleaningValid;
+ sen5xstate.one_shot_mode = oneShotMode;
+
+ if (model != SEN50) {
+ sen5xstate.has_voc_state_time = true;
+ sen5xstate.has_voc_state_valid = true;
+ sen5xstate.has_voc_state_array = true;
+
+ sen5xstate.voc_state_time = vocTime;
+ sen5xstate.voc_state_valid = vocValid;
+ // Unpack state (8 bytes)
+ sen5xstate.voc_state_array = (((uint64_t) vocState[7]) << 56) |
+ ((uint64_t) vocState[6] << 48) |
+ ((uint64_t) vocState[5] << 40) |
+ ((uint64_t) vocState[4] << 32) |
+ ((uint64_t) vocState[3] << 24) |
+ ((uint64_t) vocState[2] << 16) |
+ ((uint64_t) vocState[1] << 8) |
+ ((uint64_t) vocState[0]);
+ }
+
+ bool okay = false;
+
+ LOG_INFO("%s: state write to %s", sensorName, sen5XStateFileName);
+ pb_ostream_t stream = {&writecb, static_cast(&file), meshtastic_SEN5XState_size};
+
+ if (!pb_encode(&stream, &meshtastic_SEN5XState_msg, &sen5xstate)) {
+ LOG_ERROR("Error: can't encode protobuf %s", PB_GET_ERROR(&stream));
+ } else {
+ okay = true;
+ }
+
+ okay &= file.close();
+
+ if (okay)
+ LOG_INFO("%s: state write to %s successful", sensorName, sen5XStateFileName);
+
+ return okay;
+#else
+ LOG_ERROR("%s: ERROR - Filesystem not implemented", sensorName);
+#endif
+}
+
+bool SEN5XSensor::isActive(){
+ return state == SEN5X_MEASUREMENT || state == SEN5X_MEASUREMENT_2;
+}
+
+uint32_t SEN5XSensor::wakeUp(){
+ // uint32_t now;
+ // now = getValidTime(RTCQuality::RTCQualityDevice);
+ LOG_DEBUG("SEN5X: Waking up sensor");
+
+ // NOTE - No need to send it everytime if we switch to RHT/gas only mode
+ // // Check if state is recent, less than 10 minutes (600 seconds)
+ // if (vocStateRecent(now) && vocStateValid()) {
+ // if (!vocStateToSensor()){
+ // LOG_ERROR("SEN5X: Sending VOC state to sensor failed");
+ // }
+ // } else {
+ // LOG_DEBUG("SEN5X: No valid VOC state found. Ignoring");
+ // }
+
+ if (!sendCommand(SEN5X_START_MEASUREMENT)) {
+ LOG_ERROR("SEN5X: Error starting measurement");
+ // TODO - what should this return?? Something actually on the default interval
+ return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
+ }
+ delay(50); // From Sensirion Datasheet
+
+ // TODO - This is currently "problematic"
+ // If time is updated in between reads, there is no way to
+ // keep track of how long it has passed
+ pmMeasureStarted = getTime();
+ state = SEN5X_MEASUREMENT;
+ if (state == SEN5X_MEASUREMENT)
+ LOG_INFO("SEN5X: Started measurement mode");
+ return SEN5X_WARMUP_MS_1;
+}
+
+bool SEN5XSensor::vocStateStable()
+{
+ uint32_t now;
+ now = getTime();
+ uint32_t sinceFirstMeasureStarted = (now - rhtGasMeasureStarted);
+ LOG_DEBUG("sinceFirstMeasureStarted: %us", sinceFirstMeasureStarted);
+ return sinceFirstMeasureStarted > SEN5X_VOC_STATE_WARMUP_S;
+}
+
+bool SEN5XSensor::startCleaning()
+{
+ // Note: we only should enter here if we have a valid RTC with at least
+ // RTCQuality::RTCQualityDevice
+ state = SEN5X_CLEANING;
+
+ // Note that cleaning command can only be run when the sensor is in measurement mode
+ if (!sendCommand(SEN5X_START_MEASUREMENT)) {
+ LOG_ERROR("SEN5X: Error starting measurment mode");
+ return false;
+ }
+ delay(50); // From Sensirion Datasheet
+
+ if (!sendCommand(SEN5X_START_FAN_CLEANING)) {
+ LOG_ERROR("SEN5X: Error starting fan cleaning");
+ return false;
+ }
+ delay(20); // From Sensirion Datasheet
+
+ // This message will be always printed so the user knows the device it's not hung
+ LOG_INFO("SEN5X: Started fan cleaning it will take 10 seconds...");
+
+ uint16_t started = millis();
+ while (millis() - started < 10500) {
+ // Serial.print(".");
+ delay(500);
+ }
+ LOG_INFO("SEN5X: Cleaning done!!");
+
+ // Save timestamp in flash so we know when a week has passed
+ uint32_t now;
+ now = getValidTime(RTCQuality::RTCQualityDevice);
+ // If time is not RTCQualityNone, it will return non-zero
+ lastCleaning = now;
+ lastCleaningValid = true;
+ saveState();
+
+ idle();
+ return true;
+}
+
+bool SEN5XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
+{
+ state = SEN5X_NOT_DETECTED;
+ LOG_INFO("Init sensor: %s", sensorName);
+
+ _bus = bus;
+ _address = dev->address.address;
+
+ delay(50); // without this there is an error on the deviceReset function
+
+ if (!sendCommand(SEN5X_RESET)) {
+ LOG_ERROR("SEN5X: Error reseting device");
+ return false;
+ }
+ delay(200); // From Sensirion Datasheet
+
+ if (!findModel()) {
+ LOG_ERROR("SEN5X: error finding sensor model");
+ return false;
+ }
+
+ // Check the firmware version
+ if (!getVersion()) return false;
+ if (firmwareVer < 2) {
+ LOG_ERROR("SEN5X: error firmware is too old and will not work with this implementation");
+ return false;
+ }
+ delay(200); // From Sensirion Datasheet
+
+ // Detection succeeded
+ state = SEN5X_IDLE;
+ status = 1;
+
+ // Load state
+ loadState();
+
+ // Check if it is time to do a cleaning
+ uint32_t now;
+ int32_t passed;
+ now = getValidTime(RTCQuality::RTCQualityDevice);
+
+ // If time is not RTCQualityNone, it will return non-zero
+ if (now) {
+ if (lastCleaningValid) {
+
+ passed = now - lastCleaning; // in seconds
+
+ if (passed > ONE_WEEK_IN_SECONDS && (now > SEN5X_VOC_VALID_DATE)) {
+ // If current date greater than 01/01/2018 (validity check)
+ LOG_INFO("SEN5X: More than a week (%us) since last cleaning in epoch (%us). Trigger, cleaning...", passed, lastCleaning);
+ startCleaning();
+ } else {
+ LOG_INFO("SEN5X: Cleaning not needed (%ds passed). Last cleaning date (in epoch): %us", passed, lastCleaning);
+ }
+ } else {
+ // We assume the device has just been updated or it is new,
+ // so no need to trigger a cleaning.
+ // Just save the timestamp to do a cleaning one week from now.
+ // Otherwise, we will never trigger cleaning in some cases
+ lastCleaning = now;
+ lastCleaningValid = true;
+ LOG_INFO("SEN5X: No valid last cleaning date found, saving it now: %us", lastCleaning);
+ saveState();
+ }
+
+ if (model != SEN50) {
+ if (!vocValid) {
+ LOG_INFO("SEN5X: No valid VOC's state found");
+ } else {
+ // Check if state is recent
+ if (vocStateRecent(now)) {
+ // If current date greater than 01/01/2018 (validity check)
+ // Send it to the sensor
+ LOG_INFO("SEN5X: VOC state is valid and recent");
+ vocStateToSensor();
+ } else {
+ LOG_INFO("SEN5X: VOC state is too old or date is invalid");
+ LOG_DEBUG("SEN5X: vocTime %u, Passed %u, and now %u", vocTime, passed, now);
+ }
+ }
+ }
+ } else {
+ // TODO - Should this actually ignore? We could end up never cleaning...
+ LOG_INFO("SEN5X: Not enough RTCQuality, ignoring saved state. Trying again later");
+ }
+
+ idle(false);
+ rhtGasMeasureStarted = now;
+
+ initI2CSensor();
+ return true;
+}
+
+bool SEN5XSensor::readValues()
+{
+ if (!sendCommand(SEN5X_READ_VALUES)){
+ LOG_ERROR("SEN5X: Error sending read command");
+ return false;
+ }
+ LOG_DEBUG("SEN5X: Reading PM Values");
+ delay(20); // From Sensirion Datasheet
+
+ uint8_t dataBuffer[16];
+ size_t receivedNumber = readBuffer(&dataBuffer[0], 24);
+ if (receivedNumber == 0) {
+ LOG_ERROR("SEN5X: Error getting values");
+ return false;
+ }
+
+ // Get the integers
+ uint16_t uint_pM1p0 = static_cast((dataBuffer[0] << 8) | dataBuffer[1]);
+ uint16_t uint_pM2p5 = static_cast((dataBuffer[2] << 8) | dataBuffer[3]);
+ uint16_t uint_pM4p0 = static_cast((dataBuffer[4] << 8) | dataBuffer[5]);
+ uint16_t uint_pM10p0 = static_cast((dataBuffer[6] << 8) | dataBuffer[7]);
+
+ int16_t int_humidity = static_cast((dataBuffer[8] << 8) | dataBuffer[9]);
+ int16_t int_temperature = static_cast((dataBuffer[10] << 8) | dataBuffer[11]);
+ int16_t int_vocIndex = static_cast((dataBuffer[12] << 8) | dataBuffer[13]);
+ int16_t int_noxIndex = static_cast((dataBuffer[14] << 8) | dataBuffer[15]);
+
+ // Convert values based on Sensirion Arduino lib
+ sen5xmeasurement.pM1p0 = !isnan(uint_pM1p0) ? uint_pM1p0 / 10 : UINT16_MAX;
+ sen5xmeasurement.pM2p5 = !isnan(uint_pM2p5) ? uint_pM2p5 / 10 : UINT16_MAX;
+ sen5xmeasurement.pM4p0 = !isnan(uint_pM4p0) ? uint_pM4p0 / 10 : UINT16_MAX;
+ sen5xmeasurement.pM10p0 = !isnan(uint_pM10p0) ? uint_pM10p0 / 10 : UINT16_MAX;
+ sen5xmeasurement.humidity = !isnan(int_humidity) ? int_humidity / 100.0f : FLT_MAX;
+ sen5xmeasurement.temperature = !isnan(int_temperature) ? int_temperature / 200.0f : FLT_MAX;
+ sen5xmeasurement.vocIndex = !isnan(int_vocIndex) ? int_vocIndex / 10.0f : FLT_MAX;
+ sen5xmeasurement.noxIndex = !isnan(int_noxIndex) ? int_noxIndex / 10.0f : FLT_MAX;
+
+ LOG_DEBUG("Got: pM1p0=%u, pM2p5=%u, pM4p0=%u, pM10p0=%u",
+ sen5xmeasurement.pM1p0, sen5xmeasurement.pM2p5,
+ sen5xmeasurement.pM4p0, sen5xmeasurement.pM10p0);
+
+ if (model != SEN50) {
+ LOG_DEBUG("Got: humidity=%.2f, temperature=%.2f, vocIndex=%.2f",
+ sen5xmeasurement.humidity, sen5xmeasurement.temperature,
+ sen5xmeasurement.vocIndex);
+ }
+
+ if (model == SEN55) {
+ LOG_DEBUG("Got: noxIndex=%.2f",
+ sen5xmeasurement.noxIndex);
+ }
+
+ return true;
+}
+
+bool SEN5XSensor::readPNValues(bool cumulative)
+{
+ if (!sendCommand(SEN5X_READ_PM_VALUES)){
+ LOG_ERROR("SEN5X: Error sending read command");
+ return false;
+ }
+
+ LOG_DEBUG("SEN5X: Reading PN Values");
+ delay(20); // From Sensirion Datasheet
+
+ uint8_t dataBuffer[20];
+ size_t receivedNumber = readBuffer(&dataBuffer[0], 30);
+ if (receivedNumber == 0) {
+ LOG_ERROR("SEN5X: Error getting PN values");
+ return false;
+ }
+
+ // Get the integers
+ // uint16_t uint_pM1p0 = static_cast((dataBuffer[0] << 8) | dataBuffer[1]);
+ // uint16_t uint_pM2p5 = static_cast((dataBuffer[2] << 8) | dataBuffer[3]);
+ // uint16_t uint_pM4p0 = static_cast((dataBuffer[4] << 8) | dataBuffer[5]);
+ // uint16_t uint_pM10p0 = static_cast((dataBuffer[6] << 8) | dataBuffer[7]);
+ uint16_t uint_pN0p5 = static_cast((dataBuffer[8] << 8) | dataBuffer[9]);
+ uint16_t uint_pN1p0 = static_cast((dataBuffer[10] << 8) | dataBuffer[11]);
+ uint16_t uint_pN2p5 = static_cast((dataBuffer[12] << 8) | dataBuffer[13]);
+ uint16_t uint_pN4p0 = static_cast((dataBuffer[14] << 8) | dataBuffer[15]);
+ uint16_t uint_pN10p0 = static_cast((dataBuffer[16] << 8) | dataBuffer[17]);
+ uint16_t uint_tSize = static_cast((dataBuffer[18] << 8) | dataBuffer[19]);
+
+ // Convert values based on Sensirion Arduino lib
+ // Multiply by 100 for converting from #/cm3 to #/0.1l for PN values
+ sen5xmeasurement.pN0p5 = !isnan(uint_pN0p5) ? uint_pN0p5 / 10 * 100 : UINT32_MAX;
+ sen5xmeasurement.pN1p0 = !isnan(uint_pN1p0) ? uint_pN1p0 / 10 * 100 : UINT32_MAX;
+ sen5xmeasurement.pN2p5 = !isnan(uint_pN2p5) ? uint_pN2p5 / 10 * 100 : UINT32_MAX;
+ sen5xmeasurement.pN4p0 = !isnan(uint_pN4p0) ? uint_pN4p0 / 10 * 100 : UINT32_MAX;
+ sen5xmeasurement.pN10p0 = !isnan(uint_pN10p0) ? uint_pN10p0 / 10 * 100 : UINT32_MAX;
+ sen5xmeasurement.tSize = !isnan(uint_tSize) ? uint_tSize / 1000.0f : FLT_MAX;
+
+ // Remove accumuluative values:
+ // https://github.com/fablabbcn/smartcitizen-kit-2x/issues/85
+ if (!cumulative) {
+ sen5xmeasurement.pN10p0 -= sen5xmeasurement.pN4p0;
+ sen5xmeasurement.pN4p0 -= sen5xmeasurement.pN2p5;
+ sen5xmeasurement.pN2p5 -= sen5xmeasurement.pN1p0;
+ sen5xmeasurement.pN1p0 -= sen5xmeasurement.pN0p5;
+ }
+
+ LOG_DEBUG("Got: pN0p5=%u, pN1p0=%u, pN2p5=%u, pN4p0=%u, pN10p0=%u, tSize=%.2f",
+ sen5xmeasurement.pN0p5, sen5xmeasurement.pN1p0,
+ sen5xmeasurement.pN2p5, sen5xmeasurement.pN4p0,
+ sen5xmeasurement.pN10p0, sen5xmeasurement.tSize
+ );
+
+ return true;
+}
+
+// TODO - Decide if we want to have this here or not
+// bool SEN5XSensor::readRawValues()
+// {
+// if (!sendCommand(SEN5X_READ_RAW_VALUES)){
+// LOG_ERROR("SEN5X: Error sending read command");
+// return false;
+// }
+// delay(20); // From Sensirion Datasheet
+
+// uint8_t dataBuffer[8];
+// size_t receivedNumber = readBuffer(&dataBuffer[0], 12);
+// if (receivedNumber == 0) {
+// LOG_ERROR("SEN5X: Error getting Raw values");
+// return false;
+// }
+
+// // Get values
+// rawHumidity = static_cast((dataBuffer[0] << 8) | dataBuffer[1]);
+// rawTemperature = static_cast((dataBuffer[2] << 8) | dataBuffer[3]);
+// rawVoc = static_cast((dataBuffer[4] << 8) | dataBuffer[5]);
+// rawNox = static_cast((dataBuffer[6] << 8) | dataBuffer[7]);
+
+// return true;
+// }
+
+uint8_t SEN5XSensor::getMeasurements()
+{
+ // Try to get new data
+ if (!sendCommand(SEN5X_READ_DATA_READY)){
+ LOG_ERROR("SEN5X: Error sending command data ready flag");
+ return 2;
+ }
+ delay(20); // From Sensirion Datasheet
+
+ uint8_t dataReadyBuffer[3];
+ size_t charNumber = readBuffer(&dataReadyBuffer[0], 3);
+ if (charNumber == 0) {
+ LOG_ERROR("SEN5X: Error getting device version value");
+ return 2;
+ }
+
+ bool data_ready = dataReadyBuffer[1];
+
+ if (!data_ready) {
+ LOG_INFO("SEN5X: Data is not ready");
+ return 1;
+ }
+
+ if(!readValues()) {
+ LOG_ERROR("SEN5X: Error getting readings");
+ return 2;
+ }
+
+ if(!readPNValues(false)) {
+ LOG_ERROR("SEN5X: Error getting PN readings");
+ return 2;
+ }
+
+ // if(!readRawValues()) {
+ // LOG_ERROR("SEN5X: Error getting Raw readings");
+ // return 2;
+ // }
+
+ return 0;
+}
+
+int32_t SEN5XSensor::wakeUpTimeMs()
+{
+ return SEN5X_WARMUP_MS_2;
+}
+
+int32_t SEN5XSensor::pendingForReadyMs(){
+ uint32_t now;
+ now = getTime();
+ uint32_t sincePmMeasureStarted = (now - pmMeasureStarted)*1000;
+ LOG_DEBUG("SEN5X: Since measure started: %ums", sincePmMeasureStarted);
+
+ switch (state) {
+ case SEN5X_MEASUREMENT: {
+
+ if (sincePmMeasureStarted < SEN5X_WARMUP_MS_1) {
+ LOG_INFO("SEN5X: not enough time passed since starting measurement");
+ return SEN5X_WARMUP_MS_1 - sincePmMeasureStarted;
+ }
+
+ if (!pmMeasureStarted) {
+ pmMeasureStarted = now;
+ }
+
+ // Get PN values to check if we are above or below threshold
+ readPNValues(true);
+
+ // If the reading is low (the tyhreshold is in #/cm3) and second warmUp hasn't passed we return to come back later
+ if ((sen5xmeasurement.pN4p0 / 100) < SEN5X_PN4P0_CONC_THD && sincePmMeasureStarted < SEN5X_WARMUP_MS_2) {
+ LOG_INFO("SEN5X: Concentration is low, we will ask again in the second warm up period");
+ state = SEN5X_MEASUREMENT_2;
+ // Report how many seconds are pending to cover the first warm up period
+ return SEN5X_WARMUP_MS_2 - sincePmMeasureStarted;
+ }
+ return 0;
+ }
+ case SEN5X_MEASUREMENT_2: {
+ if (sincePmMeasureStarted < SEN5X_WARMUP_MS_2) {
+ // Report how many seconds are pending to cover the first warm up period
+ return SEN5X_WARMUP_MS_2 - sincePmMeasureStarted;
+ }
+ return 0;
+ }
+ default: {
+ return -1;
+ }
+ }
+}
+
+bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement)
+{
+ LOG_INFO("SEN5X: Attempting to get metrics");
+ if (!isActive()){
+ LOG_INFO("SEN5X: not in measurement mode");
+ return false;
+ }
+
+ uint8_t response;
+ response = getMeasurements();
+
+ if (response == 0) {
+ measurement->variant.air_quality_metrics.has_sen5xdata = true;
+ if (sen5xmeasurement.pM1p0 != UINT16_MAX) {
+ measurement->variant.air_quality_metrics.sen5xdata.has_pm10_standard = true;
+ measurement->variant.air_quality_metrics.sen5xdata.pm10_standard = sen5xmeasurement.pM1p0;
+ }
+ if (sen5xmeasurement.pM2p5 != UINT16_MAX) {
+ measurement->variant.air_quality_metrics.sen5xdata.has_pm25_standard = true;
+ measurement->variant.air_quality_metrics.sen5xdata.pm25_standard = sen5xmeasurement.pM2p5;
+ }
+ if (sen5xmeasurement.pM4p0 != UINT16_MAX) {
+ measurement->variant.air_quality_metrics.sen5xdata.has_pm40_standard = true;
+ measurement->variant.air_quality_metrics.sen5xdata.pm40_standard = sen5xmeasurement.pM4p0;
+ }
+ if (sen5xmeasurement.pM10p0 != UINT16_MAX) {
+ measurement->variant.air_quality_metrics.sen5xdata.has_pm100_standard = true;
+ measurement->variant.air_quality_metrics.sen5xdata.pm100_standard = sen5xmeasurement.pM10p0;
+ }
+ if (sen5xmeasurement.pN0p5 != UINT32_MAX) {
+ measurement->variant.air_quality_metrics.sen5xdata.has_particles_05um = true;
+ measurement->variant.air_quality_metrics.sen5xdata.particles_05um = sen5xmeasurement.pN0p5;
+ }
+ if (sen5xmeasurement.pN1p0 != UINT32_MAX) {
+ measurement->variant.air_quality_metrics.sen5xdata.has_particles_10um = true;
+ measurement->variant.air_quality_metrics.sen5xdata.particles_10um = sen5xmeasurement.pN1p0;
+ }
+ if (sen5xmeasurement.pN2p5 != UINT32_MAX) {
+ measurement->variant.air_quality_metrics.sen5xdata.has_particles_25um = true;
+ measurement->variant.air_quality_metrics.sen5xdata.particles_25um = sen5xmeasurement.pN2p5;
+ }
+ if (sen5xmeasurement.pN4p0 != UINT32_MAX) {
+ measurement->variant.air_quality_metrics.sen5xdata.has_particles_40um = true;
+ measurement->variant.air_quality_metrics.sen5xdata.particles_40um = sen5xmeasurement.pN4p0;
+ }
+ if (sen5xmeasurement.pN10p0 != UINT32_MAX) {
+ measurement->variant.air_quality_metrics.sen5xdata.has_particles_100um = true;
+ measurement->variant.air_quality_metrics.sen5xdata.particles_100um = sen5xmeasurement.pN10p0;
+ }
+ if (sen5xmeasurement.tSize != FLT_MAX) {
+ measurement->variant.air_quality_metrics.sen5xdata.has_particles_tps = true;
+ measurement->variant.air_quality_metrics.sen5xdata.particles_tps = sen5xmeasurement.tSize;
+ }
+
+ if (model != SEN50) {
+ if (sen5xmeasurement.humidity!= FLT_MAX) {
+ measurement->variant.air_quality_metrics.sen5xdata.has_pm_humidity = true;
+ measurement->variant.air_quality_metrics.sen5xdata.pm_humidity = sen5xmeasurement.humidity;
+ }
+ if (sen5xmeasurement.temperature!= FLT_MAX) {
+ measurement->variant.air_quality_metrics.sen5xdata.has_pm_temperature = true;
+ measurement->variant.air_quality_metrics.sen5xdata.pm_temperature = sen5xmeasurement.temperature;
+ }
+ if (sen5xmeasurement.noxIndex!= FLT_MAX) {
+ measurement->variant.air_quality_metrics.sen5xdata.has_pm_voc_idx = true;
+ measurement->variant.air_quality_metrics.sen5xdata.pm_voc_idx = sen5xmeasurement.vocIndex;
+ }
+ }
+
+ if (model == SEN55) {
+ if (sen5xmeasurement.noxIndex!= FLT_MAX) {
+ measurement->variant.air_quality_metrics.sen5xdata.has_pm_nox_idx = true;
+ measurement->variant.air_quality_metrics.sen5xdata.pm_nox_idx = sen5xmeasurement.noxIndex;
+ }
+ }
+
+
+ return true;
+ } else if (response == 1) {
+ // TODO return because data was not ready yet
+ // Should this return false?
+ idle();
+ return false;
+ } else if (response == 2) {
+ // Return with error for non-existing data
+ idle();
+ return false;
+ }
+
+ return true;
+}
+
+void SEN5XSensor::setMode(bool setOneShot) {
+ oneShotMode = setOneShot;
+}
+
+AdminMessageHandleResult SEN5XSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
+ meshtastic_AdminMessage *response)
+{
+ AdminMessageHandleResult result;
+ result = AdminMessageHandleResult::NOT_HANDLED;
+
+
+ switch (request->which_payload_variant) {
+ case meshtastic_AdminMessage_sensor_config_tag:
+ if (!request->sensor_config.has_sen5x_config) {
+ result = AdminMessageHandleResult::NOT_HANDLED;
+ break;
+ }
+
+ // TODO - Add admin command to set temperature offset
+ // Check for temperature offset
+ // if (request->sensor_config.sen5x_config.has_set_temperature) {
+ // this->setTemperature(request->sensor_config.sen5x_config.set_temperature);
+ // }
+
+ // Check for one-shot/continuous mode request
+ if (request->sensor_config.sen5x_config.has_set_one_shot_mode) {
+ this->setMode(request->sensor_config.sen5x_config.set_one_shot_mode);
+ }
+
+ result = AdminMessageHandleResult::HANDLED;
+ break;
+
+ default:
+ result = AdminMessageHandleResult::NOT_HANDLED;
+ }
+
+ return result;
+}
+
+#endif
\ No newline at end of file
diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.h b/src/modules/Telemetry/Sensor/SEN5XSensor.h
new file mode 100644
index 00000000000..91d7724ca23
--- /dev/null
+++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h
@@ -0,0 +1,161 @@
+#include "configuration.h"
+
+#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
+
+#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "TelemetrySensor.h"
+#include "Wire.h"
+#include "RTC.h"
+
+// Warm up times for SEN5X from the datasheet
+#ifndef SEN5X_WARMUP_MS_1
+#define SEN5X_WARMUP_MS_1 15000
+#endif
+
+#ifndef SEN5X_WARMUP_MS_2
+#define SEN5X_WARMUP_MS_2 30000
+#endif
+
+#ifndef SEN5X_I2C_CLOCK_SPEED
+#define SEN5X_I2C_CLOCK_SPEED 100000
+#endif
+
+/*
+Time after which the sensor can go to sleep, as the warmup period has passed
+and the VOCs sensor will is allowed to stop (although needs to recover the state
+each time)
+*/
+#ifndef SEN5X_VOC_STATE_WARMUP_S
+/* Note for Testing 5' is enough
+Sensirion recommends 1h
+This can be bypassed completely if switching to low-power RHT/Gas mode and setting
+SEN5X_VOC_STATE_WARMUP_S 0
+*/
+#define SEN5X_VOC_STATE_WARMUP_S 3600
+#endif
+
+#define ONE_WEEK_IN_SECONDS 604800
+
+struct _SEN5XMeasurements {
+ uint16_t pM1p0;
+ uint16_t pM2p5;
+ uint16_t pM4p0;
+ uint16_t pM10p0;
+ uint32_t pN0p5;
+ uint32_t pN1p0;
+ uint32_t pN2p5;
+ uint32_t pN4p0;
+ uint32_t pN10p0;
+ float tSize;
+ float humidity;
+ float temperature;
+ float vocIndex;
+ float noxIndex;
+};
+
+class SEN5XSensor : public TelemetrySensor
+{
+ private:
+ TwoWire * _bus{};
+ uint8_t _address{};
+
+ bool getVersion();
+ float firmwareVer = -1;
+ float hardwareVer = -1;
+ float protocolVer = -1;
+ bool findModel();
+
+ // Commands
+ #define SEN5X_RESET 0xD304
+ #define SEN5X_GET_PRODUCT_NAME 0xD014
+ #define SEN5X_GET_FIRMWARE_VERSION 0xD100
+ #define SEN5X_START_MEASUREMENT 0x0021
+ #define SEN5X_START_MEASUREMENT_RHT_GAS 0x0037
+ #define SEN5X_STOP_MEASUREMENT 0x0104
+ #define SEN5X_READ_DATA_READY 0x0202
+ #define SEN5X_START_FAN_CLEANING 0x5607
+ #define SEN5X_RW_VOCS_STATE 0x6181
+
+ #define SEN5X_READ_VALUES 0x03C4
+ #define SEN5X_READ_RAW_VALUES 0x03D2
+ #define SEN5X_READ_PM_VALUES 0x0413
+
+ #define SEN5X_VOC_VALID_TIME 600
+ #define SEN5X_VOC_VALID_DATE 1514764800
+
+ enum SEN5Xmodel { SEN5X_UNKNOWN = 0, SEN50 = 0b001, SEN54 = 0b010, SEN55 = 0b100 };
+ SEN5Xmodel model = SEN5X_UNKNOWN;
+
+ enum SEN5XState { SEN5X_OFF, SEN5X_IDLE, SEN5X_RHTGAS_ONLY, SEN5X_MEASUREMENT, SEN5X_MEASUREMENT_2, SEN5X_CLEANING, SEN5X_NOT_DETECTED };
+ SEN5XState state = SEN5X_OFF;
+ // Flag to work on one-shot (read and sleep), or continuous mode
+ bool oneShotMode = true;
+ void setMode(bool setOneShot);
+ bool vocStateValid();
+ /* Sensirion recommends taking a reading after 15 seconds,
+ if the Particle number reading is over 100#/cm3 the reading is OK,
+ but if it is lower wait until 30 seconds and take it again.
+ See: https://sensirion.com/resource/application_note/low_power_mode/sen5x
+ */
+ #define SEN5X_PN4P0_CONC_THD 100
+
+ bool sendCommand(uint16_t command);
+ bool sendCommand(uint16_t command, uint8_t* buffer, uint8_t byteNumber=0);
+ uint8_t readBuffer(uint8_t* buffer, uint8_t byteNumber); // Return number of bytes received
+ uint8_t sen5xCRC(uint8_t* buffer);
+ bool startCleaning();
+ uint8_t getMeasurements();
+ // bool readRawValues();
+ bool readPNValues(bool cumulative);
+ bool readValues();
+
+ uint32_t pmMeasureStarted = 0;
+ uint32_t rhtGasMeasureStarted = 0;
+ _SEN5XMeasurements sen5xmeasurement {};
+
+ bool idle(bool checkState=true);
+
+ protected:
+ // Store status of the sensor in this file
+ const char *sen5XStateFileName = "/prefs/sen5X.dat";
+ meshtastic_SEN5XState sen5xstate = meshtastic_SEN5XState_init_zero;
+
+ bool loadState();
+ bool saveState();
+
+ // Cleaning State
+ uint32_t lastCleaning = 0;
+ bool lastCleaningValid = false;
+
+ // VOC State
+ #define SEN5X_VOC_STATE_BUFFER_SIZE 8
+ uint8_t vocState[SEN5X_VOC_STATE_BUFFER_SIZE] {};
+ uint32_t vocTime = 0;
+ bool vocValid = false;
+
+ bool vocStateFromSensor();
+ bool vocStateToSensor();
+ bool vocStateStable();
+ bool vocStateRecent(uint32_t now);
+
+ public:
+
+ SEN5XSensor();
+ virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
+ virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
+
+ virtual bool isActive() override;
+ virtual void sleep() override;
+ virtual uint32_t wakeUp() override;
+ virtual bool canSleep() override { return true; }
+ virtual int32_t wakeUpTimeMs() override;
+ virtual int32_t pendingForReadyMs() override;
+
+ // TODO - Add a way to take averages of samples
+ // This value represents the time needed for pending data
+ AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) override;
+};
+
+
+
+#endif
\ No newline at end of file
diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.h b/src/modules/Telemetry/Sensor/TelemetrySensor.h
index af51ddfad5a..9c745422c04 100644
--- a/src/modules/Telemetry/Sensor/TelemetrySensor.h
+++ b/src/modules/Telemetry/Sensor/TelemetrySensor.h
@@ -26,7 +26,6 @@ class TelemetrySensor
this->status = 0;
}
- const char *sensorName;
meshtastic_TelemetrySensorType sensorType = meshtastic_TelemetrySensorType_SENSOR_UNSET;
unsigned status;
bool initialized = false;
@@ -56,13 +55,18 @@ class TelemetrySensor
return AdminMessageHandleResult::NOT_HANDLED;
}
+ const char *sensorName;
// TODO: delete after migration
bool hasSensor() { return nodeTelemetrySensorsMap[sensorType].first > 0; }
+
// Functions to sleep / wakeup sensors that support it
- virtual void sleep(){};
+ // These functions can save power consumption in cases like AQ
+ virtual void sleep() {};
virtual uint32_t wakeUp() { return 0; }
- // Return active by default, override per sensor
- virtual bool isActive() { return true; }
+ virtual bool isActive() { return true; } // Return true by default, override per sensor
+ virtual bool canSleep() { return false; } // Return false by default, override per sensor
+ virtual int32_t wakeUpTimeMs() { return 0; }
+ virtual int32_t pendingForReadyMs() { return 0; }
#if WIRE_INTERFACES_COUNT > 1
// Set to true if Implementation only works first I2C port (Wire)
diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp
index fc1f27ea29c..2a59c0aab3e 100644
--- a/src/nimble/NimbleBluetooth.cpp
+++ b/src/nimble/NimbleBluetooth.cpp
@@ -686,6 +686,9 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
#ifdef NIMBLE_TWO
if (ble->isDeInit)
return;
+#else
+ if (nimbleBluetooth && nimbleBluetooth->isDeInit)
+ return;
#endif
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED);
diff --git a/src/serialization/MeshPacketSerializer.cpp b/src/serialization/MeshPacketSerializer.cpp
index a12972cb002..b2ea945498b 100644
--- a/src/serialization/MeshPacketSerializer.cpp
+++ b/src/serialization/MeshPacketSerializer.cpp
@@ -140,26 +140,38 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
msgPayload["soil_temperature"] = new JSONValue(decoded->variant.environment_metrics.soil_temperature);
}
} else if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) {
- if (decoded->variant.air_quality_metrics.has_pm10_standard) {
- msgPayload["pm10"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_standard);
- }
- if (decoded->variant.air_quality_metrics.has_pm25_standard) {
- msgPayload["pm25"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_standard);
- }
- if (decoded->variant.air_quality_metrics.has_pm100_standard) {
- msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard);
- }
- if (decoded->variant.air_quality_metrics.has_pm10_environmental) {
- msgPayload["pm10_e"] =
- new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental);
- }
- if (decoded->variant.air_quality_metrics.has_pm25_environmental) {
- msgPayload["pm25_e"] =
- new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental);
- }
- if (decoded->variant.air_quality_metrics.has_pm100_environmental) {
- msgPayload["pm100_e"] =
- new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental);
+ if (decoded->variant.air_quality_metrics.has_sen5xdata) {
+ if (decoded->variant.air_quality_metrics.sen5xdata.has_pm10_standard) {
+ msgPayload["pm10"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.sen5xdata.pm10_standard);
+ }
+ if (decoded->variant.air_quality_metrics.sen5xdata.has_pm25_standard) {
+ msgPayload["pm25"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.sen5xdata.pm25_standard);
+ }
+ if (decoded->variant.air_quality_metrics.sen5xdata.has_pm100_standard) {
+ msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.sen5xdata.pm100_standard);
+ }
+ }
+ if (decoded->variant.air_quality_metrics.has_pmsa003idata) {
+ if (decoded->variant.air_quality_metrics.pmsa003idata.has_pm10_standard) {
+ msgPayload["pm10"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pmsa003idata.pm10_standard);
+ }
+ if (decoded->variant.air_quality_metrics.pmsa003idata.has_pm25_standard) {
+ msgPayload["pm25"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pmsa003idata.pm25_standard);
+ }
+ if (decoded->variant.air_quality_metrics.pmsa003idata.has_pm100_standard) {
+ msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pmsa003idata.pm100_standard);
+ }
+ }
+ if (decoded->variant.air_quality_metrics.has_scd4xdata) {
+ if (decoded->variant.air_quality_metrics.scd4xdata.has_co2) {
+ msgPayload["co2"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.scd4xdata.co2);
+ }
+ if (decoded->variant.air_quality_metrics.scd4xdata.has_co2_temperature) {
+ msgPayload["co2_temperature"] = new JSONValue(decoded->variant.air_quality_metrics.scd4xdata.co2_temperature);
+ }
+ if (decoded->variant.air_quality_metrics.scd4xdata.has_co2_humidity) {
+ msgPayload["co2_humidity"] = new JSONValue(decoded->variant.air_quality_metrics.scd4xdata.co2_humidity);
+ }
}
} else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) {
if (decoded->variant.power_metrics.has_ch1_voltage) {
diff --git a/src/serialization/MeshPacketSerializer_nRF52.cpp b/src/serialization/MeshPacketSerializer_nRF52.cpp
index 41f505b94e5..e7e07fc5e94 100644
--- a/src/serialization/MeshPacketSerializer_nRF52.cpp
+++ b/src/serialization/MeshPacketSerializer_nRF52.cpp
@@ -111,23 +111,38 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
jsonObj["payload"]["radiation"] = decoded->variant.environment_metrics.radiation;
}
} else if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) {
- if (decoded->variant.air_quality_metrics.has_pm10_standard) {
- jsonObj["payload"]["pm10"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_standard;
+ if (decoded->variant.air_quality_metrics.has_sen5xdata) {
+ if (decoded->variant.air_quality_metrics.sen5xdata.has_pm10_standard) {
+ jsonObj["payload"]["pm10"] = (unsigned int)decoded->variant.air_quality_metrics.sen5xdata.pm10_standard;
+ }
+ if (decoded->variant.air_quality_metrics.sen5xdata.has_pm25_standard) {
+ jsonObj["payload"]["pm25"] = (unsigned int)decoded->variant.air_quality_metrics.sen5xdata.pm25_standard;
+ }
+ if (decoded->variant.air_quality_metrics.sen5xdata.has_pm100_standard) {
+ jsonObj["payload"]["pm100"] = (unsigned int)decoded->variant.air_quality_metrics.sen5xdata.pm100_standard;
+ }
}
- if (decoded->variant.air_quality_metrics.has_pm25_standard) {
- jsonObj["payload"]["pm25"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_standard;
+ if (decoded->variant.air_quality_metrics.has_pmsa003idata) {
+ if (decoded->variant.air_quality_metrics.pmsa003idata.has_pm10_standard) {
+ jsonObj["payload"]["pm10"] = (unsigned int)decoded->variant.air_quality_metrics.pmsa003idata.pm10_standard;
+ }
+ if (decoded->variant.air_quality_metrics.pmsa003idata.has_pm25_standard) {
+ jsonObj["payload"]["pm25"] = (unsigned int)decoded->variant.air_quality_metrics.pmsa003idata.pm25_standard;
+ }
+ if (decoded->variant.air_quality_metrics.pmsa003idata.has_pm100_standard) {
+ jsonObj["payload"]["pm100"] = (unsigned int)decoded->variant.air_quality_metrics.pmsa003idata.pm100_standard;
+ }
}
- if (decoded->variant.air_quality_metrics.has_pm100_standard) {
- jsonObj["payload"]["pm100"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_standard;
- }
- if (decoded->variant.air_quality_metrics.has_pm10_environmental) {
- jsonObj["payload"]["pm10_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_environmental;
- }
- if (decoded->variant.air_quality_metrics.has_pm25_environmental) {
- jsonObj["payload"]["pm25_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_environmental;
- }
- if (decoded->variant.air_quality_metrics.has_pm100_environmental) {
- jsonObj["payload"]["pm100_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_environmental;
+ if (decoded->variant.air_quality_metrics.has_scd4xdata) {
+ if (decoded->variant.air_quality_metrics.scd4xdata.has_co2) {
+ jsonObj["payload"]["co2"] = (unsigned int)decoded->variant.air_quality_metrics.scd4xdata.co2;
+ }
+ if (decoded->variant.air_quality_metrics.scd4xdata.has_co2_temperature) {
+ jsonObj["payload"]["co2_temperature"] = (unsigned int)decoded->variant.air_quality_metrics.scd4xdata.co2_temperature;
+ }
+ if (decoded->variant.air_quality_metrics.scd4xdata.has_co2_humidity) {
+ jsonObj["payload"]["co2_humidity"] = (unsigned int)decoded->variant.air_quality_metrics.scd4xdata.co2has_co2_humidity;
+ }
}
} else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) {
if (decoded->variant.power_metrics.has_ch1_voltage) {
diff --git a/variants/native/portduino.ini b/variants/native/portduino.ini
index b86420291c5..f99ee9a6ffa 100644
--- a/variants/native/portduino.ini
+++ b/variants/native/portduino.ini
@@ -29,7 +29,7 @@ lib_deps =
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19
# renovate: datasource=git-refs depName=libch341-spi-userspace packageName=https://github.com/pine64/libch341-spi-userspace gitBranch=main
- https://github.com/pine64/libch341-spi-userspace/archive/23c42319a69cffcb65868e3c72e6bed83974a393.zip
+ https://github.com/pine64/libch341-spi-userspace/archive/af9bc27c9c30fa90772279925b7c5913dff789b4.zip
# renovate: datasource=custom.pio depName=adafruit/Adafruit seesaw Library packageName=adafruit/library/Adafruit seesaw Library
adafruit/Adafruit seesaw Library@1.7.9
# renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main