From 032053c2af624d543cef30af6c6a7cf233b0f88a Mon Sep 17 00:00:00 2001 From: Llewelyn Trahaearn Date: Tue, 30 Jul 2019 22:10:56 -0700 Subject: [PATCH] Include support for periodic data acquisition mode and tuning the device's behaviour. --- README.rst | 32 ++++ adafruit_sht31d.py | 311 +++++++++++++++++++++++++++---- examples/sht31d_periodic_mode.py | 31 +++ examples/sht31d_simple_mode.py | 26 +++ 4 files changed, 359 insertions(+), 41 deletions(-) create mode 100644 examples/sht31d_periodic_mode.py create mode 100644 examples/sht31d_simple_mode.py diff --git a/README.rst b/README.rst index abe215e..3c5457a 100644 --- a/README.rst +++ b/README.rst @@ -63,6 +63,38 @@ And then you can start measuring the temperature and humidity: print(sensor.temperature) print(sensor.relative_humidity) +You can instruct the sensor to periodically measure the temperature and +humidity, storing the result in its internal cache: + +.. code:: python + + sensor.mode = adafruit_sht31d.MODE_PERIODIC + +You can adjust the frequency at which the sensor periodically gathers data to: +0.5, 1, 2, 4 or 10 Hz. The following adjusts the frequency to 2 Hz: + +.. code:: python + + sensor.frequency = adafruit_sht31d.FREQUENCY_2 + +The sensor is capable of storing eight results. The sensor stores these +results in an internal FILO cache. Retrieving these results is simlilar to +taking a measurement. The sensor clears its cache once the stored data is read. +The sensor always returns eight data points. The list of results is backfilled +with the maximum output values of 130.0 ºC and 100.01831417975366 % RH: + +.. code:: python + + print(sensor.temperature) + print(sensor.relative_humidity) + +The sensor will continue to collect data at the set interval until it is +returned to single shot data acquisition mode: + +.. code:: python + + sensor.mode = adafruit_sht31d.MODE_SINGLE + Contributing ============ diff --git a/adafruit_sht31d.py b/adafruit_sht31d.py index 6a71726..ca064a0 100644 --- a/adafruit_sht31d.py +++ b/adafruit_sht31d.py @@ -1,6 +1,7 @@ # The MIT License (MIT) # # Copyright (c) 2017 Jerry Needell +# Copyright (c) 2019 Llewelyn Trahaearn # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -25,7 +26,7 @@ This is a CircuitPython driver for the SHT31-D temperature and humidity sensor. -* Author(s): Jerry Needell +* Author(s): Jerry Needell, Llewelyn Trahaearn Implementation Notes -------------------- @@ -57,18 +58,67 @@ __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_SHT31D.git" -SHT31_DEFAULT_ADDR = const(0x44) -SHT31_MEAS_HIGHREP_STRETCH = const(0x2C06) -SHT31_MEAS_MEDREP_STRETCH = const(0x2C0D) -SHT31_MEAS_LOWREP_STRETCH = const(0x2C10) -SHT31_MEAS_HIGHREP = const(0x2400) -SHT31_MEAS_MEDREP = const(0x240B) -SHT31_MEAS_LOWREP = const(0x2416) -SHT31_READSTATUS = const(0xF32D) -SHT31_CLEARSTATUS = const(0x3041) -SHT31_SOFTRESET = const(0x30A2) -SHT31_HEATEREN = const(0x306D) -SHT31_HEATERDIS = const(0x3066) +_SHT31_DEFAULT_ADDRESS = const(0x44) +_SHT31_SECONDARY_ADDRESS = const(0x45) + +_SHT31_ADDRESSES = (_SHT31_DEFAULT_ADDRESS, _SHT31_SECONDARY_ADDRESS) + +_SHT31_READSERIALNBR = const(0x3780) +_SHT31_READSTATUS = const(0xF32D) +_SHT31_CLEARSTATUS = const(0x3041) +_SHT31_HEATER_ENABLE = const(0x306D) +_SHT31_HEATER_DISABLE = const(0x3066) +_SHT31_SOFTRESET = const(0x30A2) +_SHT31_NOSLEEP = const(0x303E) +_SHT31_PERIODIC_FETCH = const(0xE000) +_SHT31_PERIODIC_BREAK = const(0x3093) + +MODE_SINGLE = 'Single' +MODE_PERIODIC = 'Periodic' + +_SHT31_MODES = (MODE_SINGLE, MODE_PERIODIC) + +REP_HIGH = 'High' +REP_MED = 'Medium' +REP_LOW = 'Low' + +_SHT31_REP = (REP_HIGH, REP_MED, REP_LOW) + +FREQUENCY_0_5 = 0.5 +FREQUENCY_1 = 1 +FREQUENCY_2 = 2 +FREQUENCY_4 = 4 +FREQUENCY_10 = 10 + +_SHT31_FREQUENCIES = (FREQUENCY_0_5, FREQUENCY_1, FREQUENCY_2, FREQUENCY_4, FREQUENCY_10) + +_SINGLE_COMMANDS = ((REP_LOW, const(False), const(0x2416)), + (REP_MED, const(False), const(0x240B)), + (REP_HIGH, const(False), const(0x2400)), + (REP_LOW, const(True), const(0x2C10)), + (REP_MED, const(True), const(0x2C0D)), + (REP_HIGH, const(True), const(0x2C06))) + +_PERIODIC_COMMANDS = ((True, None, const(0x2B32)), + (REP_LOW, FREQUENCY_0_5, const(0x202F)), + (REP_MED, FREQUENCY_0_5, const(0x2024)), + (REP_HIGH, FREQUENCY_0_5, const(0x2032)), + (REP_LOW, FREQUENCY_1, const(0x212D)), + (REP_MED, FREQUENCY_1, const(0x2126)), + (REP_HIGH, FREQUENCY_1, const(0x2130)), + (REP_LOW, FREQUENCY_2, const(0x222B)), + (REP_MED, FREQUENCY_2, const(0x2220)), + (REP_HIGH, FREQUENCY_2, const(0x2236)), + (REP_LOW, FREQUENCY_4, const(0x2329)), + (REP_MED, FREQUENCY_4, const(0x2322)), + (REP_HIGH, FREQUENCY_4, const(0x2334)), + (REP_LOW, FREQUENCY_10, const(0x272A)), + (REP_MED, FREQUENCY_10, const(0x2721)), + (REP_HIGH, FREQUENCY_10, const(0x2737))) + +_DELAY = ((REP_LOW, .0045), + (REP_MED, .0065), + (REP_HIGH, .0155)) def _crc(data): @@ -83,6 +133,19 @@ def _crc(data): crc <<= 1 return crc +def _unpack(data): + length = len(data) + crc = [None] * (length//3) + word = [None] * (length//3) + for i in range(length//6): + word[i*2], crc[i*2], word[(i*2)+1], crc[(i*2)+1] = struct.unpack('>HBHB', data[i*6:(i*6)+6]) + if crc[i*2] == _crc(data[i*6:(i*6)+2]): + length = (i+1)*6 + for i in range(length//3): + if crc[i] != _crc(data[i*3:(i*3)+2]): + raise RuntimeError("CRC mismatch") + return word[:length//3] + class SHT31D: """ @@ -91,64 +154,230 @@ class SHT31D: :param i2c_bus: The `busio.I2C` object to use. This is the only required parameter. :param int address: (optional) The I2C address of the device. """ - def __init__(self, i2c_bus, address=SHT31_DEFAULT_ADDR): + def __init__(self, i2c_bus, address=_SHT31_DEFAULT_ADDRESS): + if address not in _SHT31_ADDRESSES: + raise ValueError('Invalid address: 0x%x' % (address)) self.i2c_device = I2CDevice(i2c_bus, address) - self._command(SHT31_SOFTRESET) - time.sleep(.010) + self._mode = MODE_SINGLE + self._repeatability = REP_HIGH + self._frequency = FREQUENCY_4 + self._clock_stretching = False + self._art = False + self._last_read = 0 + self._cached_temperature = None + self._cached_humidity = None + self._reset() def _command(self, command): with self.i2c_device as i2c: i2c.write(struct.pack('>H', command)) + def _reset(self): + """ + Soft reset the device + The reset command is preceded by a break command as the + device will not respond to a soft reset when in 'Periodic' mode. + """ + self._command(_SHT31_PERIODIC_BREAK) + time.sleep(.001) + self._command(_SHT31_SOFTRESET) + time.sleep(.0015) + + def _periodic(self): + for command in _PERIODIC_COMMANDS: + if self.art == command[0] or \ + (self.repeatability == command[0] and self.frequency == command[1]): + self._command(command[2]) + time.sleep(.001) + self._last_read = 0 + def _data(self): - data = bytearray(6) - data[0] = 0xff - self._command(SHT31_MEAS_HIGHREP) - time.sleep(.5) + if self.mode == MODE_PERIODIC: + data = bytearray(48) + data[0] = 0xff + self._command(_SHT31_PERIODIC_FETCH) + time.sleep(.001) + elif self.mode == MODE_SINGLE: + data = bytearray(6) + data[0] = 0xff + for command in _SINGLE_COMMANDS: + if self.repeatability == command[0] and self.clock_stretching == command[1]: + self._command(command[2]) + if not self.clock_stretching: + for delay in _DELAY: + if self.repeatability == delay[0]: + time.sleep(delay[1]) + else: + time.sleep(.001) with self.i2c_device as i2c: i2c.readinto(data) - temperature, tcheck, humidity, hcheck = struct.unpack('>HBHB', data) - if tcheck != _crc(data[:2]): - raise RuntimeError("temperature CRC mismatch") - if hcheck != _crc(data[3:5]): - raise RuntimeError("humidity CRC mismatch") + word = _unpack(data) + length = len(word) + temperature = [None] * (length//2) + humidity = [None] * (length//2) + for i in range(length//2): + temperature[i] = -45 + (175 * (word[i*2] / 65535)) + humidity[i] = 100 * (word[(i*2)+1] / 65523) + if (len(temperature) == 1) and (len(humidity) == 1): + return temperature[0], humidity[0] return temperature, humidity + def _read(self): + if self.mode == MODE_PERIODIC and time.time() > self._last_read+1/self.frequency: + self._cached_temperature, self._cached_humidity = self._data() + self._last_read = time.time() + elif self.mode == MODE_SINGLE: + self._cached_temperature, self._cached_humidity = self._data() + return self._cached_temperature, self._cached_humidity + + @property + def mode(self): + """ + Operation mode + Allowed values are the constants MODE_* + Return the device to 'Single' mode to stop periodic data acquisition and allow it to sleep. + """ + return self._mode + + @mode.setter + def mode(self, value): + if not value in _SHT31_MODES: + raise ValueError("Mode '%s' not supported" % (value)) + if self._mode == MODE_PERIODIC and value != MODE_PERIODIC: + self._command(_SHT31_PERIODIC_BREAK) + time.sleep(.001) + if value == MODE_PERIODIC and self._mode != MODE_PERIODIC: + self._periodic() + self._mode = value + + @property + def repeatability(self): + """ + Repeatability + Allowed values are the constants REP_* + """ + return self._repeatability + + @repeatability.setter + def repeatability(self, value): + if not value in _SHT31_REP: + raise ValueError("Repeatability '%s' not supported" % (value)) + if self.mode == MODE_PERIODIC and not self._repeatability == value: + self._repeatability = value + self._periodic() + else: + self._repeatability = value + + @property + def clock_stretching(self): + """ + Control clock stretching. + This feature only affects 'Single' mode. + """ + return self._clock_stretching + + @clock_stretching.setter + def clock_stretching(self, value): + self._clock_stretching = bool(value) + + @property + def art(self): + """ + Control accelerated response time + This feature only affects 'Periodic' mode. + """ + return self._art + + @art.setter + def art(self, value): + if value: + self.frequency = FREQUENCY_4 + if self.mode == MODE_PERIODIC and not self._art == value: + self._art = bool(value) + self._periodic() + else: + self._art = bool(value) + + @property + def frequency(self): + """ + Periodic data acquisition frequency + Allowed values are the constants FREQUENCY_* + Frequency can not be modified when ART is enabled + """ + return self._frequency + + @frequency.setter + def frequency(self, value): + if self.art: + raise RuntimeError("Frequency locked to '4 Hz' when ART enabled") + if not value in _SHT31_FREQUENCIES: + raise ValueError("Data acquisition frequency '%s Hz' not supported" % (value)) + if self.mode == MODE_PERIODIC and not self._frequency == value: + self._frequency = value + self._periodic() + else: + self._frequency = value + @property def temperature(self): - """The measured temperature in degrees celsius.""" - raw_temperature, _ = self._data() - return -45 + (175 * (raw_temperature / 65535)) + """ + The measured temperature in degrees celsius. + 'Single' mode reads and returns the current temperature as a float. + 'Periodic' mode returns the most recent readings available from the sensor's cache + in a FILO list of eight floats. This list is backfilled with with the + sensor's maximum output of 130.0 when the sensor is read before the + cache is full. + """ + temperature, _ = self._read() + return temperature @property def relative_humidity(self): - """The measured relative humidity in percent.""" - _, raw_humidity = self._data() - return 100 * (raw_humidity / 65523) - - def reset(self): - """Execute a Soft RESET of the sensor.""" - self._command(SHT31_SOFTRESET) - time.sleep(.010) + """ + The measured relative humidity in percent. + 'Single' mode reads and returns the current humidity as a float. + 'Periodic' mode returns the most recent readings available from the sensor's cache + in a FILO list of eight floats. This list is backfilled with with the + sensor's maximum output of 100.01831417975366 when the sensor is read + before the cache is full. + """ + _, humidity = self._read() + return humidity @property def heater(self): - """Control the sensor internal heater.""" + """Control device's internal heater.""" return (self.status & 0x2000) != 0 @heater.setter def heater(self, value=False): if value: - self._command(SHT31_HEATEREN) + self._command(_SHT31_HEATER_ENABLE) + time.sleep(.001) else: - self._command(SHT31_HEATERDIS) + self._command(_SHT31_HEATER_DISABLE) + time.sleep(.001) @property def status(self): - """The Sensor status.""" + """Device status.""" data = bytearray(2) - self._command(SHT31_READSTATUS) + self._command(_SHT31_READSTATUS) + time.sleep(.001) with self.i2c_device as i2c: i2c.readinto(data) status = data[0] << 8 | data[1] return status + + @property + def serial_number(self): + """Device serial number.""" + data = bytearray(6) + data[0] = 0xff + self._command(_SHT31_READSERIALNBR) + time.sleep(.001) + with self.i2c_device as i2c: + i2c.readinto(data) + word = _unpack(data) + return (word[0] << 16) | word[1] diff --git a/examples/sht31d_periodic_mode.py b/examples/sht31d_periodic_mode.py new file mode 100644 index 0000000..84aa97a --- /dev/null +++ b/examples/sht31d_periodic_mode.py @@ -0,0 +1,31 @@ +#!/usr/bin/python3 +import time +import board +import busio +import adafruit_sht31d + +# Create library object using our Bus I2C port +i2c = busio.I2C(board.SCL, board.SDA) +sensor = adafruit_sht31d.SHT31D(i2c) + +print('\033[1mSensor\033[0m = SHT31-D') +print('\033[1mSerial Number\033[0m = ', sensor.serial_number, '\n') +sensor.frequency = adafruit_sht31d.FREQUENCY_1 +sensor.mode = adafruit_sht31d.MODE_PERIODIC +for i in range(3): + print('Please wait...', end='\r') + if i == 2: + sensor.heater = True + if i == 1: + time.sleep(4) + print('\033[91mCache half full.\033[0m') + else: + time.sleep(8) + if sensor.heater: + print('\033[1mHeater:\033[0m On ') + sensor.heater = False + print('\033[1mTemperature:\033[0m ', sensor.temperature) + if not sensor.heater: + print('\033[1mHeater:\033[0m Off') + print('\033[1mHumidity:\033[0m ', sensor.relative_humidity, '\n') +sensor.mode = adafruit_sht31d.MODE_SINGLE diff --git a/examples/sht31d_simple_mode.py b/examples/sht31d_simple_mode.py new file mode 100644 index 0000000..b548809 --- /dev/null +++ b/examples/sht31d_simple_mode.py @@ -0,0 +1,26 @@ +import board +import busio +import adafruit_sht31d + +# Create library object using our Bus I2C port +i2c = busio.I2C(board.SCL, board.SDA) +sensor = adafruit_sht31d.SHT31D(i2c) + +print('\033[1mSensor\033[0m = SHT31-D') +print('\033[1mSerial Number\033[0m = ', sensor.serial_number, '\n') + +for i in range(3): + if i == 0: + sensor.repeatability = adafruit_sht31d.REP_LOW + print('\033[1m\033[36mLow Repeatability:\033[0m\n') + if i == 1: + sensor.repeatability = adafruit_sht31d.REP_MED + print('\n\033[1m\033[36mMedium Repeatability:\033[0m\n') + if i == 2: + sensor.repeatability = adafruit_sht31d.REP_HIGH + sensor.clock_stretching = True + print('\n\033[1m\033[36mHigh Repeatability:\033[0m') + print('\033[1m\033[95mClock Stretching:\033[0m \033[92mEnabled\033[0m\n') + for itr in range(3): + print('\033[1mTemperature:\033[0m %0.3f ºC' % sensor.temperature) + print('\033[1mHumidity:\033[0m %0.2f %%' % sensor.relative_humidity, '\n')