Learn how to program the ESP32 or ESP8266 boards with MicroPython to publish BME680 sensor readings (temperature, humidity, pressure, gas/air quality) via MQTT to any platform that supports MQTT or any MQTT client. As an example, we’ll publish sensor readings to Node-RED Dashboard.
Recommended reading: What is MQTT and How It Works
Note: this tutorial is compatible with both the ESP32 and ESP8266 development boards.
Project Overview
The following diagram shows a high-level overview of the project we’ll build.
- The ESP requests temperature and humidity readings from the BME680 sensor;
- Temperature readings are published in the esp/bme680/temperature topic;
- Humidity readings are published in the esp/bme680/humidity topic;
- Pressure readings are published in the esp/bme680/pressure topic;
- Pressure readings are published in the esp/bme680/gas topic;
- Node-RED is subscribed to those topics;
- Node-RED receives the sensor readings and displays them on gauges;
- You can receive the readings in any other platform that supports MQTT and handle the readings as you want.
Prerequisites
Before continuing with this tutorial, make sure you complete the following prerequisites:
To follow this tutorial you need MicroPython firmware installed in your ESP32 or ESP8266 boards. You also need an IDE to write and upload the code to your board. We suggest using Thonny IDE or uPyCraft IDE:
- Thonny IDE:
- uPyCraft IDE:
- Getting Started with uPyCraft IDE
- Install uPyCraft IDE (Windows, Mac OS X, Linux)
- Flash/Upload MicroPython Firmware to ESP32 and ESP8266
MQTT Broker
To use MQTT, you need a broker. We’ll be using Mosquitto broker installed on a Raspberry Pi. Read How to Install Mosquitto Broker on Raspberry Pi.
If you’re not familiar with MQTT make sure you read our introductory tutorial: What is MQTT and How It Works
Parts Required
For this tutorial you need the following parts:
- ESP32 (read Best ESP32 development boards) or ESP8266
- BME680
- Raspberry Pi board (read Best Raspberry Pi Starter Kits)
- MicroSD Card – 16GB Class10
- Raspberry Pi Power Supply (5V 2.5A)
- Jumper wires
- Breadboard
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
umqtttsimple Library
To use MQTT with the ESP32/ESP8266 and MicroPython, we’ll use the umqttsimple.py library. Follow the next set of instructions for the IDE you’re using:
- A. Upload umqttsimple library with uPyCraft IDE
- B. Upload umqttsimple library with Thonny IDE
try:
import usocket as socket
except:
import socket
import ustruct as struct
from ubinascii import hexlify
class MQTTException(Exception):
pass
class MQTTClient:
def __init__(self, client_id, server, port=0, user=None, password=None, keepalive=0,
ssl=False, ssl_params={}):
if port == 0:
port = 8883 if ssl else 1883
self.client_id = client_id
self.sock = None
self.server = server
self.port = port
self.ssl = ssl
self.ssl_params = ssl_params
self.pid = 0
self.cb = None
self.user = user
self.pswd = password
self.keepalive = keepalive
self.lw_topic = None
self.lw_msg = None
self.lw_qos = 0
self.lw_retain = False
def _send_str(self, s):
self.sock.write(struct.pack("!H", len(s)))
self.sock.write(s)
def _recv_len(self):
n = 0
sh = 0
while 1:
b = self.sock.read(1)[0]
n |= (b & 0x7f) << sh
if not b & 0x80:
return n
sh += 7
def set_callback(self, f):
self.cb = f
def set_last_will(self, topic, msg, retain=False, qos=0):
assert 0 <= qos <= 2
assert topic
self.lw_topic = topic
self.lw_msg = msg
self.lw_qos = qos
self.lw_retain = retain
def connect(self, clean_session=True):
self.sock = socket.socket()
addr = socket.getaddrinfo(self.server, self.port)[0][-1]
self.sock.connect(addr)
if self.ssl:
import ussl
self.sock = ussl.wrap_socket(self.sock, **self.ssl_params)
premsg = bytearray(b"\x10\0\0\0\0\0")
msg = bytearray(b"\x04MQTT\x04\x02\0\0")
sz = 10 + 2 + len(self.client_id)
msg[6] = clean_session << 1
if self.user is not None:
sz += 2 + len(self.user) + 2 + len(self.pswd)
msg[6] |= 0xC0
if self.keepalive:
assert self.keepalive < 65536
msg[7] |= self.keepalive >> 8
msg[8] |= self.keepalive & 0x00FF
if self.lw_topic:
sz += 2 + len(self.lw_topic) + 2 + len(self.lw_msg)
msg[6] |= 0x4 | (self.lw_qos & 0x1) << 3 | (self.lw_qos & 0x2) << 3
msg[6] |= self.lw_retain << 5
i = 1
while sz > 0x7f:
premsg[i] = (sz & 0x7f) | 0x80
sz >>= 7
i += 1
premsg[i] = sz
self.sock.write(premsg, i + 2)
self.sock.write(msg)
#print(hex(len(msg)), hexlify(msg, ":"))
self._send_str(self.client_id)
if self.lw_topic:
self._send_str(self.lw_topic)
self._send_str(self.lw_msg)
if self.user is not None:
self._send_str(self.user)
self._send_str(self.pswd)
resp = self.sock.read(4)
assert resp[0] == 0x20 and resp[1] == 0x02
if resp[3] != 0:
raise MQTTException(resp[3])
return resp[2] & 1
def disconnect(self):
self.sock.write(b"\xe0\0")
self.sock.close()
def ping(self):
self.sock.write(b"\xc0\0")
def publish(self, topic, msg, retain=False, qos=0):
pkt = bytearray(b"\x30\0\0\0")
pkt[0] |= qos << 1 | retain
sz = 2 + len(topic) + len(msg)
if qos > 0:
sz += 2
assert sz < 2097152
i = 1
while sz > 0x7f:
pkt[i] = (sz & 0x7f) | 0x80
sz >>= 7
i += 1
pkt[i] = sz
#print(hex(len(pkt)), hexlify(pkt, ":"))
self.sock.write(pkt, i + 1)
self._send_str(topic)
if qos > 0:
self.pid += 1
pid = self.pid
struct.pack_into("!H", pkt, 0, pid)
self.sock.write(pkt, 2)
self.sock.write(msg)
if qos == 1:
while 1:
op = self.wait_msg()
if op == 0x40:
sz = self.sock.read(1)
assert sz == b"\x02"
rcv_pid = self.sock.read(2)
rcv_pid = rcv_pid[0] << 8 | rcv_pid[1]
if pid == rcv_pid:
return
elif qos == 2:
assert 0
def subscribe(self, topic, qos=0):
assert self.cb is not None, "Subscribe callback is not set"
pkt = bytearray(b"\x82\0\0\0")
self.pid += 1
struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid)
#print(hex(len(pkt)), hexlify(pkt, ":"))
self.sock.write(pkt)
self._send_str(topic)
self.sock.write(qos.to_bytes(1, "little"))
while 1:
op = self.wait_msg()
if op == 0x90:
resp = self.sock.read(4)
#print(resp)
assert resp[1] == pkt[2] and resp[2] == pkt[3]
if resp[3] == 0x80:
raise MQTTException(resp[3])
return
# Wait for a single incoming MQTT message and process it.
# Subscribed messages are delivered to a callback previously
# set by .set_callback() method. Other (internal) MQTT
# messages processed internally.
def wait_msg(self):
res = self.sock.read(1)
self.sock.setblocking(True)
if res is None:
return None
if res == b"":
raise OSError(-1)
if res == b"\xd0": # PINGRESP
sz = self.sock.read(1)[0]
assert sz == 0
return None
op = res[0]
if op & 0xf0 != 0x30:
return op
sz = self._recv_len()
topic_len = self.sock.read(2)
topic_len = (topic_len[0] << 8) | topic_len[1]
topic = self.sock.read(topic_len)
sz -= topic_len + 2
if op & 6:
pid = self.sock.read(2)
pid = pid[0] << 8 | pid[1]
sz -= 2
msg = self.sock.read(sz)
self.cb(topic, msg)
if op & 6 == 2:
pkt = bytearray(b"\x40\x02\0\0")
struct.pack_into("!H", pkt, 2, pid)
self.sock.write(pkt)
elif op & 6 == 4:
assert 0
# Checks whether a pending message from server is available.
# If not, returns immediately with None. Otherwise, does
# the same processing as wait_msg.
def check_msg(self):
self.sock.setblocking(False)
return self.wait_msg()
A. Upload umqttsimple library with uPyCraft IDE
1. Create a new file by pressing the New File button.
2. Copy the umqttsimple library code into it. You can access the umqttsimple library code in the following link:
3. Save the file by pressing the Save button.
4. Call this new file “umqttsimple.py” and press ok.
5. Click the Download and Run button.
6. The file should be saved on the device folder with the name “umqttsimple.py” as highlighted in the figure below.
Now, you can use the library functionalities in your code by importing the library.
B. Upload umqttsimple library with Thonny IDE
1. Copy the library code to a new file. The umqttsimple library code can be found here.
2. Go to File > Save as…
3. Select save to “MicroPython device“:
4. Name your file as umqttsimple.py and press the OK button:
And that’s it. The library was uploaded to your board. To make sure that it was uploaded successfully, go to File > Save as… and select the MicroPython device. Your file should be listed there:
After uploading the library to your board, you can use the library functionalities in your code by importing the library.
BME680 MicroPython Library
The library to read from the BME680 sensor isn’t part of the standard MicroPython library by default. So, you need to upload the following library to your ESP32/ESP8266 board (save it with the name bme680.py).
# Spaces, comments and some functions have been removed from the original file to save memory
# Original source: https://github.com/adafruit/Adafruit_CircuitPython_BME680/blob/master/adafruit_bme680.py
import time
import math
from micropython import const
from ubinascii import hexlify as hex
try:
import struct
except ImportError:
import ustruct as struct
_BME680_CHIPID = const(0x61)
_BME680_REG_CHIPID = const(0xD0)
_BME680_BME680_COEFF_ADDR1 = const(0x89)
_BME680_BME680_COEFF_ADDR2 = const(0xE1)
_BME680_BME680_RES_HEAT_0 = const(0x5A)
_BME680_BME680_GAS_WAIT_0 = const(0x64)
_BME680_REG_SOFTRESET = const(0xE0)
_BME680_REG_CTRL_GAS = const(0x71)
_BME680_REG_CTRL_HUM = const(0x72)
_BME280_REG_STATUS = const(0xF3)
_BME680_REG_CTRL_MEAS = const(0x74)
_BME680_REG_CONFIG = const(0x75)
_BME680_REG_PAGE_SELECT = const(0x73)
_BME680_REG_MEAS_STATUS = const(0x1D)
_BME680_REG_PDATA = const(0x1F)
_BME680_REG_TDATA = const(0x22)
_BME680_REG_HDATA = const(0x25)
_BME680_SAMPLERATES = (0, 1, 2, 4, 8, 16)
_BME680_FILTERSIZES = (0, 1, 3, 7, 15, 31, 63, 127)
_BME680_RUNGAS = const(0x10)
_LOOKUP_TABLE_1 = (2147483647.0, 2147483647.0, 2147483647.0, 2147483647.0, 2147483647.0,
2126008810.0, 2147483647.0, 2130303777.0, 2147483647.0, 2147483647.0,
2143188679.0, 2136746228.0, 2147483647.0, 2126008810.0, 2147483647.0,
2147483647.0)
_LOOKUP_TABLE_2 = (4096000000.0, 2048000000.0, 1024000000.0, 512000000.0, 255744255.0, 127110228.0,
64000000.0, 32258064.0, 16016016.0, 8000000.0, 4000000.0, 2000000.0, 1000000.0,
500000.0, 250000.0, 125000.0)
def _read24(arr):
ret = 0.0
for b in arr:
ret *= 256.0
ret += float(b & 0xFF)
return ret
class Adafruit_BME680:
def __init__(self, *, refresh_rate=10):
self._write(_BME680_REG_SOFTRESET, [0xB6])
time.sleep(0.005)
chip_id = self._read_byte(_BME680_REG_CHIPID)
if chip_id != _BME680_CHIPID:
raise RuntimeError('Failed 0x%x' % chip_id)
self._read_calibration()
self._write(_BME680_BME680_RES_HEAT_0, [0x73])
self._write(_BME680_BME680_GAS_WAIT_0, [0x65])
self.sea_level_pressure = 1013.25
self._pressure_oversample = 0b011
self._temp_oversample = 0b100
self._humidity_oversample = 0b010
self._filter = 0b010
self._adc_pres = None
self._adc_temp = None
self._adc_hum = None
self._adc_gas = None
self._gas_range = None
self._t_fine = None
self._last_reading = 0
self._min_refresh_time = 1000 / refresh_rate
@property
def pressure_oversample(self):
return _BME680_SAMPLERATES[self._pressure_oversample]
@pressure_oversample.setter
def pressure_oversample(self, sample_rate):
if sample_rate in _BME680_SAMPLERATES:
self._pressure_oversample = _BME680_SAMPLERATES.index(sample_rate)
else:
raise RuntimeError("Invalid")
@property
def humidity_oversample(self):
return _BME680_SAMPLERATES[self._humidity_oversample]
@humidity_oversample.setter
def humidity_oversample(self, sample_rate):
if sample_rate in _BME680_SAMPLERATES:
self._humidity_oversample = _BME680_SAMPLERATES.index(sample_rate)
else:
raise RuntimeError("Invalid")
@property
def temperature_oversample(self):
return _BME680_SAMPLERATES[self._temp_oversample]
@temperature_oversample.setter
def temperature_oversample(self, sample_rate):
if sample_rate in _BME680_SAMPLERATES:
self._temp_oversample = _BME680_SAMPLERATES.index(sample_rate)
else:
raise RuntimeError("Invalid")
@property
def filter_size(self):
return _BME680_FILTERSIZES[self._filter]
@filter_size.setter
def filter_size(self, size):
if size in _BME680_FILTERSIZES:
self._filter = _BME680_FILTERSIZES[size]
else:
raise RuntimeError("Invalid")
@property
def temperature(self):
self._perform_reading()
calc_temp = (((self._t_fine * 5) + 128) / 256)
return calc_temp / 100
@property
def pressure(self):
self._perform_reading()
var1 = (self._t_fine / 2) - 64000
var2 = ((var1 / 4) * (var1 / 4)) / 2048
var2 = (var2 * self._pressure_calibration[5]) / 4
var2 = var2 + (var1 * self._pressure_calibration[4] * 2)
var2 = (var2 / 4) + (self._pressure_calibration[3] * 65536)
var1 = (((((var1 / 4) * (var1 / 4)) / 8192) *
(self._pressure_calibration[2] * 32) / 8) +
((self._pressure_calibration[1] * var1) / 2))
var1 = var1 / 262144
var1 = ((32768 + var1) * self._pressure_calibration[0]) / 32768
calc_pres = 1048576 - self._adc_pres
calc_pres = (calc_pres - (var2 / 4096)) * 3125
calc_pres = (calc_pres / var1) * 2
var1 = (self._pressure_calibration[8] * (((calc_pres / 8) * (calc_pres / 8)) / 8192)) / 4096
var2 = ((calc_pres / 4) * self._pressure_calibration[7]) / 8192
var3 = (((calc_pres / 256) ** 3) * self._pressure_calibration[9]) / 131072
calc_pres += ((var1 + var2 + var3 + (self._pressure_calibration[6] * 128)) / 16)
return calc_pres/100
@property
def humidity(self):
self._perform_reading()
temp_scaled = ((self._t_fine * 5) + 128) / 256
var1 = ((self._adc_hum - (self._humidity_calibration[0] * 16)) -
((temp_scaled * self._humidity_calibration[2]) / 200))
var2 = (self._humidity_calibration[1] *
(((temp_scaled * self._humidity_calibration[3]) / 100) +
(((temp_scaled * ((temp_scaled * self._humidity_calibration[4]) / 100)) /
64) / 100) + 16384)) / 1024
var3 = var1 * var2
var4 = self._humidity_calibration[5] * 128
var4 = (var4 + ((temp_scaled * self._humidity_calibration[6]) / 100)) / 16
var5 = ((var3 / 16384) * (var3 / 16384)) / 1024
var6 = (var4 * var5) / 2
calc_hum = (((var3 + var6) / 1024) * 1000) / 4096
calc_hum /= 1000
if calc_hum > 100:
calc_hum = 100
if calc_hum < 0:
calc_hum = 0
return calc_hum
@property
def altitude(self):
pressure = self.pressure
return 44330 * (1.0 - math.pow(pressure / self.sea_level_pressure, 0.1903))
@property
def gas(self):
self._perform_reading()
var1 = ((1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._gas_range])) / 65536
var2 = ((self._adc_gas * 32768) - 16777216) + var1
var3 = (_LOOKUP_TABLE_2[self._gas_range] * var1) / 512
calc_gas_res = (var3 + (var2 / 2)) / var2
return int(calc_gas_res)
def _perform_reading(self):
if (time.ticks_diff(self._last_reading, time.ticks_ms()) * time.ticks_diff(0, 1)
< self._min_refresh_time):
return
self._write(_BME680_REG_CONFIG, [self._filter << 2])
self._write(_BME680_REG_CTRL_MEAS,
[(self._temp_oversample << 5)|(self._pressure_oversample << 2)])
self._write(_BME680_REG_CTRL_HUM, [self._humidity_oversample])
self._write(_BME680_REG_CTRL_GAS, [_BME680_RUNGAS])
ctrl = self._read_byte(_BME680_REG_CTRL_MEAS)
ctrl = (ctrl & 0xFC) | 0x01
self._write(_BME680_REG_CTRL_MEAS, [ctrl])
new_data = False
while not new_data:
data = self._read(_BME680_REG_MEAS_STATUS, 15)
new_data = data[0] & 0x80 != 0
time.sleep(0.005)
self._last_reading = time.ticks_ms()
self._adc_pres = _read24(data[2:5]) / 16
self._adc_temp = _read24(data[5:8]) / 16
self._adc_hum = struct.unpack('>H', bytes(data[8:10]))[0]
self._adc_gas = int(struct.unpack('>H', bytes(data[13:15]))[0] / 64)
self._gas_range = data[14] & 0x0F
var1 = (self._adc_temp / 8) - (self._temp_calibration[0] * 2)
var2 = (var1 * self._temp_calibration[1]) / 2048
var3 = ((var1 / 2) * (var1 / 2)) / 4096
var3 = (var3 * self._temp_calibration[2] * 16) / 16384
self._t_fine = int(var2 + var3)
def _read_calibration(self):
coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25)
coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16)
coeff = list(struct.unpack('<hbBHhbBhhbbHhhBBBHbbbBbHhbb', bytes(coeff[1:39])))
coeff = [float(i) for i in coeff]
self._temp_calibration = [coeff[x] for x in [23, 0, 1]]
self._pressure_calibration = [coeff[x] for x in [3, 4, 5, 7, 8, 10, 9, 12, 13, 14]]
self._humidity_calibration = [coeff[x] for x in [17, 16, 18, 19, 20, 21, 22]]
self._gas_calibration = [coeff[x] for x in [25, 24, 26]]
self._humidity_calibration[1] *= 16
self._humidity_calibration[1] += self._humidity_calibration[0] % 16
self._humidity_calibration[0] /= 16
self._heat_range = (self._read_byte(0x02) & 0x30) / 16
self._heat_val = self._read_byte(0x00)
self._sw_err = (self._read_byte(0x04) & 0xF0) / 16
def _read_byte(self, register):
return self._read(register, 1)[0]
def _read(self, register, length):
raise NotImplementedError()
def _write(self, register, values):
raise NotImplementedError()
class BME680_I2C(Adafruit_BME680):
def __init__(self, i2c, address=0x77, debug=False, *, refresh_rate=10):
self._i2c = i2c
self._address = address
self._debug = debug
super().__init__(refresh_rate=refresh_rate)
def _read(self, register, length):
result = bytearray(length)
self._i2c.readfrom_mem_into(self._address, register & 0xff, result)
if self._debug:
print("\t${:x} read ".format(register), " ".join(["{:02x}".format(i) for i in result]))
return result
def _write(self, register, values):
if self._debug:
print("\t${:x} write".format(register), " ".join(["{:02x}".format(i) for i in values]))
for value in values:
self._i2c.writeto_mem(self._address, register, bytearray([value & 0xFF]))
register += 1
Schematic: ESP32 with BME680
Wire the BME680 sensor to the ESP32 development board as shown in the following schematic diagram.
Learn how to use the ESP32 GPIOs with our guide: ESP32 Pinout Reference: Which GPIO pins should you use?
Schematic: ESP8266 NodeMCU with BME680
If you’re using an ESP8266 NodeMCU, follow the next diagram instead.
Learn how to use the ESP8266 GPIOs with our guide: ESP8266 Pinout Reference: Which GPIO pins should you use?
Code
After uploading the libraries to the ESP32 or ESP8266, copy the following code to the main.py file. It publishes the temperature, humidity and pressure on the esp/bme680/temperature, esp/bme680/humidity, esp/bme680/pressure, and esp/bme680/gas topics every 5 seconds.
# Complete project details at https://RandomNerdTutorials.com/micropython-mqtt-publish-bme680-esp32-esp8266/
import time
from umqttsimple import MQTTClient
import ubinascii
import machine
import micropython
import network
import esp
from bme680 import *
from machine import Pin, I2C
esp.osdebug(None)
import gc
gc.collect()
ssid = 'REPLACE_WITH_YOUR_SSID'
password = 'REPLACE_WITH_YOUR_PASSWORD'
mqtt_server = 'XXX.XXX.XXX.XXX'
#EXAMPLE IP ADDRESS
#mqtt_server = '192.168.1.106'
client_id = ubinascii.hexlify(machine.unique_id())
topic_pub_temp = b'esp/bme680/temperature'
topic_pub_hum = b'esp/bme680/humidity'
topic_pub_pres = b'esp/bme680/pressure'
topic_pub_gas = b'esp/bme680/gas'
last_message = 0
message_interval = 5
station = network.WLAN(network.STA_IF)
station.active(True)
station.connect(ssid, password)
while station.isconnected() == False:
pass
print('Connection successful')
# ESP32 - Pin assignment
#i2c = I2C(scl=Pin(22), sda=Pin(21))
# ESP8266 - Pin assignment
i2c = I2C(scl=Pin(5), sda=Pin(4))
bme = BME680_I2C(i2c=i2c)
def connect_mqtt():
global client_id, mqtt_server
client = MQTTClient(client_id, mqtt_server)
#client = MQTTClient(client_id, mqtt_server, user=your_username, password=your_password)
client.connect()
print('Connected to %s MQTT broker' % (mqtt_server))
return client
def restart_and_reconnect():
print('Failed to connect to MQTT broker. Reconnecting...')
time.sleep(10)
machine.reset()
def read_bme_sensor():
try:
temp = (b'{:.2f}'.format(bme.temperature))
#temp = (b'{0:.2f}'.format((bme.temperature) * (9/5) + 32))
hum = (b'{:.2f}'.format(bme.humidity))
pres = (b'{:.2f}'.format(bme.pressure))
gas = (b'{:.2f}'.format(bme.gas/1000))
return temp, hum, pres, gas
#else:
# return('Invalid sensor readings.')
except OSError as e:
return('Failed to read sensor.')
try:
client = connect_mqtt()
except OSError as e:
restart_and_reconnect()
while True:
try:
if (time.time() - last_message) > message_interval:
temp, hum, pres, gas = read_bme_sensor()
print(temp)
print(hum)
print(pres)
print(gas)
client.publish(topic_pub_temp, temp)
client.publish(topic_pub_hum, hum)
client.publish(topic_pub_pres, pres)
client.publish(topic_pub_gas, gas)
last_message = time.time()
except OSError as e:
restart_and_reconnect()
How the Code Works
Import the following libraries:
import time
from umqttsimple import MQTTClient
import ubinascii
import machine
import micropython
import network
import esp
from bme680 import *
from machine import Pin, I2C
In the following variables, you need to enter your network credentials and your broker IP address.
ssid = 'REPLACE_WITH_YOUR_SSID'
password = 'REPLACE_WITH_YOUR_PASSWORD'
mqtt_server = 'REPLACE_WITH_YOUR_MQTT_BROKER_IP'
For example, our broker IP address is: 192.168.1.106.
mqtt_server = '192.168.1.106'
Note: read this tutorial to see how to get your broker IP address.
To create an MQTT client, we need to get the ESP unique ID. That’s what we do in the following line (it is saved on the client_id variable).
client_id = ubinascii.hexlify(machine.unique_id())
Next, create the topics you want your ESP to be publishing in. In our example, it will publish temperature on the esp/bme680/temperature topic, humidity on the esp/bme680/humidity topic, pressure on the esp/bme680/pressure topic, and gas on the esp/bme680/gas topic .
topic_pub_temp = b'esp/bme680/temperature'
topic_pub_hum = b'esp/bme680/humidity'
topic_pub_pres = b'esp/bme680/pressure'
topic_pub_gas = b'esp/bme680/gas'
Then, create the following variables:
last_message = 0
message_interval = 5
The last_message variable will hold the last time a message was sent. The message_interval is the time between each message sent. Here, we’re setting it to 5 seconds (this means a new message will be sent every 5 seconds). You can change it, if you want.
After that, connect the ESP to your local network.
station = network.WLAN(network.STA_IF)
station.active(True)
station.connect(ssid, password)
while station.isconnected() == False:
pass
print('Connection successful')
Create an i2c instance on the ESP32 I2C pins to communicate with the BME680 sensor:
i2c = I2C(scl=Pin(22), sda=Pin(21))
If you’re using an ESP8266, use the following line instead (to use the ESP8266 default I2C pins).
i2c = I2C(scl=Pin(5), sda=Pin(4))
Create a BME680 instance on the ESP I2C pins:
bme = BME680_I2C(i2c=i2c)
Connect to MQTT Broker
The connect_mqtt() function creates an MQTT Client and connects to your broker.
def connect_mqtt():
global client_id, mqtt_server
client = MQTTClient(client_id, mqtt_server)
#client = MQTTClient(client_id, mqtt_server, user=your_username, password=your_password)
client.connect()
print('Connected to %s MQTT broker' % (mqtt_server))
return client
If your MQTT broker requires username and password, you should use the following line to pass your broker username and password as arguments.
client = MQTTClient(client_id, mqtt_server, user=your_username, password=your_password)
Restart and Reconnect
The restart_and_reconnect() function resets the ESP32/ESP8266 board. This function will be called if we’re not able to publish the readings via MQTT in case the broker disconnects.
def restart_and_reconnect():
print('Failed to connect to MQTT broker. Reconnecting...')
time.sleep(10)
machine.reset()
Read BME680 Sensor
We created a function called read_bme_sensor() that returns the current temperature, humidity and pressure readings from the BME680 sensor and handles any exceptions, in case we’re not able to get readings from the sensor.
def read_bme_sensor():
try:
temp = (b'{:.2f}'.format(bme.temperature))
#temp = (b'{0:.2f}'.format((bme.temperature) * (9/5) + 32))
hum = (b'{:.2f}'.format(bme.humidity))
pres = (b'{:.2f}'.format(bme.pressure))
gas = (b'{:.2f}'.format(bme.gas/1000))
return temp, hum, pres, gas
#else:
# return('Invalid sensor readings.')
except OSError as e:
return('Failed to read sensor.')
Publishing MQTT Messages
In the while loop, we publish new BME680 readings every 5 seconds.
First, we check if it is time to get new readings:
if (time.time() - last_message) > message_interval:
If it is, request new readings from the BME680 sensor by calling the read_bme_sensor() function. The temperature is saved on the temp variable, the humidity is saved on the hum variable and the pressure on the pres variable.
temp, hum, pres, gas = read_bme_sensor()
Finally, publish the readings by using the publish() method on the client object. The publish() method accepts as arguments the topic and the message, as follows:
client.publish(topic_pub_temp, temp)
client.publish(topic_pub_hum, hum)
client.publish(topic_pub_pres, pres)
client.publish(topic_pub_gas, gas)
Finally, update the time when the last message was sent:
last_message = time.time()
In case the ESP32 or ESP8266 disconnects from the broker, and we’re not able to publish the readings, call the restart_and_reconnect() function to reset the ESP board and try to reconnect to the broker.
except OSError as e:
restart_and_reconnect()
After uploading the code, you should get new sensor readings on the shell every 5 seconds.
Now, go to the next section to prepare Node-RED to receive the readings the ESP is publishing.
Preparing Node-RED Dashboard
The ESP32 or ESP8266 is publishing temperature readings every 10 seconds on the esp/bme680/temperature, esp/bme680/humidity, esp/bme680/pressure and esp/bme680/gas topics. Now, you can use any dashboard that supports MQTT or any other device that supports MQTT to subscribe to those topics and receive the readings.
As an example, we’ll create a simple flow using Node-RED to subscribe to those topics and display the readings on gauges.
If you don’t have Node-RED installed, follow the next tutorials:
Having Node-RED running on your Raspberry Pi, go to your Raspberry Pi IP address followed by :1880.
http://raspberry-pi-ip-address:1880
The Node-RED interface should open. Drag four MQTT in nodes, two gauge nodes, and two text fields to the flow.
Click the MQTT node and edit its properties.
The Server field refers to the MQTT broker. In our case, the MQTT broker is the Raspberry Pi, so it is set to localhost:1883. If you’re using a Cloud MQTT broker, you should change that field.
Insert the topic you want to be subscribed to and the QoS. This previous MQTT node is subscribed to the esp/bme680/temperature topic.
Click on the other MQTT in nodes and edit its properties with the same server, but for the other topics: esp/bme680/humidity, esp/bme680/pressure, and esp/bme680/gas.
Click on the gauge nodes and edit its properties for each reading. The following node is set for the temperature readings. Edit the other chart node for the humidity readings.
Wire your nodes as shown below:
Finally, deploy your flow (press the button on the upper right corner).
Alternatively, you can go to Menu > Import and copy the following to your Clipboard to create your Node-RED flow.
[{"id":"3b7f947c.9759ec","type":"mqtt in","z":"254c9c97.f85b34","name":"","topic":"esp/bme680/temperature","qos":"1","datatype":"auto","broker":"8db3fac0.99dd48","x":470,"y":2640,"wires":[["b87b21c3.96672"]]},{"id":"b87b21c3.96672","type":"ui_gauge","z":"254c9c97.f85b34","name":"","group":"37de8fe8.46846","order":2,"width":0,"height":0,"gtype":"gage","title":"Temperature","label":"ºC","format":"{{value}}","min":0,"max":"40","colors":["#00b500","#f7df09","#ca3838"],"seg1":"","seg2":"","x":690,"y":2640,"wires":[]},{"id":"f92248f4.545778","type":"mqtt in","z":"254c9c97.f85b34","name":"","topic":"esp/bme680/humidity","qos":"1","datatype":"auto","broker":"8db3fac0.99dd48","x":460,"y":2700,"wires":[["4114a401.5ac69c"]]},{"id":"4114a401.5ac69c","type":"ui_gauge","z":"254c9c97.f85b34","name":"","group":"37de8fe8.46846","order":2,"width":0,"height":0,"gtype":"gage","title":"Humidity","label":"%","format":"{{value}}","min":"30","max":"100","colors":["#53a4e6","#1d78a9","#4e38c9"],"seg1":"","seg2":"","x":680,"y":2700,"wires":[]},{"id":"ad51f895.2c2848","type":"mqtt in","z":"254c9c97.f85b34","name":"","topic":"esp/bme680/pressure","qos":"1","datatype":"auto","broker":"8db3fac0.99dd48","x":460,"y":2760,"wires":[["3a95123b.66405e"]]},{"id":"c074e688.198b78","type":"mqtt in","z":"254c9c97.f85b34","name":"","topic":"esp/bme680/gas","qos":"1","datatype":"auto","broker":"8db3fac0.99dd48","x":440,"y":2820,"wires":[["d3539c06.00a17"]]},{"id":"3a95123b.66405e","type":"ui_text","z":"254c9c97.f85b34","group":"37de8fe8.46846","order":2,"width":0,"height":0,"name":"","label":"Pressure","format":"{{msg.payload}} hPa","layout":"row-spread","x":680,"y":2760,"wires":[]},{"id":"d3539c06.00a17","type":"ui_text","z":"254c9c97.f85b34","group":"37de8fe8.46846","order":3,"width":0,"height":0,"name":"","label":"Gas","format":"{{msg.payload}} KOhm","layout":"row-spread","x":670,"y":2820,"wires":[]},{"id":"8db3fac0.99dd48","type":"mqtt-broker","z":"","name":"","broker":"localhost","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"37de8fe8.46846","type":"ui_group","z":"","name":"BME680","tab":"53b8c8f9.cfbe48","order":1,"disp":true,"width":"6","collapse":false},{"id":"53b8c8f9.cfbe48","type":"ui_tab","z":"","name":"Home","icon":"dashboard","order":5,"disabled":false,"hidden":false}]
Demonstration
Go to your Raspberry Pi IP address followed by :1880/ui.
http://raspberry-pi-ip-address:1880/ui
You should get access to the current BME680 temperature, humidity and pressure readings on the Dashboard. You can use other dashboard-type nodes to display the readings on different ways.
That’s it! You have your ESP32 or ESP8266 boards publishing BME680 temperature, humidity and pressure readings to Node-RED via MQTT using MicroPython.
Wrapping Up
MQTT is a great communication protocol to exchange small amounts of data between IoT devices. In this tutorial you’ve learned how to publish readings from a BME680 sensor with the ESP32 and ESP8266 using MicroPython to different MQTT topics. Then, you can use any device or home automation platform to subscribe to those topics and receive the readings.
Instead of a BME680 sensor, you can use any other sensor like DHT11 or DHT22 sensor or DS18B20 temperature sensor.
We have other projects/tutorials related with the BME680 sensor that you may also like:
- MicroPython: BME280 with ESP32 and ESP8266 (Pressure, Temperature, Humidity)
- Low Power Weather Station Datalogger using ESP8266 and BME280 with MicroPython
Learn more about MicroPython with our eBook: MicroPython Programming using ESP32/ESP8266.
Thank you for sharing
Very clear. Thanks a lot!
For me occured that my mqtt broker is required username and password, but in explication you appointed:
“If your MQTT broker requires username and password, you should use the following line to pass your broker username and password as arguments.
client = MQTTClient(client_id, mqtt_server, user=your_username, password=your_password)”
Not that simple, have used follow code:
client = MQTTClient(client_id, mqtt_server, user=b’your_username’, password=b’your_password’)
I didn’t figure out how to change the vertical and horizontal layout of icons in the NR Dashboard. Thanks.
Here’s a tip, for this and previous and future code:
Click on the [View Raw Code] button. That’ll open up the code in a new tab.
Save the page — Ctrl-S should be the easiest way.
Select the directory where you want to save the file. The filename’s already provided by default, e.g., umqttsimple.py
Open your favorite IDE and load the file
Just curious about this:
if (time.time() – last_message) > message_interval:
temp, hum = read_dht_sensor() // my mod, using a DHT11
print(temp)
print(hum)
print(pres) // my mod as well, including the next line
print(gas)
client.publish(topic_pub_temp, temp)
client.publish(topic_pub_hum, hum)
last_message = time.time() time.sleep(30)
except OSError as e:
restart_and_reconnect()
Why not use time.sleep() instead of comparing (time.time() – last_message) with message_interval? I used sleep(message_interval) and seem to be getting the same results
Hmm…
Lines following the if and except *OSError statements are supposed to be indented
print(pres) and print(gas) were supposed to be commented out.
Look like there’s still a lot to learn about your commenting setup
Hmm…
Lines following the if and except *OSError statements are supposed to be indented
print(pres) and print(gas) were supposed to be commented out.
Look like there’s still a lot to learn about your commenting setup
Sir, thanks a lot for your advice and comments, except for the very last sentence, do you think you invented the wheel? Man learns all his life.