In this user guide, we will learn how to publish sensor readings to ThingSpeak using Micropython and ESP32. We will use Thonny IDE to program our ESP32 board which will be connected to a temperature, humidity, and pressure sensor. Our main aim is to transmit these sensor readings to ThingSpeak easily and interactively demonstrate them. Any suitable sensor can be used such as DS18B20, BME680, LM35, and MPU6050 but for this article, we will use a BME280 sensor which is used to gauge temperature, pressure, and humidity.
We will cover the subsequent content in this article:
- Introduction to BME280 sensor
- Connecting BME280 sensor with the ESP32 development board
- Getting ThingSpeak API Ready
- Publish multiple fields of sensor readings to ThingSpeak (Temperature, humidity, and pressure)
Prerequisites
Before we start this lesson make sure you are familiar with and have the latest version of MicroPython firmware installed in your ESP32 board and have a running Integrated Development Environment (IDE) in which we will be doing the programming such as uPyCraft IDE or Thonny IDE.
- Getting Started with uPyCraft IDE on ESP32 and ESP8266
- Getting Started with Thonny MicroPython IDE for ESP32 and ESP8266
If you want to use VS Code, you can follow this guide:
- MicroPython ESP32 and ESP8266: Program with VS Code and Pymakr
- Flash MicroPython Firmware with esptool.py to ESP32 and ESP8266
BME280 sensor Introduction
The BME280 sensor is used to measure readings regarding ambient temperature, barometric pressure, and relative humidity. It is mostly used in web and mobile applications where low power consumption is key. This sensor uses I2C or SPI to communicate data with the micro-controllers. Although there are several different versions of BME280 available in the market, the one we will be studying uses the I2C communication protocol and SPI.
Recommended Readings:
Connecting BME280 sensor with the ESP32 development board
The connection of BME280 with the ESP32 boards is very easy. We have to connect the VCC terminal with 3.3V, ground with the ground (common ground), SCL of the sensor with SCL of the module, and SDA of the sensor with the SDA pin of the ESP modules.
The I2C pin in ESP32 for SDA is GPIO21 and for SCL is GPIO22.
Required Components:
We will need the following components to connect our ESP32 board with the BME280 sensor.
- ESP32 board
- BME280 Sensor
- Connecting Wires
- Breadboard
Follow the schematic diagram below for the ESP32 module and connect them accordingly.
In some BME280 sensors as seen in the above connection diagram, the SCK terminal means the SCL pin and is connected with its respective GPIO pin on the ESP board. Likewise, the SDI terminal means the SDA pin and is connected with its respective GPIO pin on the board. Vin is connected with a 3.3V pin on the module and both the ESP board and the sensor is commonly grounded.
BME280 MicroPython Library
By default, MicroPython does not have an implementation of the BME280 library. But, MicroPyhon provides I2C API of ESP32 and ESP8266 which can be used to read the temperature, humidity, and pressure values from the BME280 sensor. Fortunately, there is one library available which is developed by Robert and can be downloaded from this link.
We will use Thonny IDE to program our ESP32. Make sure you have the latest version of the IDE installed in your system. Open a new file in Thonny. Copy the library given below. Save it to ESP32 with the name BME280.py under the lib folder.
from machine import I2C
import time
# BME280 default address.
BME280_I2CADDR = 0x76
# Operating Modes
BME280_OSAMPLE_1 = 1
BME280_OSAMPLE_2 = 2
BME280_OSAMPLE_4 = 3
BME280_OSAMPLE_8 = 4
BME280_OSAMPLE_16 = 5
# BME280 Registers
BME280_REGISTER_DIG_T1 = 0x88 # Trimming parameter registers
BME280_REGISTER_DIG_T2 = 0x8A
BME280_REGISTER_DIG_T3 = 0x8C
BME280_REGISTER_DIG_P1 = 0x8E
BME280_REGISTER_DIG_P2 = 0x90
BME280_REGISTER_DIG_P3 = 0x92
BME280_REGISTER_DIG_P4 = 0x94
BME280_REGISTER_DIG_P5 = 0x96
BME280_REGISTER_DIG_P6 = 0x98
BME280_REGISTER_DIG_P7 = 0x9A
BME280_REGISTER_DIG_P8 = 0x9C
BME280_REGISTER_DIG_P9 = 0x9E
BME280_REGISTER_DIG_H1 = 0xA1
BME280_REGISTER_DIG_H2 = 0xE1
BME280_REGISTER_DIG_H3 = 0xE3
BME280_REGISTER_DIG_H4 = 0xE4
BME280_REGISTER_DIG_H5 = 0xE5
BME280_REGISTER_DIG_H6 = 0xE6
BME280_REGISTER_DIG_H7 = 0xE7
BME280_REGISTER_CHIPID = 0xD0
BME280_REGISTER_VERSION = 0xD1
BME280_REGISTER_SOFTRESET = 0xE0
BME280_REGISTER_CONTROL_HUM = 0xF2
BME280_REGISTER_CONTROL = 0xF4
BME280_REGISTER_CONFIG = 0xF5
BME280_REGISTER_PRESSURE_DATA = 0xF7
BME280_REGISTER_TEMP_DATA = 0xFA
BME280_REGISTER_HUMIDITY_DATA = 0xFD
class Device:
"""Class for communicating with an I2C device.
Allows reading and writing 8-bit, 16-bit, and byte array values to
registers on the device."""
def __init__(self, address, i2c):
"""Create an instance of the I2C device at the specified address using
the specified I2C interface object."""
self._address = address
self._i2c = i2c
def writeRaw8(self, value):
"""Write an 8-bit value on the bus (without register)."""
value = value & 0xFF
self._i2c.writeto(self._address, value)
def write8(self, register, value):
"""Write an 8-bit value to the specified register."""
b=bytearray(1)
b[0]=value & 0xFF
self._i2c.writeto_mem(self._address, register, b)
def write16(self, register, value):
"""Write a 16-bit value to the specified register."""
value = value & 0xFFFF
b=bytearray(2)
b[0]= value & 0xFF
b[1]= (value>>8) & 0xFF
self.i2c.writeto_mem(self._address, register, value)
def readRaw8(self):
"""Read an 8-bit value on the bus (without register)."""
return int.from_bytes(self._i2c.readfrom(self._address, 1),'little') & 0xFF
def readU8(self, register):
"""Read an unsigned byte from the specified register."""
return int.from_bytes(
self._i2c.readfrom_mem(self._address, register, 1),'little') & 0xFF
def readS8(self, register):
"""Read a signed byte from the specified register."""
result = self.readU8(register)
if result > 127:
result -= 256
return result
def readU16(self, register, little_endian=True):
"""Read an unsigned 16-bit value from the specified register, with the
specified endianness (default little endian, or least significant byte
first)."""
result = int.from_bytes(
self._i2c.readfrom_mem(self._address, register, 2),'little') & 0xFFFF
if not little_endian:
result = ((result << 8) & 0xFF00) + (result >> 8)
return result
def readS16(self, register, little_endian=True):
"""Read a signed 16-bit value from the specified register, with the
specified endianness (default little endian, or least significant byte
first)."""
result = self.readU16(register, little_endian)
if result > 32767:
result -= 65536
return result
def readU16LE(self, register):
"""Read an unsigned 16-bit value from the specified register, in little
endian byte order."""
return self.readU16(register, little_endian=True)
def readU16BE(self, register):
"""Read an unsigned 16-bit value from the specified register, in big
endian byte order."""
return self.readU16(register, little_endian=False)
def readS16LE(self, register):
"""Read a signed 16-bit value from the specified register, in little
endian byte order."""
return self.readS16(register, little_endian=True)
def readS16BE(self, register):
"""Read a signed 16-bit value from the specified register, in big
endian byte order."""
return self.readS16(register, little_endian=False)
class BME280:
def __init__(self, mode=BME280_OSAMPLE_1, address=BME280_I2CADDR, i2c=None,
**kwargs):
# Check that mode is valid.
if mode not in [BME280_OSAMPLE_1, BME280_OSAMPLE_2, BME280_OSAMPLE_4,
BME280_OSAMPLE_8, BME280_OSAMPLE_16]:
raise ValueError(
'Unexpected mode value {0}. Set mode to one of '
'BME280_ULTRALOWPOWER, BME280_STANDARD, BME280_HIGHRES, or '
'BME280_ULTRAHIGHRES'.format(mode))
self._mode = mode
# Create I2C device.
if i2c is None:
raise ValueError('An I2C object is required.')
self._device = Device(address, i2c)
# Load calibration values.
self._load_calibration()
self._device.write8(BME280_REGISTER_CONTROL, 0x3F)
self.t_fine = 0
def _load_calibration(self):
self.dig_T1 = self._device.readU16LE(BME280_REGISTER_DIG_T1)
self.dig_T2 = self._device.readS16LE(BME280_REGISTER_DIG_T2)
self.dig_T3 = self._device.readS16LE(BME280_REGISTER_DIG_T3)
self.dig_P1 = self._device.readU16LE(BME280_REGISTER_DIG_P1)
self.dig_P2 = self._device.readS16LE(BME280_REGISTER_DIG_P2)
self.dig_P3 = self._device.readS16LE(BME280_REGISTER_DIG_P3)
self.dig_P4 = self._device.readS16LE(BME280_REGISTER_DIG_P4)
self.dig_P5 = self._device.readS16LE(BME280_REGISTER_DIG_P5)
self.dig_P6 = self._device.readS16LE(BME280_REGISTER_DIG_P6)
self.dig_P7 = self._device.readS16LE(BME280_REGISTER_DIG_P7)
self.dig_P8 = self._device.readS16LE(BME280_REGISTER_DIG_P8)
self.dig_P9 = self._device.readS16LE(BME280_REGISTER_DIG_P9)
self.dig_H1 = self._device.readU8(BME280_REGISTER_DIG_H1)
self.dig_H2 = self._device.readS16LE(BME280_REGISTER_DIG_H2)
self.dig_H3 = self._device.readU8(BME280_REGISTER_DIG_H3)
self.dig_H6 = self._device.readS8(BME280_REGISTER_DIG_H7)
h4 = self._device.readS8(BME280_REGISTER_DIG_H4)
h4 = (h4 << 24) >> 20
self.dig_H4 = h4 | (self._device.readU8(BME280_REGISTER_DIG_H5) & 0x0F)
h5 = self._device.readS8(BME280_REGISTER_DIG_H6)
h5 = (h5 << 24) >> 20
self.dig_H5 = h5 | (
self._device.readU8(BME280_REGISTER_DIG_H5) >> 4 & 0x0F)
def read_raw_temp(self):
"""Reads the raw (uncompensated) temperature from the sensor."""
meas = self._mode
self._device.write8(BME280_REGISTER_CONTROL_HUM, meas)
meas = self._mode << 5 | self._mode << 2 | 1
self._device.write8(BME280_REGISTER_CONTROL, meas)
sleep_time = 1250 + 2300 * (1 << self._mode)
sleep_time = sleep_time + 2300 * (1 << self._mode) + 575
sleep_time = sleep_time + 2300 * (1 << self._mode) + 575
time.sleep_us(sleep_time) # Wait the required time
msb = self._device.readU8(BME280_REGISTER_TEMP_DATA)
lsb = self._device.readU8(BME280_REGISTER_TEMP_DATA + 1)
xlsb = self._device.readU8(BME280_REGISTER_TEMP_DATA + 2)
raw = ((msb << 16) | (lsb << 8) | xlsb) >> 4
return raw
def read_raw_pressure(self):
"""Reads the raw (uncompensated) pressure level from the sensor."""
"""Assumes that the temperature has already been read """
"""i.e. that enough delay has been provided"""
msb = self._device.readU8(BME280_REGISTER_PRESSURE_DATA)
lsb = self._device.readU8(BME280_REGISTER_PRESSURE_DATA + 1)
xlsb = self._device.readU8(BME280_REGISTER_PRESSURE_DATA + 2)
raw = ((msb << 16) | (lsb << 8) | xlsb) >> 4
return raw
def read_raw_humidity(self):
"""Assumes that the temperature has already been read """
"""i.e. that enough delay has been provided"""
msb = self._device.readU8(BME280_REGISTER_HUMIDITY_DATA)
lsb = self._device.readU8(BME280_REGISTER_HUMIDITY_DATA + 1)
raw = (msb << 8) | lsb
return raw
def read_temperature(self):
"""Get the compensated temperature in 0.01 of a degree celsius."""
adc = self.read_raw_temp()
var1 = ((adc >> 3) - (self.dig_T1 << 1)) * (self.dig_T2 >> 11)
var2 = ((
(((adc >> 4) - self.dig_T1) * ((adc >> 4) - self.dig_T1)) >> 12) *
self.dig_T3) >> 14
self.t_fine = var1 + var2
return (self.t_fine * 5 + 128) >> 8
def read_pressure(self):
"""Gets the compensated pressure in Pascals."""
adc = self.read_raw_pressure()
var1 = self.t_fine - 128000
var2 = var1 * var1 * self.dig_P6
var2 = var2 + ((var1 * self.dig_P5) << 17)
var2 = var2 + (self.dig_P4 << 35)
var1 = (((var1 * var1 * self.dig_P3) >> 8) +
((var1 * self.dig_P2) >> 12))
var1 = (((1 << 47) + var1) * self.dig_P1) >> 33
if var1 == 0:
return 0
p = 1048576 - adc
p = (((p << 31) - var2) * 3125) // var1
var1 = (self.dig_P9 * (p >> 13) * (p >> 13)) >> 25
var2 = (self.dig_P8 * p) >> 19
return ((p + var1 + var2) >> 8) + (self.dig_P7 << 4)
def read_humidity(self):
adc = self.read_raw_humidity()
# print 'Raw humidity = {0:d}'.format (adc)
h = self.t_fine - 76800
h = (((((adc << 14) - (self.dig_H4 << 20) - (self.dig_H5 * h)) +
16384) >> 15) * (((((((h * self.dig_H6) >> 10) * (((h *
self.dig_H3) >> 11) + 32768)) >> 10) + 2097152) *
self.dig_H2 + 8192) >> 14))
h = h - (((((h >> 15) * (h >> 15)) >> 7) * self.dig_H1) >> 4)
h = 0 if h < 0 else h
h = 419430400 if h > 419430400 else h
return h >> 12
@property
def temperature(self):
"Return the temperature in degrees."
t = self.read_temperature()
ti = t // 100
td = t - ti * 100
return "{}.{:02d}C".format(ti, td)
@property
def pressure(self):
"Return the temperature in hPa."
p = self.read_pressure() // 256
pi = p // 100
pd = p - pi * 100
return "{}.{:02d}hPa".format(pi, pd)
@property
def humidity(self):
"Return the humidity in percent."
h = self.read_humidity()
hi = h // 1024
hd = h * 100 // 1024 - hi * 100
return "{}.{:02d}%".format(hi, hd)
Getting ThingSpeak API Ready
ThingSpeak is an open-source API that is used to store or retrieve data using HTTP or MQTT protocol. This takes place over the Internet or through the LAN. We will use this API to publish sensor readings from BME280 integrated with our ESP32 board. In ThingSpeak you can access your data from anywhere in the world. You can display your data in plots and graphs.
For more information about ThingSpeak API, you can have a look at our tutorials given below:
- ESP8266 Wi-Fi Module interfacing with Arduino: Send data to server (ThingSpeak)
- ESP32 HTTP POST using Arduino IDE (ThingSpeak and IFTTT)
- HTTP GET using ESP32 and Arduino IDE (OpenWeatherMap.org and ThingSpeak)
Creating Account
ThingSpeak API is free to use but we will have to create a MathWorks Account.
First go to the following website: https://thingspeak.com/
The following window will appear. Click on the ‘Get Started for Free’ button.
Now you will be redirected to the account window. If you already have an existing MathWorks account you can use that to log in. Otherwise, you will have to create a new one. Click ‘Create One!’ to make a new MathWorks account.
When you have successfully signed in you will receive the following notification:
Click ‘OK’.
Publish to multiple fields of sensor readings to ThingSpeak (Temperature, Humidity and Pressure)
We will start by creating a new channel for our project. Go to Channels > My Channels. Then click ‘New Channel’.
You will be prompted to give a name to your channel. We will give a name, some description and mark the first field. In this section, we will show you how to publish multiple data. You can use any name, description, and field according to your preference. There are a total of eight fields that we can add to our channel at the same time. We will tick the first three fields and add the names. Click ‘Save Channel’ to proceed.
Your channel will now be created.
Go to private view and click the pencil icon on top of each Field Chart. This will let us customize the graphs according to our preference. You can add details accordingly. After you press the Save button, the chart will get updated to the settings you just set.
After that go to the API key tab and click it. You will now be able to access your unique API key. Save it and keep it secure as you will need it later in the program code.
ESP32 ThingSpeak MicroPython Sketch
Open your Thonny IDE and go to File > New to open a new file. Copy the code given below in that file. This code will work with your ESP32 board. You just have to replace the network credentials and your API key.
import machine
import urequests
from machine import Pin, SoftI2C
import network, time
import BME280
i2c = SoftI2C(scl=Pin(22), sda=Pin(21), freq=10000) #initializing the I2C method
HTTP_HEADERS = {'Content-Type': 'application/json'}
THINGSPEAK_WRITE_API_KEY = '****************'
UPDATE_TIME_INTERVAL = 5000 # in ms
last_update = time.ticks_ms()
ssid='YOUR_SSID'
password='YOUR_PASSWORD'
# Configure ESP32 as Station
sta_if=network.WLAN(network.STA_IF)
sta_if.active(True)
if not sta_if.isconnected():
print('connecting to network...')
sta_if.connect(ssid, password)
while not sta_if.isconnected():
pass
print('network config:', sta_if.ifconfig())
while True:
if time.ticks_ms() - last_update >= UPDATE_TIME_INTERVAL:
bme = BME280.BME280(i2c=i2c) #BME280 object created
temperature = bme.temperature #reading the value of temperature
humidity = bme.humidity #reading the value of humidity
pressure = bme.pressure #reading the value of pressure
bme_readings = {'field1':temperature, 'field2':pressure, 'field3':humidity}
request = urequests.post( 'http://api.thingspeak.com/update?api_key=' + THINGSPEAK_WRITE_API_KEY, json = bme_readings, headers = HTTP_HEADERS )
request.close()
print(bme_readings)
How does the Code Works?
Firstly, we will be importing all the necessary libraries required for this project. This includes the Pin class and SoftI2C class from the machine module. This is because we have to specify the pin for I2C communication. We also import the time module so that we will be able to add a delay in between our readings. Also, import the BME280 library which we have previously uploaded to ESP32. Moreover, we will require urequests and network modules as well.
import machine
import urequests
from machine import Pin, SoftI2C
import network, time
import BME280
Now, we initialize the I2C method by giving it three arguments. The first argument specifies the GPIO pin for SCL. This is given as GPIO22 for ESP32. The second parameter specifies the GPIO pin for the SDA. This is given as GPIO21 for ESP32. Keep in mind, these are the default I2C pins for SCL and SDA which we have used for the ESP32 board. The third parameter specifies the maximum frequency for SCL to be used.
i2c = SoftI2C(scl=Pin(22), sda=Pin(21), freq=10000) #initializing the I2C method
We will specify our ThingSpeak write API key in the ‘THINGSPEAK_WRITE_API_KEY’ variable that will hold our unique API key. This is the key that we saved previously when creating a new channel in ThingSpeak. Moreover, we will create another variable ‘HTTP_HEADERS’ that will be used later on while connecting with the API.
HTTP_HEADERS = {'Content-Type': 'application/json'}
THINGSPEAK_WRITE_API_KEY = '****************'
We will update the sensor readings after every 5 seconds. Moreover the ‘last_update’ variable will keep track of the time.
UPDATE_TIME_INTERVAL = 5000 # in ms
last_update = time.ticks_ms()
Connect ESP32 to Local Network
Next, we will create two variables to save the SSID and the password values. You have to replace both of them with your network credentials to successfully connect with your router.
ssid='YOUR_SSID'
password='YOUR_PASSWORD'
The following lines of code will configure the ESP32 board as station. The ESP32 will connect with the local network and the IP address will get printed in the Thonny Shell. This is necessary as we want to send sensor readings to ThingSpeak.
sta_if=network.WLAN(network.STA_IF)
sta_if.active(True)
if not sta_if.isconnected():
print('connecting to network...')
sta_if.connect(ssid, password)
while not sta_if.isconnected():
pass
print('network config:', sta_if.ifconfig())
Get Sensor Readings and Publish to ThingSpeak
Inside the while loop we will first check if it time to update the sensor readings. Then we will create an object of BME280 named bme and access the temperature, humidity and pressure through it. These sensor readings will be saved in their respective variables: temperature, humidity, and pressure.
while True:
if time.ticks_ms() - last_update >= UPDATE_TIME_INTERVAL:
bme = BME280.BME280(i2c=i2c) #BME280 object created
temperature = bme.temperature #reading the value of temperature
humidity = bme.humidity #reading the value of humidity
pressure = bme.pressure #reading the value of pressure
bme_readings = {'field1':temperature, 'field2':pressure, 'field3':humidity}
request = urequests.post( 'http://api.thingspeak.com/update?api_key=' + THINGSPEAK_WRITE_API_KEY, json = bme_readings, headers = HTTP_HEADERS )
request.close()
print(bme_readings)
Next, we will create a variable ‘bme_readings’ to hold the readings that will get published to their respective fields. In our case, we had set Field 1 for temperature readings, Field 2 for pressure readings and Field 3 for humidity readings respectively.
After that we will post these three sensor readings to their respective fields by using urequests.post() method and specifying the ThingSpeak Credentials that we defined previously. After that close the request.
Additionally, all these three readings will get printed on the shell terminal after a delay of 5 seconds.
bme_readings = {'field1':temperature, 'field2':pressure, 'field3':humidity}
request = urequests.post( 'http://api.thingspeak.com/update?api_key=' + THINGSPEAK_WRITE_API_KEY, json = bme_readings, headers = HTTP_HEADERS )
request.close()
print(bme_readings)
Demonstration
After you have uploaded your code to the ESP32 development board press its ENABLE button.
In the Thonny shell terminal you will be able to view the ESP32 board get connected. After every 5 seconds, new sensor readings will keep appearing as they get published to their respective fields.
Next, open the ThingSpeak API and you will be able to see temperature, pressure and humidity readings updating after 5 seconds in your charts.
Video demo:
Conclusion
In conclusion, we were able to learn how to publish sensor readings to ThingSpeak using MicroPython and ESP32 in a fairly easy way. Likewise, you can publish single or multiple data to ThingSpeak API which you will be able to access from anywhere around the world.
If you find this ESP32 project useful, you may also like to read:
- MicroPython: Send Sensor Readings via Email (IFTTT) with ESP32 and ESP8266
- MicroPython: OpenWeatherMap API with ESP32/ESP8266 – Sensorless Weather Station
- ESP32/ESP8266 MicroPython Web Server – Control Outputs
- MicroPython: DHT11/DHT22 Web Server with ESP32/ESP8266
- MicroPython: Timers with ESP32 and ESP8266
- ESP32/ESP8266 ADC with MicroPython
- MicroPython: Interrupts with ESP32 and ESP8266
- MicroPython: BME680 Web Server with ESP32 and ESP8266
- Push Button with ESP32 and ESP8266 using MicroPython
- MicroPython: PWM with ESP32 and ESP8266
Very interesting solutions!
I had been using the mqtt/simple routine, but just couldn’t get it to work, giving the same error. It just didn’t connect to the MQTT server. This is a different approach, that works, thank you very much!
Solution not working (you see in many places on the web):
#payload=”field1=”+str(temp)+”&field2=”+str(humi)+”&field3=”+str(pres)
#client.connect()
#client.publish(topic, payload)
#client.disconnect()
The solution presented here that works:
bme_readings = {‘field1’:temp, ‘field2’:humi, ‘field3’:pres}
request = urequests.post( ‘http://api.thingspeak.com/update?api_key=’ + THINGSPEAK_WRITE_API_KEY, json = bme_readings, headers = HTTP_HEADERS )
request.close()
print(bme_readings)
Using an api call did the job.
Thanks, Frank.
Atualize last_update after suscess request to thinkspeak in the if: statment