E-Mail-Nachrichten von ESP32 und ESP8266 in MicroPython - Teil 2 - AZ-Delivery

This post is also available as PDF document.

With the ability Send emails To be able to do this, we turned the ESP32 into a pack mule that can deliver mail worldwide. In this post we will provide the appropriate payload. To do this, we take a closer look at a BME280 in order to then develop a program that sends us the sensor data via email. Welcome to a new episode in the series

MicroPython on the ESP32 and ESP8266


Part 2 - Email messages from ESP32 and BME280

The fact that the ESP8266 cannot be used for this job is due to the limited memory. Under MicroPython only 1MB can be accessed and the kernel already occupies a large part of it. This means that a memory problem is reported when importing the BME280 module. This means that an ESP32 is mandatory for this article.


In order to be able to view the status of the circuit directly on site at any time, I gave the ESP32 a small display that is controlled via the I2C bus. It is even capable of graphics and could therefore also display changes in the measurement signal over time as a curve. The program can be canceled using the flash button. This is useful if, for example, actuators need to be switched off safely or if an abort using Ctrl+C is unsuccessful.

Figure 1: Emails from ESP32

Figure 1: Emails from ESP32

As a measurement application, I chose a climate monitor with the BME280. The Bosch sensor can record air pressure, relative humidity and temperature. With this data we will calculate the air pressure at sea level (NHN normal altitude) and the dew point.


ESP32 Dev Kit C unsoldered or

ESP32 Dev Kit C V4 unsoldered or

ESP32 NodeMCU Module WLAN WiFi Development Board with CP2102 or

NodeMCU-ESP-32S kit or

ESP32 Lolin LOLIN32 WiFi Bluetooth Dev Kit


0.91 inch OLED I2C display 128 x 32 pixels


GY-BME280 Barometric Sensor for temperature, humidity and air pressure


MB-102 breadboard breadboard with 830 contacts


Jumper Wire Cable 3 x 40 PCS. 20 cm each M2M/ F2M / F2F maybe too

65pcs. Jumper wire cable jumpers for breadboard


Logic Analyzer

The software

For flashing and programming the ESP32:

Thonny or


Firmware used for the ESP32:

v1.19.1 (2022-06-18).bin

The MicroPython programs for the project:

ssd1306.py Hardware driver for the OLED display

oled.py API for the OLED display

bme280.py API for the Bosch sensor

bme280-test.py Demo and test program for the BME280

umail.py Micro-mail module

email.py Demo program for sending emails

bme280-monitor.py Demo measurement program

MicroPython - language - modules and programs

To install Thonny you can find one here detailed instructions (english version). There is also a description of how to do this Micropython firmware (as of June 18, 2022) on the ESP chip burned becomes.

MicroPython is an interpreter language. The main difference to the Arduino IDE, where you always and only flash entire programs, is that you only need to flash the MicroPython firmware once at the beginning on the ESP32 so that the controller understands MicroPython instructions. You can use Thonny, µPyCraft or esptool.py to do this. I have the process for Thonny here described.

Once the firmware is flashed, you can have a casual conversation with your controller, test individual commands, and immediately see the response without having to compile and transfer an entire program first. That's exactly what bothers me about the Arduino IDE. You simply save a lot of time if you can carry out simple syntax and hardware tests through to testing and refining functions and entire program parts via the command line before you build a program from it. For this purpose, I always like to create small test programs. As a kind of macro, they combine recurring commands. Entire applications sometimes develop from such program fragments.


If you want the program to start autonomously when the controller is switched on, copy the program text into a newly created blank file. Save this file as boot.py in the workspace and upload it to the ESP chip. The next time you reset or switch on, the program starts automatically.

Test programs

Programs are started manually from the current editor window in the Thonny IDE using the F5 key. This is faster than clicking on the start button or using the menu run. Only the modules used in the program must be in the flash of the ESP32.

Arduino IDE again in between?

If you later want to use the controller together with the Arduino IDE, simply flash the program in the usual way. However, the ESP32/ESP8266 then forgot that it ever spoke MicroPython. Conversely, any Espressif chip that contains a compiled program from the Arduino IDE or the AT firmware or LUA or ... can easily be provided with the MicroPython firmware. The process is always like this here described.

