Source code for adafruit_ccs811

# SPDX-FileCopyrightText: 2017 Dean Miller for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""
`adafruit_ccs811`
======================================================================
This library supports the use of the CCS811 air quality sensor in CircuitPython.

Author(s): Dean Miller for Adafruit Industries

**Hardware:**

* `Adafruit CCS811 Air Quality Sensor Breakout - VOC and eCO2
  <https://www.adafruit.com/product/3566>`_

**Software and Dependencies:**

* Adafruit CircuitPython firmware for the supported boards:
  https://github.com/adafruit/circuitpython/releases

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

**Notes:**

#. `Datasheet
<https://cdn-learn.adafruit.com/assets/assets/000/044/636/original/CCS811_DS000459_2-00-1098798.pdf?1501602769>`_
"""
import time
import math
import struct

from micropython import const
from adafruit_bus_device.i2c_device import I2CDevice
from adafruit_register import i2c_bit
from adafruit_register import i2c_bits

try:
    from typing import Optional
    from busio import I2C
except ImportError:
    pass

__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_CCS811.git"


_ALG_RESULT_DATA = const(0x02)
_RAW_DATA = const(0x03)
_ENV_DATA = const(0x05)
_NTC = const(0x06)
_THRESHOLDS = const(0x10)

_BASELINE = const(0x11)

# _HW_ID = 0x20
# _HW_VERSION = 0x21
# _FW_BOOT_VERSION = 0x23
# _FW_APP_VERSION = 0x24
# _ERROR_ID = 0xE0

_SW_RESET = const(0xFF)

# _BOOTLOADER_APP_ERASE = 0xF1
# _BOOTLOADER_APP_DATA = 0xF2
# _BOOTLOADER_APP_VERIFY = 0xF3
# _BOOTLOADER_APP_START = 0xF4

DRIVE_MODE_IDLE = const(0x00)
DRIVE_MODE_1SEC = const(0x01)
DRIVE_MODE_10SEC = const(0x02)
DRIVE_MODE_60SEC = const(0x03)
DRIVE_MODE_250MS = const(0x04)

_HW_ID_CODE = const(0x81)
_REF_RESISTOR = const(100000)


[docs] class CCS811: """CCS811 gas sensor driver. :param ~busio.I2C i2c_bus: The I2C bus the BME280 is connected to :param int address: The I2C address of the CCS811. Defaults to :const:`0x5A` **Quickstart: Importing and using the CCS811** Here is an example of using the :class:`CCS811` class. First you will need to import the libraries to use the sensor .. code-block:: python import board import adafruit_ccs811 Once this is done you can define your `board.I2C` object and define your sensor object .. code-block:: python i2c = board.I2C() # uses board.SCL and board.SDA ccs811 = adafruit_ccs811.CCS811(i2c) Now you have access to the :attr:`eco2` and :attr:`tvoc` attributes. .. code-block:: python eco2 = ccs811.eco2 tvoc = ccs811.tvoc """ # set up the registers error = i2c_bit.ROBit(0x00, 0) """True when an error has occured.""" data_ready = i2c_bit.ROBit(0x00, 3) """True when new data has been read.""" app_valid = i2c_bit.ROBit(0x00, 4) fw_mode = i2c_bit.ROBit(0x00, 7) hw_id = i2c_bits.ROBits(8, 0x20, 0) int_thresh = i2c_bit.RWBit(0x01, 2) interrupt_enabled = i2c_bit.RWBit(0x01, 3) drive_mode = i2c_bits.RWBits(3, 0x01, 4) temp_offset = 0.0 """Temperature offset.""" def __init__(self, i2c_bus: I2C, address: int = 0x5A) -> None: self.i2c_device = I2CDevice(i2c_bus, address) # check that the HW id is correct if self.hw_id != _HW_ID_CODE: raise RuntimeError( "Device ID returned is not correct! Please check your wiring." ) # try to start the app buf = bytearray(1) buf[0] = 0xF4 with self.i2c_device as i2c: i2c.write(buf, end=1) time.sleep(0.1) # make sure there are no errors and we have entered application mode if self.error: raise RuntimeError( "Device returned an error! Try removing and reapplying power to " "the device and running the code again." ) if not self.fw_mode: raise RuntimeError( "Device did not enter application mode! If you got here, there may " "be a problem with the firmware on your sensor." ) self.interrupt_enabled = False # default to read every second self.drive_mode = DRIVE_MODE_1SEC self._eco2 = None # pylint: disable=invalid-name self._tvoc = None # pylint: disable=invalid-name @property def error_code(self) -> int: """Error code""" buf = bytearray(2) buf[0] = 0xE0 with self.i2c_device as i2c: i2c.write_then_readinto(buf, buf, out_end=1, in_start=1) return buf[1] def _update_data(self) -> None: if self.data_ready: buf = bytearray(9) buf[0] = _ALG_RESULT_DATA with self.i2c_device as i2c: i2c.write_then_readinto(buf, buf, out_end=1, in_start=1) self._eco2 = (buf[1] << 8) | (buf[2]) self._tvoc = (buf[3] << 8) | (buf[4]) if self.error: raise RuntimeError("Error:" + str(self.error_code)) @property def baseline(self) -> int: """ The property reads and returns the current baseline value. The returned value is packed into an integer. Later the same integer can be used in order to set a new baseline. """ buf = bytearray(3) buf[0] = _BASELINE with self.i2c_device as i2c: i2c.write_then_readinto(buf, buf, out_end=1, in_start=1) return struct.unpack("<H", buf[1:])[0] @baseline.setter def baseline(self, baseline_int: int) -> None: """ The property lets you set a new baseline. As a value accepts integer which represents packed baseline 2 bytes value. """ buf = bytearray(3) buf[0] = _BASELINE struct.pack_into("<H", buf, 1, baseline_int) with self.i2c_device as i2c: i2c.write(buf) @property def tvoc(self) -> Optional[int]: # pylint: disable=invalid-name """Total Volatile Organic Compound in parts per billion.""" self._update_data() return self._tvoc @property def eco2(self) -> Optional[int]: # pylint: disable=invalid-name """Equivalent Carbon Dioxide in parts per million. Clipped to 400 to 8192ppm.""" self._update_data() return self._eco2 @property def temperature(self) -> float: """ .. deprecated:: 1.1.5 Hardware support removed by vendor Temperature based on optional thermistor in Celsius.""" buf = bytearray(5) buf[0] = _NTC with self.i2c_device as i2c: i2c.write_then_readinto(buf, buf, out_end=1, in_start=1) vref = (buf[1] << 8) | buf[2] vntc = (buf[3] << 8) | buf[4] # From ams ccs811 app note 000925 # https://download.ams.com/content/download/9059/13027/version/1/file/CCS811_Doc_cAppNote-Connecting-NTC-Thermistor_AN000372_v1..pdf rntc = float(vntc) * _REF_RESISTOR / float(vref) ntc_temp = math.log(rntc / 10000.0) ntc_temp /= 3380.0 ntc_temp += 1.0 / (25 + 273.15) ntc_temp = 1.0 / ntc_temp ntc_temp -= 273.15 return ntc_temp - self.temp_offset
[docs] def set_environmental_data(self, humidity: int, temperature: float) -> None: """Set the temperature and humidity used when computing eCO2 and TVOC values. :param int humidity: The current relative humidity in percent. :param float temperature: The current temperature in Celsius.""" # Humidity is stored as an unsigned 16 bits in 1/512%RH. The default # value is 50% = 0x64, 0x00. As an example 48.5% humidity would be 0x61, # 0x00. humidity = int(humidity * 512) # Temperature is stored as an unsigned 16 bits integer in 1/512 degrees # there is an offset: 0 maps to -25C. The default value is 25C = 0x64, # 0x00. As an example 23.5% temperature would be 0x61, 0x00. temperature = int((temperature + 25) * 512) buf = bytearray(5) buf[0] = _ENV_DATA struct.pack_into(">HH", buf, 1, humidity, temperature) with self.i2c_device as i2c: i2c.write(buf)
[docs] def set_interrupt_thresholds( self, low_med: int, med_high: int, hysteresis: int ) -> None: """Set the thresholds used for triggering the interrupt based on eCO2. The interrupt is triggered when the value crossed a boundary value by the minimum hysteresis value. :param int low_med: Boundary between low and medium ranges :param int med_high: Boundary between medium and high ranges :param int hysteresis: Minimum difference between reads""" buf = bytearray( [ _THRESHOLDS, ((low_med >> 8) & 0xF), (low_med & 0xF), ((med_high >> 8) & 0xF), (med_high & 0xF), hysteresis, ] ) with self.i2c_device as i2c: i2c.write(buf)
[docs] def reset(self) -> None: """Initiate a software reset.""" # reset sequence from the datasheet seq = bytearray([_SW_RESET, 0x11, 0xE5, 0x72, 0x8A]) with self.i2c_device as i2c: i2c.write(seq)