Skip to content

Commit c627e60

Browse files
Merge pull request #4 from CrakeNotSnowman/voc_algorithm_dev
Adding Humidity and Temperature compensation to SGP40 in preperation for VOC Algorithm
2 parents 66ddc29 + b9ea15d commit c627e60

File tree

6 files changed

+168
-28
lines changed

6 files changed

+168
-28
lines changed

README.rst

+24-2
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,38 @@ Usage Example
6464
6565
import time
6666
import board
67-
import busio
67+
import adafruit_sgp40
6868
69-
i2c = busio.I2C(board.SCL, board.SDA)
69+
i2c = board.I2C() # uses board.SCL and board.SDA
7070
sgp = adafruit_sgp40(i2c)
7171
7272
while True:
7373
print("Measurement: ", sgp.raw)
7474
print("")
7575
sleep(1)
7676
77+
For humidity compensated raw gas readings, we'll need a secondary sensor such as the bme280
78+
79+
.. code-block:: python3
80+
81+
import time
82+
import board
83+
import adafruit_sgp40
84+
import adafruit_bme280
85+
86+
i2c = board.I2C() # uses board.SCL and board.SDA
87+
sgp = adafruit_sgp40.SGP40(i2c)
88+
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c)
89+
90+
while True:
91+
temperature = bme280.temperature
92+
humidity = bme280.relative_humidity
93+
compensated_raw_gas = sgp.measure_raw(temperature = temperature, relative_humidity = humidity)
94+
print(compensated_raw_gas)
95+
print("")
96+
time.sleep(1)
97+
98+
7799
78100
Contributing
79101
============

adafruit_sgp40.py

