Compare commits

...

63 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
Phil Howard 50902ac08e Prep for v1.0.5 2018-06-01 17:15:10 +01:00
Phil Howard 05f80690e0 Merge branch 'ayeks-offset' 2018-06-01 16:10:47 +01:00
Phil Howard 0f0be7f2c2 Tidied up temp-offset.py example 2018-06-01 16:10:34 +01:00
Phil Howard 4ae50e2f2c Fixed bug in set_temp_offset 2018-06-01 16:10:08 +01:00
Phil Howard 7e08c2d749 Fixed packaging errors 2018-06-01 16:04:37 +01:00
Phil Howard bc2f729013 Made temp-offset.py executable 2018-06-01 16:04:27 +01:00
Lars Lühr fed191e40e use math.copysign for temp offset calculation 2018-05-31 08:21:14 +00:00
Lars Lühr 8cb0dead6b added temp offset float example 2018-05-28 21:41:16 +00:00
Lars Lühr ae004aa62b accept float values in set_temp_offset 2018-05-28 21:38:19 +00:00
Lars Lühr 1c9ba41306 added set_temp_offset function and example 2018-05-28 21:18:20 +00:00
Philip Howard 40b4b63b93
Create LICENSE 2018-02-20 15:21:25 +00:00
Phil Howard da6ae97f1f Prep for v1.0.4 2017-12-04 10:37:52 +00:00
Philip Howard 097287e842
Merge pull request #8 from lowflyerUK/master
Updated mask operation in constants.py to correct gas resistance
2017-12-04 10:28:14 +00:00
lowflyerUK 02407e1d01 Updated mask operation in constants.py to correct gas resistance 2017-12-03 21:49:32 +00:00
Phil Howard 888d8b312a Convert negative gas resistance readings to unsigned int32 2017-12-01 14:08:40 +00:00
30 changed files with 915 additions and 237 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/

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Pimoroni Ltd
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 the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

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

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,3 +1,14 @@
1.0.5
-----
* New: set_temp_offset to calibrate temperature offset in degrees C
1.0.4
-----
* Fix to range_sw_err for extremely high gas readings
* Convert to unsigned int to fix negative gas readings
1.0.3
-----

21
library/LICENSE.txt Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Pimoroni Ltd
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 the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,5 +1,5 @@
include CHANGELOG.txt
include LICENSE.txt
include README.txt
include README.rst
include setup.py
include bme680.py
recursive-include bme680 *.py

80
library/README.rst Normal file
View File

@ -0,0 +1,80 @@
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,
pressure, humidity, and indoor air quality.
Installing
----------
Full install (recommended):
~~~~~~~~~~~~~~~~~~~~~~~~~~~
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:
.. 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:
.. code:: bash
curl https://get.pimoroni.com/bme680 | bash
Manual install:
~~~~~~~~~~~~~~~
Library install for Python 3:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code:: bash
sudo pip3 install bme680
Library install for Python 2:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code:: bash
sudo pip2 install bme680
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:
.. code:: bash
sudo python3 setup.py install
(or ``sudo python setup.py install`` whichever your primary Python
environment may be)
In all cases you will have to enable the i2c bus.
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

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

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.3'
__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,43 +42,50 @@ 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.get_sensor_data()
self.set_temp_offset(0)
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.
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
else:
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.
@ -69,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.
@ -85,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.
@ -104,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.
@ -127,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
@ -170,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.
@ -195,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
@ -266,98 +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)
calc_temp = (((self.calibration_data.t_fine * 5) + 128) >> 8)
self.calibration_data.t_fine = (var2 + var3) + self.offset_temp_in_t_fine
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
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)
@ -370,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
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.3',
author = 'Philip Howard',
author_email = 'phil@pimoroni.com',
description = """Python library for driving the Pimoroni BME680 Breakout""",
long_description= open('README.txt').read() + 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

View File

@ -1,3 +1,16 @@
bme680 (1.0.5) stable; urgency=low
* New: set_temp_offset to calibrate temperature offset in degrees C
-- Phil Howard <phil@pimoroni.com> Fri, 01 Jun 2018 00:00:00 +0000
bme680 (1.0.4) stable; urgency=low
* Fix to range_sw_err for extremely high gas readings
* Convert to unsigned int to fix negative gas readings
-- Phil Howard <phil@pimoroni.com> Mon, 04 Dec 2017 00:00:00 +0000
bme680 (1.0.3) stable; urgency=low
* Merged temperature compensation fix from Bosch's BME680_driver 3.5.3

View File

@ -1,3 +1,16 @@
bme680 (1.0.5) stable; urgency=low
* New: set_temp_offset to calibrate temperature offset in degrees C
-- Phil Howard <phil@pimoroni.com> Fri, 01 Jun 2018 00:00:00 +0000
bme680 (1.0.4) stable; urgency=low
* Fix to range_sw_err for extremely high gas readings
* Convert to unsigned int to fix negative gas readings
-- Phil Howard <phil@pimoroni.com> Mon, 04 Dec 2017 00:00:00 +0000
bme680 (1.0.3) stable; urgency=low
* Merged temperature compensation fix from Bosch's BME680_driver 3.5.3

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"