99
1010
1111* Author(s): Bryan Siepert
12+ Keith Murray
1213
1314Implementation 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"""
3032from time import sleep
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
4253class 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
0 commit comments