/ adafruit_sgp30.py
adafruit_sgp30.py
1 # The MIT License (MIT) 2 # 3 # Copyright (c) 2017 ladyada for Adafruit Industries 4 # 5 # Permission is hereby granted, free of charge, to any person obtaining a copy 6 # of this software and associated documentation files (the "Software"), to deal 7 # in the Software without restriction, including without limitation the rights 8 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 # copies of the Software, and to permit persons to whom the Software is 10 # furnished to do so, subject to the following conditions: 11 # 12 # The above copyright notice and this permission notice shall be included in 13 # all copies or substantial portions of the Software. 14 # 15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 # THE SOFTWARE. 22 """ 23 `adafruit_sgp30` 24 ==================================================== 25 26 I2C driver for SGP30 Sensirion VoC sensor 27 28 * Author(s): ladyada 29 30 Implementation Notes 31 -------------------- 32 33 **Hardware:** 34 35 * Adafruit `SGP30 Air Quality Sensor Breakout - VOC and eCO2 36 <https://www.adafruit.com/product/3709>`_ (Product ID: 3709) 37 38 **Software and Dependencies:** 39 40 * Adafruit CircuitPython firmware for the ESP8622 and M0-based boards: 41 https://github.com/adafruit/circuitpython/releases 42 * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice 43 """ 44 import time 45 from adafruit_bus_device.i2c_device import I2CDevice 46 from micropython import const 47 48 __version__ = "0.0.0-auto.0" 49 __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_SGP30.git" 50 51 52 # pylint: disable=bad-whitespace 53 _SGP30_DEFAULT_I2C_ADDR = const(0x58) 54 _SGP30_FEATURESETS = (0x0020, 0x0022) 55 56 _SGP30_CRC8_POLYNOMIAL = const(0x31) 57 _SGP30_CRC8_INIT = const(0xFF) 58 _SGP30_WORD_LEN = const(2) 59 # pylint: enable=bad-whitespace 60 61 62 class Adafruit_SGP30: 63 """ 64 A driver for the SGP30 gas sensor. 65 """ 66 67 def __init__(self, i2c, address=_SGP30_DEFAULT_I2C_ADDR): 68 """Initialize the sensor, get the serial # and verify that we found a proper SGP30""" 69 self._device = I2CDevice(i2c, address) 70 71 # get unique serial, its 48 bits so we store in an array 72 self.serial = self._i2c_read_words_from_cmd([0x36, 0x82], 0.01, 3) 73 # get featureset 74 featureset = self._i2c_read_words_from_cmd([0x20, 0x2F], 0.01, 1) 75 if featureset[0] not in _SGP30_FEATURESETS: 76 raise RuntimeError("SGP30 Not detected") 77 self.iaq_init() 78 79 @property 80 # pylint: disable=invalid-name 81 def TVOC(self): 82 """Total Volatile Organic Compound in parts per billion.""" 83 return self.iaq_measure()[1] 84 85 @property 86 # pylint: disable=invalid-name 87 def baseline_TVOC(self): 88 """Total Volatile Organic Compound baseline value""" 89 return self.get_iaq_baseline()[1] 90 91 @property 92 # pylint: disable=invalid-name 93 def eCO2(self): 94 """Carbon Dioxide Equivalent in parts per million""" 95 return self.iaq_measure()[0] 96 97 @property 98 # pylint: disable=invalid-name 99 def baseline_eCO2(self): 100 """Carbon Dioxide Equivalent baseline value""" 101 return self.get_iaq_baseline()[0] 102 103 @property 104 # pylint: disable=invalid-name 105 def Ethanol(self): 106 """Ethanol Raw Signal in ticks""" 107 return self.raw_measure()[1] 108 109 @property 110 # pylint: disable=invalid-name 111 def H2(self): 112 """H2 Raw Signal in ticks""" 113 return self.raw_measure()[0] 114 115 def iaq_init(self): 116 """Initialize the IAQ algorithm""" 117 # name, command, signals, delay 118 self._run_profile(["iaq_init", [0x20, 0x03], 0, 0.01]) 119 120 def iaq_measure(self): 121 """Measure the eCO2 and TVOC""" 122 # name, command, signals, delay 123 return self._run_profile(["iaq_measure", [0x20, 0x08], 2, 0.05]) 124 125 def raw_measure(self): 126 """Measure H2 and Ethanol (Raw Signals)""" 127 # name, command, signals, delay 128 return self._run_profile(["raw_measure", [0x20, 0x50], 2, 0.025]) 129 130 def get_iaq_baseline(self): 131 """Retreive the IAQ algorithm baseline for eCO2 and TVOC""" 132 # name, command, signals, delay 133 return self._run_profile(["iaq_get_baseline", [0x20, 0x15], 2, 0.01]) 134 135 def set_iaq_baseline(self, eCO2, TVOC): # pylint: disable=invalid-name 136 """Set the previously recorded IAQ algorithm baseline for eCO2 and TVOC""" 137 if eCO2 == 0 and TVOC == 0: 138 raise RuntimeError("Invalid baseline") 139 buffer = [] 140 for value in [TVOC, eCO2]: 141 arr = [value >> 8, value & 0xFF] 142 arr.append(self._generate_crc(arr)) 143 buffer += arr 144 self._run_profile(["iaq_set_baseline", [0x20, 0x1E] + buffer, 0, 0.01]) 145 146 def set_iaq_humidity(self, gramsPM3): # pylint: disable=invalid-name 147 """Set the humidity in g/m3 for eCO2 and TVOC compensation algorithm""" 148 tmp = int(gramsPM3 * 256) 149 buffer = [] 150 for value in [tmp]: 151 arr = [value >> 8, value & 0xFF] 152 arr.append(self._generate_crc(arr)) 153 buffer += arr 154 self._run_profile(["iaq_set_humidity", [0x20, 0x61] + buffer, 0, 0.01]) 155 156 # Low level command functions 157 158 def _run_profile(self, profile): 159 """Run an SGP 'profile' which is a named command set""" 160 # pylint: disable=unused-variable 161 name, command, signals, delay = profile 162 # pylint: enable=unused-variable 163 164 # print("\trunning profile: %s, command %s, %d, delay %0.02f" % 165 # (name, ["0x%02x" % i for i in command], signals, delay)) 166 return self._i2c_read_words_from_cmd(command, delay, signals) 167 168 def _i2c_read_words_from_cmd(self, command, delay, reply_size): 169 """Run an SGP command query, get a reply and CRC results if necessary""" 170 with self._device: 171 self._device.write(bytes(command)) 172 time.sleep(delay) 173 if not reply_size: 174 return None 175 crc_result = bytearray(reply_size * (_SGP30_WORD_LEN + 1)) 176 self._device.readinto(crc_result) 177 # print("\tRaw Read: ", crc_result) 178 result = [] 179 for i in range(reply_size): 180 word = [crc_result[3 * i], crc_result[3 * i + 1]] 181 crc = crc_result[3 * i + 2] 182 if self._generate_crc(word) != crc: 183 raise RuntimeError("CRC Error") 184 result.append(word[0] << 8 | word[1]) 185 # print("\tOK Data: ", [hex(i) for i in result]) 186 return result 187 188 # pylint: disable=no-self-use 189 def _generate_crc(self, data): 190 """8-bit CRC algorithm for checking data""" 191 crc = _SGP30_CRC8_INIT 192 # calculates 8-Bit checksum with given polynomial 193 for byte in data: 194 crc ^= byte 195 for _ in range(8): 196 if crc & 0x80: 197 crc = (crc << 1) ^ _SGP30_CRC8_POLYNOMIAL 198 else: 199 crc <<= 1 200 return crc & 0xFF