├── .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 |
--------------------------------------------------------------------------------