Skip to content

Commit 9b5a27a

Browse files
committed
add(component): SGP41 (VOC/NOx index)
1 parent e52f1db commit 9b5a27a

File tree

6 files changed

+284
-2
lines changed

6 files changed

+284
-2
lines changed

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ html/*
4040
.vscode/*
4141
src/.vscode/settings.json
4242

43+
# Visual Studio artifacts
44+
.vs/
45+
*.vcxproj
46+
*.vcxproj.filters
47+
*.vcxproj.user
48+
4349
.DS_STORE
4450

4551
examples/Wippersnapper_demo/build/
@@ -54,4 +60,4 @@ data/
5460
tests/
5561
venv/
5662

57-
Doxyfile
63+
Doxyfile

library.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ paragraph=Arduino application for Adafruit.io WipperSnapper
77
category=Communication
88
url=https://github.com/adafruit/Adafruit_Wippersnapper_Arduino
99
architectures=*
10-
depends=OmronD6T - Community Fork, SdFat - Adafruit Fork, Adafruit NeoPixel, Adafruit SPA06_003, Adafruit SPIFlash, ArduinoJson, Adafruit DotStar, Adafruit HDC302x, Adafruit INA219, Adafruit INA260 Library, Adafruit INA237 and INA238 Library, Adafruit LTR329 and LTR303, Adafruit LTR390 Library, Adafruit MCP3421, Adafruit MLX90632 Library, Adafruit NAU7802 Library, Adafruit SleepyDog Library, Adafruit TMP117, Adafruit TinyUSB Library, Adafruit AHTX0, Adafruit AS5600 Library, Adafruit BME280 Library, Adafruit BMP280 Library, Adafruit BMP3XX Library, Adafruit BMP5xx Library, Adafruit DPS310, Adafruit DS248x, Adafruit SCD30, Adafruit SGP30 Sensor, Adafruit SGP40 Sensor, Sensirion I2C SCD4x, Sensirion I2C SEN5X, Sensirion I2C SEN66, arduino-sht, Adafruit Si7021 Library, Adafruit MQTT Library, Adafruit MS8607, Adafruit MCP9808 Library, Adafruit MCP9600 Library, Adafruit MPL115A2, Adafruit MPRLS Library, Adafruit TSL2591 Library, Adafruit_VL53L0X, Adafruit VL53L1X, STM32duino VL53L4CD, STM32duino VL53L4CX, Adafruit_VL6180X, Adafruit PM25 AQI Sensor, Adafruit QMC5883P Library, Adafruit VCNL4020 Library, Adafruit VCNL4040, Adafruit VCNL4200 Library, Adafruit VEML7700 Library, Adafruit LC709203F, Adafruit LPS2X, Adafruit LPS28, Adafruit LPS35HW, Adafruit seesaw Library, Adafruit BME680 Library, Adafruit MAX1704X, Adafruit ADT7410 Library, Adafruit HTS221, Adafruit HTU21DF Library, Adafruit HTU31D Library, Adafruit PCT2075, hp_BH1750, ENS160 - Adafruit Fork, Adafruit BusIO, Adafruit Unified Sensor, Sensirion Core, Adafruit GFX Library, Adafruit LED Backpack Library, Adafruit LiquidCrystal, Adafruit SH110X, Adafruit SSD1306, Adafruit EPD, Adafruit ST7735 and ST7789 Library
10+
depends=OmronD6T - Community Fork, SdFat - Adafruit Fork, Adafruit NeoPixel, Adafruit SPA06_003, Adafruit SPIFlash, ArduinoJson, Adafruit DotStar, Adafruit HDC302x, Adafruit INA219, Adafruit INA260 Library, Adafruit INA237 and INA238 Library, Adafruit LTR329 and LTR303, Adafruit LTR390 Library, Adafruit MCP3421, Adafruit MLX90632 Library, Adafruit NAU7802 Library, Adafruit SleepyDog Library, Adafruit TMP117, Adafruit TinyUSB Library, Adafruit AHTX0, Adafruit AS5600 Library, Adafruit BME280 Library, Adafruit BMP280 Library, Adafruit BMP3XX Library, Adafruit BMP5xx Library, Adafruit DPS310, Adafruit DS248x, Adafruit SCD30, Adafruit SGP30 Sensor, Adafruit SGP40 Sensor, Adafruit SGP41, Sensirion I2C SCD4x, Sensirion I2C SEN5X, Sensirion I2C SEN66, arduino-sht, Adafruit Si7021 Library, Adafruit MQTT Library, Adafruit MS8607, Adafruit MCP9808 Library, Adafruit MCP9600 Library, Adafruit MPL115A2, Adafruit MPRLS Library, Adafruit TSL2591 Library, Adafruit_VL53L0X, Adafruit VL53L1X, STM32duino VL53L4CD, STM32duino VL53L4CX, Adafruit_VL6180X, Adafruit PM25 AQI Sensor, Adafruit QMC5883P Library, Adafruit VCNL4020 Library, Adafruit VCNL4040, Adafruit VCNL4200 Library, Adafruit VEML7700 Library, Adafruit LC709203F, Adafruit LPS2X, Adafruit LPS28, Adafruit LPS35HW, Adafruit seesaw Library, Adafruit BME680 Library, Adafruit MAX1704X, Adafruit ADT7410 Library, Adafruit HTS221, Adafruit HTU21DF Library, Adafruit HTU31D Library, Adafruit PCT2075, hp_BH1750, ENS160 - Adafruit Fork, Adafruit BusIO, Adafruit Unified Sensor, Sensirion Core, Adafruit GFX Library, Adafruit LED Backpack Library, Adafruit LiquidCrystal, Adafruit SH110X, Adafruit SSD1306, Adafruit EPD, Adafruit ST7735 and ST7789 Library

platformio.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ lib_deps =
4444
adafruit/Adafruit SCD30
4545
adafruit/Adafruit SGP30 Sensor
4646
adafruit/Adafruit SGP40 Sensor
47+
adafruit/Adafruit SGP41
4748
adafruit/Adafruit Si7021 Library
4849
adafruit/Adafruit SPA06_003
4950
adafruit/Adafruit VCNL4020 Library

src/components/i2c/WipperSnapper_I2C.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,17 @@ bool WipperSnapper_Component_I2C::initI2CDevice(
528528
_sgp40->configureDriver(msgDeviceInitReq);
529529
drivers.push_back(_sgp40);
530530
WS_DEBUG_PRINTLN("SGP40 Initialized Successfully!");
531+
} else if (strcmp("sgp41", msgDeviceInitReq->i2c_device_name) == 0) {
532+
_sgp41 = new WipperSnapper_I2C_Driver_SGP41(this->_i2c, i2cAddress);
533+
if (!_sgp41->begin()) {
534+
WS_DEBUG_PRINTLN("ERROR: Failed to initialize SGP41!");
535+
_busStatusResponse =
536+
wippersnapper_i2c_v1_BusResponse_BUS_RESPONSE_DEVICE_INIT_FAIL;
537+
return false;
538+
}
539+
_sgp41->configureDriver(msgDeviceInitReq);
540+
drivers.push_back(_sgp41);
541+
WS_DEBUG_PRINTLN("SGP41 Initialized Successfully!");
531542
} else if ((strcmp("sht20", msgDeviceInitReq->i2c_device_name) == 0) ||
532543
(strcmp("si7021", msgDeviceInitReq->i2c_device_name) == 0)) {
533544
_si7021 = new WipperSnapper_I2C_Driver_SI7021(this->_i2c, i2cAddress);

src/components/i2c/WipperSnapper_I2C.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
#include "drivers/WipperSnapper_I2C_Driver_SEN6X.h"
7373
#include "drivers/WipperSnapper_I2C_Driver_SGP30.h"
7474
#include "drivers/WipperSnapper_I2C_Driver_SGP40.h"
75+
#include "drivers/WipperSnapper_I2C_Driver_SGP41.h"
7576
#include "drivers/WipperSnapper_I2C_Driver_SHT3X.h"
7677
#include "drivers/WipperSnapper_I2C_Driver_SHT4X.h"
7778
#include "drivers/WipperSnapper_I2C_Driver_SHTC3.h"
@@ -205,6 +206,7 @@ class WipperSnapper_Component_I2C {
205206
WipperSnapper_I2C_Driver_SEN6X *_sen6x = nullptr;
206207
WipperSnapper_I2C_Driver_SGP30 *_sgp30 = nullptr;
207208
WipperSnapper_I2C_Driver_SGP40 *_sgp40 = nullptr;
209+
WipperSnapper_I2C_Driver_SGP41 *_sgp41 = nullptr;
208210
WipperSnapper_I2C_Driver_SPA06_003 *_spa06_003 = nullptr;
209211
WipperSnapper_I2C_Driver_PCT2075 *_pct2075 = nullptr;
210212
WipperSnapper_I2C_Driver_PM25 *_pm25 = nullptr;
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
/*!
2+
* @file WipperSnapper_I2C_Driver_SGP41.h
3+
*
4+
* Device driver for the SGP41 VOC/gas sensor.
5+
*
6+
* Adafruit invests time and resources providing this open source code,
7+
* please support Adafruit and open-source hardware by purchasing
8+
* products from Adafruit!
9+
*
10+
* Copyright (c) Tyeth Gundry 2025 for Adafruit Industries.
11+
*
12+
* MIT license, all text here must be included in any redistribution.
13+
*
14+
*/
15+
16+
#ifndef WipperSnapper_I2C_Driver_SGP41_H
17+
#define WipperSnapper_I2C_Driver_SGP41_H
18+
19+
#include "WipperSnapper_I2C_Driver.h"
20+
#include <Adafruit_SGP41.h>
21+
#include <NOxGasIndexAlgorithm.h>
22+
#include <VOCGasIndexAlgorithm.h>
23+
#include <Wire.h>
24+
25+
#define SGP41_FASTTICK_INTERVAL_MS 1000 ///< Enforce ~1 Hz cadence
26+
#define SGP41_CONDITIONING_TICKS 10 ///< Recommended warmup cycles
27+
#define SGP41_VOC_LEARNING_MS 60000UL ///< VOC index meaningful after ~60s
28+
#define SGP41_NOX_LEARNING_MS 300000UL ///< NOx index meaningful after ~300s
29+
30+
/**************************************************************************/
31+
/*!
32+
@brief Class that provides a driver interface for the SGP41 sensor.
33+
*/
34+
/**************************************************************************/
35+
class WipperSnapper_I2C_Driver_SGP41 : public WipperSnapper_I2C_Driver {
36+
public:
37+
/*******************************************************************************/
38+
/*!
39+
@brief Constructor for a SGP41 sensor.
40+
@param i2c
41+
The I2C interface.
42+
@param sensorAddress
43+
7-bit device address.
44+
*/
45+
/*******************************************************************************/
46+
WipperSnapper_I2C_Driver_SGP41(TwoWire *i2c, uint16_t sensorAddress)
47+
: WipperSnapper_I2C_Driver(i2c, sensorAddress) {
48+
_i2c = i2c;
49+
_sensorAddress = sensorAddress;
50+
_sgp41 = nullptr;
51+
}
52+
53+
/*******************************************************************************/
54+
/*!
55+
@brief Destructor for an SGP41 sensor driver.
56+
Cleans up and deallocates the underlying Adafruit_SGP41 object
57+
when the driver is destroyed.
58+
*/
59+
/*******************************************************************************/
60+
~WipperSnapper_I2C_Driver_SGP41() {
61+
if (_sgp41) {
62+
_sgp41->turnHeaterOff();
63+
delete _sgp41;
64+
_sgp41 = nullptr;
65+
}
66+
}
67+
68+
/*******************************************************************************/
69+
/*!
70+
@brief Initializes the SGP41 sensor and begins I2C.
71+
@returns True if initialized successfully, False otherwise.
72+
*/
73+
/*******************************************************************************/
74+
bool begin() {
75+
_sgp41 = new Adafruit_SGP41();
76+
if (!_sgp41 || !_sgp41->begin((uint8_t)_sensorAddress, _i2c)) {
77+
delete _sgp41;
78+
_sgp41 = nullptr;
79+
return false;
80+
}
81+
82+
_sgp41->softReset();
83+
84+
uint16_t serialNumber[3] = {0, 0, 0};
85+
_hasSerial = _sgp41->getSerialNumber(serialNumber);
86+
if (_hasSerial) {
87+
_serialNumber[0] = serialNumber[0];
88+
_serialNumber[1] = serialNumber[1];
89+
_serialNumber[2] = serialNumber[2];
90+
}
91+
92+
_selfTestResult = _sgp41->executeSelfTest();
93+
94+
// Initialize cached values
95+
_rawValue = 0;
96+
_rawNOxValue = 0;
97+
_vocIdx = 0;
98+
_noxIdx = 0;
99+
_conditioningTicks = 0;
100+
_algoStartMs = millis();
101+
_lastFastMs = millis() - SGP41_FASTTICK_INTERVAL_MS;
102+
return true;
103+
}
104+
105+
/*******************************************************************************/
106+
/*!
107+
@brief Gets the sensor's current raw unprocessed value.
108+
@param rawEvent
109+
Pointer to an Adafruit_Sensor event.
110+
@returns True if the raw value was obtained successfully, False
111+
otherwise.
112+
*/
113+
/*******************************************************************************/
114+
bool getEventRaw(sensors_event_t *rawEvent) override {
115+
if (!_sgp41)
116+
return false;
117+
rawEvent->data[0] = (float)_rawValue;
118+
return true;
119+
}
120+
121+
/*******************************************************************************/
122+
/*!
123+
@brief Gets the SGP41's current VOC reading.
124+
@param vocIndexEvent
125+
Adafruit Sensor event for VOC Index (1-500, 100 is normal)
126+
@returns True if the sensor value was obtained successfully, False
127+
otherwise.
128+
*/
129+
/*******************************************************************************/
130+
bool getEventVOCIndex(sensors_event_t *vocIndexEvent) override {
131+
if (!_sgp41)
132+
return false;
133+
// Note: VOC algorithm learning period is ~60 seconds from startup.
134+
// Values are valid for publishing immediately, but become meaningful
135+
// only after this warmup interval.
136+
vocIndexEvent->voc_index = _vocIdx;
137+
return true;
138+
}
139+
140+
/*******************************************************************************/
141+
/*!
142+
@brief Gets the SGP41's current NOx reading.
143+
@param noxIndexEvent
144+
Adafruit Sensor event for NOx Index.
145+
@returns True if the sensor value was obtained successfully, False
146+
otherwise.
147+
*/
148+
/*******************************************************************************/
149+
bool getEventNOxIndex(sensors_event_t *noxIndexEvent) override {
150+
if (!_sgp41)
151+
return false;
152+
// Note: NOx algorithm learning period is ~300 seconds from startup.
153+
// Values are valid for publishing immediately, but become meaningful
154+
// only after this warmup interval.
155+
noxIndexEvent->nox_index = _noxIdx;
156+
return true;
157+
}
158+
159+
/*******************************************************************************/
160+
/*!
161+
@brief Performs background sampling for the SGP41.
162+
163+
This method enforces a ~1 Hz cadence recommended by the sensor
164+
datasheet. On each call, it checks the elapsed time since the last
165+
poll using `millis()`. If at least SGP41_FASTTICK_INTERVAL_MS ms
166+
have passed, it reads a new raw value and VOC index from the
167+
sensor and caches them in `_rawValue` and `_vocIdx`.
168+
169+
Cached results are later returned by `getEventRaw()` and
170+
`getEventVOCIndex()` without re-triggering I2C traffic.
171+
172+
@note Called automatically from
173+
`WipperSnapper_Component_I2C::update()` once per loop iteration.
174+
Must be non-blocking (no delays). The millis-based guard ensures
175+
the sensor is not over-polled.
176+
*/
177+
/*******************************************************************************/
178+
void fastTick() override {
179+
if (!_sgp41)
180+
return;
181+
if (!gasEnabled())
182+
return;
183+
184+
uint32_t now = millis();
185+
if (now - _lastFastMs >= SGP41_FASTTICK_INTERVAL_MS) {
186+
uint16_t srawVoc = 0;
187+
uint16_t srawNox = 0;
188+
bool readOk = false;
189+
190+
if (_conditioningTicks < SGP41_CONDITIONING_TICKS) {
191+
// Conditioning is part of expected SGP41 startup usage.
192+
// It warms up the VOC sensing path and seeds early baseline behavior.
193+
// We currently use library defaults (50% RH, 25C) because Wippersnapper
194+
// does not yet provide reference humidity/temperature feeds to this
195+
// driver. Future integration point: pass external RH/T references here.
196+
readOk = _sgp41->executeConditioning(&srawVoc);
197+
srawNox = 0;
198+
_conditioningTicks++;
199+
} else {
200+
// After conditioning, 1 Hz raw sampling is expected usage for SGP41.
201+
// This call supports RH/T compensation; we currently keep defaults.
202+
// Future integration point: call
203+
// measureRawSignals(&srawVoc, &srawNox, rh, tempC)
204+
// once reference feeds are available.
205+
readOk = _sgp41->measureRawSignals(&srawVoc, &srawNox);
206+
}
207+
208+
if (readOk) {
209+
_rawValue = srawVoc;
210+
_rawNOxValue = srawNox;
211+
212+
// Follow Adafruit_SGP41 gas_index example flow:
213+
// raw ticks -> Sensirion VOC/NOx gas index algorithms.
214+
_vocIdx = _vocAlgorithm.process((int32_t)srawVoc);
215+
_noxIdx = _noxAlgorithm.process((int32_t)srawNox);
216+
217+
// Learning-time guidance (from example):
218+
// VOC becomes meaningful after ~1 minute, NOx after ~5 minutes.
219+
// Future integration point: expose a quality/status flag once
220+
// Wippersnapper signal schema has a per-reading readiness field.
221+
}
222+
223+
_lastFastMs = now;
224+
}
225+
}
226+
227+
protected:
228+
Adafruit_SGP41 *_sgp41; ///< Pointer to SGP41 sensor object
229+
230+
/**
231+
* Cached latest measurements from the sensor.
232+
* - _rawValue: raw VOC sensor output (ticks)
233+
* - _rawNOxValue: raw NOx sensor output (ticks)
234+
* - _vocIdx: calculated VOC Gas Index
235+
* - _noxIdx: calculated NOx Gas Index
236+
*/
237+
uint16_t _rawValue = 0; ///< Raw VOC sensor output (ticks)
238+
uint16_t _rawNOxValue = 0; ///< Raw NOx sensor output (ticks)
239+
float _vocIdx = 0; ///< Calculated VOC Gas Index
240+
float _noxIdx = 0; ///< Calculated NOx Gas Index
241+
VOCGasIndexAlgorithm _vocAlgorithm; ///< VOC gas index state machine
242+
NOxGasIndexAlgorithm _noxAlgorithm; ///< NOx gas index state machine
243+
uint8_t _conditioningTicks = 0; ///< Completed initial conditioning cycles
244+
uint32_t _algoStartMs = 0; ///< Timestamp when gas index learning began
245+
uint16_t _serialNumber[3] = {0, 0, 0}; ///< Optional serial number cache
246+
uint16_t _selfTestResult = 0; ///< Optional self-test cache
247+
bool _hasSerial = false; ///< True if serial number read succeeded
248+
uint32_t _lastFastMs = 0; ///< Last poll timestamp to enforce cadence
249+
250+
/*******************************************************************************/
251+
/*!
252+
@brief Returns whether VOC background sampling should be active.
253+
@return True if either VOC Index or raw value is configured to publish.
254+
*/
255+
/*******************************************************************************/
256+
inline bool gasEnabled() {
257+
return (getSensorVOCIndexPeriod() > 0) || (getSensorNOxIndexPeriod() > 0) ||
258+
(getSensorRawPeriod() > 0);
259+
}
260+
};
261+
262+
#endif // WipperSnapper_I2C_Driver_SGP41_H

0 commit comments

Comments
 (0)