drivers.SEN66.adafruit_sen6x.SEN6x

class drivers.SEN66.adafruit_sen6x.SEN6x(i2c: smbus2.SMBus | int, address: int = SEN6X_I2C_ADDRESS)

Base class for the Sensirion SEN6x environmental-sensor family.

Implements the protocol-level features that are common to the whole SEN6x family — SEN60, SEN63C, SEN65, SEN66, and SEN68 — including start/stop measurement, the data_ready poll, the 32-bit device-status register, fan cleaning, serial number and product name lookup, firmware version, the SHT-heater readback, and the per-word CRC8 used by every Sensirion command. Device-specific measurement commands are added by subclasses such as SEN66. Although named after the original CircuitPython driver, this port talks directly to an smbus2.SMBus handle.

Variables:
  • _address (int) – 7-bit I2C address (0x6B for SEN6x, 0x6C for SEN60).

  • _i2c_bus (smbus2.SMBus) – Open SMBus handle used for all I2C read/write transfers.

  • _serial_number (str | None) – Cached ASCII serial number, populated lazily on first access of serial_number.

  • _product_name (str | None) – Cached product name string, populated lazily on first access of product_name.

  • _measurement_started (bool) – True once start_measurement() has been issued without a matching stop_measurement(); used to guard idle-mode-only commands.

Example

>>> from smbus2 import SMBus
>>> bus = SMBus(1)
>>> dev = SEN6x(bus)
>>> dev.serial_number
'...'

Note

On Linux the SEN6x lives at I2C address 0x6B (SEN6X_I2C_ADDRESS); the legacy SEN60 lives at 0x6C (SEN60_I2C_ADDRESS). The constructor sleeps _SENSOR_STARTUP_TIME (1 s) before returning so that the very first command issued by the caller hits a sensor that has fully booted.

Bind the driver to an SMBus handle and wait for sensor boot.

Parameters:
  • i2c (smbus2.SMBus | int) – Either an already-open smbus2.SMBus instance or an integer bus number; in the latter case the constructor opens SMBus(int(i2c)) itself.

  • address (int) – 7-bit I2C address of the SEN6x device. Defaults to SEN6X_I2C_ADDRESS (0x6B), shared by SEN63C/SEN65/SEN66/SEN68; pass SEN60_I2C_ADDRESS (0x6C) for a SEN60.

Returns:

None

Raises:

OSError – Propagated from smbus2.SMBus when an integer bus number is given but the underlying /dev/i2c-* device cannot be opened.

Example

>>> from smbus2 import SMBus
>>> bus = SMBus(1)
>>> dev = SEN6x(bus)

Note

Blocks for _SENSOR_STARTUP_TIME seconds (1 s) to cover the worst-case SEN6x power-on time before the first Sensirion command (e.g. 0x0021 start_measurement) is sent.

static _crc8(data: bytes) int

Compute the Sensirion CRC8 checksum for an arbitrary byte string.

Implements the byte-wise CRC8 used by every Sensirion air-quality sensor: polynomial 0x31 (x**8 + x**5 + x**4 + 1), initial value 0xFF, no input or output reflection, no final XOR. Each input byte is XORed into the running CRC, then the CRC is shifted left 8 times with the polynomial XORed in whenever the high bit was set, and finally masked to 8 bits.

Parameters:

data (bytes) – Raw input bytes — typically a single big-endian 16-bit word — whose CRC8 is required.

Returns:

8-bit unsigned CRC value (0..255) suitable for appending after data on the wire, or for comparing to the CRC byte that follows data in a read response.

Return type:

int

Raises:

None

Example

>>> SEN6x._crc8(b"\xBE\xEF")
146

Note

Used directly by _write_command() to protect every argument word and by _read_data() to validate every response word.

_read_data(num_words: int, execution_time: float = _TIME_STANDARD) list[int]

Read and CRC-verify a sequence of 16-bit words from the SEN6x.

Sensirion sensors stream their responses as [MSB, LSB, CRC8] triplets per 16-bit word, so this helper sleeps execution_time first (to let the previous command finish), issues a single num_words * 3 byte smbus2.i2c_msg.read, then validates every word’s CRC8 (polynomial 0x31, init 0xFF) before unpacking each pair into a big-endian unsigned integer.

Parameters:
  • num_words (int) – Number of 16-bit words the device is expected to return after the most recent command; num_words * 3 bytes are read off the wire.

  • execution_time (float) – Seconds to sleep before issuing the read so the device has had time to prepare the response. Defaults to _TIME_STANDARD (20 ms).

Returns:

List of num_words 16-bit unsigned integers decoded from the I2C response in protocol order.

Return type:

list[int]

Raises:
  • RuntimeError – If the CRC8 of any returned word does not match the CRC byte from the device, indicating bus corruption.

  • OSError – Propagated from smbus2.SMBus.i2c_rdwr on an underlying I2C error.

