/ 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