This tutorial is a step-by-step guide that covers how to build a standalone ESP32 or ESP8266 NodeMCU Web Server that displays BME680 sensor readings using MicroPython firmware. We’ll create an ESP32/ESP8266 Web Server that is mobile responsive and it can be accessed with any device with a browser in your local network.
If you want to learn how a relay module works, read our MicroPython Guide: BME680 with ESP32 and ESP8266 (Temperature, Humidity, Pressure, Gas).
We have similar guides using Arduino IDE:
- ESP32: BME680 Environmental Sensor using Arduino IDE (Gas, Pressure, Humidity, Temperature)
- ESP8266 NodeMCU: BME680 Environmental Sensor using Arduino IDE (Gas, Pressure, Humidity, Temperature)
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
Learn more about MicroPython: MicroPython Programming with ESP32 and ESP8266 eBook.
BME680 Environmental Sensor Module
The BME680 is an environmental sensor that combines gas, pressure, humidity and temperature sensors. The gas sensor can detect a broad range of gases like volatile organic compounds (VOC). For this reason, the BME680 can be used in indoor air quality control.
The BME680 supports I2C and SPI Interfaces.
BME680 I2C
This sensor communicates using I2C communication protocol, so the wiring is very simple. You can use the default ESP32 or ESP8266 I2C pins as shown in the following table:
BME680 | ESP32 | ESP8266 |
Vin | 3.3V | 3.3V |
GND | GND | GND |
SCL | GPIO 22 | GPIO 5 (D1) |
SDA | GPIO 21 | GPIO 4 (D2) |
Parts Required
For this project you need to wire the BME680 sensor module to the ESP32 or ESP8266 I2C pins. Here’s a list of parts you need for this tutorial:
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!
Schematic – ESP32
Follow the next schematic diagram if you’re using an ESP32 board:
Recommended reading: ESP32 Pinout Reference Guide
Schematic – ESP8266
Follow the next schematic diagram if you’re using an ESP8266 board:
Recommended reading: ESP8266 Pinout Reference Guide
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
Follow the next set of instructions for the IDE you’re using:
- A. Upload BME680 library with uPyCraft IDE
- B. Upload BME680 library with Thonny IDE
A. Upload BME680 library with uPyCraft IDE
This section shows how to upload a library using uPyCraft IDE. If you’re using Thonny IDE, read the next section.
1. Create a new file by pressing the New File button (1).
2. Copy the BME680 library code into that file. The BME680 library code can be found here.
3. After copying the code, save the file by pressing the Save button (2).
4. Call this new file “bme680.py” and press ok.
5. Click the Download and Run button.
The file should be saved on the device folder with the name “bme680.py” as highlighted in the following figure.
Now, you can use the library functionalities in your code by importing the library.
B. Upload BME680 library with Thonny IDE
If you’re using Thonny IDE, follow the next steps:
1. Copy the library code to a new file. The BME680 library code can be found here.
2. Go to File > Save as…
3. Select save to “MicroPython device“:
4. Name your file as bme680.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.
Code Web Server – BME680 Temperature, Humidity, Pressure, and Gas Air Quality
Now that you know how to get temperature, humidity, pressure and gas from the BME680 sensor, we’ll display the sensor readings on a web server that you can access on your local network.
For this example, you need three files:
- bme680.py: this is the file that contains all the methods to use the BME680 sensor. That’s the file you’ve uploaded previously.
- boot.py: runs when the device starts and sets up several configuration options like your network credentials, importing libraries, setting the pins, etc.
- main.py: this is the main script where we’ll handle the web server. It executes immediately after the boot.py.
Note: It is a good practice to include the boot.py and main.py files. However, if you prefer, you can include all the code in the main.py file.
boot.py
Create a new file in your IDE called boot.py and copy the following code.
# Complete project details at https://RandomNerdTutorials.com/micropython-bme680-esp32-esp8266/
try:
import usocket as socket
except:
import socket
from time import sleep
from machine import Pin, I2C
import network
import esp
esp.osdebug(None)
import gc
gc.collect()
from bme680 import *
# ESP32 - Pin assignment
i2c = I2C(scl=Pin(22), sda=Pin(21))
# ESP8266 - Pin assignment
#i2c = I2C(scl=Pin(5), sda=Pin(4))
ssid = 'REPLACE_WITH_YOUR_SSID'
password = 'REPLACE_WITH_YOUR_PASSWORD'
station = network.WLAN(network.STA_IF)
station.active(True)
station.connect(ssid, password)
while station.isconnected() == False:
pass
print('Connection successful')
print(station.ifconfig())
This file imports the necessary libraries, defines the I2C pins to connect to the sensor and connects to your network.
In the code, we’re using the ESP32 I2C pins:
i2c = I2C(scl=Pin(22), sda=Pin(21))
If you’re using the ESP8266, comment the previous line and uncomment the following:
i2c = I2C(scl=Pin(5), sda=Pin(4))
Then, insert your network credentials in the following variables:
ssid = 'REPLACE_WITH_YOUR_SSID'
password = 'REPLACE_WITH_YOUR_PASSWORD'
main.py
In the main.py file is where we’ll create the web server and handle the requests. Copy the following code to your main.py file.
# Complete project details at https://RandomNerdTutorials.com/micropython-bme680-esp32-esp8266/
def web_page():
bme = BME680_I2C(i2c=i2c)
html = """<html><head><title>ESP with BME680</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,"><style>body { text-align: center; font-family: "Trebuchet MS", Arial;}
table { border-collapse: collapse; margin-left:auto; margin-right:auto; }
th { padding: 12px; background-color: #0043af; color: white; }
tr { border: 1px solid #ddd; padding: 12px; }
tr:hover { background-color: #bcbcbc; }
td { border: none; padding: 12px; }
.sensor { color:white; font-weight: bold; background-color: #bcbcbc; padding: 1px;
</style></head><body><h1>ESP with BME680</h1>
<table><tr><th>MEASUREMENT</th><th>VALUE</th></tr>
<tr><td>Temp. Celsius</td><td><span class="sensor">""" + str(round(bme.temperature, 2)) + """ C</span></td></tr>
<tr><td>Temp. Fahrenheit</td><td><span class="sensor">""" + str(round((bme.temperature) * (9/5) + 32, 2)) + """ F</span></td></tr>
<tr><td>Pressure</td><td><span class="sensor">""" + str(round(bme.pressure, 2)) + """ hPa</span></td></tr>
<tr><td>Humidity</td><td><span class="sensor">""" + str(round(bme.humidity, 2)) + """ %</span></td></tr>
<tr><td>Gas</td><td><span class="sensor">""" + str(round(bme.gas/1000, 2)) + """ KOhms</span></td></tr></body></html>"""
return html
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('', 80))
s.listen(5)
while True:
try:
if gc.mem_free() < 102000:
gc.collect()
conn, addr = s.accept()
conn.settimeout(3.0)
print('Got a connection from %s' % str(addr))
request = conn.recv(1024)
conn.settimeout(None)
request = str(request)
print('Content = %s' % request)
response = web_page()
conn.send('HTTP/1.1 200 OK\n')
conn.send('Content-Type: text/html\n')
conn.send('Connection: close\n\n')
conn.sendall(response)
conn.close()
except OSError as e:
conn.close()
print('Connection closed')
This code creates a socket server that sends an HTML page with the latest sensor readings when it receives a request on the ESP32 or ESP8266 IP address.
Basically, we have a function called web_page() that returns the HTML to build up the web page with the latest sensor readings. This HMTL text builds a table to display the readings:
def web_page():
bme = BME680_I2C(i2c=i2c)
html = """<html><head><title>ESP with BME680</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,"><style>body { text-align: center; font-family: "Trebuchet MS", Arial;}
table { border-collapse: collapse; margin-left:auto; margin-right:auto; }
th { padding: 12px; background-color: #0043af; color: white; }
tr { border: 1px solid #ddd; padding: 12px; }
tr:hover { background-color: #bcbcbc; }
td { border: none; padding: 12px; }
.sensor { color:white; font-weight: bold; background-color: #bcbcbc; padding: 1px;
</style></head><body><h1>ESP with BME680</h1>
<table><tr><th>MEASUREMENT</th><th>VALUE</th></tr>
<tr><td>Temp. Celsius</td><td><span class="sensor">""" + str(round(bme.temperature, 2)) + """ C</span></td></tr>
<tr><td>Temp. Fahrenheit</td><td><span class="sensor">""" + str(round((bme.temperature) * (9/5) + 32, 2)) + """ F</span></td></tr>
<tr><td>Pressure</td><td><span class="sensor">""" + str(round(bme.pressure, 2)) + """ hPa</span></td></tr>
<tr><td>Humidity</td><td><span class="sensor">""" + str(round(bme.humidity, 2)) + """ %</span></td></tr>
<tr><td>Gas</td><td><span class="sensor">""" + str(round(bme.gas/1000, 2)) + """ KOhms</span></td></tr></body></html>"""
return html
Then, we create a socket server that sends the HTML when it gets a request. The HTML text is then saved on the response variable:
response = web_page()
And sent to the client:
conn.sendall(response)
We’ve explained in great detail how these kind of web servers work in previous tutorials. So, if you want to learn how it works, you can read the following articles:
- MicroPython: ESP32/ESP8266 with DHT11/DHT22 Web Server
- ESP32/ESP8266 MicroPython Web Server – Control Outputs
Web Server Demonstration
Upload all the previous files to your ESP32 or ESP8266 board in the following order:
- bme680.py
- boot.py
- main.py
If you don’t know how to upload code, you can read our getting started guides with uPyCraft IDE, or Thonny IDE:
- Getting Started with Thonny MicroPython (Python) IDE for ESP32 and ESP8266
- Getting Started with MicroPython on ESP32 and ESP8266 (uPyCraft IDE)
After uploading the code, your ESP32 or ESP8266 IP address should be displayed on the Serial Monitor.
Open a web browser in your local network and type your ESP IP address (in our example the IP is http://192.168.1.114). You should get a page with the latest sensor readings as shown in the following figure.
Auto refresh web page
With the web server script provided in this project, you need to refresh the web page to see the latest readings. If you add the next meta tag inside the HTML <head></head> tags, your web page will auto refresh every 10 seconds:
<meta http-equiv="refresh" content="10">
Wrapping Up
We hope you’ve found this tutorial useful. We have other projects and tutorials with MicroPython that you may like:
- Low Power Weather Station Datalogger using ESP8266 and BME280 with MicroPython
- MicroPython: OLED Display with ESP32 and ESP8266
- MicroPython: ESP32/ESP8266 with DHT11/DHT22 Web Server
- MicroPython: WS2812B Addressable RGB LEDs with ESP32 and ESP8266
If you want to learn more about programming the ESP32 and ESP8266 boards with MicroPython, get access to our eBook: MicroPython Programming with ESP32 and ESP8266.
Thanks for reading.
Success! On the first try too. I made some minor format changes, including the addition of the degree symbol. Adding it in BME680.py did not work. I added it as ° in the HTML in main.py.
http://www.orangecaweather.duckdns.org:8002
The sensor is indoors. I will leave it up for a few days.