Skip to content

Commit

Permalink
Include support for periodic data acquisition mode and tuning the dev…
Browse files Browse the repository at this point in the history
…ice's behaviour.
  • Loading branch information
WoefulDerelict committed Aug 1, 2019
1 parent 40d5609 commit 9a4a3c8
Showing 1 changed file with 270 additions and 41 deletions.
311 changes: 270 additions & 41 deletions adafruit_sht31d.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
--------------------
Expand Down Expand Up @@ -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_PDA_FETCH = const(0xE000)
_SHT31_PDA_BREAK = const(0x3093)

MODE_SSDA = 'Single'
MODE_PDA = 'Periodic'

_SHT31_MODES = (MODE_SSDA, MODE_PDA)

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)

_SSDA_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)))

_PDA_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):
Expand All @@ -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:
"""
Expand All @@ -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_SSDA
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 PDA mode.
"""
self._command(_SHT31_PDA_BREAK)
time.sleep(.001)
self._command(_SHT31_SOFTRESET)
time.sleep(.0015)

def _pda(self):
for command in _PDA_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_PDA:
data = bytearray(48)
data[0] = 0xff
self._command(_SHT31_PDA_FETCH)
time.sleep(.001)
elif self.mode == MODE_SSDA:
data = bytearray(6)
data[0] = 0xff
for command in _SSDA_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_PDA 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_SSDA:
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_PDA and value != MODE_PDA:
self._command(_SHT31_PDA_BREAK)
time.sleep(.001)
if value == MODE_PDA and self._mode != MODE_PDA:
self._pda()
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_PDA and not self._repeatability == value:
self._repeatability = value
self._pda()
else:
self._repeatability = value

@property
def clock_stretching(self):
"""
Control clock stretching.
This feature only affects SSDA 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 PDA mode
"""
return self._art

@art.setter
def art(self, value):
if value:
self.frequency = FREQUENCY_4
if self.mode == MODE_PDA and not self._art == value:
self._art = bool(value)
self._pda()
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_PDA and not self._frequency == value:
self._frequency = value
self._pda()
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.
SSDA mode reads and returns the current temperature as a float.
PDA mode returns the most recent readings available from the sensor's cache
in a FILO list of seven 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.
SSDA mode reads and returns the current humidity as a float.
PDA mode returns the most recent readings available from the sensor's cache
in a FILO list of seven 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]

0 comments on commit 9a4a3c8

Please sign in to comment.