9
9
10
10
11
11
* Author(s): Bryan Siepert
12
+ Keith Murray
12
13
13
14
Implementation Notes
14
15
--------------------
15
16
16
17
**Hardware:**
17
18
18
19
* 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)
19
23
20
24
**Software and Dependencies:**
21
25
22
26
* Adafruit CircuitPython firmware for the supported boards:
23
27
https://github.com/adafruit/circuitpython/releases
24
28
25
-
26
29
* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
27
- * Adafruit's Register library: https://github.com/adafruit/Adafruit_CircuitPython_Register
28
30
29
31
"""
30
32
from time import sleep
36
38
37
39
_WORD_LEN = 2
38
40
# 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%
40
51
41
52
42
53
class SGP40 :
43
54
"""
44
- Class to use the SGP40 Ambient Light and UV sensor
55
+ Class to use the SGP40 Air Quality Sensor Breakout
45
56
46
- :param ~busio.I2C i2c: The I2C bus the SGP40 is connected to.
47
57
:param int address: The I2C address of the device. Defaults to :const:`0x59`
48
58
49
59
@@ -54,29 +64,60 @@ class SGP40:
54
64
55
65
.. code-block:: python
56
66
57
- import busio
58
67
import board
59
68
import adafruit_sgp40
69
+ # If you have a temperature sensor, like the bme280, import that here as well
70
+ # import adafruit_bme280
60
71
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
62
73
63
74
.. code-block:: python
64
75
65
- i2c = busio .I2C(board.SCL, board.SDA)
76
+ i2c = board .I2C() # uses board.SCL and board.SDA
66
77
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)
67
80
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
69
84
70
85
.. code-block:: python
71
86
72
87
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.
73
106
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
74
114
75
115
"""
76
116
77
117
def __init__ (self , i2c , address = 0x59 ):
78
118
self .i2c_device = i2c_device .I2CDevice (i2c , address )
79
119
self ._command_buffer = bytearray (2 )
120
+ self ._measure_command = _READ_CMD
80
121
81
122
self .initialize ()
82
123
@@ -117,19 +158,80 @@ def _reset(self):
117
158
try :
118
159
self ._read_word_from_command (delay_ms = 50 )
119
160
except OSError :
120
- # print("\tGot expected OSError from reset")
161
+ # Got expected OSError from reset
121
162
pass
122
163
sleep (1 )
123
164
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
+
124
203
@property
125
204
def raw (self ):
126
205
"""The raw gas value"""
127
206
# recycle a single buffer
128
- self ._command_buffer = bytearray ( _READ_CMD )
207
+ self ._command_buffer = self . _measure_command
129
208
read_value = self ._read_word_from_command (delay_ms = 250 )
130
209
self ._command_buffer = bytearray (2 )
131
210
return read_value [0 ]
132
211
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
+
133
235
def _read_word_from_command (
134
236
self ,
135
237
delay_ms = 10 ,
@@ -153,32 +255,46 @@ def _read_word_from_command(
153
255
return None
154
256
readdata_buffer = []
155
257
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
157
259
replylen = readlen * (_WORD_LEN + 1 )
158
260
# recycle buffer for read/write w/length
159
261
replybuffer = bytearray (replylen )
160
262
161
263
with self .i2c_device as i2c :
162
264
i2c .readinto (replybuffer , end = replylen )
163
265
164
- # print("Buffer:")
165
- # print(["0x{:02X}".format(i) for i in replybuffer])
166
-
167
266
for i in range (0 , replylen , 3 ):
168
267
if not self ._check_crc8 (replybuffer [i : i + 2 ], replybuffer [i + 2 ]):
169
268
raise RuntimeError ("CRC check failed while reading data" )
170
269
readdata_buffer .append (unpack_from (">H" , replybuffer [i : i + 2 ])[0 ])
171
270
172
271
return readdata_buffer
173
272
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
+
174
280
@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
+ """
176
290
crc = 0xFF
177
291
for byte in crc_buffer :
178
292
crc ^= byte
179
293
for _ in range (8 ):
180
294
if crc & 0x80 :
181
- crc = (crc << 1 ) ^ 0x31
295
+ crc = (
296
+ crc << 1
297
+ ) ^ 0x31 # 0x31 is the Seed for SGP40's CRC polynomial
182
298
else :
183
299
crc = crc << 1
184
- return crc_value == ( crc & 0xFF ) # check against the bottom 8 bits
300
+ return crc & 0xFF # Returns only bottom 8 bits
0 commit comments