Skip to content

Commit 38edec8

Browse files
committed
Compiled releasable version of code snippets into real library
1 parent b2d5cd2 commit 38edec8

File tree

13 files changed

+498
-0
lines changed

13 files changed

+498
-0
lines changed

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2017 Matthew Seal
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# htu21df Sensor Library
2+
A lightweight library for talking to an htu21df sensor over i2c. Supports python 2 or 3.
3+
4+
## Installation
5+
Use pip:
6+
7+
pip install htu21df
8+
9+
Or from any download, install with setup:
10+
11+
python setup.py install
12+
13+
## Why?
14+
It turns out that the expected interface with the htu21df sensor is a little off the standard i2c library interfaces. To get it to work on a raspberry pi took a lot of headbanging and soul searching. So instead of having others repeat the pain, I made this library which makes (semi) direct calls to the i2c io which match the sensor perfectly. I also added in a bunch of internal patches for python2 vs python3 so both can work without rewriting all the byte-level interfaces.
15+
16+
## Author
17+
Author(s): Matthew Seal
18+
19+
## License
20+
MIT

htu21/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from .htu21 import (
2+
HTU21,
3+
HTU21DF_I2C_ADDR,
4+
HTU21DF_READ_TEMP_HOLD,
5+
HTU21DF_READ_HUM_HOLD,
6+
HTU21DF_READ_TEMP_NO_HOLD,
7+
HTU21DF_READ_HUM_NO_HOLD,
8+
HTU21DF_WRITE_REG,
9+
HTU21DF_READ_REG,
10+
HTU21DF_RESET,
11+
HTU21DF_RESET_REG_VALUE)

htu21/htu21.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# https://cdn-shop.adafruit.com/datasheets/1899_HTU21D.pdf is useful for determining expected behavior
2+
import time
3+
from .retry import retry_immediately
4+
from .rawi2c import I2C, CRC8Error
5+
6+
HTU21DF_I2C_ADDR = 0x40
7+
HTU21DF_READ_TEMP_HOLD = 0xE3
8+
HTU21DF_READ_HUM_HOLD = 0xE5
9+
HTU21DF_READ_TEMP_NO_HOLD = 0xF3
10+
HTU21DF_READ_HUM_NO_HOLD = 0xF5
11+
HTU21DF_WRITE_REG = 0xE6
12+
HTU21DF_READ_REG = 0xE7
13+
HTU21DF_RESET = 0xFE
14+
HTU21DF_RESET_REG_VALUE = 0x02
15+
16+
class HTU21(object):
17+
data_retry_warpper = retry_immediately(CRC8Error, 3, lambda s, _e: s.reset())
18+
19+
def __init__(self, start_immediately=True):
20+
self.i2c = I2C(HTU21DF_I2C_ADDR)
21+
if start_immediately:
22+
self.start_sensor()
23+
24+
def start_sensor(self):
25+
self.reset()
26+
27+
def reset(self):
28+
self.i2c.write(HTU21DF_RESET)
29+
time.sleep(0.015)
30+
self.i2c.write(HTU21DF_READ_REG)
31+
if not self.i2c.read_int() == HTU21DF_RESET_REG_VALUE:
32+
raise IOError("HTU21D-F device reset failed")
33+
34+
@data_retry_warpper
35+
def read_temperature(self):
36+
self.i2c.write(HTU21DF_READ_TEMP_NO_HOLD)
37+
time.sleep(0.05)
38+
t = float(self.i2c.read_int(2, crc8=True))
39+
return (t * 175.72 / 65536) - 46.85
40+
41+
@data_retry_warpper
42+
def read_humidity(self):
43+
self.i2c.write(HTU21DF_READ_HUM_NO_HOLD)
44+
time.sleep(0.016)
45+
h = float(self.i2c.read_int(2, crc8=True))
46+
return (h * 125 / 65536) - 6
47+
48+
if __name__ == '__main__':
49+
htu = HTU21()
50+
print(htu.read_temperature())
51+
print(htu.read_humidity())

