diff --git a/src/components/i2c/WipperSnapper_I2C.cpp b/src/components/i2c/WipperSnapper_I2C.cpp index 7e02c9922..87582277b 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; @@ -1354,9 +1354,13 @@ 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) { + // 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 e76532719..96f683cd4 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 Instantiates an I2C sensor. @param i2c The I2C hardware interface, default is Wire. @param sensorAddress @@ -53,6 +53,21 @@ class WipperSnapper_I2C_Driver { /*******************************************************************************/ virtual ~WipperSnapper_I2C_Driver() {} + /*******************************************************************************/ + /*! + @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() {} + /*******************************************************************************/ /*! @brief Initializes the I2C sensor and begins I2C. @@ -1312,7 +1327,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..4c283a840 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. @@ -24,6 +26,7 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { : WipperSnapper_I2C_Driver(i2c, sensorAddress) { _i2c = i2c; _sensorAddress = sensorAddress; + _sgp30 = nullptr; } /*******************************************************************************/ @@ -32,8 +35,10 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { */ /*******************************************************************************/ ~WipperSnapper_I2C_Driver_SGP30() { - // Called when a SGP30 component is deleted. - delete _sgp30; + if (_sgp30) { + delete _sgp30; + _sgp30 = nullptr; + } } /*******************************************************************************/ @@ -44,27 +49,119 @@ class WipperSnapper_I2C_Driver_SGP30 : public WipperSnapper_I2C_Driver { /*******************************************************************************/ bool begin() { _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 + + // Initialize cached values and cadence + _eco2 = 0; + _tvoc = 0; + _lastFastMs = millis() - SGP30_FASTTICK_INTERVAL_MS; + return true; } - bool getEventECO2(sensors_event_t *senseEvent) { - bool result = _sgp30->IAQmeasure(); - if (result) { - senseEvent->eCO2 = _sgp30->eCO2; - } - return result; + /*******************************************************************************/ + /*! + @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; + senseEvent->eCO2 = _eco2; + return true; } - bool getEventTVOC(sensors_event_t *senseEvent) { - bool result = _sgp30->IAQmeasure(); - if (result) { - senseEvent->tvoc = _sgp30->TVOC; + /*******************************************************************************/ + /*! + @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; + senseEvent->tvoc = _tvoc; + return true; + } + + /*******************************************************************************/ + /*! + @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 results in `_eco2` and `_tvoc`. + + 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. + Must be non-blocking (no delays). The millis-based guard ensures + the sensor is not over-polled. + */ + /*******************************************************************************/ + void fastTick() override { + if (!_sgp30) + return; + if (!iaqEnabled()) + return; + + uint32_t now = millis(); + if (now - _lastFastMs >= SGP30_FASTTICK_INTERVAL_MS) { + if (_sgp30->IAQmeasure()) { + _eco2 = (uint16_t)_sgp30->eCO2; + _tvoc = (uint16_t)_sgp30->TVOC; + } + _lastFastMs = now; } - return result; } protected: - Adafruit_SGP30 *_sgp30; ///< Pointer to SGP30 temperature sensor object + Adafruit_SGP30 *_sgp30; ///< Pointer to SGP30 sensor object + + /** 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; + + /*******************************************************************************/ + /*! + @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); + } }; -#endif // WipperSnapper_I2C_Driver_SGP30 \ No newline at end of file +#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 3e201b106..a31d4807e 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. @@ -40,6 +42,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() { + if (_sgp40) { + delete _sgp40; + _sgp40 = nullptr; + } } /*******************************************************************************/ @@ -50,12 +67,15 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { /*******************************************************************************/ bool begin() { _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 - + // Initialize cached values + _rawValue = 0; + _vocIdx = 0; + _lastFastMs = millis() - SGP40_FASTTICK_INTERVAL_MS; return true; } @@ -64,12 +84,14 @@ 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) { - rawEvent->data[0] = (float)_sgp40->measureRaw(); + bool getEventRaw(sensors_event_t *rawEvent) override { + if (!_sgp40) + return false; + rawEvent->data[0] = (float)_rawValue; return true; } @@ -82,13 +104,67 @@ class WipperSnapper_I2C_Driver_SGP40 : public WipperSnapper_I2C_Driver { otherwise. */ /*******************************************************************************/ - bool getEventVOCIndex(sensors_event_t *vocIndexEvent) { - vocIndexEvent->voc_index = (float)_sgp40->measureVocIndex(); + bool getEventVOCIndex(sensors_event_t *vocIndexEvent) override { + if (!_sgp40) + return false; + vocIndexEvent->voc_index = (uint16_t)_vocIdx; return true; } + /*******************************************************************************/ + /*! + @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 { + if (!_sgp40) + return; + if (!vocEnabled()) + return; + + 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; ///< SEN5X driver object + Adafruit_SGP40 *_sgp40; ///< Pointer to SGP40 sensor object + + /** + * 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 + + /*******************************************************************************/ + /*! + @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); + } }; -#endif // WipperSnapper_I2C_Driver_SEN5X +#endif // WipperSnapper_I2C_Driver_SGP40_H