+134-18
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,24 @@
99
1010
1111
* Author(s): Bryan Siepert
12+
Keith Murray
1213
1314
Implementation Notes
1415
--------------------
1516
1617
**Hardware:**
1718
1819
* Adafruit SGP40 Air Quality Sensor Breakout - VOC Index <https://www.adafruit.com/product/4829>
20+
* In order to use the `measure_raw` function, a temperature and humidity sensor which
21+
updates at at least 1Hz is needed (BME280, BME688, SHT31-D, SHT40, etc. For more, see:
22+
https://www.adafruit.com/category/66)
1923
2024
**Software and Dependencies:**
2125
2226
* Adafruit CircuitPython firmware for the supported boards:
2327
https://github.com/adafruit/circuitpython/releases
2428
25-
2629
* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
27-
* Adafruit's Register library: https://github.com/adafruit/Adafruit_CircuitPython_Register
2830
2931
"""
3032
from time import sleep
@@ -36,14 +38,22 @@
3638

3739
_WORD_LEN = 2
3840
# no point in generating this each time
39-
_READ_CMD = [0x26, 0x0F, 0x7F, 0xFF, 0x8F, 0x66, 0x66, 0x93]
41+
_READ_CMD = [
42+
0x26,
43+
0x0F,
44+
0x80,
45+
0x00,
46+
0xA2,
47+
0x66,
48+
0x66,
49+
0x93,
50+
] # Generated from temp 25c, humidity 50%
4051

4152

4253
class SGP40:
4354
"""
44-
Class to use the SGP40 Ambient Light and UV sensor
55+
Class to use the SGP40 Air Quality Sensor Breakout
4556
46-
:param ~busio.I2C i2c: The I2C bus the SGP40 is connected to.
4757
:param int address: The I2C address of the device. Defaults to :const:`0x59`
4858
4959
@@ -54,29 +64,60 @@ class SGP40:
5464
5565
.. code-block:: python
5666
57-
import busio
5867
import board
5968
import adafruit_sgp40
69+
# If you have a temperature sensor, like the bme280, import that here as well
70+
# import adafruit_bme280
6071
61-
Once this is done you can define your `busio.I2C` object and define your sensor object
72+
Once this is done you can define your `board.I2C` object and define your sensor object
6273
6374
.. code-block:: python
6475
65-
i2c = busio.I2C(board.SCL, board.SDA)
76+
i2c = board.I2C() # uses board.SCL and board.SDA
6677
sgp = adafruit_sgp40.SGP40(i2c)
78+
# And if you have a temp/humidity sensor, define the sensor here as well
79+
# bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c)
6780
68-
Now you have access to the raw gas value using the :attr:`raw` attribute
81+
Now you have access to the raw gas value using the :attr:`raw` attribute.
82+
And with a temperature and humidity value, you can access the class function
83+
:meth:`measure_raw` for a humidity compensated raw reading
6984
7085
.. code-block:: python
7186
7287
raw_gas_value = sgp.raw
88+
# Lets quickly grab the humidity and temperature
89+
# temperature = bme280.temperature
90+
# humidity = bme280.relative_humidity
91+
# compensated_raw_gas = sgp.measure_raw(temperature=temperature,
92+
# relative_humidity=humidity)
93+
# temperature = temperature, relative_humidity = humidity)
94+
95+
96+
97+
.. note::
98+
The operational range of temperatures for the SGP40 is -10 to 50 degrees Celsius
99+
and the operational range of relative humidity for the SGP40 is 0 to 90 %
100+
(assuming that humidity is non-condensing).
101+
102+
Humidity compensation is further optimized for a subset of the temperature
103+
and relative humidity readings. See Figure 3 of the Sensirion datasheet for
104+
the SGP40. At 25 degrees Celsius, the optimal range for relative humidity is 8% to 90%.
105+
At 50% relative humidity, the optimal range for temperature is -7 to 42 degrees Celsius.
73106
107+
Prolonged exposures outside of these ranges may reduce sensor performance, and
108+
the sensor must not be exposed towards condensing conditions at any time.
109+
110+
For more information see:
111+
https://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/9_Gas_Sensors/Datasheets/Sensirion_Gas_Sensors_Datasheet_SGP40.pdf
112+
and
113+
https://learn.adafruit.com/adafruit-sgp40
74114
75115
"""
76116

77117
def __init__(self, i2c, address=0x59):
78118
self.i2c_device = i2c_device.I2CDevice(i2c, address)
79119
self._command_buffer = bytearray(2)
120+
self._measure_command = _READ_CMD
80121

81122
self.initialize()
82123

@@ -117,19 +158,80 @@ def _reset(self):
117158
try:
118159
self._read_word_from_command(delay_ms=50)
119160
except OSError:
120-
# print("\tGot expected OSError from reset")
161+
# Got expected OSError from reset
121162
pass
122163
sleep(1)
123164

165+
@staticmethod
166+
def _celsius_to_ticks(temperature):
167+
"""
168+
Converts Temperature in Celsius to 'ticks' which are an input parameter
169+
the sgp40 can use
170+
171+
Temperature to Ticks : From SGP40 Datasheet Table 10
172+
temp (C) | Hex Code (Check Sum/CRC Hex Code)
173+
25 | 0x6666 (CRC 0x93)
174+
-45 | 0x0000 (CRC 0x81)
175+
130 | 0xFFFF (CRC 0xAC)
176+
177+
"""
178+
temp_ticks = int(((temperature + 45) * 65535) / 175) & 0xFFFF
179+
least_sig_temp_ticks = temp_ticks & 0xFF
180+
most_sig_temp_ticks = (temp_ticks >> 8) & 0xFF
181+
182+
return [most_sig_temp_ticks, least_sig_temp_ticks]
183+
184+
@staticmethod
185+
def _relative_humidity_to_ticks(humidity):
186+
"""
187+
Converts Relative Humidity in % to 'ticks' which are an input parameter
188+
the sgp40 can use
189+
190+
Relative Humidity to Ticks : From SGP40 Datasheet Table 10
191+
Humidity (%) | Hex Code (Check Sum/CRC Hex Code)
192+
50 | 0x8000 (CRC 0xA2)
193+
0 | 0x0000 (CRC 0x81)
194+
100 | 0xFFFF (CRC 0xAC)
195+
196+
"""
197+
humidity_ticks = int((humidity * 65535) / 100 + 0.5) & 0xFFFF
198+
least_sig_rhumidity_ticks = humidity_ticks & 0xFF
199+
most_sig_rhumidity_ticks = (humidity_ticks >> 8) & 0xFF
200+
201+
return [most_sig_rhumidity_ticks, least_sig_rhumidity_ticks]
202+
124203
@property
125204
def raw(self):
126205
"""The raw gas value"""
127206
# recycle a single buffer
128-
self._command_buffer = bytearray(_READ_CMD)
207+
self._command_buffer = self._measure_command
129208
read_value = self._read_word_from_command(delay_ms=250)
130209
self._command_buffer = bytearray(2)
131210
return read_value[0]
132211

212+
def measure_raw(self, temperature=25, relative_humidity=50):
213+
"""
214+
A humidity and temperature compensated raw gas value which helps
215+
address fluctuations in readings due to changing humidity.
216+
217+
218+
:param float temperature: The temperature in degrees Celsius, defaults
219+
to :const:`25`
220+
:param float relative_humidity: The relative humidity in percentage, defaults
221+
to :const:`50`
222+
223+
The raw gas value adjusted for the current temperature (c) and humidity (%)
224+
"""
225+
# recycle a single buffer
226+
_compensated_read_cmd = [0x26, 0x0F]
227+
humidity_ticks = self._relative_humidity_to_ticks(relative_humidity)
228+
humidity_ticks.append(self._generate_crc(humidity_ticks))
229+
temp_ticks = self._celsius_to_ticks(temperature)
230+
temp_ticks.append(self._generate_crc(temp_ticks))
231+
_cmd = _compensated_read_cmd + humidity_ticks + temp_ticks
232+
self._measure_command = bytearray(_cmd)
233+
return self.raw
234+
133235
def _read_word_from_command(
134236
self,
135237
delay_ms=10,
@@ -153,32 +255,46 @@ def _read_word_from_command(
153255
return None
154256
readdata_buffer = []
155257

156-
# The number of bytes to rad back, based on the number of words to read
258+
# The number of bytes to read back, based on the number of words to read
157259
replylen = readlen * (_WORD_LEN + 1)
158260
# recycle buffer for read/write w/length
159261
replybuffer = bytearray(replylen)
160262

161263
with self.i2c_device as i2c:
162264
i2c.readinto(replybuffer, end=replylen)
163265

164-
# print("Buffer:")
165-
# print(["0x{:02X}".format(i) for i in replybuffer])
166-
167266
for i in range(0, replylen, 3):
168267
if not self._check_crc8(replybuffer[i : i + 2], replybuffer[i + 2]):
169268
raise RuntimeError("CRC check failed while reading data")
170269
readdata_buffer.append(unpack_from(">H", replybuffer[i : i + 2])[0])
171270

172271
return readdata_buffer
173272

273+
def _check_crc8(self, crc_buffer, crc_value):
274+
"""
275+
Checks that the 8 bit CRC Checksum value from the sensor matches the
276+
received data
277+
"""
278+
return crc_value == self._generate_crc(crc_buffer)
279+
174280
@staticmethod
175-
def _check_crc8(crc_buffer, crc_value):
281+
def _generate_crc(crc_buffer):
282+
"""
283+
Generates an 8 bit CRC Checksum from the input buffer.
284+
285+
This checksum algorithm is outlined in Table 7 of the SGP40 datasheet.
286+
287+
Checksums are only generated for 2-byte data packets. Command codes already
288+
contain 3 bits of CRC and therefore do not need an added checksum.
289+
"""
176290
crc = 0xFF
177291
for byte in crc_buffer:
178292
crc ^= byte
179293
for _ in range(8):
180294
if crc & 0x80:
181-
crc = (crc << 1) ^ 0x31
295+
crc = (
296+
crc << 1
297+
) ^ 0x31 # 0x31 is the Seed for SGP40's CRC polynomial
182298
else:
183299
crc = crc << 1
184-
return crc_value == (crc & 0xFF) # check against the bottom 8 bits
300+
return crc & 0xFF # Returns only bottom 8 bits

docs/conf.py

-4
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,6 @@
3434
"https://circuitpython.readthedocs.io/projects/busdevice/en/latest/",
3535
None,
3636
),
37-
"Register": (
38-
"https://circuitpython.readthedocs.io/projects/register/en/latest/",
39-
None,
40-
),
4137
"CircuitPython": ("https://circuitpython.readthedocs.io/en/latest/", None),
4238
}
4339

examples/sgp40_simpletest.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,21 @@
33
# SPDX-License-Identifier: Unlicense
44
import time
55
import board
6-
import busio
76
import adafruit_sgp40
87

9-
i2c = busio.I2C(board.SCL, board.SDA)
8+
# If you have a temperature sensor, like the bme280, import that here as well
9+
# import adafruit_bme280
10+
11+
i2c = board.I2C() # uses board.SCL and board.SDA
1012
sgp = adafruit_sgp40.SGP40(i2c)
13+
# And if you have a temp/humidity sensor, define the sensor here as well
14+
# bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c)
1115

1216
while True:
1317
print("Raw Gas: ", sgp.raw)
18+
# Lets quickly grab the humidity and temperature
19+
# temperature = bme280.temperature
20+
# humidity = bme280.relative_humidity
21+
# compensated_raw_gas = sgp.measure_raw(temperature = temperature, relative_humidity = humidity)
1422
print("")
1523
time.sleep(1)

requirements.txt

-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,3 @@
55

66
Adafruit-Blinka
77
adafruit-circuitpython-busdevice
8-
adafruit-circuitpython-register

setup.py

-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
install_requires=[
3838
"Adafruit-Blinka",
3939
"adafruit-circuitpython-busdevice",
40-
"adafruit-circuitpython-register",
4140
],
4241
# Choose your license
4342
license="MIT",

0 commit comments

Comments
 (0)