Signals on the I2C bus

Whenever there are problems with data transfer, I like to use the DSO (Digital Storage Oscilloscope), or a small tool that is worlds cheaper Logic Analyzer (LA) with 8 channels. The thing is connected to the USB bus and shows using a free softwarewhat's going on on the bus lines. An LA is worth its weight in gold where it is not the form of impulses that matters, but rather their timing. While the DSO only provides snapshots of the curve, the LA allows you to scan over a longer period of time and then zoom in on the interesting areas. By the way, you can find a description of the device in the blog post "Logic Analyzer -Part 1: Making I2C signals visible" by Bernd Albrecht. It also describes how to scan the I2C bus.

Figure 2: Logic Analyzer on the I2C bus

Figure 2: Logic Analyzer on the I2C bus

The circuit

The three parts for the circuit are quickly put together.

Figure 3: Email circuit

Figure 3: Email circuit

In addition to the sensor itself, the BME280 board also has a voltage converter and a level converter for the SCL and SDA lines. This means that no external pull-up resistors need to be installed because they are part of the level converter. The power is supplied via the USB socket from the PC or via a plug-in power supply.

The BME280

During the BMP280 can only measure temperature and air pressure, its big brother can also measure relative humidity. The numbers and meaning of the registers of the BMP280 are at the BME280 the same. In the latter case, only the area of ​​humidity is included. This is practical because it means that the MicroPython module BME280 can also be used for the BMP280.

Both sensors can be controlled as slaves via the I2C or SPI bus. However, the I2C bus is permanently set in this module, which doesn't bother me because the display also uses the same bus. The I2C bus object is therefore also created in the main program and sent to the constructors OLED() and BME280() hand over.

Data can be sent to the BME280 in single-byte mode or in multi-byte mode, then as address-value pairs. When reading out the measured values, it is practical that only the address of the first register of an entire sequence has to be specified and the BME280 increases the address itself for further read accesses (auto increment). In the MicroPython module bme280.py There are two methods for data transfer.

    def writeByteToReg(self,reg,data):
       buf[1]=data & 0xFF

   def readBytesFromReg(self,reg,num):
       return buf

So when reading, I only specify the start register and the number of bytes to be read. This creates a byte array of the desired length. To send the address I only use the first element in which I write the address. Then as many bytes as fit into the array are fetched.

The data sheet of the BME280 provides information about the register landscape. The configuration data is held in three registers. The routines for writing and reading the registers use the configuration attributes of the BME280 object and the routines for the I2C transfer.

A status register provides information about the system status of the sensor. After each measurement, the set of raw data is written to shadow registers. The Bit status.measuring is 0 if the data is ready to be read.

In the read-only register id = 0xD0 there is an identification byte that reveals the type of sensor. The routine readIDReg() returns the plain text label.

0x55: "BMP180",
0x58: "BMP280",
0x60: "BME280"

Each sensor is provided with a set of calibration data at the factory. In order for the BME280 to deliver correct values, they must be read from registers 0xE1 to 0xF0. That's what the method does getCalibrationData() automatically when instantiating a BME280 object. Along with the raw data for temperature, relative humidity and air pressure using the method readDataRaw() are read out, the final values ​​are then calculated according to the formulas in the data sheet. The methods do that calcTemperature(), calcPressureH() and calcHumidity(). The method calcPressureNN() calculates the air pressure at sea level. calcDewPoint() calculates the dew point, which is the temperature at which the invisible water vapor in the air begins to condense and form mist droplets.

The accuracy of the measurements can be adjusted using the oversampling values ​​via the control registers in 5 levels each, x1, x2, x4, x8 and x16. To do this, the configuration must first be set. The attributes are then written into the control registers. The oversampling of the moisture measurement has its own register.

>>> b.setControl(EAST=5)
>>> b.writeControlReg()
>>> b.setControlH(OSH=3)
>>> b.writeControlHReg()

