From c9ec675cb00d38adf431cfd55e416a39b05620ba Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Fri, 12 Sep 2025 09:42:27 -0700 Subject: [PATCH 01/17] SGP30 / SGP40 background sampling Add SGP30 / SGP40 background sampling. Requires frequently 1s polling or returns 0 values. --- src/components/i2c/WipperSnapper_I2C.cpp | 6 ++ .../i2c/drivers/WipperSnapper_I2C_Driver.h | 18 +++- .../drivers/WipperSnapper_I2C_Driver_SGP30.h | 69 +++++++++++---- .../drivers/WipperSnapper_I2C_Driver_SGP40.h | 84 ++++++++++++++++--- 4 files changed, 148 insertions(+), 29 deletions(-) diff --git a/src/components/i2c/WipperSnapper_I2C.cpp b/src/components/i2c/WipperSnapper_I2C.cpp index 7e02c9922..73f6cbf21 100644 --- a/src/components/i2c/WipperSnapper_I2C.cpp +++ b/src/components/i2c/WipperSnapper_I2C.cpp @@ -1347,6 +1347,11 @@ void WipperSnapper_Component_I2C::update() { msgi2cResponse.which_payload = wippersnapper_signal_v1_I2CResponse_resp_i2c_device_event_tag; + // one fast pass for all drivers every update() call + for (auto *drv : drivers) { + if (drv) drv->fastTick(); + } + long curTime; bool sensorsReturningFalse = true; int retries = 3; @@ -1354,6 +1359,7 @@ void WipperSnapper_Component_I2C::update() { while (sensorsReturningFalse && retries > 0) { sensorsReturningFalse = false; retries--; + curTime = millis(); std::vector::iterator iter, end; for (iter = drivers.begin(), end = drivers.end(); iter != end; ++iter) { diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h index e76532719..109dbca5c 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h @@ -34,7 +34,7 @@ class WipperSnapper_I2C_Driver { public: /*******************************************************************************/ /*! - @brief Instanciates an I2C sensor. + @brief Instanctiates an I2C sensor. @param i2c The I2C hardware interface, default is Wire. @param sensorAddress @@ -53,6 +53,20 @@ class WipperSnapper_I2C_Driver { /*******************************************************************************/ virtual ~WipperSnapper_I2C_Driver() {} + /*******************************************************************************/ + /*! + @brief Per-update background hook for drivers that need faster internal + sampling than the user publish interval. + Default is a no-op; override in concrete drivers (e.g., SGP30/40) + to maintain required ~1 Hz reads and accumulate/condition + values for later publish. + @note Call site: WipperSnapper_Component_I2C::update() will invoke + this once per loop for each driver. Implementations must be + non-blocking (do not delay); use millis()-based timing. + */ + /*******************************************************************************/ + virtual void fastTick() {} + /*******************************************************************************/ /*! @brief Initializes the I2C sensor and begins I2C. @@ -1312,7 +1326,7 @@ class WipperSnapper_I2C_Driver { @brief Updates the properties of a proximity sensor. @param period The time interval at which to return new data from the - proimity sensor. + proximity sensor. */ /*******************************************************************************/ virtual void updateSensorProximity(float period) { diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h index 664695191..2a4d5ff1e 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h @@ -24,6 +24,7 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { : WipperSnapper_I2C_Driver(i2c, sensorAddress) { _i2c = i2c; _sensorAddress = sensorAddress; + _sgp30 = nullptr; } /*******************************************************************************/ @@ -31,9 +32,12 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { @brief Destructor for an SGP30 sensor. */ /*******************************************************************************/ - ~WipperSnapper_I2C_Driver_SGP30() { + ~WipperSnapper_I2C_Driver_SGP30() override { // Called when a SGP30 component is deleted. - delete _sgp30; + if (_sgp30) { + delete _sgp30; + _sgp30 = nullptr; + } } /*******************************************************************************/ @@ -42,29 +46,60 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { @returns True if initialized successfully, False otherwise. */ /*******************************************************************************/ - bool begin() { + bool begin() override { _sgp30 = new Adafruit_SGP30(); - return _sgp30->begin(_i2c); + if (!_sgp30->begin(_i2c)) { + delete _sgp30; // avoid leak on init failure + _sgp30 = nullptr; + return false; + } + _sgp30->IAQinit(); // start IAQ algorithm + _lastFastMs = millis(); // reset fast sampler + _n = _eco2Sum = _tvocSum = 0; // clear accumulators + return true; } - bool getEventECO2(sensors_event_t *senseEvent) { - bool result = _sgp30->IAQmeasure(); - if (result) { - senseEvent->eCO2 = _sgp30->eCO2; - } - return result; + bool getEventECO2(sensors_event_t *senseEvent) override { + if (!_sgp30) return false; + bool ok = _sgp30->IAQmeasure(); + if (ok) senseEvent->eCO2 = _sgp30->eCO2; + return ok; + } + + bool getEventTVOC(sensors_event_t *senseEvent) override { + if (!_sgp30) return false; + bool ok = _sgp30->IAQmeasure(); + if (ok) senseEvent->tvoc = _sgp30->TVOC; + return ok; } - bool getEventTVOC(sensors_event_t *senseEvent) { - bool result = _sgp30->IAQmeasure(); - if (result) { - senseEvent->tvoc = _sgp30->TVOC; + void fastTick() override { + if (!iaqEnabled()) + return; // nothing enabled, save cycles + uint32_t now = millis(); + if (now - _lastFastMs >= 1000) { // ~1 Hz cadence + if (_sgp30 && _sgp30->IAQmeasure()) { + _eco2Sum += _sgp30->eCO2; // uint16_t in library + _tvocSum += _sgp30->TVOC; // uint16_t in library + _n++; + } + _lastFastMs = now; } - return result; } protected: - Adafruit_SGP30 *_sgp30; ///< Pointer to SGP30 temperature sensor object + Adafruit_SGP30 *_sgp30; ///< Pointer to SGP30 sensor object + + // Fast sampling state + uint32_t _lastFastMs = 0; + uint32_t _n = 0; + uint32_t _eco2Sum = 0; + uint32_t _tvocSum = 0; + + inline bool iaqEnabled() const { + // Enable IAQ background reads if either metric is requested + return (getSensorECO2Period() > 0) || (getSensorTVOCPeriod() > 0); + } }; -#endif // WipperSnapper_I2C_Driver_SGP30 \ No newline at end of file +#endif // WipperSnapper_I2C_Driver_SGP30 diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h index 3e201b106..b615698bb 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h @@ -40,6 +40,21 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { : WipperSnapper_I2C_Driver(i2c, sensorAddress) { _i2c = i2c; _sensorAddress = sensorAddress; + _sgp40 = nullptr; + } + + /*******************************************************************************/ + /*! + @brief Destructor for an SGP40 sensor driver. + Cleans up and deallocates the underlying Adafruit_SGP40 object + when the driver is destroyed. + */ + /*******************************************************************************/ + ~WipperSnapper_I2C_Driver_SGP40() override { + if (_sgp40) { + delete _sgp40; + _sgp40 = nullptr; + } } /*******************************************************************************/ @@ -48,14 +63,17 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { @returns True if initialized successfully, False otherwise. */ /*******************************************************************************/ - bool begin() { + bool begin() override { _sgp40 = new Adafruit_SGP40(); - if (!_sgp40->begin(_i2c)) { + if (!_sgp40 || !_sgp40->begin(_i2c)) { + delete _sgp40; + _sgp40 = nullptr; return false; } - - // TODO: update to use setCalibration() and pass in temp/humidity - + _lastFastMs = millis(); + _n = 0; + _vocSum = 0.0f; + _rawSum = 0; return true; } @@ -64,11 +82,19 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { @brief Gets the sensor's current raw unprocessed value. @param rawEvent Pointer to an Adafruit_Sensor event. - @returns True if the temperature was obtained successfully, False + @returns True if the raw value was obtained successfully, False otherwise. */ /*******************************************************************************/ - bool getEventRaw(sensors_event_t *rawEvent) { + bool getEventRaw(sensors_event_t *rawEvent) override { + if (!_sgp40) return false; + if (_n > 0) { + rawEvent->data[0] = (float)_rawSum / (float)_n; + _rawSum = 0; + _vocSum = 0.0f; + _n = 0; + return true; + } rawEvent->data[0] = (float)_sgp40->measureRaw(); return true; } @@ -82,13 +108,51 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { otherwise. */ /*******************************************************************************/ - bool getEventVOCIndex(sensors_event_t *vocIndexEvent) { + bool getEventVOCIndex(sensors_event_t *vocIndexEvent) override { + if (!_sgp40) return false; + if (_n > 0) { + vocIndexEvent->voc_index = _vocSum / (float)_n; + _rawSum = 0; + _vocSum = 0.0f; + _n = 0; + return true; + } vocIndexEvent->voc_index = (float)_sgp40->measureVocIndex(); return true; } + /*******************************************************************************/ + /*! + @brief Performs background sampling for the SGP40. + Runs once per second to accumulate raw and VOC index values + for later averaging in getEventRaw() and getEventVOCIndex(). + */ + /*******************************************************************************/ + void fastTick() override { + if (!_sgp40) return; + if (!vocEnabled()) return; + + uint32_t now = millis(); + if (now - _lastFastMs >= 1000) { + _rawSum += _sgp40->measureRaw(); + _vocSum += _sgp40->measureVocIndex(); + _n++; + _lastFastMs = now; + } + } + protected: - Adafruit_SGP40 *_sgp40; ///< SEN5X driver object + Adafruit_SGP40 *_sgp40; ///< SGP40 + // background accumulation state + uint32_t _lastFastMs = 0; + uint32_t _n = 0; + float _vocSum = 0.0f; + uint32_t _rawSum = 0; + + // enable fast sampling if either output is requested + inline bool vocEnabled() const { + return (getSensorVOCIndexPeriod() > 0) || (getSensorRawPeriod() > 0); + } }; -#endif // WipperSnapper_I2C_Driver_SEN5X +#endif // WipperSnapper_I2C_Driver_SGP40_H From 0b020d43e1c6b42d45b94a8cf3a9481c7fa672d2 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Fri, 12 Sep 2025 13:40:35 -0700 Subject: [PATCH 02/17] removed override / const --- src/components/i2c/WipperSnapper_I2C.cpp | 2 +- src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h | 5 +++-- src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/i2c/WipperSnapper_I2C.cpp b/src/components/i2c/WipperSnapper_I2C.cpp index 73f6cbf21..0dbb4f264 100644 --- a/src/components/i2c/WipperSnapper_I2C.cpp +++ b/src/components/i2c/WipperSnapper_I2C.cpp @@ -1032,7 +1032,7 @@ bool WipperSnapper_Component_I2C::initI2CDevice( _drivers_out.push_back(_ssd1306); WS_DEBUG_PRINTLN("SSD1306 display initialized Successfully!"); } else { - WS_DEBUG_PRINTLN("ERROR: I2C device type not found!") + WS_DEBUG_PRINTLN("ERROR: I2C device type not found!"); _busStatusResponse = wippersnapper_i2c_v1_BusResponse_BUS_RESPONSE_UNSUPPORTED_SENSOR; return false; diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h index 2a4d5ff1e..349f031ed 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h @@ -46,7 +46,7 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { @returns True if initialized successfully, False otherwise. */ /*******************************************************************************/ - bool begin() override { + bool begin() { _sgp30 = new Adafruit_SGP30(); if (!_sgp30->begin(_i2c)) { delete _sgp30; // avoid leak on init failure @@ -88,6 +88,7 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { } protected: + Adafruit_SGP30 *_sgp30; ///< Pointer to SGP30 sensor object // Fast sampling state @@ -96,7 +97,7 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { uint32_t _eco2Sum = 0; uint32_t _tvocSum = 0; - inline bool iaqEnabled() const { + inline bool iaqEnabled() { // Enable IAQ background reads if either metric is requested return (getSensorECO2Period() > 0) || (getSensorTVOCPeriod() > 0); } diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h index b615698bb..ae5383a36 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h @@ -63,7 +63,7 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { @returns True if initialized successfully, False otherwise. */ /*******************************************************************************/ - bool begin() override { + bool begin() { _sgp40 = new Adafruit_SGP40(); if (!_sgp40 || !_sgp40->begin(_i2c)) { delete _sgp40; @@ -142,6 +142,7 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { } protected: + Adafruit_SGP40 *_sgp40; ///< SGP40 // background accumulation state uint32_t _lastFastMs = 0; @@ -150,7 +151,7 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { uint32_t _rawSum = 0; // enable fast sampling if either output is requested - inline bool vocEnabled() const { + inline bool vocEnabled() { return (getSensorVOCIndexPeriod() > 0) || (getSensorRawPeriod() > 0); } }; From f98d42b0775ea8985e4633d4b441c9581cacdb6f Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Fri, 12 Sep 2025 14:02:08 -0700 Subject: [PATCH 03/17] clang --- src/components/i2c/WipperSnapper_I2C.cpp | 3 ++- .../i2c/drivers/WipperSnapper_I2C_Driver.h | 10 ++++----- .../drivers/WipperSnapper_I2C_Driver_SGP30.h | 21 +++++++++++-------- .../drivers/WipperSnapper_I2C_Driver_SGP40.h | 13 +++++++----- 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/components/i2c/WipperSnapper_I2C.cpp b/src/components/i2c/WipperSnapper_I2C.cpp index 0dbb4f264..c4d969fd5 100644 --- a/src/components/i2c/WipperSnapper_I2C.cpp +++ b/src/components/i2c/WipperSnapper_I2C.cpp @@ -1349,7 +1349,8 @@ void WipperSnapper_Component_I2C::update() { // one fast pass for all drivers every update() call for (auto *drv : drivers) { - if (drv) drv->fastTick(); + if (drv) + drv->fastTick(); } long curTime; diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h index 109dbca5c..97e818ab4 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h @@ -57,9 +57,9 @@ class WipperSnapper_I2C_Driver { /*! @brief Per-update background hook for drivers that need faster internal sampling than the user publish interval. - Default is a no-op; override in concrete drivers (e.g., SGP30/40) - to maintain required ~1 Hz reads and accumulate/condition - values for later publish. + Default is a no-op; override in concrete drivers (e.g., + SGP30/40) to maintain required ~1 Hz reads and accumulate/condition values + for later publish. @note Call site: WipperSnapper_Component_I2C::update() will invoke this once per loop for each driver. Implementations must be non-blocking (do not delay); use millis()-based timing. @@ -1413,8 +1413,8 @@ class WipperSnapper_I2C_Driver { long _ambientTempFPeriod = 0L; ///< The time period between reading the ///< ambient temp. (°F) sensor's value. long _ambientTempFPeriodPrv = - PERIOD_24HRS_AGO_MILLIS; ///< The time when the ambient temp. (°F) sensor - ///< was last read. + PERIOD_24HRS_AGO_MILLIS; ///< The time when the ambient temp. (°F) sensor + ///< was last read. long _objectTempFPeriod = 0L; ///< The time period between reading the object ///< temp. (°F) sensor's value. long _objectTempFPeriodPrv = diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h index 349f031ed..1ad7cab4b 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h @@ -49,27 +49,31 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { bool begin() { _sgp30 = new Adafruit_SGP30(); if (!_sgp30->begin(_i2c)) { - delete _sgp30; // avoid leak on init failure + delete _sgp30; // avoid leak on init failure _sgp30 = nullptr; return false; } - _sgp30->IAQinit(); // start IAQ algorithm + _sgp30->IAQinit(); // start IAQ algorithm _lastFastMs = millis(); // reset fast sampler _n = _eco2Sum = _tvocSum = 0; // clear accumulators return true; } bool getEventECO2(sensors_event_t *senseEvent) override { - if (!_sgp30) return false; + if (!_sgp30) + return false; bool ok = _sgp30->IAQmeasure(); - if (ok) senseEvent->eCO2 = _sgp30->eCO2; + if (ok) + senseEvent->eCO2 = _sgp30->eCO2; return ok; } bool getEventTVOC(sensors_event_t *senseEvent) override { - if (!_sgp30) return false; + if (!_sgp30) + return false; bool ok = _sgp30->IAQmeasure(); - if (ok) senseEvent->tvoc = _sgp30->TVOC; + if (ok) + senseEvent->tvoc = _sgp30->TVOC; return ok; } @@ -79,8 +83,8 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { uint32_t now = millis(); if (now - _lastFastMs >= 1000) { // ~1 Hz cadence if (_sgp30 && _sgp30->IAQmeasure()) { - _eco2Sum += _sgp30->eCO2; // uint16_t in library - _tvocSum += _sgp30->TVOC; // uint16_t in library + _eco2Sum += _sgp30->eCO2; // uint16_t in library + _tvocSum += _sgp30->TVOC; // uint16_t in library _n++; } _lastFastMs = now; @@ -88,7 +92,6 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { } protected: - Adafruit_SGP30 *_sgp30; ///< Pointer to SGP30 sensor object // Fast sampling state diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h index ae5383a36..92c1c10f1 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h @@ -87,7 +87,8 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { */ /*******************************************************************************/ bool getEventRaw(sensors_event_t *rawEvent) override { - if (!_sgp40) return false; + if (!_sgp40) + return false; if (_n > 0) { rawEvent->data[0] = (float)_rawSum / (float)_n; _rawSum = 0; @@ -109,7 +110,8 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { */ /*******************************************************************************/ bool getEventVOCIndex(sensors_event_t *vocIndexEvent) override { - if (!_sgp40) return false; + if (!_sgp40) + return false; if (_n > 0) { vocIndexEvent->voc_index = _vocSum / (float)_n; _rawSum = 0; @@ -129,8 +131,10 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { */ /*******************************************************************************/ void fastTick() override { - if (!_sgp40) return; - if (!vocEnabled()) return; + if (!_sgp40) + return; + if (!vocEnabled()) + return; uint32_t now = millis(); if (now - _lastFastMs >= 1000) { @@ -142,7 +146,6 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { } protected: - Adafruit_SGP40 *_sgp40; ///< SGP40 // background accumulation state uint32_t _lastFastMs = 0; From 662f61c3a786d6b07bd95cfbff723f31e84240a6 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Fri, 12 Sep 2025 15:31:48 -0700 Subject: [PATCH 04/17] clang whitespace --- src/components/i2c/drivers/WipperSnapper_I2C_Driver.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h index 97e818ab4..1ff17134f 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h @@ -1413,8 +1413,8 @@ class WipperSnapper_I2C_Driver { long _ambientTempFPeriod = 0L; ///< The time period between reading the ///< ambient temp. (°F) sensor's value. long _ambientTempFPeriodPrv = - PERIOD_24HRS_AGO_MILLIS; ///< The time when the ambient temp. (°F) sensor - ///< was last read. + PERIOD_24HRS_AGO_MILLIS; ///< The time when the ambient temp. (°F) sensor + ///< was last read. long _objectTempFPeriod = 0L; ///< The time period between reading the object ///< temp. (°F) sensor's value. long _objectTempFPeriodPrv = From b1154e23a6eb692f570f1974049ab46d6dd1161c Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Sat, 13 Sep 2025 08:02:22 -0700 Subject: [PATCH 05/17] sgp30 doxygen --- .../i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h index 1ad7cab4b..da3354ce6 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h @@ -94,12 +94,24 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { protected: Adafruit_SGP30 *_sgp30; ///< Pointer to SGP30 sensor object - // Fast sampling state + /** Millis timestamp of last 1 Hz background read. */ uint32_t _lastFastMs = 0; + + /** Number of samples accumulated since last publish. */ uint32_t _n = 0; + + /** Running sum of eCO2 samples for averaging. */ uint32_t _eco2Sum = 0; + + /** Running sum of TVOC samples for averaging. */ uint32_t _tvocSum = 0; + /*******************************************************************************/ + /*! + @brief Returns whether IAQ background sampling should be active. + @return True if either eCO2 or TVOC metrics are configured to publish. + */ + /*******************************************************************************/ inline bool iaqEnabled() { // Enable IAQ background reads if either metric is requested return (getSensorECO2Period() > 0) || (getSensorTVOCPeriod() > 0); From 5cbb3d55b8ddf31913a89eec8e96302302d9a7c4 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Sat, 13 Sep 2025 08:19:48 -0700 Subject: [PATCH 06/17] sgp40 docs doxygen --- .../i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h index 92c1c10f1..4e2b135d7 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h @@ -147,13 +147,25 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { protected: Adafruit_SGP40 *_sgp40; ///< SGP40 - // background accumulation state + + /** Millis timestamp of last 1 Hz background read. */ uint32_t _lastFastMs = 0; + + /** Number of samples accumulated since last publish. */ uint32_t _n = 0; + + /** Running sum of VOC index samples for averaging. */ float _vocSum = 0.0f; + + /** Running sum of raw samples for averaging. */ uint32_t _rawSum = 0; - // enable fast sampling if either output is requested + /*******************************************************************************/ + /*! + @brief Returns whether VOC background sampling should be active. + @return True if either VOC Index or raw value is configured to publish. + */ + /*******************************************************************************/ inline bool vocEnabled() { return (getSensorVOCIndexPeriod() > 0) || (getSensorRawPeriod() > 0); } From 281e41d687550f53266b2801c7c83cc3dfa85895 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Sun, 14 Sep 2025 08:17:32 -0700 Subject: [PATCH 07/17] sgp30 / sgp40 integer values also added sgp30 averaging option --- .../drivers/WipperSnapper_I2C_Driver_SGP30.h | 38 ++++++++++++------- .../drivers/WipperSnapper_I2C_Driver_SGP40.h | 4 +- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h index da3354ce6..b1b42cea6 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h @@ -59,23 +59,33 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { return true; } - bool getEventECO2(sensors_event_t *senseEvent) override { - if (!_sgp30) - return false; - bool ok = _sgp30->IAQmeasure(); - if (ok) - senseEvent->eCO2 = _sgp30->eCO2; - return ok; +bool getEventECO2(sensors_event_t *senseEvent) override { + if (!_sgp30) return false; + if (_n > 0) { + senseEvent->eCO2 = (uint16_t)(_eco2Sum / _n); + _eco2Sum = 0; _tvocSum = 0; _n = 0; + return true; + } + if (_sgp30->IAQmeasure()) { + senseEvent->eCO2 = (uint16_t)_sgp30->eCO2; + return true; } + return false; +} - bool getEventTVOC(sensors_event_t *senseEvent) override { - if (!_sgp30) - return false; - bool ok = _sgp30->IAQmeasure(); - if (ok) - senseEvent->tvoc = _sgp30->TVOC; - return ok; +bool getEventTVOC(sensors_event_t *senseEvent) override { + if (!_sgp30) return false; + if (_n > 0) { + senseEvent->tvoc = (uint16_t)(_tvocSum / _n); + _eco2Sum = 0; _tvocSum = 0; _n = 0; + return true; + } + if (_sgp30->IAQmeasure()) { + senseEvent->tvoc = (uint16_t)_sgp30->TVOC; + return true; } + return false; +} void fastTick() override { if (!iaqEnabled()) diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h index 4e2b135d7..6d64764e1 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h @@ -113,13 +113,13 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { if (!_sgp40) return false; if (_n > 0) { - vocIndexEvent->voc_index = _vocSum / (float)_n; + vocIndexEvent->voc_index = (uint16_t)(_vocSum / (float)_n); _rawSum = 0; _vocSum = 0.0f; _n = 0; return true; } - vocIndexEvent->voc_index = (float)_sgp40->measureVocIndex(); + vocIndexEvent->voc_index = (uint16_t)_sgp40->measureVocIndex(); return true; } From 222e45bc27af429bfe0f6510cb78951eec1c25bc Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Sun, 14 Sep 2025 08:45:12 -0700 Subject: [PATCH 08/17] style: apply clang-format to SGP30 driver --- .../drivers/WipperSnapper_I2C_Driver_SGP30.h | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h index b1b42cea6..19e011504 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h @@ -59,33 +59,39 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { return true; } -bool getEventECO2(sensors_event_t *senseEvent) override { - if (!_sgp30) return false; - if (_n > 0) { - senseEvent->eCO2 = (uint16_t)(_eco2Sum / _n); - _eco2Sum = 0; _tvocSum = 0; _n = 0; - return true; - } - if (_sgp30->IAQmeasure()) { - senseEvent->eCO2 = (uint16_t)_sgp30->eCO2; - return true; + bool getEventECO2(sensors_event_t *senseEvent) override { + if (!_sgp30) + return false; + if (_n > 0) { + senseEvent->eCO2 = (uint16_t)(_eco2Sum / _n); + _eco2Sum = 0; + _tvocSum = 0; + _n = 0; + return true; + } + if (_sgp30->IAQmeasure()) { + senseEvent->eCO2 = (uint16_t)_sgp30->eCO2; + return true; + } + return false; } - return false; -} -bool getEventTVOC(sensors_event_t *senseEvent) override { - if (!_sgp30) return false; - if (_n > 0) { - senseEvent->tvoc = (uint16_t)(_tvocSum / _n); - _eco2Sum = 0; _tvocSum = 0; _n = 0; - return true; - } - if (_sgp30->IAQmeasure()) { - senseEvent->tvoc = (uint16_t)_sgp30->TVOC; - return true; + bool getEventTVOC(sensors_event_t *senseEvent) override { + if (!_sgp30) + return false; + if (_n > 0) { + senseEvent->tvoc = (uint16_t)(_tvocSum / _n); + _eco2Sum = 0; + _tvocSum = 0; + _n = 0; + return true; + } + if (_sgp30->IAQmeasure()) { + senseEvent->tvoc = (uint16_t)_sgp30->TVOC; + return true; + } + return false; } - return false; -} void fastTick() override { if (!iaqEnabled()) From 1dc39ca176308c7c2861c5207e439fffc2b93202 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Thu, 25 Sep 2025 16:44:43 -0700 Subject: [PATCH 09/17] I2C: per-driver fastTick call site; fix fastTick docs; SGP30/SGP40: remove averaging, cache single reads; minor doc/guard fixes --- src/components/i2c/WipperSnapper_I2C.cpp | 9 +- .../i2c/drivers/WipperSnapper_I2C_Driver.h | 23 ++--- .../drivers/WipperSnapper_I2C_Driver_SGP30.h | 83 ++++++++----------- .../drivers/WipperSnapper_I2C_Driver_SGP40.h | 53 +++--------- 4 files changed, 63 insertions(+), 105 deletions(-) diff --git a/src/components/i2c/WipperSnapper_I2C.cpp b/src/components/i2c/WipperSnapper_I2C.cpp index c4d969fd5..87582277b 100644 --- a/src/components/i2c/WipperSnapper_I2C.cpp +++ b/src/components/i2c/WipperSnapper_I2C.cpp @@ -1347,12 +1347,6 @@ void WipperSnapper_Component_I2C::update() { msgi2cResponse.which_payload = wippersnapper_signal_v1_I2CResponse_resp_i2c_device_event_tag; - // one fast pass for all drivers every update() call - for (auto *drv : drivers) { - if (drv) - drv->fastTick(); - } - long curTime; bool sensorsReturningFalse = true; int retries = 3; @@ -1364,6 +1358,9 @@ void WipperSnapper_Component_I2C::update() { std::vector::iterator iter, end; for (iter = drivers.begin(), end = drivers.end(); iter != end; ++iter) { + // Per-driver fast tick (non-blocking) + (*iter)->fastTick(); + // Number of events which occurred for this driver msgi2cResponse.payload.resp_i2c_device_event.sensor_event_count = 0; diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h index 1ff17134f..4ea2869fd 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h @@ -34,7 +34,7 @@ class WipperSnapper_I2C_Driver { public: /*******************************************************************************/ /*! - @brief Instanctiates an I2C sensor. + @brief Instantiates an I2C sensor. @param i2c The I2C hardware interface, default is Wire. @param sensorAddress @@ -55,14 +55,15 @@ class WipperSnapper_I2C_Driver { /*******************************************************************************/ /*! - @brief Per-update background hook for drivers that need faster internal - sampling than the user publish interval. - Default is a no-op; override in concrete drivers (e.g., - SGP30/40) to maintain required ~1 Hz reads and accumulate/condition values - for later publish. - @note Call site: WipperSnapper_Component_I2C::update() will invoke - this once per loop for each driver. Implementations must be - non-blocking (do not delay); use millis()-based timing. + @brief Lightweight, per-update background hook for drivers that need + more frequent internal polling than the publish interval. + Default is a no-op; concrete drivers (e.g., SGP30/40) may + override this to perform a single non-blocking read and cache + results for later retrieval by getEvent*(). + @note Call site: WipperSnapper_Component_I2C::update() invokes this + once per loop for each driver. Implementations must be + non-blocking (do not delay); use millis()-based timing if + cadence is required. */ /*******************************************************************************/ virtual void fastTick() {} @@ -1413,8 +1414,8 @@ class WipperSnapper_I2C_Driver { long _ambientTempFPeriod = 0L; ///< The time period between reading the ///< ambient temp. (°F) sensor's value. long _ambientTempFPeriodPrv = - PERIOD_24HRS_AGO_MILLIS; ///< The time when the ambient temp. (°F) sensor - ///< was last read. + PERIOD_24HRS_AGO_MILLIS; ///< The time when the ambient temp. (°F) sensor + ///< was last read. long _objectTempFPeriod = 0L; ///< The time period between reading the object ///< temp. (°F) sensor's value. long _objectTempFPeriodPrv = diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h index 19e011504..4c82f36ae 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h @@ -33,7 +33,6 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { */ /*******************************************************************************/ ~WipperSnapper_I2C_Driver_SGP30() override { - // Called when a SGP30 component is deleted. if (_sgp30) { delete _sgp30; _sgp30 = nullptr; @@ -53,74 +52,62 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { _sgp30 = nullptr; return false; } - _sgp30->IAQinit(); // start IAQ algorithm - _lastFastMs = millis(); // reset fast sampler - _n = _eco2Sum = _tvocSum = 0; // clear accumulators + _sgp30->IAQinit(); // start IAQ algorithm + _did_measure = false; return true; } bool getEventECO2(sensors_event_t *senseEvent) override { if (!_sgp30) return false; - if (_n > 0) { - senseEvent->eCO2 = (uint16_t)(_eco2Sum / _n); - _eco2Sum = 0; - _tvocSum = 0; - _n = 0; - return true; + if (!_did_measure) { + _did_measure = _sgp30->IAQmeasure(); } - if (_sgp30->IAQmeasure()) { - senseEvent->eCO2 = (uint16_t)_sgp30->eCO2; - return true; - } - return false; + if (!_did_measure) + return false; + + senseEvent->eCO2 = (uint16_t)_sgp30->eCO2; + _did_measure = false; // consume cached reading + return true; } bool getEventTVOC(sensors_event_t *senseEvent) override { if (!_sgp30) return false; - if (_n > 0) { - senseEvent->tvoc = (uint16_t)(_tvocSum / _n); - _eco2Sum = 0; - _tvocSum = 0; - _n = 0; - return true; - } - if (_sgp30->IAQmeasure()) { - senseEvent->tvoc = (uint16_t)_sgp30->TVOC; - return true; + if (!_did_measure) { + _did_measure = _sgp30->IAQmeasure(); } - return false; + if (!_did_measure) + return false; + + senseEvent->tvoc = (uint16_t)_sgp30->TVOC; + _did_measure = false; // consume cached reading + return true; } + /*******************************************************************************/ + /*! + @brief Performs a lightweight background poll for the SGP30. + Calls IAQmeasure() once and caches whether it succeeded. + Cached values (eCO2, TVOC) are then available via getEvent*(). + No averaging or delay; must be non-blocking. + */ + /*******************************************************************************/ void fastTick() override { + if (!_sgp30) + return; if (!iaqEnabled()) - return; // nothing enabled, save cycles - uint32_t now = millis(); - if (now - _lastFastMs >= 1000) { // ~1 Hz cadence - if (_sgp30 && _sgp30->IAQmeasure()) { - _eco2Sum += _sgp30->eCO2; // uint16_t in library - _tvocSum += _sgp30->TVOC; // uint16_t in library - _n++; - } - _lastFastMs = now; - } + return; + + // Only perform a single IAQ measurement and cache the result + _did_measure = _sgp30->IAQmeasure(); } protected: Adafruit_SGP30 *_sgp30; ///< Pointer to SGP30 sensor object - /** Millis timestamp of last 1 Hz background read. */ - uint32_t _lastFastMs = 0; - - /** Number of samples accumulated since last publish. */ - uint32_t _n = 0; - - /** Running sum of eCO2 samples for averaging. */ - uint32_t _eco2Sum = 0; - - /** Running sum of TVOC samples for averaging. */ - uint32_t _tvocSum = 0; + /** Whether we have a fresh IAQ measurement ready. */ + bool _did_measure = false; /*******************************************************************************/ /*! @@ -134,4 +121,4 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { } }; -#endif // WipperSnapper_I2C_Driver_SGP30 +#endif // WipperSnapper_I2C_Driver_SGP30_H diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h index 6d64764e1..66632a2cb 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h @@ -70,10 +70,9 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { _sgp40 = nullptr; return false; } - _lastFastMs = millis(); - _n = 0; - _vocSum = 0.0f; - _rawSum = 0; + // Initialize cached values + _rawValue = 0; + _vocIdx = 0; return true; } @@ -89,14 +88,7 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { bool getEventRaw(sensors_event_t *rawEvent) override { if (!_sgp40) return false; - if (_n > 0) { - rawEvent->data[0] = (float)_rawSum / (float)_n; - _rawSum = 0; - _vocSum = 0.0f; - _n = 0; - return true; - } - rawEvent->data[0] = (float)_sgp40->measureRaw(); + rawEvent->data[0] = (float)_rawValue; return true; } @@ -112,22 +104,15 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { bool getEventVOCIndex(sensors_event_t *vocIndexEvent) override { if (!_sgp40) return false; - if (_n > 0) { - vocIndexEvent->voc_index = (uint16_t)(_vocSum / (float)_n); - _rawSum = 0; - _vocSum = 0.0f; - _n = 0; - return true; - } - vocIndexEvent->voc_index = (uint16_t)_sgp40->measureVocIndex(); + vocIndexEvent->voc_index = (uint16_t)_vocIdx; return true; } /*******************************************************************************/ /*! @brief Performs background sampling for the SGP40. - Runs once per second to accumulate raw and VOC index values - for later averaging in getEventRaw() and getEventVOCIndex(). + single poll (no averaging) and cache + results for getEventRaw()/getEventVOCIndex(). */ /*******************************************************************************/ void fastTick() override { @@ -136,29 +121,17 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { if (!vocEnabled()) return; - uint32_t now = millis(); - if (now - _lastFastMs >= 1000) { - _rawSum += _sgp40->measureRaw(); - _vocSum += _sgp40->measureVocIndex(); - _n++; - _lastFastMs = now; - } + // Single poll and cache latest values (no cadence/averaging) + _rawValue = _sgp40->measureRaw(); + _vocIdx = (uint16_t)_sgp40->measureVocIndex(); } protected: Adafruit_SGP40 *_sgp40; ///< SGP40 - /** Millis timestamp of last 1 Hz background read. */ - uint32_t _lastFastMs = 0; - - /** Number of samples accumulated since last publish. */ - uint32_t _n = 0; - - /** Running sum of VOC index samples for averaging. */ - float _vocSum = 0.0f; - - /** Running sum of raw samples for averaging. */ - uint32_t _rawSum = 0; + /** Cached latest measurements (no averaging). */ + uint16_t _rawValue = 0; + uint16_t _vocIdx = 0; /*******************************************************************************/ /*! From fe0ca83d47dbadeacca2958961f0ad71c62e16fc Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Thu, 25 Sep 2025 17:04:39 -0700 Subject: [PATCH 10/17] clang format --- src/components/i2c/drivers/WipperSnapper_I2C_Driver.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h index 4ea2869fd..96f683cd4 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h @@ -1414,8 +1414,8 @@ class WipperSnapper_I2C_Driver { long _ambientTempFPeriod = 0L; ///< The time period between reading the ///< ambient temp. (°F) sensor's value. long _ambientTempFPeriodPrv = - PERIOD_24HRS_AGO_MILLIS; ///< The time when the ambient temp. (°F) sensor - ///< was last read. + PERIOD_24HRS_AGO_MILLIS; ///< The time when the ambient temp. (°F) sensor + ///< was last read. long _objectTempFPeriod = 0L; ///< The time period between reading the object ///< temp. (°F) sensor's value. long _objectTempFPeriodPrv = From f9dbb5e5bdc670e384517b6f4b10cac2bfc33f38 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Thu, 25 Sep 2025 17:27:44 -0700 Subject: [PATCH 11/17] docs: add Doxygen comment for _vocIdx in SGP40 driver --- src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h index 66632a2cb..ed6488349 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h @@ -131,7 +131,7 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { /** Cached latest measurements (no averaging). */ uint16_t _rawValue = 0; - uint16_t _vocIdx = 0; + uint16_t _vocIdx = 0; ///< Index for the VOC (volatile organic compounds) sensor reading /*******************************************************************************/ /*! From 557a0f43e4c0fae4ca732ca43dff3d0623022d87 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Thu, 25 Sep 2025 17:41:47 -0700 Subject: [PATCH 12/17] docs: concise VOC index comment --- src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h index ed6488349..084a8f145 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h @@ -131,7 +131,7 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { /** Cached latest measurements (no averaging). */ uint16_t _rawValue = 0; - uint16_t _vocIdx = 0; ///< Index for the VOC (volatile organic compounds) sensor reading + uint16_t _vocIdx = 0; ///< VOC index value (0–500) /*******************************************************************************/ /*! From 0a0927dd0c7b900033efeccfadcda13c6eace6db Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Fri, 26 Sep 2025 17:39:28 -0700 Subject: [PATCH 13/17] SGP30/40: add fastTick 1Hz cadence, simplify caching, update docs --- .../i2c/drivers/WipperSnapper_I2C_Driver.h | 4 +- .../drivers/WipperSnapper_I2C_Driver_SGP30.h | 33 +++++++++++--- .../drivers/WipperSnapper_I2C_Driver_SGP40.h | 43 ++++++++++++++----- 3 files changed, 62 insertions(+), 18 deletions(-) diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h index 96f683cd4..4ea2869fd 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h @@ -1414,8 +1414,8 @@ class WipperSnapper_I2C_Driver { long _ambientTempFPeriod = 0L; ///< The time period between reading the ///< ambient temp. (°F) sensor's value. long _ambientTempFPeriodPrv = - PERIOD_24HRS_AGO_MILLIS; ///< The time when the ambient temp. (°F) sensor - ///< was last read. + PERIOD_24HRS_AGO_MILLIS; ///< The time when the ambient temp. (°F) sensor + ///< was last read. long _objectTempFPeriod = 0L; ///< The time period between reading the object ///< temp. (°F) sensor's value. long _objectTempFPeriodPrv = diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h index 4c82f36ae..b71672afb 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h @@ -4,6 +4,8 @@ #include "WipperSnapper_I2C_Driver.h" #include +#define SGP30_FASTTICK_INTERVAL_MS 1000 ///< Enforce ~1 Hz cadence + /**************************************************************************/ /*! @brief Class that provides a driver interface for a SGP30 sensor. @@ -54,6 +56,7 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { } _sgp30->IAQinit(); // start IAQ algorithm _did_measure = false; + _lastFastMs = millis() - SGP30_FASTTICK_INTERVAL_MS; return true; } @@ -87,10 +90,22 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { /*******************************************************************************/ /*! - @brief Performs a lightweight background poll for the SGP30. - Calls IAQmeasure() once and caches whether it succeeded. - Cached values (eCO2, TVOC) are then available via getEvent*(). - No averaging or delay; must be non-blocking. + @brief Performs background sampling for the SGP30. + + This method enforces a ~1 Hz cadence recommended by the sensor + datasheet. On each call, it checks the elapsed time since the + last poll using `millis()`. If at least + SGP30_FASTTICK_INTERVAL_MS have passed, it performs a single + IAQ measurement and caches the result in `_did_measure`. + + Cached values (eCO2, TVOC) are later returned by + `getEventECO2()` and `getEventTVOC()` without re-triggering I2C + traffic. + + @note Called automatically from + `WipperSnapper_Component_I2C::update()` once per loop iteration. + Must be non-blocking (no delays). The millis-based guard ensures + the sensor is not over-polled. */ /*******************************************************************************/ void fastTick() override { @@ -99,8 +114,11 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { if (!iaqEnabled()) return; - // Only perform a single IAQ measurement and cache the result - _did_measure = _sgp30->IAQmeasure(); + uint32_t now = millis(); + if (now - _lastFastMs >= SGP30_FASTTICK_INTERVAL_MS) { + _did_measure = _sgp30->IAQmeasure(); + _lastFastMs = now; + } } protected: @@ -109,6 +127,9 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { /** Whether we have a fresh IAQ measurement ready. */ bool _did_measure = false; + /** Timestamp of last poll to enforce 1 Hz cadence. */ + uint32_t _lastFastMs = 0; + /*******************************************************************************/ /*! @brief Returns whether IAQ background sampling should be active. diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h index 084a8f145..2001836f3 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h @@ -20,6 +20,8 @@ #include #include +#define SGP40_FASTTICK_INTERVAL_MS 1000 ///< Enforce ~1 Hz cadence + /**************************************************************************/ /*! @brief Class that provides a driver interface for the SGP40 sensor. @@ -73,6 +75,7 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { // Initialize cached values _rawValue = 0; _vocIdx = 0; + _lastFastMs = millis() - SGP40_FASTTICK_INTERVAL_MS; return true; } @@ -110,9 +113,21 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { /*******************************************************************************/ /*! - @brief Performs background sampling for the SGP40. - single poll (no averaging) and cache - results for getEventRaw()/getEventVOCIndex(). + @brief Performs background sampling for the SGP40. + + This method enforces a ~1 Hz cadence recommended by the sensor + datasheet. On each call, it checks the elapsed time since the last + poll using `millis()`. If at least SGP40_FASTTICK_INTERVAL_MS ms + have passed, it reads a new raw value and VOC index from the + sensor and caches them in `_rawValue` and `_vocIdx`. + + Cached results are later returned by `getEventRaw()` and + `getEventVOCIndex()` without re-triggering I2C traffic. + + @note Called automatically from + `WipperSnapper_Component_I2C::update()` once per loop iteration. + Must be non-blocking (no delays). The millis-based guard ensures + the sensor is not over-polled. */ /*******************************************************************************/ void fastTick() override { @@ -121,17 +136,25 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { if (!vocEnabled()) return; - // Single poll and cache latest values (no cadence/averaging) - _rawValue = _sgp40->measureRaw(); - _vocIdx = (uint16_t)_sgp40->measureVocIndex(); + uint32_t now = millis(); + if (now - _lastFastMs >= SGP40_FASTTICK_INTERVAL_MS) { + _rawValue = _sgp40->measureRaw(); + _vocIdx = (int32_t)_sgp40->measureVocIndex(); + _lastFastMs = now; + } } protected: - Adafruit_SGP40 *_sgp40; ///< SGP40 + Adafruit_SGP40 *_sgp40; ///< Pointer to SGP40 sensor object - /** Cached latest measurements (no averaging). */ - uint16_t _rawValue = 0; - uint16_t _vocIdx = 0; ///< VOC index value (0–500) + /** + * Cached latest measurements from the sensor. + * - _rawValue: raw sensor output (ticks) + * - _vocIdx: VOC Index (signed, per datasheet) + */ + uint16_t _rawValue = 0; ///< Raw sensor output (ticks) + int32_t _vocIdx = 0; ///< VOC Index (signed, per datasheet) + uint32_t _lastFastMs = 0; ///< Last poll timestamp to enforce 1 Hz cadence /*******************************************************************************/ /*! From 7e6e75c7395d0220684cc977d9bc3b2d04a0268c Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Fri, 26 Sep 2025 17:57:01 -0700 Subject: [PATCH 14/17] clang whitespace --- src/components/i2c/drivers/WipperSnapper_I2C_Driver.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h index 4ea2869fd..96f683cd4 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver.h @@ -1414,8 +1414,8 @@ class WipperSnapper_I2C_Driver { long _ambientTempFPeriod = 0L; ///< The time period between reading the ///< ambient temp. (°F) sensor's value. long _ambientTempFPeriodPrv = - PERIOD_24HRS_AGO_MILLIS; ///< The time when the ambient temp. (°F) sensor - ///< was last read. + PERIOD_24HRS_AGO_MILLIS; ///< The time when the ambient temp. (°F) sensor + ///< was last read. long _objectTempFPeriod = 0L; ///< The time period between reading the object ///< temp. (°F) sensor's value. long _objectTempFPeriodPrv = From 7f7f4ddaee37bd13ec9cba7de4ae5796118b3c09 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Mon, 29 Sep 2025 08:03:30 -0700 Subject: [PATCH 15/17] remove override on sgp30/sgp40 destructors --- src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h | 2 +- src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h index b71672afb..ef696b0e3 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h @@ -34,7 +34,7 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { @brief Destructor for an SGP30 sensor. */ /*******************************************************************************/ - ~WipperSnapper_I2C_Driver_SGP30() override { + ~WipperSnapper_I2C_Driver_SGP30() { if (_sgp30) { delete _sgp30; _sgp30 = nullptr; diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h index 2001836f3..a31d4807e 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP40.h @@ -52,7 +52,7 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { when the driver is destroyed. */ /*******************************************************************************/ - ~WipperSnapper_I2C_Driver_SGP40() override { + ~WipperSnapper_I2C_Driver_SGP40() { if (_sgp40) { delete _sgp40; _sgp40 = nullptr; From ed2fe6715d838f92004ed77348e37691ceda97a0 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Tue, 30 Sep 2025 11:00:56 -0700 Subject: [PATCH 16/17] sgp30 fastTick only, remove consume cache confusion --- .../drivers/WipperSnapper_I2C_Driver_SGP30.h | 70 ++++++++++++------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h index ef696b0e3..56500fc2a 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h @@ -55,36 +55,55 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { return false; } _sgp30->IAQinit(); // start IAQ algorithm - _did_measure = false; + + // Initialize cached values and cadence + _eco2 = 0; + _tvoc = 0; _lastFastMs = millis() - SGP30_FASTTICK_INTERVAL_MS; return true; } +/*******************************************************************************/ +/*! + @brief Gets the most recently cached eCO2 reading. + + This value is updated in `fastTick()` at a ~1 Hz cadence + and returned directly here without re-triggering an I2C + transaction. + + @param senseEvent + Pointer to an Adafruit Sensor event that will be populated + with the cached eCO2 value (in ppm). + + @returns True if a cached value is available, False otherwise. +*/ +/*******************************************************************************/ bool getEventECO2(sensors_event_t *senseEvent) override { if (!_sgp30) return false; - if (!_did_measure) { - _did_measure = _sgp30->IAQmeasure(); - } - if (!_did_measure) - return false; - - senseEvent->eCO2 = (uint16_t)_sgp30->eCO2; - _did_measure = false; // consume cached reading + senseEvent->eCO2 = _eco2; return true; } +/*******************************************************************************/ +/*! + @brief Gets the most recently cached TVOC reading. + + This value is updated in `fastTick()` at a ~1 Hz cadence + and returned directly here without re-triggering an I2C + transaction. + + @param senseEvent + Pointer to an Adafruit Sensor event that will be populated + with the cached TVOC value (in ppb). + + @returns True if a cached value is available, False otherwise. +*/ +/*******************************************************************************/ bool getEventTVOC(sensors_event_t *senseEvent) override { if (!_sgp30) return false; - if (!_did_measure) { - _did_measure = _sgp30->IAQmeasure(); - } - if (!_did_measure) - return false; - - senseEvent->tvoc = (uint16_t)_sgp30->TVOC; - _did_measure = false; // consume cached reading + senseEvent->tvoc = _tvoc; return true; } @@ -96,11 +115,10 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { datasheet. On each call, it checks the elapsed time since the last poll using `millis()`. If at least SGP30_FASTTICK_INTERVAL_MS have passed, it performs a single - IAQ measurement and caches the result in `_did_measure`. + IAQ measurement and caches the results in `_eco2` and `_tvoc`. - Cached values (eCO2, TVOC) are later returned by - `getEventECO2()` and `getEventTVOC()` without re-triggering I2C - traffic. + Cached values are then returned by `getEventECO2()` and + `getEventTVOC()` without re-triggering I2C traffic. @note Called automatically from `WipperSnapper_Component_I2C::update()` once per loop iteration. @@ -116,7 +134,10 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { uint32_t now = millis(); if (now - _lastFastMs >= SGP30_FASTTICK_INTERVAL_MS) { - _did_measure = _sgp30->IAQmeasure(); + if (_sgp30->IAQmeasure()) { + _eco2 = (uint16_t)_sgp30->eCO2; + _tvoc = (uint16_t)_sgp30->TVOC; + } _lastFastMs = now; } } @@ -124,8 +145,9 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { protected: Adafruit_SGP30 *_sgp30; ///< Pointer to SGP30 sensor object - /** Whether we have a fresh IAQ measurement ready. */ - bool _did_measure = false; + /** Cached latest measurements (no averaging). */ + uint16_t _eco2 = 0; ///< eCO2, in ppm + uint16_t _tvoc = 0; ///< TVOC, in ppb /** Timestamp of last poll to enforce 1 Hz cadence. */ uint32_t _lastFastMs = 0; From a0fdee7f2755b0c936dfba52c19f76c460195889 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Tue, 30 Sep 2025 12:22:33 -0700 Subject: [PATCH 17/17] SGP30: align Doxygen / clang --- .../drivers/WipperSnapper_I2C_Driver_SGP30.h | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h index 56500fc2a..4c283a840 100644 --- a/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h +++ b/src/components/i2c/drivers/WipperSnapper_I2C_Driver_SGP30.h @@ -63,21 +63,21 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { return true; } -/*******************************************************************************/ -/*! - @brief Gets the most recently cached eCO2 reading. + /*******************************************************************************/ + /*! + @brief Gets the most recently cached eCO2 reading. - This value is updated in `fastTick()` at a ~1 Hz cadence - and returned directly here without re-triggering an I2C - transaction. + This value is updated in `fastTick()` at a ~1 Hz cadence + and returned directly here without re-triggering an I2C + transaction. - @param senseEvent - Pointer to an Adafruit Sensor event that will be populated - with the cached eCO2 value (in ppm). + @param senseEvent + Pointer to an Adafruit_Sensor event that will be populated + with the cached eCO2 value (in ppm). - @returns True if a cached value is available, False otherwise. -*/ -/*******************************************************************************/ + @returns True if a cached value is available, False otherwise. + */ + /*******************************************************************************/ bool getEventECO2(sensors_event_t *senseEvent) override { if (!_sgp30) return false; @@ -85,21 +85,21 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { return true; } -/*******************************************************************************/ -/*! - @brief Gets the most recently cached TVOC reading. + /*******************************************************************************/ + /*! + @brief Gets the most recently cached TVOC reading. - This value is updated in `fastTick()` at a ~1 Hz cadence - and returned directly here without re-triggering an I2C - transaction. + This value is updated in `fastTick()` at a ~1 Hz cadence + and returned directly here without re-triggering an I2C + transaction. - @param senseEvent - Pointer to an Adafruit Sensor event that will be populated - with the cached TVOC value (in ppb). + @param senseEvent + Pointer to an Adafruit_Sensor event that will be populated + with the cached TVOC value (in ppb). - @returns True if a cached value is available, False otherwise. -*/ -/*******************************************************************************/ + @returns True if a cached value is available, False otherwise. + */ + /*******************************************************************************/ bool getEventTVOC(sensors_event_t *senseEvent) override { if (!_sgp30) return false;