Test suites and code QA fixes

This commit is contained in:
Phil Howard 2018-09-02 11:26:04 +01:00
parent c97791d720
commit 6374bc4241
15 changed files with 384 additions and 180 deletions

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/

View File

@ -1,10 +1,24 @@
language: python
sudo: false
cache: pip
git:
submodules: true
matrix:
include:
python: "2.7"
include:
- python: "2.7"
env: TOXENV=py27
- python: "3.5"
env: TOXENV=py35
- python: "2.7"
env: TOXENV=py27
install:
- pip install --ignore-installed --upgrade setuptools pip tox coveralls
script:
- pip install --ignore-installed --upgrade flake8
- flake8 --ignore F403,F405,E501
- cd library
- tox -vv
after_success: if [ "$TOXENV" == "py35" ]; then coveralls; fi

View File

@ -1,11 +1,10 @@
#!/usr/bin/env python
import bme680
import time
print("""Estimate 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.
@ -15,7 +14,7 @@ Press Ctrl+C to exit
sensor = bme680.BME680()
# 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 +28,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 +41,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 +55,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 +73,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

@ -1,23 +1,28 @@
#!/usr/bin/env python
import bme680
import time
print("""Display Temperature, Pressure, Humidity and Gas
Press Ctrl+C to exit
""")
sensor = bme680.BME680()
# 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 +32,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 +48,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,
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

@ -15,25 +15,29 @@ 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)
output = '{0:.2f} C, {1:.2f} hPa, {2:.3f} %RH'.format(
sensor.data.temperature,
sensor.data.pressure,
sensor.data.humidity)
print(output)
print("")
print('')
print("Initial readings")
print('Initial readings')
display_data()
print("SET offset 4 degrees celsius")
print('SET offset 4 degrees celsius')
display_data(4)
print("SET offset -1.87 degrees celsius")
print('SET offset -1.87 degrees celsius')
display_data(-1.87)
print("SET offset -100 degrees celsius")
print('SET offset -100 degrees celsius')
display_data(-100)
print("SET offset 0 degrees celsius")
print('SET offset 0 degrees celsius')
display_data(0)

View File

@ -1,6 +1,5 @@
#!/usr/bin/env python
import bme680
import time
print("""Display Temperature, Pressure and Humidity
@ -22,15 +21,17 @@ 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,
sensor.data.humidity)
print(output)
except KeyboardInterrupt:
pass

4
library/.coveragerc Normal file
View File

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

View File

@ -1,11 +1,23 @@
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"
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 +25,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 +41,46 @@ 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_humidity_oversample(constants.OS_2X)
self.set_pressure_oversample(constants.OS_4X)
self.set_temperature_oversample(constants.OS_8X)
self.set_filter(constants.FILTER_SIZE_3)
self.set_gas_status(constants.ENABLE_GAS_MEAS)
self.set_temp_offset(0)
self.get_sensor_data()
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 +88,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 +100,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 +116,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 +135,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 +158,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 +203,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,68 +228,68 @@ 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.humidity = self._calc_humidity(adc_hum) / 1000.0
@ -277,27 +299,28 @@ 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):
"""Convert the raw temperature to degrees C using calibration_data."""
var1 = (temperature_adc >> 3) - (self.calibration_data.par_t1 << 1)
var2 = (var1 * self.calibration_data.par_t2) >> 11
var3 = ((var1 >> 1) * (var1 >> 1)) >> 12
@ -310,12 +333,13 @@ class BME680(BME680Data):
return calc_temp
def _calc_pressure(self, pressure_adc):
"""Convert the raw pressure using calibration data."""
var1 = ((self.calibration_data.t_fine) >> 1) - 64000
var2 = ((((var1 >> 2) * (var1 >> 2)) >> 11) *
self.calibration_data.par_p6) >> 2
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 ) *
var1 = (((((var1 >> 2) * (var1 >> 2)) >> 13) *
((self.calibration_data.par_p3 << 5)) >> 3) +
((self.calibration_data.par_p2 * var1) >> 1))
var1 = var1 >> 18
@ -330,26 +354,27 @@ class BME680(BME680Data):
calc_pressure = ((calc_pressure << 1) // var1)
var1 = (self.calibration_data.par_p9 * (((calc_pressure >> 3) *
(calc_pressure >> 3)) >> 13)) >> 12
(calc_pressure >> 3)) >> 13)) >> 12
var2 = ((calc_pressure >> 2) *
self.calibration_data.par_p8) >> 13
self.calibration_data.par_p8) >> 13
var3 = ((calc_pressure >> 8) * (calc_pressure >> 8) *
(calc_pressure >> 8) *
self.calibration_data.par_p10) >> 17
(calc_pressure >> 8) *
self.calibration_data.par_p10) >> 17
calc_pressure = (calc_pressure) + ((var1 + var2 + var3 +
(self.calibration_data.par_p7 << 7)) >> 4)
(self.calibration_data.par_p7 << 7)) >> 4)
return calc_pressure
def _calc_humidity(self, humidity_adc):
"""Convert the raw humidity using calibration data."""
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
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
var3 = var1 * var2
var4 = self.calibration_data.par_h6 << 7
var4 = ((var4) + ((temp_scaled * self.calibration_data.par_h7) // (100))) >> 4
@ -357,21 +382,23 @@ class BME680(BME680Data):
var6 = (var4 * var5) >> 1
calc_hum = (((var3 + var6) >> 10) * (1000)) >> 12
return min(max(calc_hum,0),100000)
return min(max(calc_hum, 0), 100000)
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 +411,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,3 +1,5 @@
"""BME680 constants, structures and utilities."""
# BME680 General config
POLL_PERIOD_MS = 10
@ -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

14
library/setup.cfg Normal file
View File

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

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=[]
)

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,57 @@
import sys
import mock
import pytest
import bme680
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]
def test_setup_not_present():
"""Mock the adbsence of a BME680 and test initialisation."""
sys.modules['smbus'] = mock.MagicMock()
sys.modules['smbus'].SMBus = MockSMBus
with pytest.raises(RuntimeError):
sensor = bme680.BME680() # noqa F841
def test_setup_mock_present():
"""Mock the presence of a BME680 and test initialisation."""
sys.modules['smbus'] = mock.MagicMock()
sys.modules['smbus'].SMBus = MockSMBusPresent
sensor = bme680.BME680() # noqa F841

25
library/tox.ini Normal file
View File

@ -0,0 +1,25 @@
[tox]
envlist = py{27,35},qa
skip_missing_interpreters = True
[testenv]
commands =
python setup.py install
coverage run -m py.test -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