Compare commits

...

48 Commits

Author SHA1 Message Date
Christian Nicolai 76630cf903 ci: support Python 3.10+
Also update GHA and Ubuntu versions
2022-11-10 22:29:57 +01:00
cn d93b500b71 ci: fix pytest command used by tox 2022-11-03 10:59:16 +01:00
cn 2089e809e8 module: support Python 3 only 2021-05-21 14:04:19 +02:00
Christian Nicolai 294281658e ci: use older Ubuntu to retain Python 3.4
- see problematic run https://github.com/cmur2/python-bme680/actions/runs/619900122
2021-03-04 09:07:09 +01:00
cn 9f3be44151 Merge upstream 2021-01-08 11:26:06 +01:00
cn 01d590d826 ci: add GHA
- drops usage of Travis CI
2021-01-08 11:22:34 +01:00
cn 0ce1b1a83c module: add Python 3.9
- drop CI tests on unused Python versions
2021-01-06 11:46:16 +01:00
Philip Howard da98fcacb4
Merge pull request #32 from pimoroni/actions
Add GitHub actions workflow
2020-11-14 11:02:54 +00:00
Phil Howard 191dc9e115 Remove .travis.yml 2020-11-14 11:00:37 +00:00
Phil Howard 9855b10969 Add GitHub actions workflow 2020-11-14 10:57:49 +00:00
Phil Howard 6b5c136823 Merge branch 'melvinmajor-patch-1' 2020-11-02 15:59:04 +00:00
Phil Howard d7f712247a Swap from ord() to bytes 2020-11-02 15:58:50 +00:00
Phil Howard adfd58db6b Merge branch 'patch-1' of git://github.com/melvinmajor/bme680-python into melvinmajor-patch-1 2020-11-02 15:57:28 +00:00
Philip Howard 8ecf91fdd8
Merge pull request #29 from NicolaiSoeborg/patch-1
Add dependency: smbus
2020-11-02 15:55:11 +00:00
Nicolai Søborg 02ca7a4353
Update dependency requirement
If anyone is reading the file they will know to install `python3-smbus`,
but if someone just do a quick `pip3 install bme680` then make sure to pull smbus from PyPI
2020-11-01 01:11:16 +00:00
cn 7106a373e5 docs: use travis-ci.com links 2020-10-31 00:07:48 +01:00
Melvin Campos Casares 056049ee03
Fix TypeError preventing script to launch
TypeError: argument should be integer or bytes-like object, not 'str'
2020-10-21 19:07:20 +02:00
cn 28ebcb63a5 Also test on Python 3.7 and 3.8 via Tox 2020-08-09 02:15:20 +02:00
cn b36d259ae1 Remove coveralls integration 2020-08-09 02:15:20 +02:00
cn efce546001 Fix compensation tests for fixed ambient_temperature on first gas reading 2020-08-09 02:15:20 +02:00
cn cd32ec320d Merge upstream 2020-08-09 01:44:29 +02:00
Nicolai Søborg 91bb131713
Add dependency: smbus2
Fixes:

```python
>>> import bme680                                                                                                                                                                                          
>>> sensor = bme680.BME680(bme680.I2C_ADDR_PRIMARY)                                                                                                                                                        
Traceback (most recent call last):                                                                                                                                                                         
  File "<stdin>", line 1, in <module>                                                                                                                                                                      
  File "/usr/local/lib/python3.7/dist-packages/bme680/__init__.py", line 22, in __init__                                                                                                                   
    import smbus                                                                                                                                                                                           
ModuleNotFoundError: No module named 'smbus'                                                                                                                                                               
```
2020-05-29 15:35:54 +00:00
Philip Howard 7e6bdd0dc4
Merge pull request #28 from pimoroni/tests-and-qa
Tests and QA
2020-03-20 15:49:53 +00:00
Phil Howard 5806466739 Fix for test in py3 2020-03-20 15:44:06 +00:00
Phil Howard 45eeba9bb4 Minor linting fixes to examples 2020-03-20 15:38:31 +00:00
Phil Howard 90fabf53fa Expand test coverage and improve tests 2020-03-20 15:38:22 +00:00
Christian Nicolai f61ae9a86a
travis: fix build config validation problems
- https://docs.travis-ci.com/user/reference/overview/#deprecated-virtualization-environments
2020-02-28 13:23:20 +01:00
Philip Howard 3a48112445
Fix learn link for #25 (#26) 2019-11-08 11:53:07 +00:00
Sandy Macdonald 91434caf0b
Merge pull request #24 from pimoroni/examples-tidyup
Tidying up and adding examples
2019-06-09 13:14:58 +01:00
Sandy Macdonald 5a5dd139c3 Tidying up and adding examples 2019-06-09 13:11:35 +01:00
cn db4626f370 Use fixed ambient_temperature on first gas reading, it's nearly irrelevant
Note: on subsequent gas measurements the last temperature instead of 25 degree C will be used.
Nearly irrelevant because this happens to return value of _calc_heater_resistance(200):

