Feat/add sen5x (#7245)

* Move PMSA003I to separate class and update AQ telemetry

* AirQualityTelemetry module not depend on PM sensor presence

* Remove commented line

* Fixes on PMS class

* Add missing warmup period to wakeUp function

* Fixes on compilation for different variants

* Add functions to check for I2C bus speed and set it

* Add ScreenFonts.h

Co-authored-by: Hannes Fuchs <hannes.fuchs+git@0xef.de>

* PMSA003I 1st round test

* Fix I2C scan speed

* Fix minor issues and bring back I2C SPEED def

* Remove PMSA003I library as its no longer needed

* Remove unused I2C speed functions and cleanup

* Cleanup of SEN5X specific code added from switching branches
* Remove SCAN_I2C_CLOCK_SPEED block as its not needed
* Remove associated functions for setting I2C speed

* Unify build epoch to add flag in platformio-custom.py (#7917)

* Unify build_epoch replacement logic in platformio-custom

* Missed one

* Fix build error in rak_wismesh_tap_v2 (#7905)

In the logs was:
"No screen resolution defined in build_flags. Please define DISPLAY_SIZE."

set according to similar devices.

* Put guards in place around debug heap operations (#7955)

* Put guards in place around debug heap operations

* Add macros to clean up code

* Add pointer as well

* Cleanup

* Fix memory leak in NextHopRouter: always free packet copy when removing from pending

* Formatting

* Only queue 2 client notification

* Merge pull request #7965 from compumike/compumike/fix-nrf52-bluetooth-memory-leak

Fix memory leak in `NRF52Bluetooth`: allocate `BluetoothStatus` on stack, not heap

* Merge pull request #7964 from compumike/compumike/fix-nimble-bluetooth-memory-leak

Fix memory leak in `NimbleBluetooth`: allocate `BluetoothStatus` on stack, not heap

* Update protobufs (#7973)

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* T-Lora Pager: Support LR1121 and SX1280 models (#7956)

* T-Lora Pager: Support LR1121 and SX1280 models

* Remove ifdefs

* Trunk

* Trunk

* Static memory pool allocation (#7966)

* Static memory pool

* Initializer

* T-Lora Pager: Support LR1121 and SX1280 models (#7956)

* T-Lora Pager: Support LR1121 and SX1280 models

* Remove ifdefs

---------

Co-authored-by: WillyJL <me@willyjl.dev>

* Portduino dynamic alloc

* Missed

* Drop the limit

* Update meshtastic-esp8266-oled-ssd1306 digest to 0cbc26b (#7977)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Fix json report crashes on esp32 (#7978)

* Tweak maximums

* Fix DRAM overflow on old esp32 targets

* Guard bad time warning logs using GPS_DEBUG (#7897)

In 2.7.7 / 2.7.8 we introduced some new checks for time accuracy.

In combination, these result in a spamming of the logs when a bad time is found

When the GPS is active, we're calling the GPS thread every 0.2secs.

So this log could be printed 4,500 times in a no-lock scenario :)

Reserve this experience for developers using GPS_DEBUG.

Fixes https://github.com/meshtastic/firmware/issues/7896

* Scale probe buffer size based on current baud rate (#7975)

* Scale probe buffer size based on current baud rate

* Throttle bad time validation logging and fix time comparison logic

* Remove comment

* Missed the other instances

* Copy pasta

* Fix GPS gm_mktime memory leak (#7981)

* Fix overflow of time value (#7984)

* Fix overflow of time value

* Revert "Fix overflow of time value"

This reverts commit 084796920179e80a7500d36c25fd4d82b3ef4214.

* That got boogered up

* Remove PMSA003 include from modules

* Add flag to exclude air quality module

* Rework PMSA003I to align with new I2C scanner

* Reworks AQ telemetry to match new dynamic allocation method
* Adds VBLE_I2C_CLOCK_SPEED build flag for sensors with different I2C speed requirements
* Reworks PMSA003I

* Move add sensor template to separate file

* Split telemetry on screen options

* Add variable I2C clock compile flag

* Added to Seeed Xiao S3 as demo

* Fix drawFrame in AQ module

* Module settings override to i2cScan module function

* Move to CAN_RECLOCK_I2C per architecture

* Add reclock function in TelemetrySensor.cpp
* Add flag in ESP32 common

* Minor fix

* Move I2C reclock function to src/detect

* Fix uninitMemberVar errors and compile issue

* Make sleep, wakeUp functions generic

* Fix STM32 builds

* Add exclude AQ sensor to builds that have environmental sensor excludes
* Add includes to AddI2CSensorTemplate.h

* SEN5X first pass

* WIP Sen5X functions

* Further (non-working) progress in SEN5X

* WIP Sen5X functions

* Changes on SEN5X library - removing pm_env as well

* Small cleanup of SEN5X sensors

* Minor change for SEN5X detection

* Remove dup code

* Enable PM sensor before sending telemetry.

This enables the PM sensor for a predefined period to allow for warmup.

Once telemetry is sent, the sensor shuts down again.

* Small cleanups in SEN5X sensor

* Add dynamic measurement interval for SEN5X

* Only disable SEN5X if enough time after reading.

* Idle for SEN5X on communication error

* Cleanup of logs and remove unnecessary delays

* Small TODO

* Settle on uint16_t for SEN5X PM data

* Make AQTelemetry sensors non-exclusive

* Implementation of cleaning in FS prefs and cleanup

* Remove unnecessary LOGS
* Add cleaning date storage in FS
* Report non-cumulative PN

* Bring back detection code for SEN5X after branch rebase

* Add placeholder for admin message

* Add VOC measurements and persistence (WIP)

* Adds VOC measurements and state
* Still not working on VOC Index persistence
* Should it stay in continuous mode?

* Add one-shot mode config flag to SEN5X

* Add nan checks on sensor data from SEN5X

* Working implementation on VOCState

* Adds initial timer for SEN55 to not sleep if VOCstate is not stable (1h)
* Adds conditions for stability and sensor state

* Fixes on VOC state and mode swtiching

* Adds a new RHT/Gas only mode, with 3600s stabilization time
* Fixes the VOCState buffer mismatch
* Fixes SEN50/54/55 model mistake

* Adapt SEN5X to new sensor list structure. Improve reclock.

* Improve reClockI2C conditions for different variants
* Add sleep, wakeUp, pendingForReady, hasSleep functions to PM sensors to save battery
* Add SEN5X

* Fix merge errors

* Update library dependencies in platformio.ini

* Fix unitialized variables in SEN5X constructor

* Fix missing import

* Cleanup of SEN5X class

* Exclude AQ sensor from wio-e5 due to flash limitations

* Fix I2C clock change logic

* Fix trunk

* Fix on condition in reclock

* Add check on polling interval of sen5x

---------

Co-authored-by: Hannes Fuchs <hannes.fuchs+git@0xef.de>
Co-authored-by: Nashui-Yan <yannashui10@gmail.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Tom Fifield <tom@tomfifield.net>
Co-authored-by: Mike Robbins <mrobbins@alum.mit.edu>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
Co-authored-by: WillyJL <me@willyjl.dev>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
This commit is contained in:
oscgonfer
2026-02-08 07:08:09 -06:00
committed by GitHub
co-authored by GitHub Hannes Fuchs Nashui-Yan Ben Meadors Tom Fifield Mike Robbins github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> thebentern WillyJL renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
parent 53231ae4b1
commit 7cbab4838c
16 changed files with 1401 additions and 113 deletions
+2 -1
View File
@@ -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
sensirion/Sensirion I2C SCD4x@1.1.0
+1
View File
@@ -241,6 +241,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define BQ27220_ADDR 0x55 // same address as TDECK_KB
#define BQ25896_ADDR 0x6B
#define LTR553ALS_ADDR 0x23
#define SEN5X_ADDR 0x69
// -----------------------------------------------------------------------------
// ACCELEROMETER
+2 -1
View File
@@ -88,7 +88,8 @@ class ScanI2C
BH1750,
DA217,
CHSC6X,
CST226SE
CST226SE,
SEN5X
} DeviceType;
// typedef uint8_t DeviceAddress;
+66 -8
View File
@@ -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,45 @@ 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 +608,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 +621,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;
+31
View File
@@ -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;
}
+7 -37
View File
@@ -1,41 +1,11 @@
#ifdef CAN_RECLOCK_I2C
#ifndef RECLOCK_I2C_
#define RECLOCK_I2C_
#include "ScanI2CTwoWire.h"
#include <Wire.h>
#include <stdint.h>
uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus)
{
uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force);
uint32_t currentClock;
/* 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.
*/
#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
if (currentClock != desiredClock) {
LOG_DEBUG("Changing I2C clock to %u", desiredClock);
i2cBus->setClock(desiredClock);
}
return currentClock;
}
#endif
-1
View File
@@ -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
+61 -19
View File
@@ -10,7 +10,6 @@
#include "PowerFSM.h"
#include "RTC.h"
#include "Router.h"
#include "Sensor/AddI2CSensorTemplate.h"
#include "UnitConversions.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
@@ -20,7 +19,9 @@
#include <Throttle.h>
// Sensors
#include "Sensor/AddI2CSensorTemplate.h"
#include "Sensor/PMSA003ISensor.h"
#include "Sensor/SEN5XSensor.h"
void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
{
@@ -42,6 +43,7 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
// order by priority of metrics/values (low top, high bottom)
addSensor<PMSA003ISensor>(i2cScanner, ScanI2C::DeviceType::PMSA003I);
addSensor<SEN5XSensor>(i2cScanner, ScanI2C::DeviceType::SEN5X);
}
int32_t AirQualityTelemetryModule::runOnce()
@@ -85,10 +87,27 @@ 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;
}
}
}
}
@@ -109,9 +128,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 +186,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_pm10_standard || m.has_pm25_standard || m.has_pm100_standard;
if (!hasAny) {
display->drawString(x, currentY, "No Telemetry");
@@ -225,9 +252,10 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack
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);
// TODO - Decide what to do with these
// 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);
#endif
// release previous packet before occupying a new spot
if (lastMeasurementPacket != nullptr)
@@ -247,10 +275,8 @@ bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m)
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
for (TelemetrySensor *sensor : sensors) {
LOG_INFO("Reading AQ sensors");
LOG_DEBUG("Reading %s", sensor->sensorName);
valid = valid && sensor->getMetrics(m);
hasSensor = true;
}
@@ -291,12 +317,14 @@ 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);
LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u", m.variant.air_quality_metrics.pm10_standard,
m.variant.air_quality_metrics.pm25_standard, m.variant.air_quality_metrics.pm100_standard);
if (m.variant.air_quality_metrics.has_pm10_environmental)
LOG_INFO("pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u",
m.variant.air_quality_metrics.pm10_environmental, m.variant.air_quality_metrics.pm25_environmental,
m.variant.air_quality_metrics.pm100_environmental);
meshtastic_MeshPacket *p = allocDataProtobuf(m);
p->to = dest;
@@ -331,6 +359,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;
}
+70 -22
View File
@@ -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;
@@ -49,30 +52,37 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement)
{
if (!isActive()) {
LOG_WARN("PMSA003I is not active");
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;
}
@@ -86,7 +96,7 @@ 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;
}
@@ -136,20 +146,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
@@ -3,6 +3,7 @@
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "RTC.h"
#include "TelemetrySensor.h"
#define PMSA003I_I2C_CLOCK_SPEED 100000
@@ -19,6 +20,9 @@ class PMSA003ISensor : public TelemetrySensor
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;
private:
enum class State { IDLE, ACTIVE };
@@ -26,6 +30,7 @@ class PMSA003ISensor : public TelemetrySensor
uint16_t computedChecksum = 0;
uint16_t receivedChecksum = 0;
uint32_t pmMeasureStarted = 0;
uint8_t buffer[PMSA003I_FRAME_LENGTH]{};
TwoWire *_bus{};
@@ -0,0 +1,957 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
#include "../detect/reClockI2C.h"
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "FSCommon.h"
#include "SEN5XSensor.h"
#include "SPILock.h"
#include "SafeFile.h"
#include "TelemetrySensor.h"
#include <float.h> // FLT_MAX
#include <pb_decode.h>
#include <pb_encode.h>
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, &nothing, 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<uint8_t>((command & 0xFF00) >> 8);
toSend[i++] = static_cast<uint8_t>((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 delay 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
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;
}
// 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()
{
#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<Print *>(&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()
{
LOG_DEBUG("SEN5X: Waking up sensor");
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) {
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<uint16_t>((dataBuffer[0] << 8) | dataBuffer[1]);
uint16_t uint_pM2p5 = static_cast<uint16_t>((dataBuffer[2] << 8) | dataBuffer[3]);
uint16_t uint_pM4p0 = static_cast<uint16_t>((dataBuffer[4] << 8) | dataBuffer[5]);
uint16_t uint_pM10p0 = static_cast<uint16_t>((dataBuffer[6] << 8) | dataBuffer[7]);
int16_t int_humidity = static_cast<int16_t>((dataBuffer[8] << 8) | dataBuffer[9]);
int16_t int_temperature = static_cast<int16_t>((dataBuffer[10] << 8) | dataBuffer[11]);
int16_t int_vocIndex = static_cast<int16_t>((dataBuffer[12] << 8) | dataBuffer[13]);
int16_t int_noxIndex = static_cast<int16_t>((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<uint16_t>((dataBuffer[0] << 8) | dataBuffer[1]);
// uint16_t uint_pM2p5 = static_cast<uint16_t>((dataBuffer[2] << 8) | dataBuffer[3]);
// uint16_t uint_pM4p0 = static_cast<uint16_t>((dataBuffer[4] << 8) | dataBuffer[5]);
// uint16_t uint_pM10p0 = static_cast<uint16_t>((dataBuffer[6] << 8) | dataBuffer[7]);
uint16_t uint_pN0p5 = static_cast<uint16_t>((dataBuffer[8] << 8) | dataBuffer[9]);
uint16_t uint_pN1p0 = static_cast<uint16_t>((dataBuffer[10] << 8) | dataBuffer[11]);
uint16_t uint_pN2p5 = static_cast<uint16_t>((dataBuffer[12] << 8) | dataBuffer[13]);
uint16_t uint_pN4p0 = static_cast<uint16_t>((dataBuffer[14] << 8) | dataBuffer[15]);
uint16_t uint_pN10p0 = static_cast<uint16_t>((dataBuffer[16] << 8) | dataBuffer[17]);
uint16_t uint_tSize = static_cast<uint16_t>((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;
}
uint8_t SEN5XSensor::getMeasurements()
{
uint32_t now;
now = getTime();
// 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 dataReady = dataReadyBuffer[1];
uint32_t sinceLastDataPollMs = (now - lastDataPoll) * 1000;
// Check if data is ready, and if since last time we requested is less than SEN5X_POLL_INTERVAL
if (!dataReady && (sinceLastDataPollMs > SEN5X_POLL_INTERVAL)) {
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;
}
lastDataPoll = now;
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);
lastDataPoll = now;
// 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) {
if (sen5xmeasurement.pM1p0 != UINT16_MAX) {
measurement->variant.air_quality_metrics.has_pm10_standard = true;
measurement->variant.air_quality_metrics.pm10_standard = sen5xmeasurement.pM1p0;
}
if (sen5xmeasurement.pM2p5 != UINT16_MAX) {
measurement->variant.air_quality_metrics.has_pm25_standard = true;
measurement->variant.air_quality_metrics.pm25_standard = sen5xmeasurement.pM2p5;
}
if (sen5xmeasurement.pM4p0 != UINT16_MAX) {
measurement->variant.air_quality_metrics.has_pm40_standard = true;
measurement->variant.air_quality_metrics.pm40_standard = sen5xmeasurement.pM4p0;
}
if (sen5xmeasurement.pM10p0 != UINT16_MAX) {
measurement->variant.air_quality_metrics.has_pm100_standard = true;
measurement->variant.air_quality_metrics.pm100_standard = sen5xmeasurement.pM10p0;
}
if (sen5xmeasurement.pN0p5 != UINT32_MAX) {
measurement->variant.air_quality_metrics.has_particles_05um = true;
measurement->variant.air_quality_metrics.particles_05um = sen5xmeasurement.pN0p5;
}
if (sen5xmeasurement.pN1p0 != UINT32_MAX) {
measurement->variant.air_quality_metrics.has_particles_10um = true;
measurement->variant.air_quality_metrics.particles_10um = sen5xmeasurement.pN1p0;
}
if (sen5xmeasurement.pN2p5 != UINT32_MAX) {
measurement->variant.air_quality_metrics.has_particles_25um = true;
measurement->variant.air_quality_metrics.particles_25um = sen5xmeasurement.pN2p5;
}
if (sen5xmeasurement.pN4p0 != UINT32_MAX) {
measurement->variant.air_quality_metrics.has_particles_40um = true;
measurement->variant.air_quality_metrics.particles_40um = sen5xmeasurement.pN4p0;
}
if (sen5xmeasurement.pN10p0 != UINT32_MAX) {
measurement->variant.air_quality_metrics.has_particles_100um = true;
measurement->variant.air_quality_metrics.particles_100um = sen5xmeasurement.pN10p0;
}
if (sen5xmeasurement.tSize != FLT_MAX) {
measurement->variant.air_quality_metrics.has_particles_tps = true;
measurement->variant.air_quality_metrics.particles_tps = sen5xmeasurement.tSize;
}
if (model != SEN50) {
if (sen5xmeasurement.humidity != FLT_MAX) {
measurement->variant.air_quality_metrics.has_pm_humidity = true;
measurement->variant.air_quality_metrics.pm_humidity = sen5xmeasurement.humidity;
}
if (sen5xmeasurement.temperature != FLT_MAX) {
measurement->variant.air_quality_metrics.has_pm_temperature = true;
measurement->variant.air_quality_metrics.pm_temperature = sen5xmeasurement.temperature;
}
if (sen5xmeasurement.noxIndex != FLT_MAX) {
measurement->variant.air_quality_metrics.has_pm_voc_idx = true;
measurement->variant.air_quality_metrics.pm_voc_idx = sen5xmeasurement.vocIndex;
}
}
if (model == SEN55) {
if (sen5xmeasurement.noxIndex != FLT_MAX) {
measurement->variant.air_quality_metrics.has_pm_nox_idx = true;
measurement->variant.air_quality_metrics.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
+170
View File
@@ -0,0 +1,170 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "RTC.h"
#include "TelemetrySensor.h"
#include "Wire.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_POLL_INTERVAL
#define SEN5X_POLL_INTERVAL 1000
#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;
uint32_t lastDataPoll = 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;
AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
meshtastic_AdminMessage *response) override;
};
#endif
@@ -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
// 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)
+12 -12
View File
@@ -149,18 +149,18 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
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_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);
// }
} else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) {
if (decoded->variant.power_metrics.has_ch1_voltage) {
msgPayload["voltage_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_voltage);
@@ -120,15 +120,15 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
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_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;
// }
} else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) {
if (decoded->variant.power_metrics.has_ch1_voltage) {
jsonObj["payload"]["voltage_ch1"] = decoded->variant.power_metrics.ch1_voltage;
+1
View File
@@ -17,5 +17,6 @@ build_flags =
-DPIN_SERIAL2_RX=PA3
-DHAS_GPS=1
-DGPS_SERIAL_PORT=Serial2
-DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1
upload_port = stlink