Skip to content

Adding Humidity and Temperature compensation to SGP40 in preperation for VOC Algorithm #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Jun 6, 2021
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
282c85e
Fixed spelling error in comment, added a generate_crc function, as we…
KeithTheEE May 27, 2021
bf3ed0f
Adjusted the default _READ_CMD bytearray to reflect the sample in the…
KeithTheEE May 27, 2021
2f379a2
Removed duplicate code between check crc and generate crc. _check_crc…
KeithTheEE May 28, 2021
dfa6d8f
Adjusted raw to pull from self._measure_command so either the preset …
KeithTheEE May 28, 2021
1696e3c
Adjusted for sgp40 program structure for precommit
KeithTheEE May 28, 2021
fc4dd0a
Merge branch 'adafruit:main' into voc_algorithm_dev
KeithTheEE May 28, 2021
db971d2
Added some explaination to measure_raw, reordered the functions to ke…
KeithTheEE Jun 3, 2021
755eb04
Merge branch 'voc_algorithm_dev' of https://github.com/CrakeNotSnowma…
KeithTheEE Jun 3, 2021
6806081
Adjusted the reference to the method measure_raw for sphinx documenta…
KeithTheEE Jun 3, 2021
1ef9836
Update README.rst to remove busio
KeithTheEE Jun 4, 2021
b8e4e93
Update adafruit_sgp40.py to replace busio reference with board
KeithTheEE Jun 4, 2021
1b84c71
Removing Busio from readme example
KeithTheEE Jun 4, 2021
2f52d8b
Defining i2c with board instead of busio
KeithTheEE Jun 4, 2021
425014d
Defining i2c with board instead of busio
KeithTheEE Jun 4, 2021
94ad780
Reformating varible assignment in the example using measure_raw
KeithTheEE Jun 4, 2021
473ad2f
Clarifying Temperature in Celsius
KeithTheEE Jun 4, 2021
711e161
Clarifying Temperature in Celsius
KeithTheEE Jun 4, 2021
7d8fae6
Clarifying Temperature in Celsius
KeithTheEE Jun 4, 2021
28773c0
Removing Busio from readme example
KeithTheEE Jun 4, 2021
cc919c2
Removed Register library from the requirements since it's not in use
KeithTheEE Jun 4, 2021
bedde22
Added link to datasheet and learn guide, fixed some formatting, and c…
KeithTheEE Jun 4, 2021
3c044df
Documented the parameter inputs for measure_raw
KeithTheEE Jun 4, 2021
94aded3
Removed references to busio in comments
KeithTheEE Jun 4, 2021
b93f0de
Removed references to busio in comments
KeithTheEE Jun 4, 2021
e1c8114
Returned reference to the Bus Device Library
KeithTheEE Jun 4, 2021
b9ea15d
Removed busio reference from simpletest example
KeithTheEE Jun 4, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,38 @@ Usage Example

import time
import board
import busio
import adafruit_sgp40

i2c = busio.I2C(board.SCL, board.SDA)
i2c = board.I2C() # uses board.SCL and board.SDA
sgp = adafruit_sgp40(i2c)

while True:
print("Measurement: ", sgp.raw)
print("")
sleep(1)

For humidity compensated raw gas readings, we'll need a secondary sensor such as the bme280

.. code-block:: python3

import time
import board
import adafruit_sgp40
import adafruit_bme280

i2c = board.I2C() # uses board.SCL and board.SDA
sgp = adafruit_sgp40.SGP40(i2c)
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c)

while True:
temperature = bme280.temperature
humidity = bme280.relative_humidity
compensated_raw_gas = sgp.measure_raw(temperature = temperature, relative_humidity = humidity)
print(compensated_raw_gas)
print("")
time.sleep(1)



Contributing
============
Expand Down
149 changes: 134 additions & 15 deletions adafruit_sgp40.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@


* Author(s): Bryan Siepert
Keith Murray

Implementation Notes
--------------------

**Hardware:**