Example

>>> dev._read_data(1)
[...]

Note

CRC is the same Sensirion variant used in _write_command(); one CRC byte follows every word in the response.

_write_command(command: int, data: list[int] | None = None, execution_time: float = _TIME_STANDARD) None

Send a Sensirion 16-bit command, with optional CRC-protected data.

Packs the command big-endian into a bytearray; if a data payload is supplied, each 16-bit word is also packed big-endian and followed by its CRC8 byte (polynomial 0x31, init 0xFF) before being concatenated to the buffer. The whole buffer is then sent in a single smbus2.i2c_msg.write transfer and the call sleeps for execution_time so the sensor has time to act before the host issues the matching read.

Parameters:
  • command (int) – 16-bit Sensirion command ID (the value already encodes the Sensirion command CRC bits; callers pass constants such as _START_MEASUREMENT / 0x0021).

  • data (list[int] | None) – Optional list of 16-bit argument words to append after the command; each word is CRC8-protected on the wire. Defaults to None (command only).

  • execution_time (float) – Seconds to sleep after the write transfer so the device can execute the command. Defaults to _TIME_STANDARD (20 ms).

Returns:

None

Raises:

OSError – Propagated from smbus2.SMBus.i2c_rdwr if the I2C transaction fails (NACK, bus error, etc.).

Example

>>> dev._write_command(0x0021)

Note

All SEN6x command IDs are 16-bit big-endian; per-word CRC8 is mandatory for both writes and reads. The polynomial is 0x31 (x**8 + x**5 + x**4 + 1) with 0xFF initialisation, matching the Sensirion air- quality sensor convention.

check_sensor_errors() None

Raise if any critical sensor-error flags are currently set.

Reads device_status once and raises a RuntimeError if either the fan-error bit is set (which invalidates every channel) or any of the PM, gas, or RH+T error bits are set. Intended to be called immediately before a measurement read so callers can fail fast rather than persisting bogus values.

Parameters:

None

Returns:

Returns silently when no critical error bits are set.

Return type:

None

Raises:
  • RuntimeError – When the fan-error bit is set (“Fan error detected - measurements unreliable”), or when any of the PM, gas, or RH+T error bits are set (“Sensor errors detected: …”).

  • OSError – Propagated from the underlying SMBus transfer used by device_status.

Example

>>> dev.check_sensor_errors()

Note

Internally triggers Sensirion command 0xD206 (device_status) via device_status; does not clear any flags.

clear_device_status() None

Clear all sticky error and warning flags in the status register.

Sensirion error flags are “sticky” — once set they persist across the underlying condition disappearing, until the host explicitly clears them or the sensor is reset. This call issues the clear command; flags whose underlying condition is still active will be re-asserted on the next status read.

Parameters:

None

Returns:

None

Raises:

OSError – Propagated from the underlying SMBus transfer.

Example

>>> dev.clear_device_status()

Note

Sensirion command 0xD210 (clear_device_status) with an execution delay of _TIME_STANDARD (20 ms).

reset() None

Reset the SEN6x sensor and wait for it to come back up.

Issues the Sensirion reset command, clears the local _measurement_started flag and the cached serial number and product name, then sleeps _SENSOR_STARTUP_TIME so the sensor has time to reboot before further commands are sent. All on-device configuration values (temperature offset, VOC/NOx tuning, ambient pressure/altitude, CO2 ASC state, etc.) are restored to their factory defaults.

Parameters:

None

Returns:

None

Raises:

OSError – Propagated from the underlying SMBus transfer.

Example

>>> dev.reset()

Note

Sensirion command 0xD304 (reset) with an execution time of _TIME_RESET (1.2 s); a further 1 s startup sleep is applied here, matching the constructor.

start_fan_cleaning() None

Spin the fan up to maximum for 10 s to blow out accumulated dust.

Spins the SEN6x fan up to its maximum speed for ten seconds so any dust that has settled in the PM channel is blown clear. The command is only valid when the sensor is in idle mode; callers are expected to wait at least 10 seconds after invoking this before calling start_measurement() again.

Parameters:

None

Returns:

None

Raises:
  • RuntimeError – If the sensor is currently in measurement mode (self._measurement_started is True); callers must stop_measurement() first.

  • OSError – Propagated from the underlying SMBus transfer.

Example

>>> dev.stop_measurement()
>>> dev.start_fan_cleaning()

Note

Sensirion command 0x5607 (start_fan_cleaning) with an execution delay of _TIME_STANDARD (20 ms); the actual cleaning runs for ~10 s on the device side.

start_measurement() None

Switch the sensor into continuous measurement mode.

