diff --git a/.gitignore b/.gitignore index 810752e..5efe1d1 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ library/debian/ pip-log.txt pip-delete-this-directory.txt .DS_Store +.vscode/ +.coverage +.tox/ +.pytest_cache/ diff --git a/.travis.yml b/.travis.yml index 22d7e8c..42479fe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/README.md b/README.md index c07e0d1..696a0b8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # BME680 -[![Build Status](https://travis-ci.org/pimoroni/bme680-python.svg?branch=master)](https://travis-ci.org/pimoroni/bme680-python) +[![Build Status](https://travis-ci.com/pimoroni/bme680-python.svg?branch=master)](https://travis-ci.com/pimoroni/bme680-python) +[![Coverage Status](https://coveralls.io/repos/github/pimoroni/bme680-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/bme680-python?branch=master) +[![PyPi Package](https://img.shields.io/pypi/v/bme680.svg)](https://pypi.python.org/pypi/bme680) +[![Python Versions](https://img.shields.io/pypi/pyversions/bme680.svg)](https://pypi.python.org/pypi/bme680) https://shop.pimoroni.com/products/bme680 diff --git a/examples/indoor-air-quality.py b/examples/indoor-air-quality.py index bf68fa9..0a60643 100755 --- a/examples/indoor-air-quality.py +++ b/examples/indoor-air-quality.py @@ -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. @@ -13,9 +12,12 @@ 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 +31,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 +44,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 +58,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 +76,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: diff --git a/examples/read-all.py b/examples/read-all.py index 24baef5..1c23ed0 100755 --- a/examples/read-all.py +++ b/examples/read-all.py @@ -1,23 +1,31 @@ #!/usr/bin/env python - import bme680 import time -sensor = bme680.BME680() +print("""Display 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 +35,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 +51,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) diff --git a/examples/setup.cfg b/examples/setup.cfg new file mode 100644 index 0000000..9e0f472 --- /dev/null +++ b/examples/setup.cfg @@ -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 diff --git a/examples/temp-offset.py b/examples/temp-offset.py index 3f468b4..2ccd73d 100755 --- a/examples/temp-offset.py +++ b/examples/temp-offset.py @@ -4,7 +4,10 @@ import bme680 print("""Display Temperature, Pressure and Humidity with different offsets. """) -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 @@ -15,25 +18,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) - diff --git a/examples/temp-press-hum.py b/examples/temp-press-hum.py index b21d0f4..8a2702d 100755 --- a/examples/temp-press-hum.py +++ b/examples/temp-press-hum.py @@ -1,6 +1,5 @@ #!/usr/bin/env python import bme680 -import time print("""Display Temperature, Pressure and Humidity @@ -11,7 +10,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 +24,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 - diff --git a/library/.coveragerc b/library/.coveragerc new file mode 100644 index 0000000..65527f4 --- /dev/null +++ b/library/.coveragerc @@ -0,0 +1,4 @@ +[run] +source = bme680 +omit = + .tox/* diff --git a/library/README.rst b/library/README.rst index a325ea8..08ce725 100644 --- a/library/README.rst +++ b/library/README.rst @@ -1,6 +1,8 @@ BME680 ====== +|Build Status| |Coverage Status| |PyPi Package| |Python Versions| + https://shop.pimoroni.com/products/bme680 The state-of-the-art BME680 breakout lets you measure temperature, @@ -20,6 +22,8 @@ Terminal on your Raspberry Pi desktop, as illustrated below: .. figure:: http://get.pimoroni.com/resources/github-repo-terminal.png :alt: Finding the terminal + Finding the terminal + In the new terminal window type the command exactly as it appears below (check for typos) and follow the on-screen instructions: @@ -66,3 +70,11 @@ Documentation & Support - Guides and tutorials - https://learn.pimoroni.com/bme680 - Get help - http://forums.pimoroni.com/c/support +.. |Build Status| image:: https://travis-ci.com/pimoroni/bme680-python.svg?branch=master + :target: https://travis-ci.com/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 diff --git a/library/bme680/__init__.py b/library/bme680/__init__.py index ecc4f26..62aa7ea 100644 --- a/library/bme680/__init__.py +++ b/library/bme680/__init__.py @@ -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 diff --git a/library/bme680/constants.py b/library/bme680/constants.py index 5cc0840..5033acc 100644 --- a/library/bme680/constants.py +++ b/library/bme680/constants.py @@ -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 diff --git a/library/setup.cfg b/library/setup.cfg new file mode 100644 index 0000000..81dc0bf --- /dev/null +++ b/library/setup.cfg @@ -0,0 +1,14 @@ +[flake8] +exclude = + test.py + .tox + .eggs + .git + __pycache__ + build + dist + tests +ignore = + E501 + F403 + F405 diff --git a/library/setup.py b/library/setup.py index b47bfce..f40026b 100755 --- a/library/setup.py +++ b/library/setup.py @@ -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=[] ) diff --git a/library/tests/setup.cfg b/library/tests/setup.cfg new file mode 100644 index 0000000..23a30f5 --- /dev/null +++ b/library/tests/setup.cfg @@ -0,0 +1,8 @@ +[flake8] +exclude = + __pycache__ +ignore = + D100 # Do not require docstrings + E501 + F403 + F405 \ No newline at end of file diff --git a/library/tests/test_setup.py b/library/tests/test_setup.py new file mode 100644 index 0000000..95b7391 --- /dev/null +++ b/library/tests/test_setup.py @@ -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 diff --git a/library/tox.ini b/library/tox.ini new file mode 100644 index 0000000..89947cb --- /dev/null +++ b/library/tox.ini @@ -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