ambient_temperature = 30 -> int(84.9119287004)
ambient_temperature = 25 -> int(84.9119068473)
ambient_temperature = 20 -> int(84.9118849941)
ambient_temperature = 10 -> int(84.9118412877)
2018-12-24 23:18:33 +01:00
Giampiero Baggiani e827e5d622 added check on constants.__dict__ for micropython support (#18)
* added check on constants.__dict__ for micropython support
2018-12-10 09:55:07 +00:00
cn 5a4709992e Add fork description to README 2018-12-07 12:04:25 +01:00
cn 6442510ab0 Use floating point precision for measurement compensations to yield sensor resolution
According to the datasheet the resolution delivered by the sensor is better
than 1 degree Celsius, 1 Pascal, 1 percent relative humidity etc. The user
should have the possibility to get floating point precision and figure out
what the measurement accuracy allows given their settings.
2018-12-07 11:39:50 +01:00
cn db02dd0d65 Increase poll period by x5 to allow successful measurement on max oversampling 2018-12-07 11:39:50 +01:00
cn c85e3de250 Remove redundant tweaking of measurement settings after soft reset, let user choose once 2018-12-07 11:39:50 +01:00
cn 405867a4aa Use standard units for pressure measures, let user choose conversion 2018-12-07 11:39:45 +01:00
Philip Howard e97c5f9241
Merge pull request #17 from pimoroni/tests
Fixed Travis badge URL
2018-09-03 11:42:59 +01:00
Phil Howard e43b362a33 Fixed Travis badge URL 2018-09-03 11:39:10 +01:00
Philip Howard 15171e57f6
Merge pull request #16 from pimoroni/tests
Test suites and code QA fixes
2018-09-03 11:17:21 +01:00
Phil Howard 8dce708fef Check for both i2c addresses in examples 2018-09-03 11:13:32 +01:00
Phil Howard df382a50aa Updated badges 2018-09-02 11:31:16 +01:00
Phil Howard 6374bc4241 Test suites and code QA fixes 2018-09-02 11:26:04 +01:00
Phil Howard c97791d720 Updated ignored QA warnings 2018-08-28 21:09:53 +01:00
Phil Howard 4b61558566 Added Build Status 2018-08-28 20:41:16 +01:00
Phil Howard a4a0cfcd2a Added flake8 install for travis 2018-08-28 20:33:03 +01:00
Phil Howard b2641c48df Travis flake8 checks 2018-08-28 20:30:59 +01:00
Phil Howard 2dc799a7a4 Added BG tooling 2018-08-14 09:30:42 +00:00
24 changed files with 750 additions and 272 deletions

36
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,36 @@
---
name: ci
on:
push:
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:
schedule:
- cron: '47 4 * * 4' # weekly on thursday morning
jobs:
build:
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
python-version:
- '3.7'
- '3.9'
- '3.10'
- '3.11'
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade setuptools tox
- name: Test
working-directory: library
run: |
tox -e py

4
.gitignore vendored
View File

@ -19,3 +19,7 @@ library/debian/
pip-log.txt
pip-delete-this-directory.txt
.DS_Store
.vscode/
.coverage
.tox/
.pytest_cache/

44
Makefile Normal file
View File

@ -0,0 +1,44 @@
.PHONY: usage install uninstall
usage:
@echo "Usage: make <target>, where target is one of:\n"
@echo "install: install the library locally from source"
@echo "uninstall: uninstall the local library"
@echo "python-readme: generate library/README.rst from README.md"
@echo "python-wheels: build python .whl files for distribution"
@echo "python-sdist: build python source distribution"
@echo "python-clean: clean python build and dist directories"
@echo "python-dist: build all python distribution files"
install:
./install.sh
uninstall:
./uninstall.sh
python-readme: library/README.rst
python-license: library/LICENSE.txt
library/README.rst: README.md
pandoc --from=markdown --to=rst -o library/README.rst README.md
library/LICENSE.txt: LICENSE
cp LICENSE library/LICENSE.txt
python-wheels: python-readme python-license
cd library; python3 setup.py bdist_wheel
cd library; python setup.py bdist_wheel
python-sdist: python-readme python-license
cd library; python setup.py sdist
python-clean:
-rm -r library/dist
-rm -r library/build
-rm -r library/*.egg-info
python-dist: python-clean python-wheels python-sdist
ls library/dist
python-deploy: python-dist
twine upload library/dist/*

View File

@ -1,40 +1,20 @@
# BME680
![ci](https://github.com/cmur2/python-bme680/workflows/ci/badge.svg?branch=master)
https://shop.pimoroni.com/products/bme680
The state-of-the-art BME680 breakout lets you measure temperature, pressure, humidity, and indoor air quality.
## Installing
## About this Fork
### Full install (recommended):
In general this fork tries to improve some details of the [original pimoroni/bme680-python repo](https://github.com/pimoroni/bme680-python):
We've created an easy installation script that will install all pre-requisites and get your BME680
up and running with minimal efforts. To run it, fire up Terminal which you'll find in Menu -> Accessories -> Terminal
on your Raspberry Pi desktop, as illustrated below:
- use standard unit for pressure measurement (Pascal instead Hectopascal, user can convert if he likes)
- floating point precision for measurement compensations (user has all precision available to choose *based on measurement settings and datasheet* how much is trustable)
- no redundant tweaking of measurement settings after soft reset
![Finding the terminal](http://get.pimoroni.com/resources/github-repo-terminal.png)
In the new terminal window type the command exactly as it appears below (check for typos) and follow the on-screen instructions:
```bash
curl https://get.pimoroni.com/bme680 | bash
```
### Manual install:
#### Library install for Python 3:
```bash
sudo pip3 install bme680
```
#### Library install for Python 2:
```bash
sudo pip2 install bme680
```
### Development:
## Development
If you want to contribute, or like living on the edge of your seat by having the latest code, you should clone this repository, `cd` to the library directory, and run:
@ -47,5 +27,5 @@ In all cases you will have to enable the i2c bus.
## Documentation & Support
* Guides and tutorials - https://learn.pimoroni.com/bme680
* Guides and tutorials - https://learn.pimoroni.com/bme680-breakout
* Get help - http://forums.pimoroni.com/c/support

View File

@ -0,0 +1,57 @@
#!/usr/bin/env python
import time
import bme680
from subprocess import PIPE, Popen
print("""compensated-temperature.py - Use the CPU temperature to compensate temperature
readings from the BME680 sensor. Method adapted from Initial State's Enviro pHAT
review: https://medium.com/@InitialState/tutorial-review-enviro-phat-for-raspberry-pi-4cd6d8c63441
Press Ctrl+C to exit!
""")
try:
sensor = bme680.BME680(bme680.I2C_ADDR_PRIMARY)
except IOError:
sensor = bme680.BME680(bme680.I2C_ADDR_SECONDARY)
# These oversampling settings can be tweaked to
# change the balance between accuracy and noise in
# the data.
sensor.set_humidity_oversample(bme680.OS_2X)
sensor.set_pressure_oversample(bme680.OS_4X)
sensor.set_temperature_oversample(bme680.OS_8X)
sensor.set_filter(bme680.FILTER_SIZE_3)
# Gets the CPU temperature in degrees C
def get_cpu_temperature():
process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE)
output, _error = process.communicate()
return float(output[output.index(b'=') + 1:output.rindex(b"'")])
factor = 1.0 # Smaller numbers adjust temp down, vice versa
smooth_size = 10 # Dampens jitter due to rapid CPU temp changes
cpu_temps = []
while True:
if sensor.get_sensor_data():
cpu_temp = get_cpu_temperature()
cpu_temps.append(cpu_temp)
if len(cpu_temps) > smooth_size:
cpu_temps = cpu_temps[1:]
smoothed_cpu_temp = sum(cpu_temps) / float(len(cpu_temps))
raw_temp = sensor.data.temperature
comp_temp = raw_temp - ((smoothed_cpu_temp - raw_temp) / factor)
print("Compensated temperature: {:05.2f} *C".format(comp_temp))
time.sleep(1.0)

View File

@ -3,19 +3,22 @@
import bme680
import time
print("""Estimate indoor air quality
print("""indoor-air-quality.py - Estimates indoor air quality.
Runs the sensor for a burn-in period, then uses a
Runs the sensor for a burn-in period, then uses a
combination of relative humidity and gas resistance
to estimate indoor air quality as a percentage.
Press Ctrl+C to exit
Press Ctrl+C to exit!
""")
sensor = bme680.BME680()
try:
sensor = bme680.BME680(bme680.I2C_ADDR_PRIMARY)
except IOError:
sensor = bme680.BME680(bme680.I2C_ADDR_SECONDARY)
# These oversampling settings can be tweaked to
# These oversampling settings can be tweaked to
# change the balance between accuracy and noise in
# the data.
@ -29,7 +32,7 @@ sensor.set_gas_heater_temperature(320)
sensor.set_gas_heater_duration(150)
sensor.select_gas_heater_profile(0)
# start_time and curr_time ensure that the
# start_time and curr_time ensure that the
# burn_in_time (in seconds) is kept track of.
start_time = time.time()
@ -42,13 +45,13 @@ try:
# Collect gas resistance burn-in values, then use the average
# of the last 50 values to set the upper limit for calculating
# gas_baseline.
print("Collecting gas resistance burn-in data for 5 mins\n")
print('Collecting gas resistance burn-in data for 5 mins\n')
while curr_time - start_time < burn_in_time:
curr_time = time.time()
if sensor.get_sensor_data() and sensor.data.heat_stable:
gas = sensor.data.gas_resistance
burn_in_data.append(gas)
print("Gas: {0} Ohms".format(gas))
print('Gas: {0} Ohms'.format(gas))
time.sleep(1)
gas_baseline = sum(burn_in_data[-50:]) / 50.0
@ -56,11 +59,13 @@ try:
# Set the humidity baseline to 40%, an optimal indoor humidity.
hum_baseline = 40.0
# This sets the balance between humidity and gas reading in the
# This sets the balance between humidity and gas reading in the
# calculation of air_quality_score (25:75, humidity:gas)
hum_weighting = 0.25
print("Gas baseline: {0} Ohms, humidity baseline: {1:.2f} %RH\n".format(gas_baseline, hum_baseline))
print('Gas baseline: {0} Ohms, humidity baseline: {1:.2f} %RH\n'.format(
gas_baseline,
hum_baseline))
while True:
if sensor.get_sensor_data() and sensor.data.heat_stable:
@ -72,22 +77,31 @@ try:
# Calculate hum_score as the distance from the hum_baseline.
if hum_offset > 0:
hum_score = (100 - hum_baseline - hum_offset) / (100 - hum_baseline) * (hum_weighting * 100)
hum_score = (100 - hum_baseline - hum_offset)
hum_score /= (100 - hum_baseline)
hum_score *= (hum_weighting * 100)
else:
hum_score = (hum_baseline + hum_offset) / hum_baseline * (hum_weighting * 100)
hum_score = (hum_baseline + hum_offset)
hum_score /= hum_baseline
hum_score *= (hum_weighting * 100)
# Calculate gas_score as the distance from the gas_baseline.
if gas_offset > 0:
gas_score = (gas / gas_baseline) * (100 - (hum_weighting * 100))
gas_score = (gas / gas_baseline)
gas_score *= (100 - (hum_weighting * 100))
else:
gas_score = 100 - (hum_weighting * 100)
# Calculate air_quality_score.
# Calculate air_quality_score.
air_quality_score = hum_score + gas_score
print("Gas: {0:.2f} Ohms,humidity: {1:.2f} %RH,air quality: {2:.2f}".format(gas, hum, air_quality_score))
print('Gas: {0:.2f} Ohms,humidity: {1:.2f} %RH,air quality: {2:.2f}'.format(
gas,
hum,
air_quality_score))
time.sleep(1)
except KeyboardInterrupt:

View File

@ -3,21 +3,30 @@
import bme680
import time
sensor = bme680.BME680()
print("""read-all.py - Displays temperature, pressure, humidity, and gas.
Press Ctrl+C to exit!
""")
try:
sensor = bme680.BME680(bme680.I2C_ADDR_PRIMARY)
except IOError:
sensor = bme680.BME680(bme680.I2C_ADDR_SECONDARY)
# These calibration data can safely be commented
# out, if desired.
print("Calibration data:")
print('Calibration data:')
for name in dir(sensor.calibration_data):
if not name.startswith('_'):
value = getattr(sensor.calibration_data, name)
if isinstance(value, int):
print("{}: {}".format(name, value))
print('{}: {}'.format(name, value))
# These oversampling settings can be tweaked to
# These oversampling settings can be tweaked to
# change the balance between accuracy and noise in
# the data.
@ -27,12 +36,12 @@ sensor.set_temperature_oversample(bme680.OS_8X)
sensor.set_filter(bme680.FILTER_SIZE_3)
sensor.set_gas_status(bme680.ENABLE_GAS_MEAS)
print("\n\nInitial reading:")
print('\n\nInitial reading:')
for name in dir(sensor.data):
value = getattr(sensor.data, name)
if not name.startswith('_'):
print("{}: {}".format(name, value))
print('{}: {}'.format(name, value))
sensor.set_gas_heater_temperature(320)
sensor.set_gas_heater_duration(150)
@ -43,14 +52,19 @@ sensor.select_gas_heater_profile(0)
# sensor.set_gas_heater_profile(200, 150, nb_profile=1)
# sensor.select_gas_heater_profile(1)
print("\n\nPolling:")
print('\n\nPolling:')
try:
while True:
if sensor.get_sensor_data():
output = "{0:.2f} C,{1:.2f} hPa,{2:.2f} %RH".format(sensor.data.temperature, sensor.data.pressure, sensor.data.humidity)
output = '{0:.2f} C,{1:.2f} hPa,{2:.2f} %RH'.format(
sensor.data.temperature,
sensor.data.pressure / 100.0,
sensor.data.humidity)
if sensor.data.heat_stable:
print("{0},{1} Ohms".format(output, sensor.data.gas_resistance))
print('{0},{1} Ohms'.format(
output,
sensor.data.gas_resistance))
else:
print(output)

8
examples/setup.cfg Normal file
View File

@ -0,0 +1,8 @@
[flake8]
ignore =
E501 # line too long
F403
F405
# Don't require docstrings in example code
D100 # Missing docstring in public module
D103 # Missing docstring in public function

View File

@ -1,39 +0,0 @@
#!/usr/bin/env python
import bme680
print("""Display Temperature, Pressure and Humidity with different offsets.
""")
sensor = bme680.BME680()
# These oversampling settings can be tweaked to
# change the balance between accuracy and noise in
# the data.
sensor.set_humidity_oversample(bme680.OS_2X)
sensor.set_pressure_oversample(bme680.OS_4X)
sensor.set_temperature_oversample(bme680.OS_8X)
sensor.set_filter(bme680.FILTER_SIZE_3)
def display_data(offset=0):
sensor.set_temp_offset(offset)
sensor.get_sensor_data()
output = "{0:.2f} C, {1:.2f} hPa, {2:.3f} %RH".format(sensor.data.temperature, sensor.data.pressure, sensor.data.humidity)
print(output)
print("")
print("Initial readings")
display_data()
print("SET offset 4 degrees celsius")
display_data(4)
print("SET offset -1.87 degrees celsius")
display_data(-1.87)
print("SET offset -100 degrees celsius")
display_data(-100)
print("SET offset 0 degrees celsius")
display_data(0)

50
examples/temperature-offset.py Executable file
View File

@ -0,0 +1,50 @@
#!/usr/bin/env python
import bme680
print("""temperature-offset.py - Displays temperature, pressure, and humidity with different offsets.
Press Ctrl+C to exit!
""")
try:
sensor = bme680.BME680(bme680.I2C_ADDR_PRIMARY)
except IOError:
sensor = bme680.BME680(bme680.I2C_ADDR_SECONDARY)
# These oversampling settings can be tweaked to
# change the balance between accuracy and noise in
# the data.
sensor.set_humidity_oversample(bme680.OS_2X)
sensor.set_pressure_oversample(bme680.OS_4X)
sensor.set_temperature_oversample(bme680.OS_8X)
sensor.set_filter(bme680.FILTER_SIZE_3)
def display_data(offset=0):
sensor.set_temp_offset(offset)
sensor.get_sensor_data()
output = '{0:.2f} C, {1:.2f} hPa, {2:.3f} %RH'.format(
sensor.data.temperature,
sensor.data.pressure / 100.0,
sensor.data.humidity)
print(output)
print('')
print('Initial readings')
display_data()
print('SET offset 4 degrees celsius')
display_data(4)
print('SET offset -1.87 degrees celsius')
display_data(-1.87)
print('SET offset -100 degrees celsius')
display_data(-100)
print('SET offset 0 degrees celsius')
display_data(0)

View File

@ -1,8 +1,8 @@
#!/usr/bin/env python
import bme680
import time
print("""Display Temperature, Pressure and Humidity
import bme680
print("""temperature-pressure-humidity.py - Displays temperature, pressure, and humidity.
If you don't need gas readings, then you can read temperature,
pressure and humidity quickly.
@ -11,7 +11,10 @@ Press Ctrl+C to exit
""")
sensor = bme680.BME680()
try:
sensor = bme680.BME680(bme680.I2C_ADDR_PRIMARY)
except IOError:
sensor = bme680.BME680(bme680.I2C_ADDR_SECONDARY)
# These oversampling settings can be tweaked to
# change the balance between accuracy and noise in
@ -22,15 +25,15 @@ sensor.set_pressure_oversample(bme680.OS_4X)
sensor.set_temperature_oversample(bme680.OS_8X)
sensor.set_filter(bme680.FILTER_SIZE_3)
print("Polling:")
print('Polling:')
try:
while True:
if sensor.get_sensor_data():
output = "{0:.2f} C,{1:.2f} hPa,{2:.3f} %RH".format(sensor.data.temperature, sensor.data.pressure, sensor.data.humidity)
output = '{0:.2f} C,{1:.2f} hPa,{2:.3f} %RH'.format(
sensor.data.temperature,
sensor.data.pressure / 100.0,
sensor.data.humidity)
print(output)
except KeyboardInterrupt:
pass

22
install.sh Executable file
View File

@ -0,0 +1,22 @@
#!/bin/bash
printf "BME680 Python Library: Installer\n\n"
if [ $(id -u) -ne 0 ]; then
printf "Script must be run as root. Try 'sudo ./install.sh'\n"
exit 1
fi
cd library
printf "Installing for Python 2..\n"
python setup.py install
if [ -f "/usr/bin/python3" ]; then
printf "Installing for Python 3..\n"
python3 setup.py install
fi
cd ..
printf "Done!\n"

4
library/.coveragerc Normal file
View File

@ -0,0 +1,4 @@
[run]
source = bme680
omit =
.tox/*

View File

@ -1,6 +1,8 @@
BME680
======
|Build Status| |Coverage Status| |PyPi Package| |Python Versions|
https://shop.pimoroni.com/products/bme680
The state-of-the-art BME680 breakout lets you measure temperature,
@ -20,6 +22,8 @@ Terminal on your Raspberry Pi desktop, as illustrated below:
.. figure:: http://get.pimoroni.com/resources/github-repo-terminal.png
:alt: Finding the terminal
Finding the terminal
In the new terminal window type the command exactly as it appears below
(check for typos) and follow the on-screen instructions:
@ -66,3 +70,11 @@ Documentation & Support
- Guides and tutorials - https://learn.pimoroni.com/bme680
- Get help - http://forums.pimoroni.com/c/support
.. |Build Status| image:: https://travis-ci.org/pimoroni/bme680-python.svg?branch=master
:target: https://travis-ci.org/pimoroni/bme680-python
.. |Coverage Status| image:: https://coveralls.io/repos/github/pimoroni/bme680-python/badge.svg?branch=master
:target: https://coveralls.io/github/pimoroni/bme680-python?branch=master
.. |PyPi Package| image:: https://img.shields.io/pypi/v/bme680.svg
:target: https://pypi.python.org/pypi/bme680
.. |Python Versions| image:: https://img.shields.io/pypi/pyversions/bme680.svg
:target: https://pypi.python.org/pypi/bme680

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,11 +1,24 @@
from .constants import *
"""BME680 Temperature, Pressure, Humidity & Gas Sensor."""
from .constants import lookupTable1, lookupTable2
from .constants import BME680Data
from . import constants
import math
import time
__version__ = '1.0.5'
# Export constants to global namespace
# so end-users can "from BME680 import NAME"
if hasattr(constants, '__dict__'):
for key in constants.__dict__:
value = constants.__dict__[key]
if key not in globals():
globals()[key] = value
class BME680(BME680Data):
"""BOSCH BME680
"""BOSCH BME680.
Gas, pressure, temperature and humidity sensor.
@ -13,7 +26,14 @@ class BME680(BME680Data):
:param i2c_device: Optional smbus or compatible instance for facilitating i2c communications.
"""
def __init__(self, i2c_addr=I2C_ADDR_PRIMARY, i2c_device=None):
def __init__(self, i2c_addr=constants.I2C_ADDR_PRIMARY, i2c_device=None):
"""Initialise BME680 sensor instance and verify device presence.
:param i2c_addr: i2c address of BME680
:param i2c_device: Optional SMBus-compatible instance for i2c transport
"""
BME680Data.__init__(self)
self.i2c_addr = i2c_addr
@ -22,45 +42,41 @@ class BME680(BME680Data):
import smbus
self._i2c = smbus.SMBus(1)
self.chip_id = self._get_regs(CHIP_ID_ADDR, 1)
if self.chip_id != CHIP_ID:
raise RuntimeError("BME680 Not Found. Invalid CHIP ID: 0x{0:02x}".format(self.chip_id))
self.chip_id = self._get_regs(constants.CHIP_ID_ADDR, 1)
if self.chip_id != constants.CHIP_ID:
raise RuntimeError('BME680 Not Found. Invalid CHIP ID: 0x{0:02x}'.format(self.chip_id))
self.soft_reset()
self.set_power_mode(SLEEP_MODE)
self.set_power_mode(constants.SLEEP_MODE)
self._get_calibration_data()
self.set_humidity_oversample(OS_2X)
self.set_pressure_oversample(OS_4X)
self.set_temperature_oversample(OS_8X)
self.set_filter(FILTER_SIZE_3)
self.set_gas_status(ENABLE_GAS_MEAS)
self.set_temp_offset(0)
self.get_sensor_data()
self.ambient_temperature = 25 # only for gas heater temperature, nearly irrelevant
def _get_calibration_data(self):
"""Retrieves the sensor calibration data and stores it in .calibration_data"""
calibration = self._get_regs(COEFF_ADDR1, COEFF_ADDR1_LEN)
calibration += self._get_regs(COEFF_ADDR2, COEFF_ADDR2_LEN)
"""Retrieve the sensor calibration data and store it in .calibration_data."""
calibration = self._get_regs(constants.COEFF_ADDR1, constants.COEFF_ADDR1_LEN)
calibration += self._get_regs(constants.COEFF_ADDR2, constants.COEFF_ADDR2_LEN)
heat_range = self._get_regs(ADDR_RES_HEAT_RANGE_ADDR, 1)
heat_value = twos_comp(self._get_regs(ADDR_RES_HEAT_VAL_ADDR, 1), bits=8)
sw_error = twos_comp(self._get_regs(ADDR_RANGE_SW_ERR_ADDR, 1), bits=8)
heat_range = self._get_regs(constants.ADDR_RES_HEAT_RANGE_ADDR, 1)
heat_value = constants.twos_comp(self._get_regs(constants.ADDR_RES_HEAT_VAL_ADDR, 1), bits=8)
sw_error = constants.twos_comp(self._get_regs(constants.ADDR_RANGE_SW_ERR_ADDR, 1), bits=8)
self.calibration_data.set_from_array(calibration)
self.calibration_data.set_other(heat_range, heat_value, sw_error)
def soft_reset(self):
"""Initiate a soft reset"""
self._set_regs(SOFT_RESET_ADDR, SOFT_RESET_CMD)
time.sleep(RESET_PERIOD / 1000.0)
"""Trigger a soft reset."""
self._set_regs(constants.SOFT_RESET_ADDR, constants.SOFT_RESET_CMD)
time.sleep(constants.RESET_PERIOD / 1000.0)
def set_temp_offset(self, value):
"""Set temperature offset in celsius
"""Set temperature offset in celsius.
If set, the temperature t_fine will be increased by given value in celsius.
:param value: Temperature offset in Celsius, eg. 4, -8, 1.25
"""
if value == 0:
self.offset_temp_in_t_fine = 0
@ -68,8 +84,8 @@ class BME680(BME680Data):
self.offset_temp_in_t_fine = int(math.copysign((((int(abs(value) * 100)) << 8) - 128) / 5, value))
def set_humidity_oversample(self, value):
"""Set humidity oversampling
"""Set humidity oversampling.
A higher oversampling value means more stable sensor readings,
with less noise and jitter.
@ -80,15 +96,15 @@ class BME680(BME680Data):
"""
self.tph_settings.os_hum = value
self._set_bits(CONF_OS_H_ADDR, OSH_MSK, OSH_POS, value)
self._set_bits(constants.CONF_OS_H_ADDR, constants.OSH_MSK, constants.OSH_POS, value)
def get_humidity_oversample(self):
"""Get humidity oversampling"""
return (self._get_regs(CONF_OS_H_ADDR, 1) & OSH_MSK) >> OSH_POS
"""Get humidity oversampling."""
return (self._get_regs(constants.CONF_OS_H_ADDR, 1) & constants.OSH_MSK) >> constants.OSH_POS
def set_pressure_oversample(self, value):
"""Set temperature oversampling
"""Set temperature oversampling.
A higher oversampling value means more stable sensor readings,
with less noise and jitter.
@ -96,18 +112,18 @@ class BME680(BME680Data):
causing a slower response time to fast transients.
:param value: Oversampling value, one of: OS_NONE, OS_1X, OS_2X, OS_4X, OS_8X, OS_16X
"""
self.tph_settings.os_pres = value
self._set_bits(CONF_T_P_MODE_ADDR, OSP_MSK, OSP_POS, value)
self._set_bits(constants.CONF_T_P_MODE_ADDR, constants.OSP_MSK, constants.OSP_POS, value)
def get_pressure_oversample(self):
"""Get pressure oversampling"""
return (self._get_regs(CONF_T_P_MODE_ADDR, 1) & OSP_MSK) >> OSP_POS
"""Get pressure oversampling."""
return (self._get_regs(constants.CONF_T_P_MODE_ADDR, 1) & constants.OSP_MSK) >> constants.OSP_POS
def set_temperature_oversample(self, value):
"""Set pressure oversampling
"""Set pressure oversampling.
A higher oversampling value means more stable sensor readings,
with less noise and jitter.
@ -115,18 +131,18 @@ class BME680(BME680Data):
causing a slower response time to fast transients.
:param value: Oversampling value, one of: OS_NONE, OS_1X, OS_2X, OS_4X, OS_8X, OS_16X
"""
self.tph_settings.os_temp = value
self._set_bits(CONF_T_P_MODE_ADDR, OST_MSK, OST_POS, value)
self._set_bits(constants.CONF_T_P_MODE_ADDR, constants.OST_MSK, constants.OST_POS, value)
def get_temperature_oversample(self):
"""Get temperature oversampling"""
return (self._get_regs(CONF_T_P_MODE_ADDR, 1) & OST_MSK) >> OST_POS
"""Get temperature oversampling."""
return (self._get_regs(constants.CONF_T_P_MODE_ADDR, 1) & constants.OST_MSK) >> constants.OST_POS
def set_filter(self, value):
"""Set IIR filter size
"""Set IIR filter size.
Optionally remove short term fluctuations from the temperature and pressure readings,
increasing their resolution but reducing their bandwidth.
@ -138,40 +154,42 @@ class BME680(BME680Data):
"""
self.tph_settings.filter = value
self._set_bits(CONF_ODR_FILT_ADDR, FILTER_MSK, FILTER_POS, value)
self._set_bits(constants.CONF_ODR_FILT_ADDR, constants.FILTER_MSK, constants.FILTER_POS, value)
def get_filter(self):
"""Get filter size"""
return (self._get_regs(CONF_ODR_FILT_ADDR, 1) & FILTER_MSK) >> FILTER_POS
"""Get filter size."""
return (self._get_regs(constants.CONF_ODR_FILT_ADDR, 1) & constants.FILTER_MSK) >> constants.FILTER_POS
def select_gas_heater_profile(self, value):
"""Set current gas sensor conversion profile: 0 to 9
"""Set current gas sensor conversion profile.
Select one of the 10 configured heating durations/set points.
:param value: Profile index from 0 to 9
"""
if value > NBCONV_MAX or value < NBCONV_MIN:
raise ValueError("Profile '{}' should be between {} and {}".format(value, NBCONV_MIN, NBCONV_MAX))
if value > constants.NBCONV_MAX or value < constants.NBCONV_MIN:
raise ValueError("Profile '{}' should be between {} and {}".format(value, constants.NBCONV_MIN, constants.NBCONV_MAX))
self.gas_settings.nb_conv = value
self._set_bits(CONF_ODR_RUN_GAS_NBC_ADDR, NBCONV_MSK, NBCONV_POS, value)
self._set_bits(constants.CONF_ODR_RUN_GAS_NBC_ADDR, constants.NBCONV_MSK, constants.NBCONV_POS, value)
def get_gas_heater_profile(self):
"""Get gas sensor conversion profile: 0 to 9"""
return self._get_regs(CONF_ODR_RUN_GAS_NBC_ADDR, 1) & NBCONV_MSK
"""Get gas sensor conversion profile: 0 to 9."""
return self._get_regs(constants.CONF_ODR_RUN_GAS_NBC_ADDR, 1) & constants.NBCONV_MSK
def set_gas_status(self, value):
"""Enable/disable gas sensor"""
"""Enable/disable gas sensor."""
self.gas_settings.run_gas = value
self._set_bits(CONF_ODR_RUN_GAS_NBC_ADDR, RUN_GAS_MSK, RUN_GAS_POS, value)
self._set_bits(constants.CONF_ODR_RUN_GAS_NBC_ADDR, constants.RUN_GAS_MSK, constants.RUN_GAS_POS, value)
def get_gas_status(self):
"""Get the current gas status"""
return (self._get_regs(CONF_ODR_RUN_GAS_NBC_ADDR, 1) & RUN_GAS_MSK) >> RUN_GAS_POS
"""Get the current gas status."""
return (self._get_regs(constants.CONF_ODR_RUN_GAS_NBC_ADDR, 1) & constants.RUN_GAS_MSK) >> constants.RUN_GAS_POS
def set_gas_heater_profile(self, temperature, duration, nb_profile=0):
"""Set temperature and duration of gas sensor heater
"""Set temperature and duration of gas sensor heater.
:param temperature: Target temperature in degrees celsius, between 200 and 400
:param durarion: Target duration in milliseconds, between 1 and 4032
:param nb_profile: Target profile, between 0 and 9
@ -181,23 +199,23 @@ class BME680(BME680Data):
self.set_gas_heater_duration(duration, nb_profile=nb_profile)
def set_gas_heater_temperature(self, value, nb_profile=0):
"""Set gas sensor heater temperature
"""Set gas sensor heater temperature.
:param value: Target temperature in degrees celsius, between 200 and 400
When setting an nb_profile other than 0,
make sure to select it with select_gas_heater_profile.
"""
if nb_profile > NBCONV_MAX or value < NBCONV_MIN:
raise ValueError("Profile '{}' should be between {} and {}".format(nb_profile, NBCONV_MIN, NBCONV_MAX))
if nb_profile > constants.NBCONV_MAX or value < constants.NBCONV_MIN:
raise ValueError('Profile "{}" should be between {} and {}'.format(nb_profile, constants.NBCONV_MIN, constants.NBCONV_MAX))
self.gas_settings.heatr_temp = value
temp = int(self._calc_heater_resistance(self.gas_settings.heatr_temp))
self._set_regs(RES_HEAT0_ADDR + nb_profile, temp)
self._set_regs(constants.RES_HEAT0_ADDR + nb_profile, temp)
def set_gas_heater_duration(self, value, nb_profile=0):
"""Set gas sensor heater duration
"""Set gas sensor heater duration.
Heating durations between 1 ms and 4032 ms can be configured.
Approximately 20-30 ms are necessary for the heater to reach the intended target temperature.
@ -206,70 +224,70 @@ class BME680(BME680Data):
When setting an nb_profile other than 0,
make sure to select it with select_gas_heater_profile.
"""
if nb_profile > NBCONV_MAX or value < NBCONV_MIN:
raise ValueError("Profile '{}' should be between {} and {}".format(nb_profile, NBCONV_MIN, NBCONV_MAX))
if nb_profile > constants.NBCONV_MAX or value < constants.NBCONV_MIN:
raise ValueError('Profile "{}" should be between {} and {}'.format(nb_profile, constants.NBCONV_MIN, constants.NBCONV_MAX))
self.gas_settings.heatr_dur = value
temp = self._calc_heater_duration(self.gas_settings.heatr_dur)
self._set_regs(GAS_WAIT0_ADDR + nb_profile, temp)
self._set_regs(constants.GAS_WAIT0_ADDR + nb_profile, temp)
def set_power_mode(self, value, blocking=True):
"""Set power mode"""
if value not in (SLEEP_MODE, FORCED_MODE):
print("Power mode should be one of SLEEP_MODE or FORCED_MODE")
"""Set power mode."""
if value not in (constants.SLEEP_MODE, constants.FORCED_MODE):
raise ValueError('Power mode should be one of SLEEP_MODE or FORCED_MODE')
self.power_mode = value
self._set_bits(CONF_T_P_MODE_ADDR, MODE_MSK, MODE_POS, value)
self._set_bits(constants.CONF_T_P_MODE_ADDR, constants.MODE_MSK, constants.MODE_POS, value)
while blocking and self.get_power_mode() != self.power_mode:
time.sleep(POLL_PERIOD_MS / 1000.0)
time.sleep(constants.POLL_PERIOD_MS / 1000.0)
def get_power_mode(self):
"""Get power mode"""
self.power_mode = self._get_regs(CONF_T_P_MODE_ADDR, 1)
"""Get power mode."""
self.power_mode = self._get_regs(constants.CONF_T_P_MODE_ADDR, 1)
return self.power_mode
def get_sensor_data(self):
"""Get sensor data.
Stores data in .data and returns True upon success.
"""
self.set_power_mode(FORCED_MODE)
self.set_power_mode(constants.FORCED_MODE)
for attempt in range(10):
status = self._get_regs(FIELD0_ADDR, 1)
status = self._get_regs(constants.FIELD0_ADDR, 1)
if (status & NEW_DATA_MSK) == 0:
time.sleep(POLL_PERIOD_MS / 1000.0)
if (status & constants.NEW_DATA_MSK) == 0:
time.sleep(constants.POLL_PERIOD_MS / 1000.0)
continue
regs = self._get_regs(FIELD0_ADDR, FIELD_LENGTH)
regs = self._get_regs(constants.FIELD0_ADDR, constants.FIELD_LENGTH)
self.data.status = regs[0] & NEW_DATA_MSK
self.data.status = regs[0] & constants.NEW_DATA_MSK
# Contains the nb_profile used to obtain the current measurement
self.data.gas_index = regs[0] & GAS_INDEX_MSK
self.data.gas_index = regs[0] & constants.GAS_INDEX_MSK
self.data.meas_index = regs[1]
adc_pres = (regs[2] << 12) | (regs[3] << 4) | (regs[4] >> 4)
adc_temp = (regs[5] << 12) | (regs[6] << 4) | (regs[7] >> 4)
adc_hum = (regs[8] << 8) | regs[9]
adc_gas_res = (regs[13] << 2) | (regs[14] >> 6)
gas_range = regs[14] & GAS_RANGE_MSK
gas_range = regs[14] & constants.GAS_RANGE_MSK
self.data.status |= regs[14] & GASM_VALID_MSK
self.data.status |= regs[14] & HEAT_STAB_MSK
self.data.status |= regs[14] & constants.GASM_VALID_MSK
self.data.status |= regs[14] & constants.HEAT_STAB_MSK
self.data.heat_stable = (self.data.status & HEAT_STAB_MSK) > 0
self.data.heat_stable = (self.data.status & constants.HEAT_STAB_MSK) > 0
temperature = self._calc_temperature(adc_temp)
self.data.temperature = temperature / 100.0
self.ambient_temperature = temperature # Saved for heater calc
self.ambient_temperature = temperature # Saved for heater calc
self.data.pressure = self._calc_pressure(adc_pres) / 100.0
self.data.pressure = self._calc_pressure(adc_pres)
self.data.humidity = self._calc_humidity(adc_hum) / 1000.0
self.data.gas_resistance = self._calc_gas_resistance(adc_gas_res, gas_range)
return True
@ -277,101 +295,106 @@ class BME680(BME680Data):
return False
def _set_bits(self, register, mask, position, value):
"""Mask out and set one or more bits in a register"""
"""Mask out and set one or more bits in a register."""
temp = self._get_regs(register, 1)
temp &= ~mask
temp |= value << position
self._set_regs(register, temp)
def _set_regs(self, register, value):
"""Set one or more registers"""
"""Set one or more registers."""
if isinstance(value, int):
self._i2c.write_byte_data(self.i2c_addr, register, value)
else:
self._i2c.write_i2c_block_data(self.i2c_addr, register, value)
def _get_regs(self, register, length):
"""Get one or more registers"""
"""Get one or more registers."""
if length == 1:
return self._i2c.read_byte_data(self.i2c_addr, register)
else:
return self._i2c.read_i2c_block_data(self.i2c_addr, register, length)
def _calc_temperature(self, temperature_adc):
var1 = (temperature_adc >> 3) - (self.calibration_data.par_t1 << 1)
var2 = (var1 * self.calibration_data.par_t2) >> 11
var3 = ((var1 >> 1) * (var1 >> 1)) >> 12
var3 = ((var3) * (self.calibration_data.par_t3 << 4)) >> 14
"""Convert the raw temperature to degrees C using calibration_data."""
var1 = (temperature_adc / 8.0) - (self.calibration_data.par_t1 * 2.0)
var2 = (var1 * self.calibration_data.par_t2) / 2048.0
var3 = ((var1 / 2.0) * (var1 / 2.0)) / 4096.0
var3 = ((var3) * (self.calibration_data.par_t3 * 16.0)) / 16384.0
# Save teperature data for pressure calculations
self.calibration_data.t_fine = (var2 + var3) + self.offset_temp_in_t_fine
calc_temp = (((self.calibration_data.t_fine * 5) + 128) >> 8)
calc_temp = (((self.calibration_data.t_fine * 5.0) + 128.0) / 256.0)
return calc_temp
def _calc_pressure(self, pressure_adc):
var1 = ((self.calibration_data.t_fine) >> 1) - 64000
var2 = ((((var1 >> 2) * (var1 >> 2)) >> 11) *
self.calibration_data.par_p6) >> 2
var2 = var2 + ((var1 * self.calibration_data.par_p5) << 1)
var2 = (var2 >> 2) + (self.calibration_data.par_p4 << 16)
var1 = (((((var1 >> 2) * (var1 >> 2)) >> 13 ) *
((self.calibration_data.par_p3 << 5)) >> 3) +
((self.calibration_data.par_p2 * var1) >> 1))
var1 = var1 >> 18
"""Convert the raw pressure using calibration data."""
var1 = (self.calibration_data.t_fine / 2.0) - 64000.0
var2 = ((((var1 / 4.0) * (var1 / 4.0)) / 2048.0) *
self.calibration_data.par_p6) / 4.0
var2 = var2 + ((var1 * self.calibration_data.par_p5) * 2.0)
var2 = (var2 / 4.0) + (self.calibration_data.par_p4 * 65536.0)
var1 = (((((var1 / 4.0) * (var1 / 4.0)) / 8192.0) *
((self.calibration_data.par_p3 * 32.0)) / 8.0) +
((self.calibration_data.par_p2 * var1) / 2.0))
var1 = var1 / 262144.0
var1 = ((32768 + var1) * self.calibration_data.par_p1) >> 15
calc_pressure = 1048576 - pressure_adc
calc_pressure = ((calc_pressure - (var2 >> 12)) * (3125))
var1 = ((32768.0 + var1) * self.calibration_data.par_p1) / 32768.0
calc_pressure = 1048576.0 - pressure_adc
calc_pressure = ((calc_pressure - (var2 / 4096.0)) * (3125.0))
if calc_pressure >= (1 << 31):
calc_pressure = ((calc_pressure // var1) << 1)
calc_pressure = ((calc_pressure / var1) * 2.0)
else:
calc_pressure = ((calc_pressure << 1) // var1)
calc_pressure = ((calc_pressure * 2.0) / var1)
var1 = (self.calibration_data.par_p9 * (((calc_pressure >> 3) *
(calc_pressure >> 3)) >> 13)) >> 12
var2 = ((calc_pressure >> 2) *
self.calibration_data.par_p8) >> 13
var3 = ((calc_pressure >> 8) * (calc_pressure >> 8) *
(calc_pressure >> 8) *
self.calibration_data.par_p10) >> 17
var1 = (self.calibration_data.par_p9 * (((calc_pressure / 8.0) *
(calc_pressure / 8.0)) / 8192.0)) / 4096.0
var2 = ((calc_pressure / 4.0) *
self.calibration_data.par_p8) / 8192.0
var3 = ((calc_pressure / 256.0) * (calc_pressure / 256.0) *
(calc_pressure / 256.0) *
self.calibration_data.par_p10) / 131072.0
calc_pressure = (calc_pressure) + ((var1 + var2 + var3 +
(self.calibration_data.par_p7 << 7)) >> 4)
(self.calibration_data.par_p7 * 128.0)) / 16.0)
return calc_pressure
def _calc_humidity(self, humidity_adc):
temp_scaled = ((self.calibration_data.t_fine * 5) + 128) >> 8
var1 = (humidity_adc - ((self.calibration_data.par_h1 * 16))) \
- (((temp_scaled * self.calibration_data.par_h3) // (100)) >> 1)
var2 = (self.calibration_data.par_h2
* (((temp_scaled * self.calibration_data.par_h4) // (100))
+ (((temp_scaled * ((temp_scaled * self.calibration_data.par_h5) // (100))) >> 6)
// (100)) + (1 * 16384))) >> 10
"""Convert the raw humidity using calibration data."""
temp_scaled = ((self.calibration_data.t_fine * 5.0) + 128.0) / 256.0
var1 = (humidity_adc - ((self.calibration_data.par_h1 * 16.0))) -\
(((temp_scaled * self.calibration_data.par_h3) / (100.0)) / 2.0)
var2 = (self.calibration_data.par_h2 *
(((temp_scaled * self.calibration_data.par_h4) / (100.0)) +
(((temp_scaled * ((temp_scaled * self.calibration_data.par_h5) / (100.0))) / 64.0) /
(100.0)) + (1.0 * 16384.0))) / 1024.0
var3 = var1 * var2
var4 = self.calibration_data.par_h6 << 7
var4 = ((var4) + ((temp_scaled * self.calibration_data.par_h7) // (100))) >> 4
var5 = ((var3 >> 14) * (var3 >> 14)) >> 10
var6 = (var4 * var5) >> 1
calc_hum = (((var3 + var6) >> 10) * (1000)) >> 12
var4 = self.calibration_data.par_h6 * 128.0
var4 = ((var4) + ((temp_scaled * self.calibration_data.par_h7) / (100.0))) / 16.0
var5 = ((var3 / 16384.0) * (var3 / 16384.0)) / 1024.0
var6 = (var4 * var5) / 2.0
calc_hum = (((var3 + var6) / 1024.0) * (1000.0)) / 4096.0
return min(max(calc_hum,0),100000)
return min(max(calc_hum, 0.0), 100000.0)
def _calc_gas_resistance(self, gas_res_adc, gas_range):
"""Convert the raw gas resistance using calibration data."""
var1 = ((1340 + (5 * self.calibration_data.range_sw_err)) * (lookupTable1[gas_range])) >> 16
var2 = (((gas_res_adc << 15) - (16777216)) + var1)
var3 = ((lookupTable2[gas_range] * var1) >> 9)
calc_gas_res = ((var3 + (var2 >> 1)) / var2)
if calc_gas_res < 0:
calc_gas_res = (1<<32) + calc_gas_res
calc_gas_res = (1 << 32) + calc_gas_res
return calc_gas_res
def _calc_heater_resistance(self, temperature):
temperature = min(max(temperature,200),400)
"""Convert raw heater resistance using calibration data."""
temperature = min(max(temperature, 200), 400)
var1 = ((self.ambient_temperature * self.calibration_data.par_gh3) / 1000) * 256
var2 = (self.calibration_data.par_gh1 + 784) * (((((self.calibration_data.par_gh2 + 154009) * temperature * 5) / 100) + 3276800) / 10)
@ -384,6 +407,7 @@ class BME680(BME680Data):
return heatr_res
def _calc_heater_duration(self, duration):
"""Calculate correct value for heater duration setting from milliseconds."""
if duration < 0xfc0:
factor = 0

View File

@ -1,5 +1,7 @@
"""BME680 constants, structures and utilities."""
# BME680 General config
POLL_PERIOD_MS = 10
POLL_PERIOD_MS = 50
# BME680 I2C addresses
I2C_ADDR_PRIMARY = 0x76
@ -118,7 +120,7 @@ TMP_BUFFER_LENGTH = 40
REG_BUFFER_LENGTH = 6
FIELD_DATA_LENGTH = 3
GAS_REG_BUF_LENGTH = 20
GAS_HEATER_PROF_LEN_MAX = 10
GAS_HEATER_PROF_LEN_MAX = 10
# Settings selector
OST_SEL = 1
@ -133,7 +135,7 @@ GAS_SENSOR_SEL = GAS_MEAS_SEL | RUN_GAS_SEL | NBCONV_SEL
# Number of conversion settings
NBCONV_MIN = 0
NBCONV_MAX = 9 # Was 10, but there are only 10 settings: 0 1 2 ... 8 9
NBCONV_MAX = 9 # Was 10, but there are only 10 settings: 0 1 2 ... 8 9
# Mask definitions
GAS_MEAS_MSK = 0x30
@ -214,30 +216,37 @@ REG_HCTRL_INDEX = 0
# Look up tables for the possible gas range values
lookupTable1 = [2147483647, 2147483647, 2147483647, 2147483647,
2147483647, 2126008810, 2147483647, 2130303777, 2147483647,
2147483647, 2143188679, 2136746228, 2147483647, 2126008810,
2147483647, 2147483647]
2147483647, 2126008810, 2147483647, 2130303777, 2147483647,
2147483647, 2143188679, 2136746228, 2147483647, 2126008810,
2147483647, 2147483647]
lookupTable2 = [4096000000, 2048000000, 1024000000, 512000000,
255744255, 127110228, 64000000, 32258064,
16016016, 8000000, 4000000, 2000000,
1000000, 500000, 250000, 125000]
255744255, 127110228, 64000000, 32258064,
16016016, 8000000, 4000000, 2000000,
1000000, 500000, 250000, 125000]
def bytes_to_word(msb, lsb, bits=16, signed=False):
"""Convert a most and least significant byte into a word."""
# TODO: Reimpliment with struct
word = (msb << 8) | lsb
if signed:
word = twos_comp(word, bits)
return word
def twos_comp(val, bits=16):
"""Convert two bytes into a two's compliment signed word."""
# TODO: Reimpliment with struct
if val & (1 << (bits - 1)) != 0:
val = val - (1 << bits)
return val
# Sensor field data structure
class FieldData:
def __init__(self):
"""Structure for storing BME680 sensor data."""
def __init__(self): # noqa D107
# Contains new_data, gasm_valid & heat_stab
self.status = None
self.heat_stable = False
@ -254,10 +263,11 @@ class FieldData:
# Gas resistance in Ohms
self.gas_resistance = None
# Structure to hold the Calibration data
class CalibrationData:
def __init__(self):
"""Structure for storing BME680 calibration data."""
def __init__(self): # noqa D107
self.par_h1 = None
self.par_h2 = None
self.par_h3 = None
@ -291,6 +301,7 @@ class CalibrationData:
self.range_sw_err = None
def set_from_array(self, calibration):
"""Set paramaters from an array of bytes."""
# Temperature related coefficients
self.par_t1 = bytes_to_word(calibration[T1_MSB_REG], calibration[T1_LSB_REG])
self.par_t2 = bytes_to_word(calibration[T2_MSB_REG], calibration[T2_LSB_REG], bits=16, signed=True)
@ -323,15 +334,20 @@ class CalibrationData:
self.par_gh3 = twos_comp(calibration[GH3_REG], bits=8)
def set_other(self, heat_range, heat_value, sw_error):
"""Set other values."""
self.res_heat_range = (heat_range & RHRANGE_MSK) // 16
self.res_heat_val = heat_value
self.range_sw_err = (sw_error & RSERROR_MSK) // 16
# BME680 sensor settings structure which comprises of ODR,
# over-sampling and filter settings.
class TPHSettings:
def __init__(self):
"""Structure for storing BME680 sensor settings.
Comprises of output data rate, over-sampling and filter settings.
"""
def __init__(self): # noqa D107
# Humidity oversampling
self.os_hum = None
# Temperature oversampling
@ -341,11 +357,11 @@ class TPHSettings:
# Filter coefficient
self.filter = None
# BME680 gas sensor which comprises of gas settings
## and status parameters
class GasSettings:
def __init__(self):
"""Structure for storing BME680 gas settings and status."""
def __init__(self): # noqa D107
# Variable to store nb conversion
self.nb_conv = None
# Variable to store heater control
@ -357,10 +373,11 @@ class GasSettings:
# Pointer to store duration profile
self.heatr_dur = None
# BME680 device structure
class BME680Data:
def __init__(self):
"""Structure to represent BME680 device."""
def __init__(self): # noqa D107
# Chip Id
self.chip_id = None
# Device Id

15
library/setup.cfg Normal file
View File

@ -0,0 +1,15 @@
[flake8]
exclude =
test.py
.tox
.eggs
.git
__pycache__
build
dist
tests
ignore =
E501
F403
F405
W504

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
"""
Copyright (c) 2016 Pimoroni
Copyright (c) 2016 Pimoroni.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
@ -38,17 +38,17 @@ classifiers = ['Development Status :: 5 - Production/Stable',
'Topic :: System :: Hardware']
setup(
name = 'bme680',
version = '1.0.5',
author = 'Philip Howard',
author_email = 'phil@pimoroni.com',
description = """Python library for driving the Pimoroni BME680 Breakout""",
long_description= open('README.rst').read() + "\n" + open('CHANGELOG.txt').read(),
license = 'MIT',
keywords = 'Raspberry Pi',
url = 'http://www.pimoroni.com',
classifiers = classifiers,
packages = ['bme680'],
py_modules = [],
install_requires= []
name='bme680',
version='1.0.5',
author='Philip Howard',
author_email='phil@pimoroni.com',
description="""Python library for driving the Pimoroni BME680 Breakout""",
long_description=open('README.rst').read() + '\n' + open('CHANGELOG.txt').read(),
license='MIT',
keywords='Raspberry Pi',
url='http://www.pimoroni.com',
classifiers=classifiers,
packages=['bme680'],
py_modules=[],
install_requires=['smbus'] # preferably: install `python3-smbus` instead of relying on this
)

101
library/tests/conftest.py Normal file
View File

@ -0,0 +1,101 @@
import sys
import mock
import pytest
import bme680
from bme680.constants import CalibrationData
class MockSMBus:
"""Mock a basic non-presence SMBus device to cause BME680 to fail.
Returns 0 in all cases, so that CHIP_ID will never match.
"""
def __init__(self, bus): # noqa D107
pass
def read_byte_data(self, addr, register):
"""Return 0 for all read attempts."""
return 0
class MockSMBusPresent:
"""Mock enough of the BME680 for the library to initialise and test."""
def __init__(self, bus):
"""Initialise with test data."""
self.regs = [0 for _ in range(256)]
self.regs[bme680.CHIP_ID_ADDR] = bme680.CHIP_ID
def read_byte_data(self, addr, register):
"""Read a single byte from fake registers."""
return self.regs[register]
def write_byte_data(self, addr, register, value):
"""Write a single byte to fake registers."""
self.regs[register] = value
def read_i2c_block_data(self, addr, register, length):
"""Read up to length bytes from register."""
return self.regs[register:register + length]
@pytest.fixture(scope='function', autouse=False)
def smbus_notpresent():
"""Mock smbus module."""
smbus = mock.MagicMock()
smbus.SMBus = MockSMBus
sys.modules['smbus'] = smbus
yield smbus
del sys.modules['smbus']
@pytest.fixture(scope='function', autouse=False)
def smbus():
"""Mock smbus module."""
smbus = mock.MagicMock()
smbus.SMBus = MockSMBusPresent
sys.modules['smbus'] = smbus
yield smbus
del sys.modules['smbus']
@pytest.fixture(scope='function', autouse=False)
def calibration():
"""Mock bme680 calibration."""
calibration = CalibrationData()
# Dump of calibration data borrowed from:
# https://github.com/pimoroni/bme680-python/issues/11
data = {
'par_gh1': -30,
'par_gh2': -24754,
'par_gh3': 18,
'par_h1': 676,
'par_h2': 1029,
'par_h3': 0,
'par_h4': 45,
'par_h5': 20,
'par_h6': 120,
'par_h7': -100,
'par_p1': 36673,
'par_p10': 30,
'par_p2': -10515,
'par_p3': 88,
'par_p4': 7310,
'par_p5': -129,
'par_p6': 30,
'par_p7': 46,
'par_p8': -3177,
'par_p9': -2379,
'par_t1': 26041,
'par_t2': 26469,
'par_t3': 3,
'range_sw_err': 0,
'res_heat_range': 1,
'res_heat_val': 48,
't_fine': 136667
}
for k, v in data.items():
setattr(calibration, k, v)
return calibration

8
library/tests/setup.cfg Normal file
View File

@ -0,0 +1,8 @@
[flake8]
exclude =
__pycache__
ignore =
D100 # Do not require docstrings
E501
F403
F405

View File

@ -0,0 +1,42 @@
import pytest
import bme680
def test_calc_temperature(smbus, calibration):
"""Validate temperature calculation against mock calibration data."""
sensor = bme680.BME680()
sensor.calibration_data = calibration
assert sensor._calc_temperature(501240) == pytest.approx(2669.81, 0.01)
assert sensor.calibration_data.t_fine == pytest.approx(136668.78, 0.01)
def test_calc_pressure(smbus, calibration):
"""Validate pressure calculation against mock calibration data."""
sensor = bme680.BME680()
sensor.calibration_data = calibration
sensor._calc_temperature(501240)
assert sensor._calc_pressure(353485) == pytest.approx(98716.92, 0.01)
def test_calc_humidity(smbus, calibration):
"""Validate humidity calculation against mock calibration data."""
sensor = bme680.BME680()
sensor.calibration_data = calibration
sensor._calc_temperature(501240)
assert sensor._calc_humidity(19019) == pytest.approx(42410.28, 0.01)
def test_calc_gas_resistance(smbus, calibration):
"""Validate gas calculation against mock calibration data."""
sensor = bme680.BME680()
sensor.calibration_data = calibration
assert int(sensor._calc_gas_resistance(0, 0)) == 12946860
def test_temp_offset(smbus, calibration):
"""Validate temperature calculation with offset against mock calibration data."""
sensor = bme680.BME680()
sensor.calibration_data = calibration
sensor.set_temp_offset(1.99)
assert sensor._calc_temperature(501240) == pytest.approx(2868.30, 0.01)
assert sensor.calibration_data.t_fine == pytest.approx(146831.78, 0.01)

View File

@ -0,0 +1,13 @@
import pytest
import bme680
def test_setup_not_present(smbus_notpresent):
"""Mock the adbsence of a BME680 and test initialisation."""
with pytest.raises(RuntimeError):
sensor = bme680.BME680() # noqa F841
def test_setup_mock_present(smbus):
"""Mock the presence of a BME680 and test initialisation."""
sensor = bme680.BME680() # noqa F841

25
library/tox.ini Normal file
View File

@ -0,0 +1,25 @@
[tox]
envlist = py{37,38,39},qa
skip_missing_interpreters = True
[testenv]
commands =
python setup.py install
coverage run -m pytest -v -r wsx
coverage report
deps =
mock
pytest>=3.1
pytest-cov
[testenv:qa]
commands =
flake8
flake8 tests/
flake8 ../examples/
rstcheck README.rst
deps =
flake8
flake8-docstrings
flake8-quotes
rstcheck

24
uninstall.sh Executable file
View File

@ -0,0 +1,24 @@
#!/bin/bash
PACKAGE="bme680"
printf "BME680 Python Library: Uninstaller\n\n"
if [ $(id -u) -ne 0 ]; then
printf "Script must be run as root. Try 'sudo ./uninstall.sh'\n"
exit 1
fi
cd library
printf "Unnstalling for Python 2..\n"
pip uninstall $PACKAGE
if [ -f "/usr/bin/pip3" ]; then
printf "Uninstalling for Python 3..\n"
pip3 uninstall $PACKAGE
fi
cd ..
printf "Done!\n"