* Adafruit SGP40 Air Quality Sensor Breakout - VOC Index <https://www.adafruit.com/product/4829>
* In order to use the `measure_raw` function, a temperature and humidity sensor which
updates at at least 1Hz is needed (BME280, BME688, SHT31-D, SHT40, etc. For more, see:
https://www.adafruit.com/category/66)

**Software and Dependencies:**

Expand All @@ -24,7 +28,6 @@


* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
* Adafruit's Register library: https://github.com/adafruit/Adafruit_CircuitPython_Register

"""
from time import sleep
Expand All @@ -36,12 +39,21 @@

_WORD_LEN = 2
# no point in generating this each time
_READ_CMD = [0x26, 0x0F, 0x7F, 0xFF, 0x8F, 0x66, 0x66, 0x93]
_READ_CMD = [
0x26,
0x0F,
0x80,
0x00,
0xA2,
0x66,
0x66,
0x93,
] # Generated from temp 25c, humidity 50%


class SGP40:
"""
Class to use the SGP40 Ambient Light and UV sensor
Class to use the SGP40 Air Quality Sensor Breakout

:param ~busio.I2C i2c: The I2C bus the SGP40 is connected to.
:param int address: The I2C address of the device. Defaults to :const:`0x59`
Expand All @@ -57,26 +69,58 @@ class SGP40:
import busio
import board
import adafruit_sgp40
# If you have a temperature sensor, like the bme280, import that here as well
# import adafruit_bme280

Once this is done you can define your `busio.I2C` object and define your sensor object
Once this is done you can define your `board.I2C` object and define your sensor object

.. code-block:: python

i2c = busio.I2C(board.SCL, board.SDA)
i2c = board.I2C() # uses board.SCL and board.SDA
sgp = adafruit_sgp40.SGP40(i2c)
# And if you have a temp/humidity sensor, define the sensor here as well
# bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c)

Now you have access to the raw gas value using the :attr:`raw` attribute
Now you have access to the raw gas value using the :attr:`raw` attribute.
And with a temperature and humidity value, you can access the class function
:meth:`measure_raw` for a humidity compensated raw reading

.. code-block:: python

raw_gas_value = sgp.raw
# Lets quickly grab the humidity and temperature
# temperature = bme280.temperature
# humidity = bme280.relative_humidity
# compensated_raw_gas = sgp.measure_raw(temperature=temperature,
# relative_humidity=humidity)
# temperature = temperature, relative_humidity = humidity)



.. note::
The operational range of temperatures for the SGP40 is -10 to 50 degrees Celsius
and the operational range of relative humidity for the SGP40 is 0 to 90 %
(assuming that humidity is non-condensing).

Humidity compensation is further optimized for a subset of the temperature
and relative humidity readings. See Figure 3 of the Sensirion datasheet for
the SGP40. At 25 degrees Celsius, the optimal range for relative humidity is 8% to 90%.
At 50% relative humidity, the optimal range for temperature is -7 to 42 degrees Celsius.

Prolonged exposures outside of these ranges may reduce sensor performance, and
the sensor must not be exposed towards condensing conditions at any time.

For more information see:
https://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/9_Gas_Sensors/Datasheets/Sensirion_Gas_Sensors_Datasheet_SGP40.pdf
and
https://learn.adafruit.com/adafruit-sgp40

"""

def __init__(self, i2c, address=0x59):
self.i2c_device = i2c_device.I2CDevice(i2c, address)
self._command_buffer = bytearray(2)
self._measure_command = _READ_CMD

self.initialize()

Expand Down Expand Up @@ -117,19 +161,80 @@ def _reset(self):
try:
self._read_word_from_command(delay_ms=50)
except OSError:
# print("\tGot expected OSError from reset")
# Got expected OSError from reset
pass
sleep(1)

@staticmethod
def _celsius_to_ticks(temperature):
"""
Converts Temperature in Celsius to 'ticks' which are an input parameter
the sgp40 can use

Temperature to Ticks : From SGP40 Datasheet Table 10
temp (C) | Hex Code (Check Sum/CRC Hex Code)
25 | 0x6666 (CRC 0x93)
-45 | 0x0000 (CRC 0x81)
130 | 0xFFFF (CRC 0xAC)

"""
temp_ticks = int(((temperature + 45) * 65535) / 175) & 0xFFFF
least_sig_temp_ticks = temp_ticks & 0xFF
most_sig_temp_ticks = (temp_ticks >> 8) & 0xFF

return [most_sig_temp_ticks, least_sig_temp_ticks]

@staticmethod
def _relative_humidity_to_ticks(humidity):
"""
Converts Relative Humidity in % to 'ticks' which are an input parameter
the sgp40 can use

Relative Humidity to Ticks : From SGP40 Datasheet Table 10
Humidity (%) | Hex Code (Check Sum/CRC Hex Code)
50 | 0x8000 (CRC 0xA2)
0 | 0x0000 (CRC 0x81)
100 | 0xFFFF (CRC 0xAC)

"""
humidity_ticks = int((humidity * 65535) / 100 + 0.5) & 0xFFFF
least_sig_rhumidity_ticks = humidity_ticks & 0xFF
most_sig_rhumidity_ticks = (humidity_ticks >> 8) & 0xFF

return [most_sig_rhumidity_ticks, least_sig_rhumidity_ticks]

@property
def raw(self):
"""The raw gas value"""
# recycle a single buffer
self._command_buffer = bytearray(_READ_CMD)
self._command_buffer = self._measure_command
read_value = self._read_word_from_command(delay_ms=250)
self._command_buffer = bytearray(2)
return read_value[0]

def measure_raw(self, temperature=25, relative_humidity=50):
"""
A humidity and temperature compensated raw gas value which helps
address fluctuations in readings due to changing humidity.


:param float temperature: The temperature in degrees Celsius, defaults
to :const:`25`
:param float relative_humidity: The relative humidity in percentage, defaults
to :const:`50`

The raw gas value adjusted for the current temperature (c) and humidity (%)
"""
# recycle a single buffer
_compensated_read_cmd = [0x26, 0x0F]
humidity_ticks = self._relative_humidity_to_ticks(relative_humidity)
humidity_ticks.append(self._generate_crc(humidity_ticks))
temp_ticks = self._celsius_to_ticks(temperature)
temp_ticks.append(self._generate_crc(temp_ticks))
_cmd = _compensated_read_cmd + humidity_ticks + temp_ticks
self._measure_command = bytearray(_cmd)
return self.raw

def _read_word_from_command(
self,
delay_ms=10,
Expand All @@ -153,32 +258,46 @@ def _read_word_from_command(
return None
readdata_buffer = []

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

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

# print("Buffer:")
# print(["0x{:02X}".format(i) for i in replybuffer])

for i in range(0, replylen, 3):
if not self._check_crc8(replybuffer[i : i + 2], replybuffer[i + 2]):
raise RuntimeError("CRC check failed while reading data")
readdata_buffer.append(unpack_from(">H", replybuffer[i : i + 2])[0])

return readdata_buffer

def _check_crc8(self, crc_buffer, crc_value):
"""
Checks that the 8 bit CRC Checksum value from the sensor matches the
received data
"""
return crc_value == self._generate_crc(crc_buffer)

@staticmethod
def _check_crc8(crc_buffer, crc_value):
def _generate_crc(crc_buffer):
"""
Generates an 8 bit CRC Checksum from the input buffer.

This checksum algorithm is outlined in Table 7 of the SGP40 datasheet.

Checksums are only generated for 2-byte data packets. Command codes already
contain 3 bits of CRC and therefore do not need an added checksum.
"""
crc = 0xFF
for byte in crc_buffer:
crc ^= byte
for _ in range(8):
if crc & 0x80:
crc = (crc << 1) ^ 0x31
crc = (
crc << 1
) ^ 0x31 # 0x31 is the Seed for SGP40's CRC polynomial
else:
crc = crc << 1
return crc_value == (crc & 0xFF) # check against the bottom 8 bits
return crc & 0xFF # Returns only bottom 8 bits
4 changes: 0 additions & 4 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@
"https://circuitpython.readthedocs.io/projects/busdevice/en/latest/",
None,
),
"Register": (
"https://circuitpython.readthedocs.io/projects/register/en/latest/",
None,
),
"CircuitPython": ("https://circuitpython.readthedocs.io/en/latest/", None),
}

Expand Down
9 changes: 9 additions & 0 deletions examples/sgp40_simpletest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,19 @@
import busio
import adafruit_sgp40

# If you have a temperature sensor, like the bme280, import that here as well
# import adafruit_bme280

i2c = busio.I2C(board.SCL, board.SDA)
sgp = adafruit_sgp40.SGP40(i2c)
# And if you have a temp/humidity sensor, define the sensor here as well
# bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c)

while True:
print("Raw Gas: ", sgp.raw)
# Lets quickly grab the humidity and temperature
# temperature = bme280.temperature
# humidity = bme280.relative_humidity
# compensated_raw_gas = sgp.measure_raw(temperature = temperature, relative_humidity = humidity)
print("")
time.sleep(1)
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,3 @@

Adafruit-Blinka
adafruit-circuitpython-busdevice
adafruit-circuitpython-register
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
install_requires=[
"Adafruit-Blinka",
"adafruit-circuitpython-busdevice",
"adafruit-circuitpython-register",
],
# Choose your license
license="MIT",
Expand Down