The starting values ​​that the constructor sets are via the methods showControl() and showConfig() available.

>>> b.showConfig()
Standby= 125  filter= 16
(2, 16)
>>> b.showControl()
EAST= 1  OSP= 3  Fashion= 3  OSH= 2
(1, 3, 3, 2)

Then it's time for a first Test program.

# bme280-test.py
from machine import Pin code, SoftI2C
from bme280 import BME280
from time import sleep
import os,sys

i2c=SoftI2C(scl=Pin code(22),sda=Pin code(21),freq=100000)

except OSError as e:
   print("No BME280 can be addressed",e)

# b.getCalibrationData()
# b.printCalibrationData()

print(b.calcPressureH(),"hPa @ location")
print(b.calcPressureNN(450),"hPa @ Sea level")

Because an error message pops up when starting this lean program with an ESP8266, the I2C interface does not have to be determined by the program, but is set directly to an ESP32. The ESP8266 is simply too narrow in terms of memory.

>>> %run -c $EDITOR_CONTENT
Traceback (most recent call load):
 File "", line 3, in <modules>
MemoryError: memory allocation failed, allocating 360 bytes

During development, it is useful to know the calibration data from the BME280's NVM (Non Volatile Memory) in order to manually check the calculated final values ​​using the formulas given in the data sheet. b.getCalibrationData() and b.printCalibrationData() retrieve and display the values.

I already mentioned above that the measured values ​​are transferred to a shadow memory as soon as the measurement is finished. In order to obtain reliable values ​​after the start, the first group is read in and discarded. The following calcXY() calls then request a new set of raw data each time. This ensures that the correct temperature value is available for the pressure and humidity calculation. A program run now delivers output in the terminal that should look similar to the following.

17.78 °C
983.2547 hPa @ Location
1037.413 hPa @ Sea level
42.30078 %RH
4.789577 °C

It is not difficult to send this data by email, we just have to use the program email.py from the last episode bme280-test.py combine. I have the final product bme280-monitor.py called. Let's take a look at how it works.

import umail
import network
import sys
from time import sleep, ticks_ms
from machine import SoftI2C,Pin code
from bme280 import BME280
from oled import OLED

The two project specific imports are umail and bme280. The corresponding files must be uploaded to ESP32 because they are not part of the MicroPython kernel like the other additions.

To access the WLAN you must enter your own credentials.

# Enter your own access data here
myPass = 'nightingale'

You also need data for G-Mail access including an app password. How you get to both is in the previous episode explained. Also don't forget about recipient_email Enter your email address.

# email data
sender_email = 'ernohub@gmail.com'
sender_name = 'ESP32' #sender name
sender_app_password = 'xxxxxxxxxxxxxxxx'
recipient_email ='my@mail.org'

For the OLED display and the BME280 we need the I2C bus. Both devices support speeds up to 400,000 kHz. We discard the first data set from the BME280, which means we don't do anything with it.

i2c=SoftI2C(scl=Pin code(22),sda=Pin code(21),freq=100000)

d.writeAt("BME280 MAILER",0,0)


The flash button is the button for the orderly exit from the main loop.

button=Pin code(0,Pin code.IN,Pin code.PULL_UP)

interval=3600 # Send interval in seconds

An email is sent every two hours after data has been collected. You can of course adjust the period according to your needs.

temp,location,seaLevel,relHum,dew point=0,0,0,0,0

We initialize the variables for the measured values ​​with 0. The method for this is a bit unusual. Why does that work? We use the method of packing and unpacking tuples. The MicroPython interpreter first turns the five zeros and the assignment operator "=" into a tuple. This process is called packing.

>>> x=0,0,0,0,0
>>> x
(0, 0, 0, 0, 0)

The contents of the tuple are immediately distributed again among the five variables, i.e. unpacked.

>>> a,b,c,d,e=(0, 0, 0, 0, 0)
>>> a; b; c; d; e
>>> a,b,c,d,e = 0,0,0,0,0

So packing and unpacking happens in one line.