htu21/rawi2c.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Parts transcribed from https://www.raspberrypi.org/forums/viewtopic.php?t=76688
2+
import array, fcntl, os, sys
3+
4+
python_2 = sys.version_info[0] == 2
5+
6+
def bus_path(bus):
7+
return "/dev/i2c-{}".format(bus)
8+
9+
def available_bus():
10+
for bus in range(5):
11+
if os.access(bus_path(bus), os.F_OK):
12+
return bus
13+
raise IOError("No detectable i2c bus available")
14+
15+
def array_to_int_be(byte_array):
16+
outcome = 0
17+
for val in byte_array:
18+
outcome = (outcome << 8) + val
19+
return outcome
20+
21+
def array_to_int_le(byte_array):
22+
return array_to_int_be(reversed(list(byte_array)))
23+
24+
def array_to_int(byte_array, big_endian=True):
25+
return array_to_int_be(byte_array) if big_endian else array_to_int_le(byte_array)
26+
27+
def crc8check(values, big_endian=True):
28+
# Ported and refactored from Sparkfun Arduino HTU21D Library: https://github.com/sparkfun/HTU21D_Breakout
29+
remainder = array_to_int(values, big_endian)
30+
31+
# POLYNOMIAL = 0x0131 = x^8 + x^5 + x^4 + 1
32+
# divsor = 0x988000 is the 0x0131 polynomial shifted to farthest left of three bytes
33+
divsor = 0x988000
34+
35+
for i in range(0, 16):
36+
if remainder & 1 << (23 - i):
37+
remainder ^= divsor
38+
divsor = divsor >> 1
39+
40+
return remainder == 0
41+
42+
def try_ord(val):
43+
if isinstance(val, int):
44+
return val
45+
return ord(val)
46+
47+
def any_py_bytes(bytes_str):
48+
if python_2 and not isinstance(bytes_str, basestring):
49+
bytes_str = chr(bytes_str)
50+
elif not python_2 and not isinstance(bytes_str, (str, bytes)):
51+
bytes_str = chr(bytes_str)
52+
if not python_2 and isinstance(bytes_str, str):
53+
bytes_str = bytes_str.encode('charmap')
54+
return bytes_str
55+
56+
class CRC8Error(IOError): pass
57+
58+
class I2C(object):
59+
I2C_SLAVE=0x0703
60+
61+
def __init__(self, addr, bus=None):
62+
if bus is None:
63+
bus = available_bus()
64+
self._fd = open(bus_path(bus), 'rb+', 0)
65+
self.set_address(addr)
66+
67+
def set_address(self, addr):
68+
fcntl.ioctl(self._fd, self.I2C_SLAVE, addr)
69+
self.addr = addr
70+
71+
def write(self, byte):
72+
self._fd.write(any_py_bytes(byte))
73+
74+
def read(self):
75+
return any_py_bytes(self._fd.read(1))
76+
77+
def read_many(self, num_bytes, big_endian=True, crc8=False):
78+
if crc8:
79+
num_bytes += 1
80+
result = list(any_py_bytes(self._fd.read(num_bytes)))
81+
if crc8:
82+
if not crc8check(map(try_ord, result), big_endian):
83+
raise CRC8Error("Bad i2c checksum")
84+
result = result[:-1] if big_endian else result[1:]
85+
return result
86+
87+
def read_int(self, num_bytes=1, big_endian=True, crc8=False):
88+
results = map(try_ord, self.read_many(num_bytes, big_endian, crc8))
89+
return array_to_int(results, big_endian)
90+
91+
def close(self):
92+
if self._fd is not None:
93+
self._fd.close()
94+
self._fd = None
95+
96+
def __enter__(self):
97+
return self
98+
99+
def __exit__(self, exc_type, exc_val, exc_tb):
100+
self.close()
101+
102+
def __del__(self):
103+
self.close()

htu21/retry.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import wrapt
2+
3+
def retry_immediately(PossibleExceptions, max_retries=1, failure_callback=None):
4+
try:
5+
iter(PossibleExceptions)
6+
except TypeError:
7+
PossibleExceptions = [PossibleExceptions]
8+
9+
@wrapt.decorator
10+
def wrapped_retry(wrapped, instance, args, kwargs):
11+
retries = max_retries
12+
while retries >= 0:
13+
try:
14+
return wrapped(*args, **kwargs)
15+
except tuple(PossibleExceptions) as e:
16+
if failure_callback:
17+
failure_callback(instance, e)
18+
retries -= 1
19+
20+
return wrapped_retry

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
wrapt>=1.0.0,<2.0a0
2+
mock

setup.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import os
2+
import sys
3+
from subprocess import call
4+
from setuptools import setup
5+
6+
python_2 = sys.version_info[0] == 2
7+
def read(fname):
8+
with open(fname, 'rU' if python_2 else 'r') as fhandle:
9+
return fhandle.read()
10+
11+
version = '0.1.3'
12+
required = [req.strip() for req in read('requirements.txt').splitlines() if req.strip()]
13+
setup(
14+
name='htu21df',
15+
version=version,
16+
author='Matthew Seal',
17+
author_email='mseal007@gmail.com',
18+
description='A simple program for controlling an htu21d-f sensor from a Raspberry Pi',
19+
install_requires=required,
20+
license='MIT',
21+
packages=['htu21'],
22+
test_suite='tests',
23+
zip_safe=False,
24+
url='https://github.com/MSeal/htu21df_sensor',
25+
download_url='https://github.com/MSeal/htu21df_sensor/tarball/v' + version,
26+
keywords=['sensors', 'raspberry_pi', 'adafruit', 'scripting'],
27+
classifiers=[
28+
'Development Status :: 4 - Beta Development Status',
29+
'Topic :: Utilities',
30+
'License :: OSI Approved :: MIT License',
31+
'Natural Language :: English',
32+
'Programming Language :: Python'
33+
]
34+
)

tests/__init__.py

Whitespace-only changes.

tests/fakebus.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from htu21.rawi2c import any_py_bytes
2+
3+
class FakeBus(object):
4+
def __init__(self, *args, **kwargs):
5+
self.open()
6+
self.clear_buffers()
7+
8+
def clear_buffers(self):
9+
self.write_array = []
10+
self.read_array = []
11+
12+
def add_read_byte(self, value):
13+
self.read_array.append(any_py_bytes(value))
14+
15+
def add_many_read_bytes(self, values):
16+
for val in values:
17+
self.add_read_byte(val)
18+
19+
def write(self, byte):
20+
if not self.connected:
21+
raise IOError('Device is disconnected')
22+
self.write_array.append(byte)
23+
24+
def read_byte(self):
25+
if not self.connected:
26+
raise IOError('Device is disconnected')
27+
if not self.read_array:
28+
raise IOError('No readable values available')
29+
return self.read_array.pop(0)
30+
31+
def read(self, num_bytes=1):
32+
return ''.join(self.read_byte().decode('charmap') for _ in range(num_bytes))
33+
34+
def close(self):
35+
self.connected = False
36+
37+
def open(self):
38+
self.connected = True

0 commit comments

Comments
 (0)