Sends the Sensirion start measurement command and sets the _measurement_started flag so subsequent calls become no-ops; the sensor then continuously updates its internal result registers and raises the data_ready flag once per measurement cycle.

Parameters:

None

Returns:

None

Raises:

OSError – Propagated from the underlying SMBus transfer.

Example

>>> dev.start_measurement()

Note

Sensirion command 0x0021 (start_measurement) with an execution delay of _TIME_START_MEASUREMENT (50 ms); first valid measurement frame is available about _FIRST_MEASUREMENT_DELAY (1.1 s) later.

stop_measurement() None

Return the sensor to idle mode so configuration can be changed.

Sends the Sensirion stop measurement command and clears the _measurement_started flag. Many configuration commands (CO2 FRC/ASC, temperature acceleration, VOC/NOx tuning, altitude, fan cleaning, …) require the device to be in idle mode and must be preceded by this call.

Parameters:

None

Returns:

None

Raises:

OSError – Propagated from the underlying SMBus transfer.

Example

>>> dev.stop_measurement()

Note

Sensirion command 0x0104 (stop_measurement) with an execution time of _TIME_STOP_MEASUREMENT (1 s).

_address: int = 107
_measurement_started: bool = False
_product_name: str | None = None
_serial_number: str | None = None
property data_ready: bool

Whether a fresh measurement frame is waiting on the SEN6x.

Parameters:

None

Returns:

True when the sensor has produced a new measurement that has not yet been read; False if the sensor is in idle mode or the previous frame has already been consumed.

Return type:

bool

Raises:

Example

>>> dev.start_measurement()
>>> dev.data_ready
True

Note

Sensirion command 0x0202 (data_ready); only the least-significant bit of the returned word carries the ready flag.

property device_status: DeviceStatus

Current 32-bit device status register, parsed into a typed object.

Parameters:

None

Returns:

A fresh DeviceStatus wrapping the two 16-bit words just read from the device, exposing individual fan/PM/gas/RH+T/CO2 flags and the DeviceStatus.errors and DeviceStatus.warnings aggregates.

Return type:

DeviceStatus

Raises:

Example

>>> dev.device_status.errors
False

Note

Sensirion command 0xD206 (device_status); the two response words are concatenated MSB-first into a 32-bit value before being handed to DeviceStatus.

property error_status_description: dict[str, str]

Human-readable summary of which error flags are set and why it matters.

Parameters:

None

Returns:

Mapping from short error tag ("fan", "pm", "gas", "rht", "co2") to a short English sentence explaining what the flag means for the measurement results; entries for flags that are not set are omitted, so an empty dict means the sensor is healthy.

Return type:

dict[str, str]

Raises:

Example

>>> dev.error_status_description
{}

Note

Backed by Sensirion command 0xD206 (device_status); the descriptions exposed here match the wording in the SEN66 datasheet.

property product_name: str

The SEN6x product name string as reported by the device.

Parameters:

None

Returns:

ASCII product name (for example "SEN66") with trailing null bytes stripped. Cached after the first read.

Return type:

str

Raises:

Example

>>> dev.product_name
'SEN66'

Note

Sensirion command 0xD014 (get_product_name); the response is 32 bytes (16 words) of null-padded ASCII.

property serial_number: str

The SEN6x serial number as an ASCII string (up to 32 characters).

Parameters:

None

Returns:

ASCII serial number read from the sensor, with any trailing null bytes stripped. The value is cached after the first successful read so subsequent accesses are free.

Return type:

str

Raises:

Example

>>> dev.serial_number
'...'

Note

Sensirion command 0xD033 (get_serial_number); the response is 32 bytes (16 words) of null-padded ASCII.

property sht_heater_measurements: dict[str, float | None]

RH+T values produced while the SHT internal heater is active.

Available on firmware >= 4.0 only; intended to monitor the progress of an SHT heater cycle (used to drive moisture out of the RH+T sensor). While heating is still in progress the sensor returns the unknown sentinel value for one or both channels, which is surfaced here as None.

Parameters:

None

Returns:

Mapping with keys "humidity" (relative humidity, percent) and "temperature" (degrees Celsius); either value is None when heating has not yet finished.

Return type:

dict[str, float | None]

Raises:

Example

>>> dev.sht_heater_measurements
{'humidity': 35.4, 'temperature': 24.8}

Note

Sensirion command 0x6790 (activate_sht_heater); response uses scale factors 100 for humidity and 200 for temperature.

property version: tuple[int, int]

The on-device firmware version as a (major, minor) tuple.

Parameters:

None

Returns:

(major_version, minor_version) pair decoded from the high and low bytes of the single 16-bit response word.

Return type:

tuple[int, int]

Raises:

Example

>>> dev.version
(4, 0)

Note

Sensirion command 0xD100 (read_version); the response packs major in the high byte and minor in the low byte of one word.