From 15297cb56fc7820d0e9f5e25702ffb66d62fbe1b Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 11 Feb 2026 21:13:34 +0100 Subject: [PATCH] feat/add sfa30 (#9372) * 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 * Initial implementation for SFA30Sensor * 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 * 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 * Add functional SCD4X * Fix screen frame for CO2 * Add admin commands to SCD4X class * Add further admin commands and fixes. * 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 * 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 * Small reordering in PMS class for consistency * If one sensor fails, AQ telemetry still reports data * Small formatting fix * Add SEN5X to AQI in ScanI2C * SCD4X now part of AQ module with template list * Fixes difference between idle and sleep * In LowPower, sleep is disabled * Requires testing for I2C clock comms for commands * Remove unnecessary import * Add co2 to serialized AQ metrics * Add SFA30 with new sensor template in AQ module * Update library dependencies in platformio.ini * Fix unitialized variables in SEN5X constructor * Fix missing import * Fix uninitMemberVars * Fix import error for SCD4X * Fix I2CClock logic * Fix not reclocking back to 700000Hz * Fix multiple sensors being read simultaneously * The logic in AQ module is different to the one in EnvironmentTelemetryModule. In Env module, if any sensor fails to getMetrics, no valid flag for the module. This prevents other sensors to report data. * Fix pending clock change in PMSA003 * Cleanup of SEN5X class * Exclude AQ sensor from wio-e5 due to flash limitations * Fix I2C clock change logic * Make sure clock is always set to needed value * Fix returns * Fix trunk * Fix on condition in reclock * Fix trunk * Final SFA30 class implementation * Add HCHO to screen and improve logs * Add metrics to mesh packet serializer * Minor fixes in logs * OCD tidy up of logs * Fix sleep function * Remove old I2C_CLOCK_SPEED code --------- Co-authored-by: nikl Co-authored-by: Hannes Fuchs Co-authored-by: Nashui-Yan Co-authored-by: Ben Meadors Co-authored-by: Tom Fifield Co-authored-by: Mike Robbins 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 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 4 + src/configuration.h | 1 + src/detect/ScanI2C.cpp | 2 +- src/detect/ScanI2C.h | 5 +- src/detect/ScanI2CTwoWire.cpp | 9 + src/main.cpp | 1 - src/modules/Telemetry/AirQualityTelemetry.cpp | 44 ++-- src/modules/Telemetry/AirQualityTelemetry.h | 3 +- .../Telemetry/Sensor/PMSA003ISensor.cpp | 4 + src/modules/Telemetry/Sensor/SCD4XSensor.cpp | 2 +- src/modules/Telemetry/Sensor/SEN5XSensor.cpp | 16 +- src/modules/Telemetry/Sensor/SFA30Sensor.cpp | 198 ++++++++++++++++++ src/modules/Telemetry/Sensor/SFA30Sensor.h | 39 ++++ src/serialization/MeshPacketSerializer.cpp | 9 + .../MeshPacketSerializer_nRF52.cpp | 9 + 15 files changed, 320 insertions(+), 26 deletions(-) create mode 100644 src/modules/Telemetry/Sensor/SFA30Sensor.cpp create mode 100644 src/modules/Telemetry/Sensor/SFA30Sensor.h diff --git a/platformio.ini b/platformio.ini index b7123b865..b9f1e580b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -214,6 +214,8 @@ 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 + # renovate: datasource=custom.pio depName=Sensirion I2C SFA3x packageName=sensirion/library/Sensirion I2C SFA3x + sensirion/Sensirion I2C SFA3x@1.0.0 ; Same as environmental_extra but without BSEC (saves ~3.5KB DRAM for original ESP32 targets) [environmental_extra_no_bsec] @@ -242,3 +244,5 @@ 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 + # renovate: datasource=custom.pio depName=Sensirion I2C SFA3x packageName=sensirion/library/Sensirion I2C SFA3x + sensirion/Sensirion I2C SFA3x@1.0.0 diff --git a/src/configuration.h b/src/configuration.h index 5d8d764c7..5ec856f88 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -217,6 +217,7 @@ along with this program. If not, see . #define SHTC3_ADDR 0x70 #define LPS22HB_ADDR 0x5C #define LPS22HB_ADDR_ALT 0x5D +#define SFA30_ADDR 0x5D #define SHT31_4x_ADDR 0x44 #define SHT31_4x_ADDR_ALT 0x45 #define PMSA003I_ADDR 0x12 diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp index bf01a0365..21aceffd1 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, SEN5X, SCD4X}; + ScanI2C::DeviceType types[] = {PMSA003I, SEN5X, SCD4X, SFA30}; return firstOfOrNONE(2, types); } diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index 528159661..161192766 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -89,8 +89,9 @@ class ScanI2C DA217, CHSC6X, CST226SE, - CW2015, - SEN5X + SEN5X, + SFA30, + CW2015 } DeviceType; // typedef uint8_t DeviceAddress; diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index e5e69dca7..2802854ab 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -1,4 +1,6 @@ #include "ScanI2CTwoWire.h" +#include "configuration.h" +#include "detect/ScanI2C.h" #if !MESHTASTIC_EXCLUDE_I2C @@ -456,6 +458,13 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) break; case LPS22HB_ADDR_ALT: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xD060), 48); // get device marking + if (registerValue != 0) { + type = SFA30; + logFoundDevice("SFA30", (uint8_t)addr.address); + break; + } + // TODO - What happens with these two? SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address) SCAN_SIMPLE_CASE(QMC6310U_ADDR, QMC6310U, "QMC6310U", (uint8_t)addr.address) diff --git a/src/main.cpp b/src/main.cpp index 153b9847e..45685f6cd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -681,7 +681,6 @@ void setup() scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614); 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 cc1b54373..d7127bb01 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -1,3 +1,4 @@ +#include "DebugConfiguration.h" #include "configuration.h" #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR @@ -25,6 +26,9 @@ #if __has_include() #include "Sensor/SCD4XSensor.h" #endif +#if __has_include() +#include "Sensor/SFA30Sensor.h" +#endif void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { @@ -50,6 +54,9 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::SCD4X); #endif +#if __has_include() + addSensor(i2cScanner, ScanI2C::DeviceType::SFA30); +#endif } int32_t AirQualityTelemetryModule::runOnce() @@ -71,7 +78,8 @@ int32_t AirQualityTelemetryModule::runOnce() } if (firstTime) { - // This is the first time the OSThread library has called this function, so do some setup + // This is the first time the OSThread library has called this function, so + // do some setup firstTime = false; if (moduleConfig.telemetry.air_quality_enabled) { @@ -221,6 +229,8 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta entries.push_back("PM10: " + String(m.pm100_standard) + "ug/m3"); if (m.has_co2) entries.push_back("CO2: " + String(m.co2) + "ppm"); + if (m.has_form_formaldehyde) + entries.push_back("HCHO: " + String(m.form_formaldehyde) + "ppb"); // === Show first available metric on top-right of first line === if (!entries.empty()) { @@ -256,17 +266,19 @@ 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); + if (t->variant.air_quality_metrics.has_pm10_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); - // 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); + if (t->variant.air_quality_metrics.has_co2) + LOG_INFO("CO2=%i, CO2_T=%.2f, CO2_H=%.2f", t->variant.air_quality_metrics.co2, + t->variant.air_quality_metrics.co2_temperature, t->variant.air_quality_metrics.co2_humidity); - 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); + if (t->variant.air_quality_metrics.has_form_formaldehyde) + LOG_INFO("HCHO=%.2f, HCHO_T=%.2f, HCHO_H=%.2f", t->variant.air_quality_metrics.form_formaldehyde, + t->variant.air_quality_metrics.form_temperature, t->variant.air_quality_metrics.form_humidity); #endif // release previous packet before occupying a new spot if (lastMeasurementPacket != nullptr) @@ -354,10 +366,18 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) m.variant.air_quality_metrics.has_co2_humidity; if (hasAnyCO2) { - LOG_INFO("Send: co2=%i, co2_t=%f, co2_rh=%f", m.variant.air_quality_metrics.co2, + LOG_INFO("Send: co2=%i, co2_t=%.2f, co2_rh=%.2f", m.variant.air_quality_metrics.co2, m.variant.air_quality_metrics.co2_temperature, m.variant.air_quality_metrics.co2_humidity); } + bool hasAnyHCHO = m.variant.air_quality_metrics.has_form_formaldehyde || + m.variant.air_quality_metrics.has_form_temperature || m.variant.air_quality_metrics.has_form_humidity; + + if (hasAnyHCHO) { + LOG_INFO("Send: hcho=%.2f, hcho_t=%.2f, hcho_rh=%.2f", m.variant.air_quality_metrics.form_formaldehyde, + m.variant.air_quality_metrics.form_temperature, m.variant.air_quality_metrics.form_humidity); + } + meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; p->decoded.want_response = false; @@ -426,4 +446,4 @@ AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule( return result; } -#endif \ No newline at end of file +#endif diff --git a/src/modules/Telemetry/AirQualityTelemetry.h b/src/modules/Telemetry/AirQualityTelemetry.h index 2b88b74ba..197491f2d 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.h +++ b/src/modules/Telemetry/AirQualityTelemetry.h @@ -64,8 +64,9 @@ class AirQualityTelemetryModule : private concurrency::OSThread, bool firstTime = true; meshtastic_MeshPacket *lastMeasurementPacket; uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute + // uint32_t sendToPhoneIntervalMs = 1000; // Send to phone every minute uint32_t lastSentToMesh = 0; uint32_t lastSentToPhone = 0; }; -#endif \ No newline at end of file +#endif diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index bc067c04c..a752f2ab8 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -138,6 +138,10 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) measurement->variant.air_quality_metrics.has_particles_100um = true; measurement->variant.air_quality_metrics.particles_100um = read16(buffer, 26); + LOG_DEBUG("Got %s readings: pM1p0_standard=%u, pM2p5_standard=%u, pM10p0_standard=%u", sensorName, + measurement->variant.air_quality_metrics.pm10_standard, measurement->variant.air_quality_metrics.pm25_standard, + measurement->variant.air_quality_metrics.pm100_standard); + return true; } diff --git a/src/modules/Telemetry/Sensor/SCD4XSensor.cpp b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp index 579d9873b..6572ef9b1 100644 --- a/src/modules/Telemetry/Sensor/SCD4XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp @@ -125,7 +125,7 @@ bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) reClockI2C(currentClock, _bus, false); #endif - LOG_DEBUG("%s readings: %u ppm, %.2f degC, %.2f %rh", sensorName, co2, temperature, humidity); + LOG_DEBUG("Got %s readings: co2=%u, co2_temp=%.2f, co2_hum%.2f", sensorName, co2, temperature, humidity); if (error != SCD4X_NO_ERROR) { LOG_DEBUG("%s: Error while getting measurements: %u", sensorName, error); if (co2 == 0) { diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index 299a1f7df..0a9db4dff 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -665,16 +665,16 @@ bool SEN5XSensor::readValues() 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); + LOG_DEBUG("Got %s readings: pM1p0=%u, pM2p5=%u, pM4p0=%u, pM10p0=%u", sensorName, 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); + LOG_DEBUG("Got %s readings: humidity=%.2f, temperature=%.2f, vocIndex=%.2f", sensorName, sen5xmeasurement.humidity, + sen5xmeasurement.temperature, sen5xmeasurement.vocIndex); } if (model == SEN55) { - LOG_DEBUG("Got: noxIndex=%.2f", sen5xmeasurement.noxIndex); + LOG_DEBUG("Got %s readings: noxIndex=%.2f", sensorName, sen5xmeasurement.noxIndex); } return true; @@ -727,9 +727,9 @@ bool SEN5XSensor::readPNValues(bool cumulative) 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); + LOG_DEBUG("Got %s readings: pN0p5=%u, pN1p0=%u, pN2p5=%u, pN4p0=%u, pN10p0=%u, tSize=%.2f", sensorName, + sen5xmeasurement.pN0p5, sen5xmeasurement.pN1p0, sen5xmeasurement.pN2p5, sen5xmeasurement.pN4p0, + sen5xmeasurement.pN10p0, sen5xmeasurement.tSize); return true; } diff --git a/src/modules/Telemetry/Sensor/SFA30Sensor.cpp b/src/modules/Telemetry/Sensor/SFA30Sensor.cpp new file mode 100644 index 000000000..42a77db40 --- /dev/null +++ b/src/modules/Telemetry/Sensor/SFA30Sensor.cpp @@ -0,0 +1,198 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() + +#include "../detect/reClockI2C.h" +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "SFA30Sensor.h" + +SFA30Sensor::SFA30Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SFA30, "SFA30"){}; + +bool SFA30Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + + _bus = bus; + _address = dev->address.address; + +#ifdef SFA30_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SFA30_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 /* SFA30_I2C_CLOCK_SPEED */ + + sfa30.begin(*_bus, _address); + delay(20); + + if (this->isError(sfa30.deviceReset())) { +#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return false; + } + + state = State::IDLE; + if (this->isError(sfa30.startContinuousMeasurement())) { +#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return false; + } + + LOG_INFO("%s starting measurement", sensorName); + +#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + status = 1; + state = State::ACTIVE; + measureStarted = getTime(); + LOG_INFO("%s Enabled", sensorName); + + initI2CSensor(); + return true; +}; + +bool SFA30Sensor::isError(uint16_t response) +{ + if (response == SFA30_NO_ERROR) + return false; + + // TODO - Check error to char conversion + LOG_ERROR("%s: %s", sensorName, response); + return true; +} + +void SFA30Sensor::sleep() +{ +#ifdef SFA30_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SFA30_I2C_CLOCK_SPEED */ + + // Note - not recommended for this sensor on a periodic basis + if (this->isError(sfa30.stopMeasurement())) { + LOG_ERROR("%s: can't stop measurement"); + }; + +#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + LOG_INFO("%s: stop measurement"); + state = State::IDLE; + measureStarted = 0; +} + +uint32_t SFA30Sensor::wakeUp() +{ +#ifdef SFA30_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SFA30_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 /* SFA30_I2C_CLOCK_SPEED */ + + LOG_INFO("Waking up %s", sensorName); + if (this->isError(sfa30.startContinuousMeasurement())) { +#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return 0; + } + +#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + state = State::ACTIVE; + measureStarted = getTime(); + return SFA30_WARMUP_MS; +} + +int32_t SFA30Sensor::wakeUpTimeMs() +{ + return SFA30_WARMUP_MS; +} + +bool SFA30Sensor::canSleep() +{ + // Sleep is disabled in this sensor because readings are not tested with periodic sleep + // with such low power consumption, prefered to keep it active + return false; +} + +bool SFA30Sensor::isActive() +{ + return state == State::ACTIVE; +} + +int32_t SFA30Sensor::pendingForReadyMs() +{ + uint32_t now; + now = getTime(); + uint32_t sinceHchoMeasureStarted = (now - measureStarted) * 1000; + LOG_DEBUG("%s: Since measure started: %ums", sensorName, sinceHchoMeasureStarted); + + if (sinceHchoMeasureStarted < SFA30_WARMUP_MS) { + LOG_INFO("%s: not enough time passed since starting measurement", sensorName); + return SFA30_WARMUP_MS - sinceHchoMeasureStarted; + } + return 0; +} + +bool SFA30Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + float hcho = 0.0; + float humidity = 0.0; + float temperature = 0.0; + +#ifdef SFA30_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SFA30_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 /* SFA30_I2C_CLOCK_SPEED */ + + if (this->isError(sfa30.readMeasuredValues(hcho, humidity, temperature))) { + LOG_WARN("%s: No values", sensorName); + return false; + } + +#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + measurement->variant.air_quality_metrics.has_form_temperature = true; + measurement->variant.air_quality_metrics.has_form_humidity = true; + measurement->variant.air_quality_metrics.has_form_formaldehyde = true; + + measurement->variant.air_quality_metrics.form_temperature = temperature; + measurement->variant.air_quality_metrics.form_humidity = humidity; + measurement->variant.air_quality_metrics.form_formaldehyde = hcho; + + LOG_DEBUG("Got %s readings: hcho=%.2f, hcho_temp=%.2f, hcho_hum=%.2f", sensorName, hcho, temperature, humidity); + + return true; +} +#endif diff --git a/src/modules/Telemetry/Sensor/SFA30Sensor.h b/src/modules/Telemetry/Sensor/SFA30Sensor.h new file mode 100644 index 000000000..9fa9c85fc --- /dev/null +++ b/src/modules/Telemetry/Sensor/SFA30Sensor.h @@ -0,0 +1,39 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "RTC.h" +#include "TelemetrySensor.h" +#include + +#define SFA30_I2C_CLOCK_SPEED 100000 +#define SFA30_WARMUP_MS 10000 +#define SFA30_NO_ERROR 0 + +class SFA30Sensor : public TelemetrySensor +{ + private: + enum class State { IDLE, ACTIVE }; + State state = State::IDLE; + uint32_t measureStarted = 0; + + SensirionI2cSfa3x sfa30; + TwoWire *_bus{}; + uint8_t _address{}; + bool isError(uint16_t response); + + public: + SFA30Sensor(); + 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; + virtual int32_t wakeUpTimeMs() override; + virtual int32_t pendingForReadyMs() override; +}; + +#endif diff --git a/src/serialization/MeshPacketSerializer.cpp b/src/serialization/MeshPacketSerializer.cpp index 042bc3763..819ba3da5 100644 --- a/src/serialization/MeshPacketSerializer.cpp +++ b/src/serialization/MeshPacketSerializer.cpp @@ -158,6 +158,15 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, if (decoded->variant.air_quality_metrics.has_co2_humidity) { msgPayload["co2_humidity"] = new JSONValue(decoded->variant.air_quality_metrics.co2_humidity); } + if (decoded->variant.air_quality_metrics.has_form_formaldehyde) { + msgPayload["form_formaldehyde"] = new JSONValue(decoded->variant.air_quality_metrics.form_formaldehyde); + } + if (decoded->variant.air_quality_metrics.has_form_temperature) { + msgPayload["form_temperature"] = new JSONValue(decoded->variant.air_quality_metrics.form_temperature); + } + if (decoded->variant.air_quality_metrics.has_form_humidity) { + msgPayload["form_humidity"] = new JSONValue(decoded->variant.air_quality_metrics.form_humidity); + } } 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); diff --git a/src/serialization/MeshPacketSerializer_nRF52.cpp b/src/serialization/MeshPacketSerializer_nRF52.cpp index a0ad4e4b9..bd0a29c51 100644 --- a/src/serialization/MeshPacketSerializer_nRF52.cpp +++ b/src/serialization/MeshPacketSerializer_nRF52.cpp @@ -129,6 +129,15 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, if (decoded->variant.air_quality_metrics.has_co2_humidity) { jsonObj["payload"]["co2_humidity"] = decoded->variant.air_quality_metrics.co2_humidity; } + if (decoded->variant.air_quality_metrics.has_form_formaldehyde) { + jsonObj["payload"]["form_formaldehyde"] = decoded->variant.air_quality_metrics.form_formaldehyde; + } + if (decoded->variant.air_quality_metrics.has_form_temperature) { + jsonObj["payload"]["form_temperature"] = decoded->variant.air_quality_metrics.form_temperature; + } + if (decoded->variant.air_quality_metrics.has_form_humidity) { + jsonObj["payload"]["form_humidity"] = decoded->variant.air_quality_metrics.form_humidity; + } } 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;