"""Driver for the Sensirion SCD40 CO2, temperature, and humidity sensor.
Provides I2C communication with the SCD40 photoacoustic NDIR CO2 sensor via
the Adafruit CircuitPython SCD4x library. The driver supports periodic
measurement mode, automatic and manual calibration, altitude compensation,
and factory reset.
The SCD40 uses a photoacoustic sensing principle for CO2 measurement,
combined with a Sensirion humidity and temperature sensor on the same chip.
Hardware:
- Interface: I2C (bus 0)
- Address: 0x62
- Supply: 3.3V
- CO2 range: 400-2000 ppm (extended to 5000 ppm)
- CO2 accuracy: +/-(50 ppm + 5% of reading)
- Measurement interval: 5 seconds (periodic mode)
Typical usage::
sensor = SCD40()
sensor.initializeSensor(auto=True, alt=0)
co2, temp, hum = sensor.getCO2(data=True)
Note:
Requires ``adafruit-circuitpython-scd4x``, ``board``, and ``busio``.
The sensor requires a 5-second warm-up period after starting periodic
measurements before the first valid reading is available.
"""
import time
import adafruit_scd4x
import board
import busio
from utils.oizom_logger import OizomLogger
# -----------------------------------------------------------------------------
# Configure logging
# -----------------------------------------------------------------------------
basic_logger = OizomLogger(__name__).get()
context_logger = OizomLogger(__name__)
[docs]
class SCD40:
"""I2C driver for the Sensirion SCD40 CO2 sensor.
Wraps the Adafruit SCD4x library providing CO2, temperature, and humidity
readout with configurable auto-calibration and altitude compensation.
Settings are persisted to the sensor's EEPROM.
Attributes:
co2: Last CO2 reading in ppm.
temp: Last temperature reading in degrees Celsius.
hum: Last relative humidity reading in percent.
DEBUG: Enable verbose sensor data logging.
"""
co2 = 0
temp = 0
hum = 0
DEBUG = False
[docs]
def __init__(self) -> None:
try:
time.sleep(0.5)
self.i2c = busio.I2C(board.D1, board.D0)
self.scd4x = adafruit_scd4x.SCD4X(self.i2c)
self.scd4x.start_periodic_measurement()
time.sleep(1)
except Exception as e:
context_logger.error_with_context("SCD40", f"__init__: {e}")
[docs]
def initializeSensor(self, auto: bool = False, alt: int = 0) -> None:
try:
self.scd4x.stop_periodic_measurement()
context_logger.info_with_context("SCD40", "Serial number: %s", [hex(i) for i in self.scd4x.serial_number])
# The reinit command reinitializes the sensor by reloading user settings
# from EEPROM.
# self.scd4x.reinit()
# The perform_factory_reset command resets all configuration settings
# stored in the EEPROM and erases the FRC and ASC algorithm history.
# self.scd4x.factory_reset()
# The perform_self_test feature can be used as an end-of-line test to
# check sensor functionality and the customer power supply to the sensor.
# self.scd4x.self_test()
# You can set the temperature offset (default hardware is 4*C)
# self.scd4x.temperature_offset = 5.4
# You can set the altitude offset (default hardware is 0m)
self.scd4x.altitude = alt
# Set the current state (enabled / disabled) of the automatic self-calibration.
# By default, ASC is enabled
self.scd4x.self_calibration_enabled = auto
# Once set, you may want to permanently write these settings to EEPROM
self.scd4x.persist_settings()
if self.DEBUG:
context_logger.info_with_context(
"SCD40",
"SCD40's temperature offset: %s",
self.scd4x.temperature_offset,
)
if self.DEBUG:
context_logger.info_with_context(
"SCD40",
"SCD40's altitude: %s meters above sea level",
self.scd4x.altitude,
)
context_logger.info_with_context(
"SCD40",
"SCD40's self-calibration enabled: %s",
self.scd4x.self_calibration_enabled,
)
self.scd4x.start_periodic_measurement()
time.sleep(1)
except Exception as e:
context_logger.error_with_context("SCD40", f"initializeSensor: {e}")
[docs]
def factoryReset(self) -> None:
try:
# The perform_factory_reset command resets all configuration settings
# stored in the EEPROM and erases the FRC and ASC algorithm history.
self.scd4x.stop_periodic_measurement()
self.scd4x.factory_reset()
if self.DEBUG:
context_logger.info_with_context("SCD40", "SCD40 factory reset done")
time.sleep(1)
except Exception as e:
context_logger.error_with_context("SCD40", f"factoryReset: {e}")
[docs]
def getCO2(self, data: bool) -> list[float]:
if data and self.scd4x.data_ready:
self.co2 = 0.0
self.temp = 0.0
self.hum = 0.0
self.co2 = self.scd4x.CO2
self.temp = self.scd4x.temperature
self.hum = self.scd4x.relative_humidity
if self.DEBUG:
context_logger.info_with_context(
"SCD40",
f"Sensor Data -> CO2:{self.co2}, Temp:{self.temp}, Hum:{self.hum}",
)
return [self.co2, self.temp, self.hum]
if __name__ == "__main__":
try:
elt = SCD40()
while 1:
context_logger.info_with_context("SCD40", f"CO2 Data: {elt.getCO2(True)}")
time.sleep(5)
except Exception as e:
context_logger.error_with_context("SCD40", f"main: {e}")