connectStatus = {
   1000: "STAT_IDLE",
   1010: "STAT_GOT_IP",
   201:  "NO AP FOUND",
   5:    "UNKNOWN"

The Dictionary connectStatus helps translate the status codes from the WLAN interface into plain text.

The result of the function querying the MAC address nic.config('mac') is quite cryptic as a bytes object.

>>> nic = network.WIRELESS INTERNET ACCESS(network.STA_IF)
>>> nic.active(True)
>>> nic.config('mac')

The function hexMac() turns it into plain text, which you have to enter in the router so that the MAC filter allows the ESP32 entry.

>>> hexMac(b'\xf0\x08\xd1\xd2\x1e\x94')
def hexMac(byteMac):
The hexMAC function takes the MAC address in bytecode and
forms a string for the return
 macString =""
 for i in range(0,len(byteMac)):     # For all byte values
   macString += hex(byteMac[i])[2:]  # String from 2 to end
   if i <len(byteMac)-1 :            # Delimiter
     macString +="-"                 # except for the last byte
 return macString

The Closure Time-out() creates a software timer when called. The returned function compareWe assign () to the identifier later send to. This name is an alias for the function compare(). Let's call send() on, then we get as a result True or False. This tells us whether the timer has already expired or not.

def Time-out(t):
   def compare():
       return int(ticks_diff(ticks_ms(),begin)) >= t
   return compare

The function getValues() reads the values ​​of the BME280 and uses them to build strings that are sent to the variables temp, location, sealevel, relHum and dew point be handed over. Because this is done from a function, the variables must be declared global. Without the global keyword, these variables would be local to the function and the assigned contents could not be retrieved outside the function.

By specifying the format {:0.2f}, the floating point values ​​returned from the calcXY() functions are output to two decimal places. This type of formatting is the easiest way to mix strings and numeric values.

def getValues():
   global temp,location,seaLevel,relHum,dew point
   temp="{:0.2f} *C".format(bme.calcTemperature())
   location="{:0.2f} hPa @ location".\
   seaLevel="{:0.2f} hPa @ Sea level".\
   relHum="{:0.2f} %RH".format(bme.calcHumidity())
   dew point="{:0.2f} *C".format(bme.calcDewPoint())

This is followed by establishing a connection to the WLAN router. So that the ESP32's access point interface doesn't bother us, it is deactivated. This is especially important with the ESP8266, but it doesn't hurt with the ESP32 either.

# **************** Connect to router *******************

nic = network.WIRELESS INTERNET ACCESS(network.STA_IF)  # creates WiFi object
nic.active(True)                    # don't turn it on
MAC = nic.config('mac')   # get binary MAC address and
myMac=hexMac(MAC)         Convert # to hex digit sequence
print("STATION MAC: \t"+myMac+"\n") # spend

Then we create a station interface object and activate it. We pass on the read MAC address for translation hexMac().

After a short break, we establish the connection. SSID and password are passed and the status is queried. As long as the ESP32 has not yet received an IP address from the DHCP server, points are displayed every second on the display and in the terminal area of ​​Thonny. In the display this is done by slicing the string points. To stay on one line in the terminal, we tell the print statement to replace the end-of-line character "\n" with nothing ' '.

Then we query the status again and display the connection data.

print("\nStatus: ",connectStatus[nic.status()])
STAconf = nic.ifconfig()
print("STA IP:\t\t",STAconf[0],"\nSTA-NETMASK:\t",\
     STAconf[1], "\nSTA GATEWAY:\t",STAconf[2] ,Sep='')

Before entering the main loop, we set the timer to the time in interval. Because the timer ticks in milliseconds, the value is multiplied by 1000. We set the first email to be sent immediately now on True. But this variable has a second meaning. If an event occurs that requires the email to be sent immediately, then the triggering process can now on True and thus arrange for emails to be sent outside of the fixed time period.

oldPres= oldPres=float.float(seaLevel.split(" ")[0])

Then we get the current values. We already remember the air pressure oldPres. And set another timer for half an hour. In the main loop we check if the timer control() has already expired. In this case, we remember the current air pressure value and reset the timer.

while 1:
   if control():
       oldPres=float.float(seaLevel.split(" ")[0])

The new values ​​are read in and the change in air pressure is checked. If the air pressure falls by more than two hPa during the control period, then a thunderstorm could be approaching and a weather warning is being prepared. The subject is changed and now becomes True. To compare the numerical values, they must be extracted from the string. To do this, I split the string at the spaces " ". From the obtained list I take the first element and convert it to a floating point number.

>>> seaLevel
'1037.62 hPa @ Sea level'
>>> seaLevel.split(" ")
['1037.62', 'hPa', '@', 'Sea', 'level']
>>> seaLevel.split(" ")[0]
>>> float.float(seaLevel.split(" ")[0])
   if oldPres - float.float(seaLevel.split(" ")[0]) > 2:
       email_subject ='Storm coming'

The output of the values ​​in the terminal and in the display is not spectacular. False in the writeAt-Commands to the display prevent the display from flickering. It causes the changes to initially only occur in the background in the buffer. Only with the last one writeAtcommand, the buffer contents are sent to the OLED display.

An email will be sent if the interval timer expires, or if now the value True has.

    if send() or now:
       # ************ Send an email ****************
       smtp = umail.SMTP('smtp.gmail.com', 465,
                         ssl=True, debug=True)
       smtp.Log in(sender_email, sender_app_password)
       smtp.write("From:" + sender_name + "<"+ \
       smtp.write("Subject:" + email_subject + "\n")
       smtp.write("Climate values ​​from ESP32\n")
       smtp.write(dew point+"\n")
       if email_subject == 'Storm coming'
       email_subject ='weather report'

We establish a connection to the provider and send the username and app password, then the recipient address, the sender and the subject. After the measurement data has been transferred, we send it to the recipient, i.e. ourselves, and terminate the connection.

Clean-up work follows. The interval timer is reset when it has expired and the alarm is reset and the subject is returned to normal operation.

Querying the status of the flash button completes the program.

    if button.value() == 0:

The sending of an email “on request”, in this case the thunderstorm warning, can of course also be triggered by various other events. Anything that can be detected by a sensor can trigger an email. The main loop runs continuously and can query additional sensors at any time. People moving around a room, the water level in the rain barrel, storms, lights on or off are just a few possibilities. Let your imagination run wild. The complete program is of course available for download.

Have fun crafting and programming!

DisplaysEsp-32Esp-8266Projekte für anfängerSensoren


Rainer Hoffmann

Rainer Hoffmann

Im Programm “bme280-monitor.py” führt die Anweisung
“if email_subject == ‘Unwetter im Anzug’”
dazu, das nach Ablauf von 2 Stunden die email im loop gesendet wird. Es muss doch jedes mal wenn eine mail gesendet wurde das ZeitIntervall neu gesetzt werden. Die Abfrage kann m.E. weg.
Da für mich microPython Neuland ist, wundere ich mich darüber, dass es eine Variable “senden” gibt aber “senden()” abgefragt wird (" if senden() or jetzt:"). Mag sein, dass ich da etwas falsch verstehe, aber der loop ist real.
Die von mir bereits erwähnte “brownout” Fehlermeldung ist mit Thonny 4.1.2 und 4.2.3 unter Windows 11 auch wieder da. Ich habe das Programm aber unter Thonny 4.0.2 und Windows 10 testen können. Aber das ist eine andere Baustelle.

Rainer Hoffmann

Rainer Hoffmann

Nach dem Anschluss des OLED-Displays und des BME280 ist der Fehler wegen “brownout” verschwunden (!?) und mit einer kleinen Änderung in Modul “e_mail.py” läuft die Anwendung fehlerlos. Folgende Korrektur musste ich vornehmen:
In der Anweisung:
smtp = umail.SMTP
musste der Parameter “debug=True” entfernt werden.
Herzlichen Dank an den Author des Blogbeitrages.

Klaus-Peter Schildt

Klaus-Peter Schildt

Hallo Jürgen,
die Dateien in den Flash hochladen war die Lösung. Schade, das immer die Hardware präsent sein muss, um das Programm zu testen. Ich habe div. LILYGO T-Displays V1.1 aus meiner Asservaten Sammlung im Einsatz. Pins und Display Driver sind verschieden, sollte trotzdem funktionieren. Alles gemeinsam mit RCWL-0516 Mikrowellenradar Bewegungssensor in vierfacher Ausführung die Türen im Wohnmobil zu sichern, ist das Ziel. Aus gegebenen Anlass ist das leider dringend notwendig. Mit C++ läuft es schon.
Micropython scheint ja in die Zukunft zu weisen. Es bleibt spannend.
vG. Klaus-Peter

Rainer Hoffmann

Rainer Hoffmann

Habe es mit einem “ESP32 D1 Mini NodeMCU” versucht
und bekomme einen “Brownout detector was triggered” (= zu wenig power) von Thonny an den Latz geknallt wenn ich “e_mail.py” starte. Ich messe an meinem USB-Anschluss zwar nur 4,15 V aber hatte damit noch nie Probleme mit Arduino-Anwendungen für unterschiedliche ESP32 Modelle. Hat jemand eine Idee woran die Fehlermeldung liegen könnte?

Rainer Hoffmann

Rainer Hoffmann

@Klaus-Peter Schildt
Kann deine Verzweiflung gut verstehen, da ich ich die gleichen Probleme hatte. Speicher mal die Dateien auf deinen Controller und lade und starte die Programme von dort. Damit bin ich schon mal ein Stück weiter gekommen. Die Implementierung der Import-Funktion in microPython ist auf jeden Fall großer Mist.



Hallo, Herr Schildt,
Es sieht so aus als wären die Dateien oled.py, ssd1306.py, umail.py und bme280.py zwar im Arbeitsverzeichnis, aber nicht in den Flash des ESP32 hochgeladen worden. Die anderen Module:
import sys # ok
from time import sleep, ticks_ms, ticks_diff # ok
from machine import SoftI2C,Pin # ok
befinden sich bereits im Kernel von MicroPython, deshalb klappt da auch der Import klaglos.
Die Dateien in den Flash hochladen.

Im Zweifelsfall kann auch die Firmware von micropython.org Fehler verursachen, weil die Leute dort an Dingen, die in einer Version v1.19 noch funktioniert haben in v1.20 z. B. nicht mehr funktionieren. Das war in den Versionen ab 1.15 öfter der Fall. Hab mir da schon oft den Wolf gesucht. Also am besten die Firmware verwenden, die in der Liste steht. Wenn’s damit funktioniert, kann man es ja mit der neuesten dann auch versuchen.

Klaus-Peter Schildt

Klaus-Peter Schildt

Hallo Respekt, gute Arbeit. Genau das was ich brauche um mich sinnvoll in Micropython zu versuchen.
Ich bin am verzweifeln:
Habe Thonny 4.1.2 installiert, MicroPython v1.20.0 on 2023-04-26; ESP32 WROOM module.
Beginne mit bme280-monitor.py. Mit den ersten Zeilen: schon div. Fehler
import umail, umail.py im gleichen Verzeichnis trotzdem ImportError: no module named ‘umail’
import sys # ok
from time import sleep, ticks_ms, ticks_diff # ok
from machine import SoftI2C,Pin # ok
mit PyPi Neueste stabile Version: 0.6 installiert:
from bme280 import BME280 => ImportError: no module named ‘bme280’
from oled import OLED # oled => ist in PyPi nicht vorhanden.
Alternativ versucht: ssd1306-oled => mit PyPi Neueste stabile Version:0.1.1 installiert
import ssd1306-oled => SyntaxError: invalid syntax
Was mache ich verkehrt?
VG Peter

Leave a comment

All comments are moderated before being published

Recommended blog posts

  1. ESP32 jetzt über den Boardverwalter installieren - AZ-Delivery
  2. Internet-Radio mit dem ESP32 - UPDATE - AZ-Delivery
  3. Arduino IDE - Programmieren für Einsteiger - Teil 1 - AZ-Delivery
  4. ESP32 - das Multitalent - AZ-Delivery