├── .gitignore ├── BME680-wipy.JPG ├── LICENSE ├── README.md ├── adds-leak-sensor ├── BME-680+leak.JPG ├── README.md └── main.py ├── lib ├── bme680.py ├── constants.py ├── i2c.py └── main.py ├── main.py └── simple-readout └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Object file 2 | *.o 3 | 4 | # Ada Library Information 5 | *.ali 6 | -------------------------------------------------------------------------------- /BME680-wipy.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robmarkcole/bme680-mqtt-micropython/988be02c5e9c76967257e1e9e14dafff700d42f5/BME680-wipy.JPG -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Robin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bme680-mqtt-micropython 2 | Publish data from the bme680 sensor over MQTT using micropython. Makes use of: 3 | * https://github.com/gkluoe/bme680/blob/master/library/bme680/i2c.py 4 | * https://github.com/pimoroni/bme680 5 | Am using in my leak sensor rig: 6 | * https://www.hackster.io/robin-cole/micropython-leak-detector-with-adafruit-and-home-assistant-a2fa9e 7 | 8 | [On Wipy 3](https://docs.pycom.io/datasheets/development/wipy3#pinout), P9 = SDA and P10 = SCL. 9 | Read sensor bytes using `machine` with: 10 | ```python 11 | from machine import I2C 12 | i2c = I2C(0) # using defauls P9 and P10 13 | i2c.scan() # returns [119] which is hex 0x77 14 | i2c.readfrom(0x77, 5) # read 5 bytes 15 | ``` 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /adds-leak-sensor/BME-680+leak.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robmarkcole/bme680-mqtt-micropython/988be02c5e9c76967257e1e9e14dafff700d42f5/adds-leak-sensor/BME-680+leak.JPG -------------------------------------------------------------------------------- /adds-leak-sensor/README.md: -------------------------------------------------------------------------------- 1 | Adds an analogue leak sensor, a dirt cheap (£1.26) sensor. 2 | 3 | Sensor: https://www.amazon.co.uk/gp/product/B00K67Z76O/ref=oh_aui_detailpage_o06_s00?ie=UTF8&psc=1 4 | 5 | Add to [home-assistant](https://home-assistant.io/) using an mqtt [binary sensor](https://home-assistant.io/components/binary_sensor.mqtt/): 6 | ``` 7 | - platform: mqtt 8 | name: "Wipy water monitor" 9 | state_topic: "bme680-water" 10 | ``` 11 | 12 | 13 | -------------------------------------------------------------------------------- /adds-leak-sensor/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import bme680 3 | from i2c import I2CAdapter 4 | from mqtt import MQTTClient 5 | import time 6 | import machine 7 | 8 | 9 | # timeout for mqtt 10 | def settimeout(duration): 11 | pass 12 | 13 | 14 | # Function for taking average of 100 analog readings 15 | def smooth_reading(): 16 | avg = 0 17 | _AVG_NUM = 100 18 | for _ in range(_AVG_NUM): 19 | avg += apin() 20 | avg /= _AVG_NUM 21 | return(avg) 22 | 23 | 24 | # MQTT setup 25 | client = MQTTClient("wipy", "192.168.0.30", port=1883) 26 | client.settimeout = settimeout 27 | client.connect() 28 | mqtt_topic = "bme680" 29 | 30 | # bme680 31 | i2c_dev = I2CAdapter() 32 | sensor = bme680.BME680(i2c_device=i2c_dev) 33 | 34 | # These oversampling settings can be tweaked to 35 | # change the balance between accuracy and noise in 36 | # the data. 37 | sensor.set_humidity_oversample(bme680.OS_2X) 38 | sensor.set_pressure_oversample(bme680.OS_4X) 39 | sensor.set_temperature_oversample(bme680.OS_8X) 40 | sensor.set_filter(bme680.FILTER_SIZE_3) 41 | 42 | # Moisture sensor 43 | adc = machine.ADC() 44 | apin = adc.channel(pin='P16', attn=3) 45 | 46 | print("Polling:") 47 | try: 48 | while True: 49 | if sensor.get_sensor_data(): 50 | 51 | output = "{} C, {} hPa, {} RH, {} RES,".format( 52 | sensor.data.temperature, 53 | sensor.data.pressure, 54 | sensor.data.humidity, 55 | sensor.data.gas_resistance) 56 | 57 | print(output) 58 | client.publish(mqtt_topic, output) 59 | # Publish on individual topics for consistency with rpi repo. 60 | client.publish('bme680-humidity', str(sensor.data.humidity)) 61 | client.publish('bme680-temperature', str(sensor.data.temperature)) 62 | client.publish('bme680-pressure', str(sensor.data.pressure)) 63 | client.publish('bme680-air_qual', str(sensor.data.gas_resistance)) 64 | 65 | # Read the analogue water sensor 66 | _THRESHOLD = 3000 67 | analog_val = smooth_reading() 68 | print(analog_val) 69 | if analog_val < _THRESHOLD: 70 | print("Water_detected!") 71 | client.publish('bme680-water', "ON") 72 | else: 73 | client.publish('bme680-water', "OFF") 74 | time.sleep(2) 75 | 76 | except KeyboardInterrupt: 77 | pass 78 | -------------------------------------------------------------------------------- /lib/bme680.py: -------------------------------------------------------------------------------- 1 | from constants import * 2 | import math 3 | import time 4 | 5 | __version__ = '1.0.2' 6 | 7 | class BME680(BME680Data): 8 | """BOSCH BME680 9 | 10 | Gas, pressure, temperature and humidity sensor. 11 | 12 | :param i2c_addr: One of I2C_ADDR_PRIMARY (0x76) or I2C_ADDR_SECONDARY (0x77) 13 | :param i2c_device: Optional smbus or compatible instance for facilitating i2c communications. 14 | 15 | """ 16 | def __init__(self, i2c_addr=I2C_ADDR_PRIMARY, i2c_device=None): 17 | BME680Data.__init__(self) 18 | 19 | self.i2c_addr = i2c_addr 20 | self._i2c = i2c_device 21 | if self._i2c is None: 22 | import smbus 23 | self._i2c = smbus.SMBus(1) 24 | 25 | self.chip_id = self._get_regs(CHIP_ID_ADDR, 1) 26 | if self.chip_id != CHIP_ID: 27 | raise RuntimeError("BME680 Not Found. Invalid CHIP ID: 0x{0:02x}".format(self.chip_id)) 28 | 29 | self.soft_reset() 30 | self.set_power_mode(SLEEP_MODE) 31 | 32 | self._get_calibration_data() 33 | 34 | self.set_humidity_oversample(OS_2X) 35 | self.set_pressure_oversample(OS_4X) 36 | self.set_temperature_oversample(OS_8X) 37 | self.set_filter(FILTER_SIZE_3) 38 | self.set_gas_status(ENABLE_GAS_MEAS) 39 | 40 | self.get_sensor_data() 41 | 42 | def _get_calibration_data(self): 43 | """Retrieves the sensor calibration data and stores it in .calibration_data""" 44 | calibration = self._get_regs(COEFF_ADDR1, COEFF_ADDR1_LEN) 45 | calibration += self._get_regs(COEFF_ADDR2, COEFF_ADDR2_LEN) 46 | 47 | heat_range = self._get_regs(ADDR_RES_HEAT_RANGE_ADDR, 1) 48 | heat_value = twos_comp(self._get_regs(ADDR_RES_HEAT_VAL_ADDR, 1), bits=8) 49 | sw_error = twos_comp(self._get_regs(ADDR_RANGE_SW_ERR_ADDR, 1), bits=8) 50 | 51 | self.calibration_data.set_from_array(calibration) 52 | self.calibration_data.set_other(heat_range, heat_value, sw_error) 53 | 54 | def soft_reset(self): 55 | """Initiate a soft reset""" 56 | self._set_regs(SOFT_RESET_ADDR, SOFT_RESET_CMD) 57 | time.sleep(RESET_PERIOD / 1000.0) 58 | 59 | def set_humidity_oversample(self, value): 60 | """Set humidity oversampling 61 | 62 | A higher oversampling value means more stable sensor readings, 63 | with less noise and jitter. 64 | 65 | However each step of oversampling adds about 2ms to the latency, 66 | causing a slower response time to fast transients. 67 | 68 | :param value: Oversampling value, one of: OS_NONE, OS_1X, OS_2X, OS_4X, OS_8X, OS_16X 69 | 70 | """ 71 | self.tph_settings.os_hum = value 72 | self._set_bits(CONF_OS_H_ADDR, OSH_MSK, OSH_POS, value) 73 | 74 | def get_humidity_oversample(self): 75 | """Get humidity oversampling""" 76 | return (self._get_regs(CONF_OS_H_ADDR, 1) & OSH_MSK) >> OSH_POS 77 | 78 | def set_pressure_oversample(self, value): 79 | """Set temperature oversampling 80 | 81 | A higher oversampling value means more stable sensor readings, 82 | with less noise and jitter. 83 | 84 | However each step of oversampling adds about 2ms to the latency, 85 | causing a slower response time to fast transients. 86 | 87 | :param value: Oversampling value, one of: OS_NONE, OS_1X, OS_2X, OS_4X, OS_8X, OS_16X 88 | 89 | """ 90 | self.tph_settings.os_pres = value 91 | self._set_bits(CONF_T_P_MODE_ADDR, OSP_MSK, OSP_POS, value) 92 | 93 | def get_pressure_oversample(self): 94 | """Get pressure oversampling""" 95 | return (self._get_regs(CONF_T_P_MODE_ADDR, 1) & OSP_MSK) >> OSP_POS 96 | 97 | def set_temperature_oversample(self, value): 98 | """Set pressure oversampling 99 | 100 | A higher oversampling value means more stable sensor readings, 101 | with less noise and jitter. 102 | 103 | However each step of oversampling adds about 2ms to the latency, 104 | causing a slower response time to fast transients. 105 | 106 | :param value: Oversampling value, one of: OS_NONE, OS_1X, OS_2X, OS_4X, OS_8X, OS_16X 107 | 108 | """ 109 | self.tph_settings.os_temp = value 110 | self._set_bits(CONF_T_P_MODE_ADDR, OST_MSK, OST_POS, value) 111 | 112 | def get_temperature_oversample(self): 113 | """Get temperature oversampling""" 114 | return (self._get_regs(CONF_T_P_MODE_ADDR, 1) & OST_MSK) >> OST_POS 115 | 116 | def set_filter(self, value): 117 | """Set IIR filter size 118 | 119 | Optionally remove short term fluctuations from the temperature and pressure readings, 120 | increasing their resolution but reducing their bandwidth. 121 | 122 | Enabling the IIR filter does not slow down the time a reading takes, but will slow 123 | down the BME680s response to changes in temperature and pressure. 124 | 125 | When the IIR filter is enabled, the temperature and pressure resolution is effectively 20bit. 126 | When it is disabled, it is 16bit + oversampling-1 bits. 127 | 128 | """ 129 | self.tph_settings.filter = value 130 | self._set_bits(CONF_ODR_FILT_ADDR, FILTER_MSK, FILTER_POS, value) 131 | 132 | def get_filter(self): 133 | """Get filter size""" 134 | return (self._get_regs(CONF_ODR_FILT_ADDR, 1) & FILTER_MSK) >> FILTER_POS 135 | 136 | def select_gas_heater_profile(self, value): 137 | """Set current gas sensor conversion profile: 0 to 9 138 | 139 | Select one of the 10 configured heating durations/set points. 140 | 141 | """ 142 | if value > NBCONV_MAX or value < NBCONV_MIN: 143 | raise ValueError("Profile '{}' should be between {} and {}".format(value, NBCONV_MIN, NBCONV_MAX)) 144 | 145 | self.gas_settings.nb_conv = value 146 | self._set_bits(CONF_ODR_RUN_GAS_NBC_ADDR, NBCONV_MSK, NBCONV_POS, value) 147 | 148 | def get_gas_heater_profile(self): 149 | """Get gas sensor conversion profile: 0 to 9""" 150 | return self._get_regs(CONF_ODR_RUN_GAS_NBC_ADDR, 1) & NBCONV_MSK 151 | 152 | def set_gas_status(self, value): 153 | """Enable/disable gas sensor""" 154 | self.gas_settings.run_gas = value 155 | self._set_bits(CONF_ODR_RUN_GAS_NBC_ADDR, RUN_GAS_MSK, RUN_GAS_POS, value) 156 | 157 | def get_gas_status(self): 158 | """Get the current gas status""" 159 | return (self._get_regs(CONF_ODR_RUN_GAS_NBC_ADDR, 1) & RUN_GAS_MSK) >> RUN_GAS_POS 160 | 161 | def set_gas_heater_profile(self, temperature, duration, nb_profile=0): 162 | """Set temperature and duration of gas sensor heater 163 | 164 | :param temperature: Target temperature in degrees celsius, between 200 and 400 165 | :param durarion: Target duration in milliseconds, between 1 and 4032 166 | :param nb_profile: Target profile, between 0 and 9 167 | 168 | """ 169 | self.set_gas_heater_temperature(temperature, nb_profile=nb_profile) 170 | self.set_gas_heater_duration(duration, nb_profile=nb_profile) 171 | 172 | def set_gas_heater_temperature(self, value, nb_profile=0): 173 | """Set gas sensor heater temperature 174 | 175 | :param value: Target temperature in degrees celsius, between 200 and 400 176 | 177 | When setting an nb_profile other than 0, 178 | make sure to select it with select_gas_heater_profile. 179 | 180 | """ 181 | if nb_profile > NBCONV_MAX or value < NBCONV_MIN: 182 | raise ValueError("Profile '{}' should be between {} and {}".format(nb_profile, NBCONV_MIN, NBCONV_MAX)) 183 | 184 | self.gas_settings.heatr_temp = value 185 | temp = int(self._calc_heater_resistance(self.gas_settings.heatr_temp)) 186 | self._set_regs(RES_HEAT0_ADDR + nb_profile, temp) 187 | 188 | def set_gas_heater_duration(self, value, nb_profile=0): 189 | """Set gas sensor heater duration 190 | 191 | Heating durations between 1 ms and 4032 ms can be configured. 192 | Approximately 20-30 ms are necessary for the heater to reach the intended target temperature. 193 | 194 | :param value: Heating duration in milliseconds. 195 | 196 | When setting an nb_profile other than 0, 197 | make sure to select it with select_gas_heater_profile. 198 | 199 | """ 200 | if nb_profile > NBCONV_MAX or value < NBCONV_MIN: 201 | raise ValueError("Profile '{}' should be between {} and {}".format(nb_profile, NBCONV_MIN, NBCONV_MAX)) 202 | 203 | self.gas_settings.heatr_dur = value 204 | temp = self._calc_heater_duration(self.gas_settings.heatr_dur) 205 | self._set_regs(GAS_WAIT0_ADDR + nb_profile, temp) 206 | 207 | def set_power_mode(self, value, blocking=True): 208 | """Set power mode""" 209 | if value not in (SLEEP_MODE, FORCED_MODE): 210 | print("Power mode should be one of SLEEP_MODE or FORCED_MODE") 211 | 212 | self.power_mode = value 213 | 214 | self._set_bits(CONF_T_P_MODE_ADDR, MODE_MSK, MODE_POS, value) 215 | 216 | while blocking and self.get_power_mode() != self.power_mode: 217 | time.sleep(POLL_PERIOD_MS / 1000.0) 218 | 219 | def get_power_mode(self): 220 | """Get power mode""" 221 | self.power_mode = self._get_regs(CONF_T_P_MODE_ADDR, 1) 222 | return self.power_mode 223 | 224 | def get_sensor_data(self): 225 | """Get sensor data. 226 | 227 | Stores data in .data and returns True upon success. 228 | 229 | """ 230 | self.set_power_mode(FORCED_MODE) 231 | 232 | for attempt in range(10): 233 | status = self._get_regs(FIELD0_ADDR, 1) 234 | 235 | if (status & NEW_DATA_MSK) == 0: 236 | time.sleep(POLL_PERIOD_MS / 1000.0) 237 | continue 238 | 239 | regs = self._get_regs(FIELD0_ADDR, FIELD_LENGTH) 240 | 241 | self.data.status = regs[0] & NEW_DATA_MSK 242 | # Contains the nb_profile used to obtain the current measurement 243 | self.data.gas_index = regs[0] & GAS_INDEX_MSK 244 | self.data.meas_index = regs[1] 245 | 246 | adc_pres = (regs[2] << 12) | (regs[3] << 4) | (regs[4] >> 4) 247 | adc_temp = (regs[5] << 12) | (regs[6] << 4) | (regs[7] >> 4) 248 | adc_hum = (regs[8] << 8) | regs[9] 249 | adc_gas_res = (regs[13] << 2) | (regs[14] >> 6) 250 | gas_range = regs[14] & GAS_RANGE_MSK 251 | 252 | self.data.status |= regs[14] & GASM_VALID_MSK 253 | self.data.status |= regs[14] & HEAT_STAB_MSK 254 | 255 | self.data.heat_stable = (self.data.status & HEAT_STAB_MSK) > 0 256 | 257 | temperature = self._calc_temperature(adc_temp) 258 | self.data.temperature = temperature / 100.0 259 | self.ambient_temperature = temperature # Saved for heater calc 260 | 261 | self.data.pressure = self._calc_pressure(adc_pres) / 100.0 262 | self.data.humidity = self._calc_humidity(adc_hum) / 1000.0 263 | self.data.gas_resistance = self._calc_gas_resistance(adc_gas_res, gas_range) 264 | return True 265 | 266 | return False 267 | 268 | def _set_bits(self, register, mask, position, value): 269 | """Mask out and set one or more bits in a register""" 270 | temp = self._get_regs(register, 1) 271 | temp &= ~mask 272 | temp |= value << position 273 | self._set_regs(register, temp) 274 | 275 | def _set_regs(self, register, value): 276 | """Set one or more registers""" 277 | if isinstance(value, int): 278 | self._i2c.write_byte_data(self.i2c_addr, register, value) 279 | else: 280 | self._i2c.write_i2c_block_data(self.i2c_addr, register, value) 281 | 282 | def _get_regs(self, register, length): 283 | """Get one or more registers""" 284 | if length == 1: 285 | return self._i2c.read_byte_data(self.i2c_addr, register) 286 | else: 287 | return self._i2c.read_i2c_block_data(self.i2c_addr, register, length) 288 | 289 | def _calc_temperature(self, temperature_adc): 290 | var1 = (temperature_adc >> 3) - (self.calibration_data.par_t1 << 1) 291 | var2 = (var1 * self.calibration_data.par_t2) >> 11 292 | var3 = ((var1 >> 1) * (var1 >> 1)) >> 12 293 | var3 = ((var3) * (self.calibration_data.par_t3 << 4)) >> 14 294 | 295 | # Save teperature data for pressure calculations 296 | self.calibration_data.t_fine = (var2 + var3) 297 | calc_temp = (((self.calibration_data.t_fine * 5) + 128) >> 8) 298 | 299 | return calc_temp 300 | 301 | def _calc_pressure(self, pressure_adc): 302 | var1 = ((self.calibration_data.t_fine) >> 1) - 64000 303 | var2 = ((((var1 >> 2) * (var1 >> 2)) >> 11) * 304 | self.calibration_data.par_p6) >> 2 305 | var2 = var2 + ((var1 * self.calibration_data.par_p5) << 1) 306 | var2 = (var2 >> 2) + (self.calibration_data.par_p4 << 16) 307 | var1 = (((((var1 >> 2) * (var1 >> 2)) >> 13 ) * 308 | ((self.calibration_data.par_p3 << 5)) >> 3) + 309 | ((self.calibration_data.par_p2 * var1) >> 1)) 310 | var1 = var1 >> 18 311 | 312 | var1 = ((32768 + var1) * self.calibration_data.par_p1) >> 15 313 | calc_pressure = 1048576 - pressure_adc 314 | calc_pressure = ((calc_pressure - (var2 >> 12)) * (3125)) 315 | 316 | if calc_pressure >= (1 << 31): 317 | calc_pressure = ((calc_pressure // var1) << 1) 318 | else: 319 | calc_pressure = ((calc_pressure << 1) // var1) 320 | 321 | var1 = (self.calibration_data.par_p9 * (((calc_pressure >> 3) * 322 | (calc_pressure >> 3)) >> 13)) >> 12 323 | var2 = ((calc_pressure >> 2) * 324 | self.calibration_data.par_p8) >> 13 325 | var3 = ((calc_pressure >> 8) * (calc_pressure >> 8) * 326 | (calc_pressure >> 8) * 327 | self.calibration_data.par_p10) >> 17 328 | 329 | calc_pressure = (calc_pressure) + ((var1 + var2 + var3 + 330 | (self.calibration_data.par_p7 << 7)) >> 4) 331 | 332 | return calc_pressure 333 | 334 | def _calc_humidity(self, humidity_adc): 335 | temp_scaled = ((self.calibration_data.t_fine * 5) + 128) >> 8 336 | var1 = (humidity_adc - ((self.calibration_data.par_h1 * 16))) \ 337 | - (((temp_scaled * self.calibration_data.par_h3) // (100)) >> 1) 338 | var2 = (self.calibration_data.par_h2 339 | * (((temp_scaled * self.calibration_data.par_h4) // (100)) 340 | + (((temp_scaled * ((temp_scaled * self.calibration_data.par_h5) // (100))) >> 6) 341 | // (100)) + (1 * 16384))) >> 10 342 | var3 = var1 * var2 343 | var4 = self.calibration_data.par_h6 << 7 344 | var4 = ((var4) + ((temp_scaled * self.calibration_data.par_h7) // (100))) >> 4 345 | var5 = ((var3 >> 14) * (var3 >> 14)) >> 10 346 | var6 = (var4 * var5) >> 1 347 | calc_hum = (((var3 + var6) >> 10) * (1000)) >> 12 348 | 349 | return min(max(calc_hum,0),100000) 350 | 351 | def _calc_gas_resistance(self, gas_res_adc, gas_range): 352 | var1 = ((1340 + (5 * self.calibration_data.range_sw_err)) * (lookupTable1[gas_range])) >> 16 353 | var2 = (((gas_res_adc << 15) - (16777216)) + var1) 354 | var3 = ((lookupTable2[gas_range] * var1) >> 9) 355 | calc_gas_res = ((var3 + (var2 >> 1)) / var2) 356 | 357 | return calc_gas_res 358 | 359 | def _calc_heater_resistance(self, temperature): 360 | temperature = min(max(temperature,200),400) 361 | 362 | var1 = ((self.ambient_temperature * self.calibration_data.par_gh3) / 1000) * 256 363 | var2 = (self.calibration_data.par_gh1 + 784) * (((((self.calibration_data.par_gh2 + 154009) * temperature * 5) / 100) + 3276800) / 10) 364 | var3 = var1 + (var2 / 2) 365 | var4 = (var3 / (self.calibration_data.res_heat_range + 4)) 366 | var5 = (131 * self.calibration_data.res_heat_val) + 65536 367 | heatr_res_x100 = (((var4 / var5) - 250) * 34) 368 | heatr_res = ((heatr_res_x100 + 50) / 100) 369 | 370 | return heatr_res 371 | 372 | def _calc_heater_duration(self, duration): 373 | if duration < 0xfc0: 374 | factor = 0 375 | 376 | while duration > 0x3f: 377 | duration /= 4 378 | factor += 1 379 | 380 | return int(duration + (factor * 64)) 381 | 382 | return 0xff 383 | -------------------------------------------------------------------------------- /lib/constants.py: -------------------------------------------------------------------------------- 1 | # BME680 General config 2 | POLL_PERIOD_MS = 10 3 | 4 | # BME680 I2C addresses 5 | I2C_ADDR_PRIMARY = 0x76 6 | I2C_ADDR_SECONDARY = 0x77 7 | 8 | # BME680 unique chip identifier 9 | CHIP_ID = 0x61 10 | 11 | # BME680 coefficients related defines 12 | COEFF_SIZE = 41 13 | COEFF_ADDR1_LEN = 25 14 | COEFF_ADDR2_LEN = 16 15 | 16 | # BME680 field_x related defines 17 | FIELD_LENGTH = 15 18 | FIELD_ADDR_OFFSET = 17 19 | 20 | # Soft reset command 21 | SOFT_RESET_CMD = 0xb6 22 | 23 | # Error code definitions 24 | OK = 0 25 | # Errors 26 | E_NULL_PTR = -1 27 | E_COM_FAIL = -2 28 | E_DEV_NOT_FOUND = -3 29 | E_INVALID_LENGTH = -4 30 | 31 | # Warnings 32 | W_DEFINE_PWR_MODE = 1 33 | W_NO_NEW_DATA = 2 34 | 35 | # Info's 36 | I_MIN_CORRECTION = 1 37 | I_MAX_CORRECTION = 2 38 | 39 | # Register map 40 | # Other coefficient's address 41 | ADDR_RES_HEAT_VAL_ADDR = 0x00 42 | ADDR_RES_HEAT_RANGE_ADDR = 0x02 43 | ADDR_RANGE_SW_ERR_ADDR = 0x04 44 | ADDR_SENS_CONF_START = 0x5A 45 | ADDR_GAS_CONF_START = 0x64 46 | 47 | # Field settings 48 | FIELD0_ADDR = 0x1d 49 | 50 | # Heater settings 51 | RES_HEAT0_ADDR = 0x5a 52 | GAS_WAIT0_ADDR = 0x64 53 | 54 | # Sensor configuration registers 55 | CONF_HEAT_CTRL_ADDR = 0x70 56 | CONF_ODR_RUN_GAS_NBC_ADDR = 0x71 57 | CONF_OS_H_ADDR = 0x72 58 | MEM_PAGE_ADDR = 0xf3 59 | CONF_T_P_MODE_ADDR = 0x74 60 | CONF_ODR_FILT_ADDR = 0x75 61 | 62 | # Coefficient's address 63 | COEFF_ADDR1 = 0x89 64 | COEFF_ADDR2 = 0xe1 65 | 66 | # Chip identifier 67 | CHIP_ID_ADDR = 0xd0 68 | 69 | # Soft reset register 70 | SOFT_RESET_ADDR = 0xe0 71 | 72 | # Heater control settings 73 | ENABLE_HEATER = 0x00 74 | DISABLE_HEATER = 0x08 75 | 76 | # Gas measurement settings 77 | DISABLE_GAS_MEAS = 0x00 78 | ENABLE_GAS_MEAS = 0x01 79 | 80 | # Over-sampling settings 81 | OS_NONE = 0 82 | OS_1X = 1 83 | OS_2X = 2 84 | OS_4X = 3 85 | OS_8X = 4 86 | OS_16X = 5 87 | 88 | # IIR filter settings 89 | FILTER_SIZE_0 = 0 90 | FILTER_SIZE_1 = 1 91 | FILTER_SIZE_3 = 2 92 | FILTER_SIZE_7 = 3 93 | FILTER_SIZE_15 = 4 94 | FILTER_SIZE_31 = 5 95 | FILTER_SIZE_63 = 6 96 | FILTER_SIZE_127 = 7 97 | 98 | # Power mode settings 99 | SLEEP_MODE = 0 100 | FORCED_MODE = 1 101 | 102 | # Delay related macro declaration 103 | RESET_PERIOD = 10 104 | 105 | # SPI memory page settings 106 | MEM_PAGE0 = 0x10 107 | MEM_PAGE1 = 0x00 108 | 109 | # Ambient humidity shift value for compensation 110 | HUM_REG_SHIFT_VAL = 4 111 | 112 | # Run gas enable and disable settings 113 | RUN_GAS_DISABLE = 0 114 | RUN_GAS_ENABLE = 1 115 | 116 | # Buffer length macro declaration 117 | TMP_BUFFER_LENGTH = 40 118 | REG_BUFFER_LENGTH = 6 119 | FIELD_DATA_LENGTH = 3 120 | GAS_REG_BUF_LENGTH = 20 121 | GAS_HEATER_PROF_LEN_MAX = 10 122 | 123 | # Settings selector 124 | OST_SEL = 1 125 | OSP_SEL = 2 126 | OSH_SEL = 4 127 | GAS_MEAS_SEL = 8 128 | FILTER_SEL = 16 129 | HCNTRL_SEL = 32 130 | RUN_GAS_SEL = 64 131 | NBCONV_SEL = 128 132 | GAS_SENSOR_SEL = GAS_MEAS_SEL | RUN_GAS_SEL | NBCONV_SEL 133 | 134 | # Number of conversion settings 135 | NBCONV_MIN = 0 136 | NBCONV_MAX = 9 # Was 10, but there are only 10 settings: 0 1 2 ... 8 9 137 | 138 | # Mask definitions 139 | GAS_MEAS_MSK = 0x30 140 | NBCONV_MSK = 0X0F 141 | FILTER_MSK = 0X1C 142 | OST_MSK = 0XE0 143 | OSP_MSK = 0X1C 144 | OSH_MSK = 0X07 145 | HCTRL_MSK = 0x08 146 | RUN_GAS_MSK = 0x10 147 | MODE_MSK = 0x03 148 | RHRANGE_MSK = 0x30 149 | RSERROR_MSK = 0xf0 150 | NEW_DATA_MSK = 0x80 151 | GAS_INDEX_MSK = 0x0f 152 | GAS_RANGE_MSK = 0x0f 153 | GASM_VALID_MSK = 0x20 154 | HEAT_STAB_MSK = 0x10 155 | MEM_PAGE_MSK = 0x10 156 | SPI_RD_MSK = 0x80 157 | SPI_WR_MSK = 0x7f 158 | BIT_H1_DATA_MSK = 0x0F 159 | 160 | # Bit position definitions for sensor settings 161 | GAS_MEAS_POS = 4 162 | FILTER_POS = 2 163 | OST_POS = 5 164 | OSP_POS = 2 165 | OSH_POS = 0 166 | RUN_GAS_POS = 4 167 | MODE_POS = 0 168 | NBCONV_POS = 0 169 | 170 | # Array Index to Field data mapping for Calibration Data 171 | T2_LSB_REG = 1 172 | T2_MSB_REG = 2 173 | T3_REG = 3 174 | P1_LSB_REG = 5 175 | P1_MSB_REG = 6 176 | P2_LSB_REG = 7 177 | P2_MSB_REG = 8 178 | P3_REG = 9 179 | P4_LSB_REG = 11 180 | P4_MSB_REG = 12 181 | P5_LSB_REG = 13 182 | P5_MSB_REG = 14 183 | P7_REG = 15 184 | P6_REG = 16 185 | P8_LSB_REG = 19 186 | P8_MSB_REG = 20 187 | P9_LSB_REG = 21 188 | P9_MSB_REG = 22 189 | P10_REG = 23 190 | H2_MSB_REG = 25 191 | H2_LSB_REG = 26 192 | H1_LSB_REG = 26 193 | H1_MSB_REG = 27 194 | H3_REG = 28 195 | H4_REG = 29 196 | H5_REG = 30 197 | H6_REG = 31 198 | H7_REG = 32 199 | T1_LSB_REG = 33 200 | T1_MSB_REG = 34 201 | GH2_LSB_REG = 35 202 | GH2_MSB_REG = 36 203 | GH1_REG = 37 204 | GH3_REG = 38 205 | 206 | # BME680 register buffer index settings 207 | REG_FILTER_INDEX = 5 208 | REG_TEMP_INDEX = 4 209 | REG_PRES_INDEX = 4 210 | REG_HUM_INDEX = 2 211 | REG_NBCONV_INDEX = 1 212 | REG_RUN_GAS_INDEX = 1 213 | REG_HCTRL_INDEX = 0 214 | 215 | # Look up tables for the possible gas range values 216 | lookupTable1 = [2147483647, 2147483647, 2147483647, 2147483647, 217 | 2147483647, 2126008810, 2147483647, 2130303777, 2147483647, 218 | 2147483647, 2143188679, 2136746228, 2147483647, 2126008810, 219 | 2147483647, 2147483647] 220 | 221 | lookupTable2 = [4096000000, 2048000000, 1024000000, 512000000, 222 | 255744255, 127110228, 64000000, 32258064, 223 | 16016016, 8000000, 4000000, 2000000, 224 | 1000000, 500000, 250000, 125000] 225 | 226 | def bytes_to_word(msb, lsb, bits=16, signed=False): 227 | word = (msb << 8) | lsb 228 | if signed: 229 | word = twos_comp(word, bits) 230 | return word 231 | 232 | def twos_comp(val, bits=16): 233 | if val & (1 << (bits - 1)) != 0: 234 | val = val - (1 << bits) 235 | return val 236 | 237 | # Sensor field data structure 238 | 239 | class FieldData: 240 | def __init__(self): 241 | # Contains new_data, gasm_valid & heat_stab 242 | self.status = None 243 | self.heat_stable = False 244 | # The index of the heater profile used 245 | self.gas_index = None 246 | # Measurement index to track order 247 | self.meas_index = None 248 | # Temperature in degree celsius x100 249 | self.temperature = None 250 | # Pressure in Pascal 251 | self.pressure = None 252 | # Humidity in % relative humidity x1000 253 | self.humidity = None 254 | # Gas resistance in Ohms 255 | self.gas_resistance = None 256 | 257 | # Structure to hold the Calibration data 258 | 259 | class CalibrationData: 260 | def __init__(self): 261 | self.par_h1 = None 262 | self.par_h2 = None 263 | self.par_h3 = None 264 | self.par_h4 = None 265 | self.par_h5 = None 266 | self.par_h6 = None 267 | self.par_h7 = None 268 | self.par_gh1 = None 269 | self.par_gh2 = None 270 | self.par_gh3 = None 271 | self.par_t1 = None 272 | self.par_t2 = None 273 | self.par_t3 = None 274 | self.par_p1 = None 275 | self.par_p2 = None 276 | self.par_p3 = None 277 | self.par_p4 = None 278 | self.par_p5 = None 279 | self.par_p6 = None 280 | self.par_p7 = None 281 | self.par_p8 = None 282 | self.par_p9 = None 283 | self.par_p10 = None 284 | # Variable to store t_fine size 285 | self.t_fine = None 286 | # Variable to store heater resistance range 287 | self.res_heat_range = None 288 | # Variable to store heater resistance value 289 | self.res_heat_val = None 290 | # Variable to store error range 291 | self.range_sw_err = None 292 | 293 | def set_from_array(self, calibration): 294 | # Temperature related coefficients 295 | self.par_t1 = bytes_to_word(calibration[T1_MSB_REG], calibration[T1_LSB_REG]) 296 | self.par_t2 = bytes_to_word(calibration[T2_MSB_REG], calibration[T2_LSB_REG], bits=16, signed=True) 297 | self.par_t3 = twos_comp(calibration[T3_REG], bits=8) 298 | 299 | # Pressure related coefficients 300 | self.par_p1 = bytes_to_word(calibration[P1_MSB_REG], calibration[P1_LSB_REG]) 301 | self.par_p2 = bytes_to_word(calibration[P2_MSB_REG], calibration[P2_LSB_REG], bits=16, signed=True) 302 | self.par_p3 = twos_comp(calibration[P3_REG], bits=8) 303 | self.par_p4 = bytes_to_word(calibration[P4_MSB_REG], calibration[P4_LSB_REG], bits=16, signed=True) 304 | self.par_p5 = bytes_to_word(calibration[P5_MSB_REG], calibration[P5_LSB_REG], bits=16, signed=True) 305 | self.par_p6 = twos_comp(calibration[P6_REG], bits=8) 306 | self.par_p7 = twos_comp(calibration[P7_REG], bits=8) 307 | self.par_p8 = bytes_to_word(calibration[P8_MSB_REG], calibration[P8_LSB_REG], bits=16, signed=True) 308 | self.par_p9 = bytes_to_word(calibration[P9_MSB_REG], calibration[P9_LSB_REG], bits=16, signed=True) 309 | self.par_p10 = calibration[P10_REG] 310 | 311 | # Humidity related coefficients 312 | self.par_h1 = (calibration[H1_MSB_REG] << HUM_REG_SHIFT_VAL) | (calibration[H1_LSB_REG] & BIT_H1_DATA_MSK) 313 | self.par_h2 = (calibration[H2_MSB_REG] << HUM_REG_SHIFT_VAL) | (calibration[H2_LSB_REG] >> HUM_REG_SHIFT_VAL) 314 | self.par_h3 = twos_comp(calibration[H3_REG], bits=8) 315 | self.par_h4 = twos_comp(calibration[H4_REG], bits=8) 316 | self.par_h5 = twos_comp(calibration[H5_REG], bits=8) 317 | self.par_h6 = calibration[H6_REG] 318 | self.par_h7 = twos_comp(calibration[H7_REG], bits=8) 319 | 320 | # Gas heater related coefficients 321 | self.par_gh1 = twos_comp(calibration[GH1_REG], bits=8) 322 | self.par_gh2 = bytes_to_word(calibration[GH2_MSB_REG], calibration[GH2_LSB_REG], bits=16, signed=True) 323 | self.par_gh3 = twos_comp(calibration[GH3_REG], bits=8) 324 | 325 | def set_other(self, heat_range, heat_value, sw_error): 326 | self.res_heat_range = (heat_range & RHRANGE_MSK) // 16 327 | self.res_heat_val = heat_value 328 | self.range_sw_err = (sw_error * RSERROR_MSK) // 16 329 | 330 | # BME680 sensor settings structure which comprises of ODR, 331 | # over-sampling and filter settings. 332 | 333 | class TPHSettings: 334 | def __init__(self): 335 | # Humidity oversampling 336 | self.os_hum = None 337 | # Temperature oversampling 338 | self.os_temp = None 339 | # Pressure oversampling 340 | self.os_pres = None 341 | # Filter coefficient 342 | self.filter = None 343 | 344 | # BME680 gas sensor which comprises of gas settings 345 | ## and status parameters 346 | 347 | class GasSettings: 348 | def __init__(self): 349 | # Variable to store nb conversion 350 | self.nb_conv = None 351 | # Variable to store heater control 352 | self.heatr_ctrl = None 353 | # Run gas enable value 354 | self.run_gas = None 355 | # Pointer to store heater temperature 356 | self.heatr_temp = None 357 | # Pointer to store duration profile 358 | self.heatr_dur = None 359 | 360 | # BME680 device structure 361 | 362 | class BME680Data: 363 | def __init__(self): 364 | # Chip Id 365 | self.chip_id = None 366 | # Device Id 367 | self.dev_id = None 368 | # SPI/I2C interface 369 | self.intf = None 370 | # Memory page used 371 | self.mem_page = None 372 | # Ambient temperature in Degree C 373 | self.ambient_temperature = None 374 | # Field Data 375 | self.data = FieldData() 376 | # Sensor calibration data 377 | self.calibration_data = CalibrationData() 378 | # Sensor settings 379 | self.tph_settings = TPHSettings() 380 | # Gas Sensor settings 381 | self.gas_settings = GasSettings() 382 | # Sensor power modes 383 | self.power_mode = None 384 | # New sensor fields 385 | self.new_fields = None 386 | -------------------------------------------------------------------------------- /lib/i2c.py: -------------------------------------------------------------------------------- 1 | 2 | try: 3 | from machine import I2C 4 | except ImportError: 5 | raise ImportError("Can't find the micropython machine.I2C class: " 6 | "perhaps you don't need this adapter?") 7 | 8 | 9 | class I2CAdapter(I2C): 10 | """ Adds some of the SMBus I2c methods to the micropython I2c class, 11 | for enhanced compatibility. 12 | 13 | Use it like you would the machine.I2C class: 14 | 15 | from bme680.i2c import I2CAdapter 16 | 17 | i2c_dev = I2CAdapter(1, pins=('G15','G10'), baudrate=100000) 18 | sensor = bme680.BME680(i2c_device=i2c_dev) 19 | 20 | """ 21 | 22 | def read_byte_data(self, addr, register): 23 | """ Read a single byte from register of device at addr 24 | Returns a single byte """ 25 | return self.readfrom_mem(addr, register, 1)[0] 26 | 27 | def read_i2c_block_data(self, addr, register, length): 28 | """ Read a block of length from register of device at addr 29 | Returns a bytes object filled with whatever was read """ 30 | return self.readfrom_mem(addr, register, length) 31 | 32 | def write_byte_data(self, addr, register, data): 33 | """ Write a single byte of data to register of device at addr 34 | Returns None """ 35 | return self.writeto_mem(addr, register, data) 36 | 37 | def write_i2c_block_data(self, addr, register, data): 38 | """ Write multiple bytes of data to register of device at addr 39 | Returns None """ 40 | return self.writeto_mem(addr, register, data) 41 | -------------------------------------------------------------------------------- /lib/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import bme680 3 | from i2c import I2CAdapter 4 | from mqtt import MQTTClient 5 | import time 6 | 7 | def settimeout(duration): 8 | pass 9 | 10 | client = MQTTClient("wipy", "192.168.0.30", port=1883) 11 | client.settimeout = settimeout 12 | client.connect() 13 | 14 | i2c_dev = I2CAdapter() 15 | sensor = bme680.BME680(i2c_device=i2c_dev) 16 | 17 | # These oversampling settings can be tweaked to 18 | # change the balance between accuracy and noise in 19 | # the data. 20 | sensor.set_humidity_oversample(bme680.OS_2X) 21 | sensor.set_pressure_oversample(bme680.OS_4X) 22 | sensor.set_temperature_oversample(bme680.OS_8X) 23 | sensor.set_filter(bme680.FILTER_SIZE_3) 24 | 25 | print("Polling:") 26 | try: 27 | while True: 28 | if sensor.get_sensor_data(): 29 | 30 | output = "{} C, {} hPa, {} RH, {} RES,".format( 31 | sensor.data.temperature, 32 | sensor.data.pressure, 33 | sensor.data.humidity, 34 | sensor.data.gas_resistance) 35 | 36 | print(output) 37 | client.publish("test", output) 38 | time.sleep(1) 39 | except KeyboardInterrupt: 40 | pass 41 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import bme680 3 | from i2c import I2CAdapter 4 | from mqtt import MQTTClient 5 | import time 6 | 7 | def settimeout(duration): 8 | pass 9 | 10 | client = MQTTClient("wipy", "192.168.0.30", port=1883) 11 | client.settimeout = settimeout 12 | client.connect() 13 | mqtt_topic = "bme680" 14 | 15 | i2c_dev = I2CAdapter() 16 | sensor = bme680.BME680(i2c_device=i2c_dev) 17 | 18 | # These oversampling settings can be tweaked to 19 | # change the balance between accuracy and noise in 20 | # the data. 21 | sensor.set_humidity_oversample(bme680.OS_2X) 22 | sensor.set_pressure_oversample(bme680.OS_4X) 23 | sensor.set_temperature_oversample(bme680.OS_8X) 24 | sensor.set_filter(bme680.FILTER_SIZE_3) 25 | 26 | print("Polling:") 27 | try: 28 | while True: 29 | if sensor.get_sensor_data(): 30 | 31 | output = "{} C, {} hPa, {} RH, {} RES,".format( 32 | sensor.data.temperature, 33 | sensor.data.pressure, 34 | sensor.data.humidity, 35 | sensor.data.gas_resistance) 36 | 37 | print(output) 38 | client.publish(mqtt_topic, output) 39 | # Publish on individual topics for consistency with rpi repo. 40 | client.publish('bme680-humidity', str(sensor.data.humidity)) 41 | client.publish('bme680-temperature', str(sensor.data.temperature)) 42 | client.publish('bme680-pressure', str(sensor.data.pressure)) 43 | client.publish('bme680-air_qual', str(sensor.data.gas_resistance)) 44 | time.sleep(5) 45 | 46 | except KeyboardInterrupt: 47 | pass 48 | -------------------------------------------------------------------------------- /simple-readout/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import bme680 3 | from i2c import I2CAdapter 4 | import time 5 | 6 | i2c_dev = I2CAdapter() 7 | sensor = bme680.BME680(i2c_device=i2c_dev) 8 | 9 | # These oversampling settings can be tweaked to 10 | # change the balance between accuracy and noise in 11 | # the data. 12 | sensor.set_humidity_oversample(bme680.OS_2X) 13 | sensor.set_pressure_oversample(bme680.OS_4X) 14 | sensor.set_temperature_oversample(bme680.OS_8X) 15 | sensor.set_filter(bme680.FILTER_SIZE_3) 16 | 17 | print("Polling:") 18 | try: 19 | while True: 20 | if sensor.get_sensor_data(): 21 | 22 | output = "{} C, {} hPa, {} RH, {} RES,".format( 23 | sensor.data.temperature, 24 | sensor.data.pressure, 25 | sensor.data.humidity, 26 | sensor.data.gas_resistance) 27 | 28 | print(output) 29 | time.sleep(1) 30 | except KeyboardInterrupt: 31 | pass 32 | --------------------------------------------------------------------------------