0% found this document useful (0 votes)
217 views

OceanofPDF - Com MicroPython and The Internet of Things - Miguel Grinberg

Uploaded by

witolo8352
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
217 views

OceanofPDF - Com MicroPython and The Internet of Things - Miguel Grinberg

Uploaded by

witolo8352
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 144

MicroPython and the

Internet of Things
Miguel Grinberg

2022-04-29

Contents
Preface
1 Who This Book Is For
2 Requirements
3 How To Work With The Example Code
4 Conventions Used In This Book
Chapter 1 Welcome
1.1 About This Tutorial
1.2 Requirements
1.3 The Shopping List
1.3.1 Microcontroller
1.3.2 Temperarure and Humidity Sensor
1.3.3 Screen
1.3.4 Push Button
1.3.5 Breadboard and Jumper Wires
1.3.6 USB to MicroUSB Cable
1.3.7 Power Bank (Optional)
1.4 Let’s Build Some Stuff!
Chapter 2 Hello, MicroPython!
2.1 The ESP8266 Microcontroller Board
2.2 The Breadboard
2.3 Wiring the Breadboard Power Strips
2.4 Setting Up Your Computer
2.5 Flashing MicroPython with esptool.py
2.6 Using the MicroPython REPL with rshell
2.7 Playing with the On-Board LEDs
Chapter 3 Building a MicroPython Application
3.1 The MicroPython File System
3.2 Blinking Lights Application
3.3 Buttons
3.4 Input Pins
3.5 A Note on Short Circuits
3.6 Pull-Up Resistors
3.7 Wiring of the Button
3.8 Writing Better Code
Chapter 4 Wi-Fi and the Cloud
4.1 The Two ESP8266 Wi-Fi Interfaces
4.1.1 The Access Point Interface
4.1.2 The Station Interface
4.2 The MicroPython WebREPL
4.3 Using a Configuration File
4.4 Sending HTTP Requests
4.5 Setting Up a IFTTT Webhook
4.6 Emulating the Amazon Dash Button
4.7 Reporting Errors
4.8 The Deep Sleep State
4.9 Adding a Debug Mode
4.10 Using an External Reset Button
Chapter 5 Temperature and Humidity Sensor
5.1 The DHT22 Temperature and Humidity Sensor
5.2 Obtaining Sensor Readings
5.3 Weather Station Application
5.4 Logging to the Cloud
5.5 Deep Sleep with a Wake Up Alarm
5.6 Extending the RST Pin
Chapter 6 Working with a Screen
6.1 The SSD1306 OLED Screen
6.2 Controlling the Screen from MicroPython
6.3 Displaying Temperature and Humidity on the
Screen
6.4 Using Drawing Primitives
6.5 Drawing Images
6.6 Custom Fonts
6.7 The End
6.7.1 Building Custom MicroPython firmwares
6.7.2 Installing Packages from PyPI
6.7.3 Implementing a Web Server
6.7.4 Using Other Microcontroller Boards
6.7.5 Using MicroPython Derivatives
6.7.6 Learning Electronics and Digital Circuits
6.7.7 Running MicroPython on your Computer

Preface
In 2018 I moved to a new house that was very cold. The
heating controller in this house was one of the newest, as it
gave me the option to see and change the heating
schedules from my smartphone or computer. Yet, I found
that I constantly needed to make manual adjustments to
the heating schedule because I was cold. After an
investigation I determined that the thermostats installed in
the house were very inaccurate, often reporting a
temperature that was a few degrees higher than what I
measured with my own thermometer. Because I could
easily start and stop the heating by sending HTTP requests
to the heating controller, I decided to build my own
thermostats and use them instead of the stock ones. This
was the excuse that I made to learn MicroPython!
I built two Wi-Fi thermostats for less than $10 USD each
and installed them in the two levels of the house, and that
solved my heating problem without having to make any
major modifications in a house I did not own. I hope after
you learn how to work with MicroPython you will also come
up with ideas for little devices that can improve your life in
some way.
1 Who This Book Is For
This book is for curious developers interested in learning
how to create small Internet enabled devices, in the so
called “Internet of Things” category. The book is written in
tutorial form, with each chapter introducing new concepts
that build upon the previous material.

2 Requirements
The first chapter of the book includes a “shopping list” of
hardware components that you need to build the examples
featured in this book. In addition to that, you will need a
computer which will be used to program the
microcontroller. You can use any Windows, Mac OS or
Linux computer, as long as it has one available USB port
where the microcontroller can be connected.

3 How To Work With The


Example Code
I have released the complete source code for this book on a
GitHub repository. In general you will not need this
repository as the entire source code for all the examples is
shown in the videos and written articles, but having the
complete code examples in a centralized location can help
you find mistakes in your own code. Also, in the last
chapter of the book I am using some images files and third-
party code that you will be able to download from this
repository.

4 Conventions Used In
This Book
This book frequently includes commands that you need to
type in a terminal session. For these commands, a $ will be
shown as a command prompt. This is a standard prompt
for many Linux shells, but may look unfamiliar to Microsoft
Windows users. For example:
$ python hello.py
hello

In a lot of the terminal examples, you are going to be


required to have an activated virtual environment (do not
worry if you don’t know what this is yet, you will find out
very soon!). For those examples, the prompt will appear as
(venv) $:

(venv) $ python hello.py


hello

You will also need to interact with the MicroPython REPL,


the interactive Python prompt. Examples that show
statements that need to be entered in a Python interpreter
session will use a >>> prompt, as in the following example:
>>> print('hello!')
hello

In all cases, lines that are not prefixed with a $ or >>>


prompt, are output printed by the command, and should
not be typed.
OceanofPDF.com
Chapter 1
Welcome

1.1 About This Tutorial


This is a tutorial for Python beginners who want to learn to
program devices to interact with the physical world and
with the so called “Cloud”. You are going to learn how to
program with MicroPython, a version of the Python
language designed to run on microcontrollers. The
applications that you are going to learn how to write are
going to read data from sensors, display information on
small screens, react to the push a button, and upload or
download data from the Internet. Lots of cool stuff!
This tutorial is focused on the software side more than on
the hardware side. I’m going to use a high-level “Lego”
approach for building circuits, so instead of having to
solder components into circuit boards, you’ll be building
your experiments on breadboards, and interconnecting
components with jumper wires. You will be able to
assemble and disassemble your circuits with ease, and
more importantly, reuse components as you move through
the tutorial and later when you build your own projects.
1.2 Requirements
To be able to follow this tutorial you just need to have basic
coding experience with Python. You do not need to have
any previous knowledge of microcontrollers, electronics, or
even MicroPython.
You will also need a Windows, Mac or Linux computer with
a free USB port, as you will connect a microcontroller to
your computer to program it. If your computer is one of the
newer ones that only come with USB-C ports, then you are
going to need an adapter so that you can connect a regular
USB cable.

1.3 The Shopping List


In this section I will give you a short list with the
components that you are going to need to build all the
examples featured in this tutorial. As a reference, I’m
providing links to Amazon US and UK for all of them
(disclaimer: my Amazon affiliate link is embedded in these
links), but you do not need to buy the exact products I’m
linking. There are many manufacturers for these
components, so use the links I provide as a reference and
then if you prefer, buy your components from your favorite
retailer. If you want a recommendation other than Amazon,
Ebay is also a great place to shop for electronic
components, and you’ll probably find better prices.

1.3.1 Microcontroller
MicroPython runs on a few different types of
microcontrollers, but for this tutorial I’m going to work
with just one model: the ESP8266. Note that there are a
few different boards that you can buy with the same chip.
The model that you want to get is the one informally
referred to as “development board”, and more formally
known with the ESP-12 model name. These boards come
with the microcontroller mounted on it, a small printed Wi-
Fi antenna, a micro-USB input for power and programming,
and 30 pins that insert straight into a breadboard.
The cost of these devices is so low that you are going to
find that most sellers offer them in pairs or in four-packs. I
recommend that you do order at least two.

Buy from Amazon US - Buy from Amazon UK

1.3.2 Temperarure and Humidity


Sensor
To demonstrate how to work with sensors, I’m going to use
a DHT22 temperature and humidity sensor. This little
device reads and reports the temperature and humidity
levels in the environment. Depending on where you buy it,
you may find models that come with three or four pins.
Both are fine for this tutorial.

Buy from Amazon US - Buy from Amazon UK

1.3.3 Screen
Some of the examples will display information on a small
128x64 pixel OLED screen. Once again, these are so cheap
that you may want to get a few.
Buy from Amazon US - Buy from Amazon UK

1.3.4 Push Button


Some of the examples will be triggered by a button push.
You are not going to need more than one button at a time
for this tutorial, but as with the above you may want to also
get a pack, since buttons are always handy to have.
Buy from Amazon US - Buy from Amazon UK

1.3.5 Breadboard and Jumper Wires


To be able to easily build and take apart your circuits
without soldering, you are going to need a breadboard and
some jumper wires. I recommend that you get more than
one breadboard as well.
Buy from Amazon US - Buy from Amazon UK

1.3.6 USB to MicroUSB Cable


You will need a USB cable to connect your microcontroller
to your computer. This is going to provide power, and also
a serial connection through which you’ll upload code into
the microcontroller memory. This is the same cable that
charge most Android phones, so you may already have one
you can use.
Buy from Amazon US - Buy from Amazon UK

1.3.7 Power Bank (Optional)


A power bank is not required for this tutorial, but if you
have one, you will be able to use it to power your
microcontroller independently of the computer after you
have uploaded your code.

1.4 Let’s Build Some


Stuff!
Once you have the materials listed above you are ready to
move on to the next chapter and start building stuff!
OceanofPDF.com
Chapter 2
Hello, MicroPython!

2.1 The ESP8266


Microcontroller Board
To begin, I want to give you a very brief overview of the
components in your ESP8266 microcontroller. Below you
can see a diagram that should more or less match your
board (though maybe not exactly, since this board is open
source and there are a lot of different manufacturers):
The ESP8266 microcontroller chip is the bigger silver
colored box. Above it you can see a zig-zag gold line. That
is the Wi-Fi antenna. You can see 15 pins on each side,
each with a label that should be printed on the board, with
names such as D0, D1, 3V3, GND, etc. To make matters
confusing, the diagram shows that all the pins have other
labels shown on the outside, usually associated with their
function.
There are three pins labeled 3V3 or 3.3V, which provide
electric current. The name derives from the voltage, which
is 3.3 volts. These pins can be used to deliver input current
to other components that are part of your circuit. There
are also four GND or “ground” pins. These provide a ground
connection, which is required to close the circuit when you
connect a component. Electric current always flows
towards the ground, so when you connect a device to the
microcontroller you will be making two actual connections.
A connection from a 3.3V pin will deliver current to the
component, and a connection from the component to a GND
pin will close the circuit, effectively forcing the current to
flow through the component. As you will learn later in this
tutorial, all components have a clearly marked pin for input
current typically called VCC, and one for connection to
ground called GND.
The pins labeled with a D<n> are data pins. These are the
pins that are used for communication between the
microcontroller and other connected devices. This is going
to be covered in later chapters of this tutorial.
The board has two small built-in LEDs (Light Emitting
Diodes, or just tiny light bulbs if you prefer) that you can
see in yellow in the diagram. The top LED is connected to
the pin labeled D4 or GPIO2, while the bottom LED is
connected to D0, which is also GPIO16. By the end of this
chapter you will know how to turn the LEDs on and off with
Python code!
Out of the remaining pins, the A0 is interesting to mention
because it is the only “analog” pin in the board. All the
other pins can have a binary value, either 0 or 1, but the
analog pin can have a range of values, determined by the
amount of electrical current passing through the pin. Note
that this tutorial does not make use of the analog pin.
Another interesting pin is RST, which can be used to send a
reset signal to the board. In future chapters you are going
to learn how to use this pin to “wake” the microcontroller
when it is in a deep sleep state.
The pin labeled Vin can be used to provide power to the
board, as an alternative to the much more convenient
micro-USB port. All the examples you are going to build in
this tutorial are powered through the micro-USB port, so
you are not going to use this pin.
The remaining pins are in most boards reserved for the
microcontroller’s own use, so there isn’t a lot of interesting
features to mention there. If you are curious about any use
you can give them in your particular board, you should
consult the documentation that is specific to it, as these
pins are not always wired in the same way.
In the bottom center you have the micro-USB input
connector. In this tutorial you will always power the board
through this connector. If you connect the other end of the
cable to your computer, then the board is going to appear
as a serial device in your computer. This serial connection
is going to be used to upload code into the microcontroller.
To complete this overview, note that the board has two
small buttons labeled RST and FLASH, on the sides of the
micro-USB port. The FLASH button prepares the board to be
flashed with a new firmware, but this button is rarely
needed, as current versions of the board can be put in flash
mode through the serial connection. The RST button is, as
I’m sure you can guess, a plain and simple reset button,
and pressing it restarts the board.

2.2 The Breadboard


The first task that I’m going to show you how to do is to set
up the microcontroller board on a breadboard. So first, let
me tell you how breadboards work. Here is a diagram that
should match the breadboard that you have:
The key to understand how to work with a breadboard is to
visualize the internal connection between the holes. Once
you know which holes are internally connected, you know
that you can create a connection between two pins by
inserting them in a pair of connected holes. It’s really that
simple.
Below I created a diagram that shows these internal
connections between holes:
The top two and top bottom rows of holes are usually
referred to as the “power strips”, and they actually work in
a similar way to a household power strip. For example,
when you connect a pin that delivers current (such as any
of the 3.3V pins in the ESP8266 board) into any of the holes
in the top row, then all the remaining holes in that row can
be used by other devices to draw power for themselves, so
effectively this is equivalent to having a direct connection
between each device and the power source. This top line is
marked with a red line and in some breadboards with a “+”
label, which indicates that as a convention, it should be
used for current, as in my example. The second row of
holes is marked in blue and with a “-“ label. The
convention is to use this line as a ground connection. There
are two more positive and negative power strips at the
bottom of the breadboard.
The connections used by the wholes in the middle are more
tricky to understand. Here the holes are grouped in fives.
The ten rows of holes in the two middle sections are
labeled with the letters a through j, but unlike the power
strip rows, these rows are not connected. The connections
for these pins are vertical, and cover just the group of five
vertical holes in each section. The columns are labeled
from 1 to 30, but in many breadboards you can only see a
label every five columns. Using these labels, you can see
that the a1, b1, c1, d1, and e1 holes are all connected with
each other, so any pins that are plugged into these holes
will be all connected. Similarly, pins f1, g1, h1, i1 and j1 are
connected between them, but they do not share a
connection with the top five holes.
If you think this is starting to make sense, you are ready to
install the microcontroller into the breadboard. Depending
on the physical dimensions of your microcontroller, there
are two possible ways to do this.
Most likely you have a newer microcontroller board, which
is the smaller kind. To find out for sure, place the
breadboard on your desk with a red power strip on top and
column 1 to the right, and then see if you can align your
microcontroller in the middle section, with the micro-USB
port facing right. Do not insert it into the breadboard yet.
The top 15 pins from the microcontroller should be in the b
row of your breadboard, columns 1 on the right to 15 on
the left. The bottom strip of microcontroller pins should be
aligned with the i row. The a and j rows should be free and
visible from the top. Here is a diagram:

If you were able to align the pins as shown in the diagram,


then gently press on the board until the pins are fully
inserted into the holes. If your microcontroller is too large
to fit in the way I described, then you have one of the
bigger boards. These work in exactly the same way as the
newer counterparts, but have slightly larger dimensions,
which means that there is no way to fit them in the middle
section of the breadboard while leaving at least a row on
each side to make connections. The solution that I have
used for these boards is to straddle them across two
breadboards, as show in the diagram below:

With this set up, the top row of pins in the microcontroller
board is in row j of the top breadboard, and each pin has
four free holes to make connections. The bottom row of
microcontroller pins fits in the a row of the bottom
breadboard, also leaving four holes for connections to each
pin.
In the diagrams that follow I’m going to assume you have
the newer and smaller board installed on a single
breadboard, since the older boards are mostly out of
circulation by now.

2.3 Wiring the


Breadboard Power Strips
The next task is to set up the delivery of power to the
breadboard. I explained above that the top and bottom two
rows of holes are used to deliver current and ground to
other devices. While in this chapter there are no additional
devices that need to make use of power, it is always a good
idea to have the power properly distributed on the
breadboard for when it is needed.
To do this you are going to need four jumper wires. If you
have different kinds of wires, make sure the ones you select
have all male pins. If you have wires of several colors
available, you may want to use two red wires for the
positive connections, and two black or blue wires for the
ground.
The connections that you need to make are four:

From one of the top 3V3 pins (for example, the a1 hole)
to any hole in the breadboard’s top row
From one of the top GND pins (for example, the a2 hole)
to any hole in the breadboard’s second row from the
top
From one of the bottom 3V3 pins (for example, the j5
hole) to any hole in the breadboard’s second row from
the bottom
From one of the bottom GND pins (for example, the j2
hole) to any hole in the breadboard’s bottom row

The end result should look more or less like the following
diagram:

In case this isn’t clear, no other component is drawing


power from the microcontroller at this time, so these
connections that supply power and ground to the
breadboard aren’t really doing anything yet, but it’s nice to
have the breadboard in a state that is ready to support
additional components later on.
This is enough work in the physical world for this chapter.
Now I’m going to take to the software side and show you
how to prepare your computer so that you can install and
run MicroPython on your board.

2.4 Setting Up Your


Computer
If you have a Linux computer, then you do not need to
install any device drivers for the microcontroller to be
recognized. But if you have a Mac or a Windows machine,
a driver is needed to allow the computer to recognize the
microcontroller as a serial device. To download the driver
installer package visit this page:
https://www.silabs.com/products/development-
tools/software/usb-to-uart-bridge-vcp-drivers.
The tools that you are going to use to communicate with
the ESP8266 are written in Python, so what you need to do
now (if you haven’t yet) is install Python on your computer.
I have tested this tools with Python 3.7, which is the
current release of Python as I’m writing this. If your
operating system does not provide a pre-packaged Python,
you can go to https://python.org to download an official
build for any of the supported operating systems.
Start by opening a terminal window. On Linux or Mac I will
assume that you are running a bash, zsh or similar shell,
while on Windows it will be the Command Prompt window.
First create a new directory where you are going to store
files associated with this tutorial:
$ mkdir micropython-tutorial
$ cd micropython-tutorial

In all the command that you are going to see in this tutorial
I’m going to use the $ as an indication that you are in the
command prompt. The $ is not part of the command.
The next step is to create a Python virtual environment.
This is the recommended way to install packages, so that
they are private to your project instead of installed system-
wide. You can create a virtual environment with the
following command:
$ python -m venv venv
This command is asking Python to run the venv package
(the first venv) and create a virtual environment named venv
(the second venv). If you want to use a different name for
your virtual environment, replace the second venv with
name you want to use. After the command completes, you
should see a subdirectory with this name. Inside this
subdirectory there is a private copy of the Python
interpreter.
Note that if the command above does not work, it is
possible that in your system the Python interpreter is called
python3 and not python. In that case, the command should
be:
$ python3 -m venv venv

Now you are going to activate this new virtual


environment. The virtual environment activation
configures the Python interpreter installed inside the
virtual environment as the currently active Python that is
invoked when you type python in the command line. This
activation is temporary, by the way, nothing in your system
is modified. The command to do the activation is different
depending on the operating system. If you are using Linux
or Mac, the command is:
$ source venv/bin/activate
(venv) $ _

If you are using Windows, the activation command is:


$ venv\Scripts\activate
(venv) $ _

See how as a response to running this command, the


prompt is modified to indicate that venv is activated.
With the virtual environment activated, you are ready to
install two packages that are going to help you manage
your board. For this you are going to use the Python
installer utility, called pip. This utility is already installed in
the virtual environment:
(venv) $ pip install esptool rshell

At this point your computer should be ready, but I want to


give you some additional information on virtual
environments in case you are not familiar with them before
I move on to the next task.
If you want to deactivate an activated virtual environment
and return to the globally available Python interpreter, use
the deactivate command:
(venv) $ deactivate
$ _

Something that confuses a lot of beginners is that virtual


environment activations are local to each terminal session,
so when you work with multiple terminals at the same time,
you have to run the activation command in all of them. And
if you close a terminal window while the virtual
environment was activated, then the activation is lost and
you will need to re-issue it when you open a new terminal.
You will also need to repeat the activation of the virtual
environment after you reboot your computer.
Just so that this is completely clear, let’s assume you’ve
done all the steps above to install esptool and rshell, then
turned your computer off and on again and now want to
resume working on this project. The virtual environment is
still stored in your computer and it still contains the
installed packages, but it is not activated, so when you run
python you will be using the system-wide interpreter, and if
you try to run esptool or rshell you are going to get errors
since these are not installed globally. To go back to your
virtual environment, you would need to open a terminal
window and enter the following commands. For Linux and
Mac:
$ cd micropython-tutorial
$ source venv/bin/activate
(venv) $ _

And if you are on Windows:


> cd micropython-tutorial
> venv\Scripts\activate
(venv) > _

And now you should be back in business!

2.5 Flashing
MicroPython with
esptool.py
At this point you have a brand new ESP8266 board nicely
set up in a breadboard, in factory new condition. Now is
time to install MicroPython on it!
Let’s begin by downloading an official release of
MicroPython from micropython.org. The downloads page
has a section for ESP8266 builds, so find that section, and
from there download the most recent build. At the time I’m
writing this, the current version is 1.10, and the firmware
file is called esp8266-20190125-v1.10.bin. By the time you do
this you may find a newer release. Download this file using
your web browser, and then to keep things organized you
may want to move it from your downloads directory into
the micropython-tutorial directory that you created above.
Next connect your USB cable. Plug the regular USB end
into a free USB port on your computer, and the micro-USB
end into your ESP8266 board. You will probably see one of
the LEDs blink when the board powers on, but of course
nothing else will happen because there is no software
installed.
Before flashing a new firmware into the board it is a good
idea to erase any previous data. This is something that you
should always do so that the new firmware runs from a
clean state. Here is the command that erases the entire
memory of your board:
(venv) $ esptool.py erase_flash

This command is going to scan all the serial ports that are
active in your computer to locate the one that is connected
to the microcontroller board. If a serial port is found, then
the command is going to proceed with the requested task
and erase all the memory in the board. Below you can see
an example output of this command:
(venv) $ esptool.py erase_flash
esptool.py v2.6
Found 2 serial ports
Serial port /dev/cu.SLAB_USBtoUART
Connecting........_
Detecting chip type... ESP8266
Chip is ESP8266EX
Features: WiFi
MAC: 84:0e:8d:8d:22:78
Uploading stub...
Running stub...
Stub running...
Erasing flash (this may take a while)...
Chip erase completed successfully in 2.0s
Hard resetting via RTS pin...

If you get a similar output to the above, then


congratulations, your board is fully operational and you are
going to have no trouble controlling it from your computer.
If the esptool.py command cannot find your board, then it
will show an error message. If this is the case, then you
need to check that you’ve followed my instructions
correctly. In particular, make sure you have installed the
serial port driver, and that your USB port and USB cable
are working well. You need to make sure the above
command works before you can continue.
On some Linux distributions you may get a “Permission
Error” when you run the above command. This is because
on Linux the serial port devices are only accessible to users
that are in the dialout group. Some Linux distributions
assign this group to the main user account automatically,
but others do not. If you get permission errors, you can
add yourself to the dialout group with the following
statement:
(venv) $ usermod -a -G dialout <your-username>

Note that this change does not take effect immediately. You
will need to log out and back in for the new group
membership to be effective.
Now that the board is completely erased, you can flash the
micropython build that you just downloaded. This is also
done with the esptool.py command:
(venv) $ esptool.py write_flash 0 esp8266-20190125-v1.10.bin

This command is going to write the contents of the


MicroPython .bin file to the board at address 0. This
command should take about 30 seconds and at the end of it
MicroPython should be installed in your board. If you want
the flash operation to go faster, you can try to increase the
transfer speed, which for me has always worked well. Use
the following command to flash the firmware at fast speed:
(venv) $ esptool.py --baud 460800 write_flash 0 esp8266-20190125-v1.10.bin
If you use the fast speed transfer the firmware should flash
in about 10 seconds.

2.6 Using the


MicroPython REPL with
rshell
Finally, the moment you were waiting for has come. You
are now ready to start MicroPython on your ESP8266
board.
What I’m going to show you in this chapter is how to
connect to the Python prompt running on your board. This
is called the REPL, which is short for “Read-Eval-Print-
Loop”. This is the standard Python prompt that you are
probably used to see when working with the regular Python
interpreter, but this time it is going to be running on your
board, and to interact with it you are going to use the serial
connection to your computer. Ready?
Look in the output of any of the esptool.py commands that
you issued earlier for a line that starts with Serial port.
What follows in that line is the name of the serial port
device that is attached to your microcontroller board. On a
Linux or Mac computer this is going to be a name with the
format /dev/<something>, while on Windows, it is going to be
COM<number>. You can see above that for the board that I’m
currently using the device name is /dev/cu.SLAB_USBtoUART.
To connect to your board and open a REPL session, enter
the following command:
(venv) $ rshell --port <your board serial port name>
For example, on my system the command that I need to
issue is:
(venv) $ rshell --port /dev/cu.SLAB_USBtoUART

This command will bring you into the rshell prompt. Here
is an example of what you should see if everything is
working well:
(venv) $ rshell --port /dev/cu.SLAB_USBtoUART
Using buffer-size of 32
Connecting to /dev/cu.SLAB_USBtoUART (buffer-size 32)...
Testing if ubinascii.unhexlify exists ... Y
Retrieving root directories ... /boot.py/
Setting time ... Mar 23, 2019 19:08:42
Evaluating board_name ... pyboard
Retrieving time epoch ... Jan 01, 2000
Welcome to rshell. Use Control-D (or the exit command) to exit rshell.
/Users/miguel/micropython-tutorial> _

If you are following this tutorial on Windows, note that


rshell has a history of problems when running on Windows.
When I tested the current version, which is 0.0.20, I found it
would not start. To make it work I had to add one option to
the rshell command, the -a option, which configures it to
use ASCII data transfers instead of binary transfers. Here
is the command for Windows:
(venv) $ rshell -a --port COM3

From this prompt you can perform management tasks


related to your microcontroller board, and also start a
Python REPL that you can use to interact with the board in
real time. Let’s go ahead and do just that, by entering the
repl command:

/Users/miguel/micropython-tutorial> repl
Entering REPL. Use Control-X to exit.
>
MicroPython v1.10-8-g8b7039d7d on 2019-01-26; ESP module with ESP8266
Type "help()" for more information.
>>>
>>> _
And now you are finally seeing MicroPython in all its glory!
In case you are wondering, your computer is playing a very
minor role now. It is displaying the output of MicroPython
running in the board to your terminal, and reading
keystrokes that you press on your keyboard and
transmitting them to the board so that MicroPython can
evaluate them there. Even though this prompt looks
suspiciously similar to the regular Python one, this is all
running in the board and your computer is acting as a
dumb terminal.
To make sure everything is working, type a simple Python
sentence:
>>> print('Hello, MicroPython!')
Hello, MicroPython!
>>> _

2.7 Playing with the On-


Board LEDs
But of course now that you have a MicroPython prompt
running, I have to give you something to play with that is
more interesting than printing simple text strings to the
console. As a nice and gentle introduction to working with
a hardware device, I’m going to show you how to operate
the on-board LEDs.
As I mentioned earlier, this board has two LEDs. To be
more accurate, this is almost always the case, but some of
the older ESP8266 development boards come with only one
LED. Let’s first try to control the most common one, the
one that all boards have:
>>> import machine
>>> led = machine.Pin(2, machine.Pin.OUT)

The machine import is one of the built-in packages that allow


Python code to interface with the board. One of the most
important feature of this package is the Pin() class, which is
used to read or write to the data pins.
If you recall from earlier, I mentioned that the LED that is
located next to the Wi-Fi antenna is attached to the D4 pin.
So why am I initializing the pin with a 2? This is because
naming of pins in microcontrollers is usually very
confusing. The names of the pins that are printed on the
board are really not that useful, what really matters in
terms of addressing a pin is its GPIO number. GPIO stands
for General Purpose Input/Output, and is the name that is
giving to the set of pins that are available to be used by
applications. If you go back to the pin diagram at the top of
this article, you will notice that D4 is also GPIO2, so I’m using
2 as the pin number.

The Pin() class constructor takes a second argument, which


is a constant that declares this pin as an output pin. That
means that the pin will be configured to be written to.
Obviously pins can also be configured for input, and you
will learn about that later in the tutorial.
Continuing with the theme of confusing things, the LED is
wired to the pin in reverse, so when the pin is set to 0 the
LED turns on, and when the pin is set to 1 the LED turns
off. When you initialize pin 2 as output using the above
Python statement, you are probably going to see the LED in
the board turn on immediately, because by default the state
of this pin is likely 0.
If you want to turn off the LED, you can write a 1 to the pin:
>>> led.value(1)
Or if you prefer a shorter form:
>>> led.on()

To turn the LED back on, use one of the following


commands:
>>> led.value(0)
>>> led.off()

Remember that the LED is wired in reverse, so on() turns it


off, and off() turns it on!
Do you want to see if you’ve got a second LED in your
board? The second LED is on pin D0, also known as GPIO16.
If it exists, it is also wired in reverse. Let’s create a led2 pin
for it:
>>> led2 = machine.Pin(16, machine.Pin.OUT)
>>> led2.off()

If your board has a second LED, the above code should turn
it on. This second LED, when present, is located near the
RST button.

The MicroPython interpreter supports control structures in


the same way as standard Python does. With a little bit of
creativity, you can create a little light show using a while
loop, the two LEDs and the time.sleep() function:
>>> import time
>>> while True:
... led.off()
... led2.on()
... time.sleep(0.5)
... led.on()
... led2.off()
... time.sleep(0.5)
...

After you type the last time.sleep(0.5) and press Enter, the
cursor will move to the next line at the same level of
indentation, assuming you will enter more statements
inside the while-loop. To tell the REPL that you are done
with this loop, you have to press the delete or backspace
key to unindent the cursor. Then press Enter on the empty
line to tell the REPL that the statement is complete.
If you are not familiar with the time.sleep() function, this is
one that exists also in the regular Python interpreter, and is
used to pause the execution of the script for the request
number of seconds. The 0.5 that I’m passing as an
argument means half a second.
If your board has the two LEDs, you should see them blink
in alternate fashion. On boards with only one LED you will
just see the one LED blink on an off.

When you are done admiring the light show, you can press
Ctrl-C to break out of the while-loop and stop it. Then you
can press Ctrl-X to exit the REPL and go back to the rshell
prompt. Finally you can press Ctrl-D to exit rshell and
return to your terminal prompt.
As you probably noticed, the while-loop that you typed in
the REPL is gone the moment you interrupt the script by
pressing Ctrl-C. If you wanted to restart the lights, you
would need to type the code all over again, or maybe use
the up arrow to recall previous lines that you have entered
one by one and in the right order. Either way, this type of
coding on-the-fly is great when you are trying things out,
but it is not a good workflow when you are writing an
application. In the next chapter I’m going to show you how
to create a proper MicroPython application, and how to
install it on your board so that it runs automatically when
the board is connected to power.
OceanofPDF.com
Chapter 3
Building a MicroPython
Application

3.1 The MicroPython File


System
Like with standard Python, MicroPython supports reading
and writing files from the file system. But what file system
is there in a microcontroller board? In the case of the
ESP8266 board, there is actually no file storage, so
MicroPython allocates a portion of the flash memory and
creates a virtual file system in it. The flash memory is also
where the MicroPython firmware is stored. As you recall, in
the previous chapter you used the esptool.py command to
write the firmware to this memory. This type of memory
preserves its contents even when it loses power, so it is
perfect to also use as file storage. The ESP8266
development board that I recommended for this tutorial
comes with 4MB of flash memory, which is enough to hold
the MicroPython firmware plus a small file system.
I want to note that you may find that for other
microcontroller boards file storage might be implemented
differently. For example, other boards supported by
MicroPython include a micro-SD card slot, so then the file
system comes from the SD card.
You may wonder why I am talking about files. The reason is
that there are two files that have special significance in
MicroPython, with the names boot.py and main.py. As part of
the normal start up procedure, MicroPython looks for files
with these names in the internal file system, and if it finds
them it executes them automatically. As you are going to
see soon, the MicroPython firmware already provides a
boot.py file, but it does not include main.py, which is left for
the developer to provide. So to create an application that is
preserved in the flash memory of the board, all that needs
to be done is write it to the board as main.py.
The rshell tool that is used to access the MicroPython REPL
provides a set of file system functions. So let’s connect the
board to the computer with the USB cable, and then start
rshell. If you need a refresher on how to start it, consult
the previous chapter.
When you are in the rshell prompt, type help to see all the
commands it offers:
/Users/miguel/micropython-tutorial> help

Documented commands (type help <topic>):


========================================
args cat connect echo exit filetype ls repl rsync
boards cd cp edit filesize help mkdir rm shell

Use Control-D (or the exit command) to exit rshell.

If you want to know more about any of these commands,


you can type help <command> to get additional
documentation. The commands that are interesting in
terms of file management are ls, cat, cp, rm and mkdir. If you
have any experience with Unix-based tools you probably
know these commands already. Here is a short summary of
what each command does:
ls: show the contents of a directory (similar to dir on
Windows)
cat: show the contents of a file (similar to type on
Windows)
cp: copy a file (similar to copy on Windows)
rm: delete a file (similar to del on Windows)
mkdir: create a directory

But these commands work in a very interesting way that


allows you to work with the file system in your computer
and the one in the board at the same time. You can refer to
any files in your computer using their path and file names,
but the special directory /pyboard is mapped to the
MicroPython’s file system.
For example, the ls /pyboard command lists the contents of
the root directory in your MicroPython board:
/Users/migue/micropython-tutorial> ls /pyboard
boot.py

And the ls / command lists the contents of the root


directory on your computer:
/Users/miguel/micropython-tutorial> ls /
Applications/ System/ bin/ etc/ opt/ tmp/
Library/ Users/ cores/ home/ private/ usr/
Network/ Volumes/ dev/ net/ sbin/ var/

If you want to see the contents of boot.py in your board you


can use cat /pyboard/boot.py:
/Users/miguel/micropython-tutorial> cat /pyboard/boot.py
# This file is executed on every boot (including wake-boot from deepsleep)
#import esp
#esp.osdebug(None)
import uos, machine
#uos.dupterm(None, 1) # disable REPL on UART(0)
import gc
#import webrepl
#webrepl.start()
gc.collect()
While there is nothing wrong with modifying and/or adding
code to boot.py, I prefer to leave this file alone, since there
are situations where MicroPython itself modifies this file. I
prefer to do all my coding on main.py which is a file I can
take complete ownership of.

3.2 Blinking Lights


Application
To keep all your tutorial files organized, let’s create a
subdirectory that is going to hold the code for this chapter.
So make sure your current directory is set to micropython-
tutorial and create a chapter3 subdirectory:

(venv) $ mkdir chapter3


(venv) $ cd chapter3

Now use your favorite text editor to create main.py. For this
first version I just copied the while-loop that I created in
the previous chapter:
import machine
import time

led = machine.Pin(2, machine.Pin.OUT)


led2 = machine.Pin(16, machine.Pin.OUT)
while True:
led.on()
led2.off()
time.sleep(0.5)
led.off()
led2.on()
time.sleep(0.5)

To upload this file to the microcontroller board, you can use


the cp command. To avoid having to jump from the terminal
prompt to the rshell prompt just so that you can type the cp
command and then exit rshell again, this time let’s run the
cp command directly as part of the rshell invocation:

(venv) $ rshell --port <board serial port name> cp main.py /pyboard

Don’t forget to insert the correct serial port device name


for your board in the command above. This command
copies the file main.py from the current directory to the
/pyboard directory, which is the root directory of the
MicroPython file system. If everything went well, the lights
on your board should start to blink as soon as the command
ends, and the application will always run when you power
your board up. You can even unplug it from your computer
and plug it to a different power source, such as a cell phone
charger you may have lying around, and the application will
always run automatically.
If you want to uninstall this application from your board,
you can use the rm command to delete main.py from the
internal file system:
(venv) $ rshell --port <board serial port name> rm /pyboard/main.py

Note that I reference this file as /pyboard/main.py, since I


want the command to act on the board’s file system. It is
unfortunate that this is a likely source of mistakes, because
if you were to enter this command as rm main.py it would be
the original file on your computer that would get deleted!
So be very careful with destructive commands such as rm.

3.3 Buttons
Something that you are going to find useful in the future
when you build your own projects is having the
microcontroller react to changes in the physical world. The
best example of that would be to change the behavior of the
application when a button is pressed. In this section I’m
going to show you how to add a stop button, which as its
name implies, will stop the blinking lights loop when
pressed. In this chapter I’m going to present a fairly simple
way to do this, but the topic of buttons is not over and will
be revisited in future chapters.
There are many kinds of buttons, but in general a button
has two terminals or pins. When the button is in a pressed
state, the two terminals are internally connected and
current flows through the button. When the button is in a
released state there is no connection, so electricity cannot
pass. The buttons that I’m using have actually four pins
and not two, to make it easier to mount them on a
breadboard:

Assuming you are using the same kind of push buttons as I


am, if you orient the button with two pins at the top and the
other two at the bottom, then you should consider the two
left-side pins as a group representing one of two button
terminals, and the two right side pins as the other.
Pressing the button connects the two groups of pins, and
electricity flows from one side of the button to the other.
When you connect the button you can pick any of the two
pins on the left for one connection, and any of the pins on
the right for the other, whichever are more convenient.

3.4 Input Pins


The high-level implementation to read the state of a button
from MicroPython is to have one of the available GPIO pins
connected to the button in such a way that the value of the
pin changes between 0 and 1 as the button is pressed and
released. Which state is associated with 0 and which with 1
does not really matter, all that matters is that 0 is mapped
to one of the states and 1 is mapped to the other.
Sounds pretty simple, right? Unfortunately, to help you
understand how this works I’m going to need to explain a
couple of basic electricity concepts, because as easy as it
sounds, this needs to be done with care, as a mistake here
can damage your microcontroller and we do not want that.
So bear with me while I go over this. I promise that in the
end, the solution to get the button working is indeed
simple.
As a general principle, if you want the input pin to read a
value of 1, then you have to deliver current to it, and this is
easy because there are pins with current available in the
microcontroller. If you create a connection from a 3V3 pin to
the left side of a button, and another from the right side to
the GPIO pin, then when you press the button it will close
the connection and the current will flow towards the pin
and thus the pin will read as a 1.
The problem occurs when the button is not pressed. In this
situation, the GPIO pin is not connected to anything
because the button in a released state keeps the circuit
open. A pin that is not connected to anything is said to be
in a “floating” state, and in this state, the value of the pin is
unpredictable and can sometimes read as a 0 or as a 1
depending on how electricity flows through the rest of the
circuit. If you want the pin to read a consistent 0, then it
needs to be connected to a GND pin, which basically makes
sure that any electricity that passes through the pin is
immediately flushed away.
In other words, the button needs to be wired such that it
connects the GPIO pin to 3V3 when it is pressed, and to GND
when it is not pressed, or viceversa, and this presents a bit
of a problem because the button has only two terminals
(two groups of two terminals actually, but effectively it’s
two usable connections) and there are three pins at play
here, the GPIO pin, 3V3 and GND.

3.5 A Note on Short


Circuits
The reason why there are current and ground connections
is that electricity always wants to flow between these two.
In general it is assumed that electricity flows towards the
ground, but this is somewhat of an abstraction because in
reality electrons flow in the reverse direction. The only way
to force electricity to flow through a component is to
connect it to current on one side, and to ground on the
other. Without the ground connection, electricity does not
flow.
But what happens when you connect a 3V3 pin to a GND pin
with no component in between? This is called a short
circuit and it is bad! The electrical current will have no
resistance at all, so it will flow uninterrupted and in large
amounts, causing the circuit to just overheat if you are
lucky, but most likely to burn your microcontroller. So
under no circumstance you should create a direct
connection between a pin that has current and a ground
pin.
This is a problem because a button isn’t your typical
component which receives an electric current and
consumes it to perform a task. When you press a button, a
direct connection between its two terminals is made,
exactly as if the connection was made by a cable, or in
other words, without nothing that consumes all that
electricity flowing through it. So a badly connected button
can cause a short circuit when pressed!
To prevent short circuits, all connections from current to
ground must have something in between that consumes the
electricity that flows through the wire. If you don’t have
any component to use, then you have to use a resistor,
which is a simple electrical component that doesn’t do
anything besides slowing down the flow of current that
passes through it.

3.6 Pull-Up Resistors


With all the information about circuits, resistors and the
flow of electricity in mind, I can now show you one possible
way to connect a button to a GPIO pin so that the pin value
maps to the button state. By now I’m sure you realize that
this isn’t as simple as it sounds, because the method by
which the button is connected to the GPIO pin and to 3V3
and GND needs to take into account that the GPIO pin in
question must never be left in a floating state, and also that
there is never a risk of a short circuit. Take a look at the
following diagram:

According to this diagram, when the button is pressed, a


connection from 3V3 to GND is made, so electricity flows
directly between these two. To avoid a short circuit, a
resistor is placed between 3V3 and the button. The GPIO pin
is connected in between the resistor and the button, so that
when the button is pressed the GPIO pin has a direct
connection to GND, which keeps its value at 0.
You can argue that the GPIO pin is also connected to 3V3
with a resistor in between, and in fact it is, but when the
button is pressed electricity will prefer to flow through the
path that leads to the ground, so no electricity will enter
the GPIO pin because ground is directly connected to it.
When the button is released, a connection from 3V3 to GND
does not exist, so electricity cannot flow through that path
and has to find another way. The only other possible way is
to take the path to the right into the GPIO pin, and this will
make the pin read as a 1. The current that gets into the pin
is going to be reduced by the resistor, but the size of the
resistor is chosen specifically so that it still allows enough
current to pass through to make the pin go to a high state
while eliminating the possibility of a short circuit.
So to summarize, using the wiring shown in the diagram
above, if the GPIO pin reads as a 0 it means that the button
is pressed, and if it reads as a 1 it means that the button is
released. This is another case of a reverse wiring,
somewhat similar to the on-board LEDs for which the
“active” state is also 0. In this type of circuit the resistor is
called a pull-up resistor, because it raises the pin to a value
of 1 in the absence of any other input such as a button
press.

3.7 Wiring of the Button


Are you worried or confused about working with these
strange pull-up resistors? You don’t need to be. The
ESP8266 microcontroller comes equipped with
programmable pull-up resistors for all the GPIO pins except
GPIO16, so all you need to do is configure them from
MicroPython!
When you tell the microcontroller to enable the pull-up
resistor on a given GPIO pin, it automatically makes an
internal connection from 3V3 to the pin passing through the
resistor. The part that needs to be made outside of the
microcontroller is the connection to GND, which as you see in
the wiring diagram from the previous section, passes
through the button. Here is how I implemented that
connection on the breadboard:

Here I’m using pin D5 / GPIO14. A connection is made from


this pin into the right side of the push button with a jumper
wire. A second jumper wire is then used to make a
connection from the left side of the button to GND, which can
be accessed from any hole in the second power strip row,
assuming you have the power strips in your breadboard
wired appropriately.
If you need to see how this looks in real life, here are two
views of my project:
Let’s jump into the REPL so that I can show you how to
configure and read this button.
MicroPython v1.10-8-g8b7039d7d on 2019-01-26; ESP module with ESP8266
Type "help()" for more information.
>>>
>>> import machine
>>> button = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_UP)
>>> button.value()
1

The pin that is connected to the button is instantiated in a


similar way to those that have LEDs, but this time the
second argument is set to IN to indicate that this is a pin
that is going to be read. The third argument enables the
pull-up resistor logic that makes it possible to connect a
button and read it reliably and safely.
You can see above that when I asked for the value of the pin
I obtained a 1. This was with the button released. Now I
can press the button and read the pin again:
>>> button.value()
0

So there you have it, this is a button that can be easily read
from Python code!
Now let’s add the button logic to the blinking light
application. Here is a new version of this application that
stops blinking the lights when the button is pressed:
import machine
import time

led = machine.Pin(2, machine.Pin.OUT)


led2 = machine.Pin(16, machine.Pin.OUT)
button = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_UP)
while button.value():
led.on()
led2.off()
time.sleep(0.5)
led.off()
led2.on()
time.sleep(0.5)
led.on()
led2.on()

In this version I replaced the while True I used previously


with a while button.value(), which will make the loop
continue for as long as the value of the button is not zero.
Effectively that means that the loop will continue to run
until the button is pressed, which makes the value of the
pin zero. And as a nice little detail, when the loop exits I’m
turning both LEDs off, so that the board is left in a clean
state without any LEDs on.
To try this new version of the application, make sure you
have assembled the circuit with the button, and then
upload the main.py file to your board with rshell:
(venv) $ rshell --port <board serial port name> cp main.py /pyboard

After that, whenever you power the device the blinking


lights will start, and as soon as you press the button they
will stop!
Something that I need to note here is that these button
presses are not cached of buffered anywhere, so the button
needs to be pressed at the exact moment the code is
checking the state of the pin. Given that a loop cycle lasts
for one second, it is possible, if you press and release the
button very quickly, that the button press will be missed.
For now I’m going to leave this limitation, so you should
remember that the button needs to be kept pressed for up
to a second to give the loop a chance to see it. Better
button implementations are coming later in this tutorial.

3.8 Writing Better Code


I think this is a good time to begin a discussion on good
software practices. Consider the little blinking lights
application from above. Just by reading the code, it isn’t
very clear what the 2, the 16 and the 14 mean, right? You
know these are the GPIO numbers for some pins, but for
anyone who isn’t familiar with the project this is going to
look a bit like magic, and in fact, numbers that appear in
source code without any explanation or justification are
often called magic numbers for that reason.
What can be done to improve the readability of this
program? A good practice is to always avoid magic
numbers. Instead of writing these numbers directly, it is
better to define constants that have self-explanatory names
with those values. Here is the application from above with
the magic numbers removed:
import machine
import time

LED_PIN = 2 # D4
LED2_PIN = 16 # D0
BUTTON_PIN = 14 # D5

led = machine.Pin(LED_PIN, machine.Pin.OUT)


led2 = machine.Pin(LED2_PIN, machine.Pin.OUT)
button = machine.Pin(BUTTON_PIN, machine.Pin.IN, machine.Pin.PULL_UP)
while button.value():
led.on()
led2.off()
time.sleep(0.5)
led.off()
led2.on()
time.sleep(0.5)
led.on()
led2.on()

Do you see how this is much better? Now it is more clear


that these three numbers correspond to pins that are
connected to two LEDs and a button. Having each pin
defined on its own line even allows me to write the “D” pin
labels as comments!
There is another little inconvenience with this application,
which I’m going to show to you with an example. Make
sure the main.py application is copied to your board and
then enter the MicroPython REPL using rshell.
Once you are in the Python prompt, you can start the
application with:
>>> import main

This command is going to block, because of the while-loop


that runs for as long as you don’t press the button. Press
the button, and the loop will exit, and this will make the
Python prompt appear again.
Now, how do you start the lights again? If you run import
main again, nothing happens, you just get the prompt back.
The problem is that main.py was already imported once, so it
is cached by MicroPython. When you import a module that
was already imported, there is an optimization that makes
Python immediately return the cached copy without re-
running the code.
Really the only way to restart the lights at this point is to
reset the board, either by pressing the physical reset
button on the board, or by pressing Ctrl-D on the REPL,
which triggers a soft reset.
I hope you agree that it would be more convenient if there
was a function that you can call to make the lights run as
many times as you want, and without a direct relationship
to an import statement, which only works the first time.
The solution is actually simple. The blinking loop can be
moved to a function, which I’m going to call blink(). Then
in the last line of main.py the blink() function is invoked:
import machine
import time

LED_PIN = 2 # D4
LED2_PIN = 16 # D0
BUTTON_PIN = 14 # D5

def blink():
led = machine.Pin(LED_PIN, machine.Pin.OUT)
led2 = machine.Pin(LED2_PIN, machine.Pin.OUT)
button = machine.Pin(BUTTON_PIN, machine.Pin.IN, machine.Pin.PULL_UP)
while button.value():
led.on()
led2.off()
time.sleep(0.5)
led.off()
led2.on()
time.sleep(0.5)
led.on()
led2.on()

blink()

This is equivalent to the previous version, but now the


blinking logic can be invoked from the REPL as a function,
and that call can be repeated as desired. When you run
import main the lights will run as before, but once you break
out of the while-loop with Ctrl-C or by pressing the button
you can re-run the lights with:
>>> main.blink()

Each time you stop the lights you can repeat the above call
to restart them.
And, if having to type main.blink() to run the lights seems
too long, you can import the blink() function as a top-level
symbol with Python’s from ... import ... syntax, and then
invoke blink() directly:
>>> from main import blink
>>> blink()

I think the application is now looking pretty good, but


there’s going to be more coding tips in the chapters that
follow. In the next chapter I’m going to go over what is
probably the most important feature of the ESP8266
microcontroller: its Wi-Fi support!
OceanofPDF.com
Chapter 4
Wi-Fi and the Cloud

4.1 The Two ESP8266


Wi-Fi Interfaces
The ESP8266 microcontroller has two Wi-Fi interfaces,
which in Wi-Fi jargon are called access point and station
interfaces. The access point interface allows devices with
Wi-Fi such as your laptop to connect as clients to the
microcontroller board. The station interface allows the
microcontroller board itself to connect to other Wi-Fi
access points such as the router in your home as a client.

4.1.1 The Access Point Interface


Current releases of MicroPython have the access point
interface enabled by default. Connect your microcontroller
board to a power source and then check the list of available
Wi-Fi connections on your computer. While the
microcontroller is running, you should see a MicroPython-
XXXXXX connection:
The default password for this connection is micropythoN (yes,
that last N is uppercase). If you like, you can connect to
your microcontroller with this password, but be aware that
the microcontroller is not connected to the Internet
through this interface so there is pretty much nothing you
can do at this point while connected to it over Wi-Fi.
This interface can be managed using the network package
from MicroPython. Here is how to create an object that
represents this interface:
>>> import network
>>> ap_if = network.WLAN(network.AP_IF)

Now you can check if this interface is active:


>>> ap_if.active()
True

You can also get its settings:


>>> ap_if.ifconfig()
('192.168.4.1', '255.255.255.0', '192.168.4.1', '208.67.222.222')

The settings are, in order, the IP address, the subnet mask,


gateway and DNS server. Don’t worry if you don’t know
what these are, just know that they are the parameters that
define this networking interface.
If you plan on using the access point, it is a good idea to
change the network name and password, since you do not
want some random person walking by your house getting
offered to connect to the MicroPython-XXXXXX network with its
default password on their phone! You can change the
network name and password as follows:
>>> ap_if.config(essid='network name', password='password')

You can deactivate the access point if you don’t plan on


using it, which will also prevent it from appearing as a
connection choice on phones and laptops:
>>> ap_if.active(False)

Or activate it again:
>>> ap_if.active(True)

4.1.2 The Station Interface


The station interface in your microcontroller is more
interesting, because it will allow you to connect your board
to the Wi-Fi network in your home and get access to the
Internet through it:
>>> import network
>>> sta_if = network.WLAN(network.STA_IF)

The station interface is not active by default, so it needs to


be activated if you intend to use it:
>>> sta_if.active(True)
You can have it detect Wi-Fi networks that are within
reach:
>>> sta_if.scan()
[(b'NETGEAR99', b'\xa0@\xa0\x91}\xac', 3, -72, 3, 0),
(b'test-net', b'pP\xaf\xc9\x90\xd2', 11, -73, 3, 0)]

The tuples returned here have the parameters of each


available Wi-Fi connection. In particular, the first element
in each tuple is the SSID name, the third element is the
channel number, and the fourth is the RSSI or signal
strength indicator.
Then you can connect to your Wi-Fi router with a simple
connect() call, and you can ensure the connection was made
with isconnected():
>>> sta_if.connect('your SSID', 'your Wi-Fi password')
>>> sta_if.isconnected()
True

As with the access point interface, you can also get the
parameters of the connection:
>>> sta_if.ifconfig()
('192.168.0.45', '255.255.255.0', '192.168.0.1', '192.168.0.1')

In case this isn’t clear, when you connect to your


microcontroller board using one of the Wi-Fi interfaces, the
USB cable is only used for providing power to the board.
You could plug your microcontroller to the wall using a cell
phone charger or adapter and you will still be able to
connect to it using these two interfaces.

4.2 The MicroPython


WebREPL
MicroPython includes an experimental version of the REPL
that works over Wi-Fi and can be accessed from a web
browser. To enable this option, start a standard REPL
session with rshell and run the following command:
>>> import webrepl_setup

This is going to start an interactive configuration session


that will ask you a few simple questions:
WebREPL daemon auto-start status: disabled

Would you like to (E)nable or (D)isable it running on boot?


(Empty line to quit)
> E
To enable WebREPL, you must set password for it
New password (4-9 chars): <enter a password here>
Confirm password: <enter a password again>
Changes will be activated after reboot
Would you like to reboot now? (y/n) y

Remember the password that you entered as part of this


questionnaire, because you are going to need it later.
To use the WebREPL you need to download a client
application, which runs on your web browser. You can
download a zip file with this client from its git repository.
The download above is a zip file, which will likely be named
webrepl-master.zip. You need to extract the contents of this
zip into a directory, and then from that directory open the
file named webrepl.html in your web browser.
You can see that in the top left corner of the WebREPL
client’s window there is a text entry box that reads
ws://192.168.4.1:8266/, followed by a “Connect” button. If
you are connected to the microcontroller board through its
access point interface, then the URL that appears in the
entry field is the correct one, so you can press the Connect
button to open the REPL.
If you connected the board’s station interface to your home
Wi-Fi, then you need to find out what is the IP address that
was assigned to the board, and replace it in the text box.
For example, you can see above that for my board, the
sta_if.ifconfig() call shows that the IP address of the board
as 192.168.0.45, so in my case the connection URL for the
WebREPL should be changed to ws://192.168.0.45:8266.
Make sure you only replace the IP address. The URL
scheme needs to be ws:// (for WebSocket), and the port
needs to be 8266. Once you set the correct IP address you
can press the Connect button.
You will need to enter the WebREPL password that you
selected during the setup, and then you will have access to
the same REPL interface, but on your browser.
The WebREPL supports copying files between your
computer and the microcontroller, which is done on the
REPL’s side panel. Unfortunately at the time I’m writing
this the file upload and download options do not appear to
be working. I’m sure this will eventually work well, so if
you find that you like the WebREPL more than the rshell
REPL, then you can use it in place of rshell’s cp command
to install application files on the board.
If you think you are going to use the WebREPL, then you
can leave it enabled. But if you don’t plan on using it to
replace rshell, you can disable it by running import
webrepl_setup again.
4.3 Using a
Configuration File
To connect the board to your Wi-Fi router I had you type
your router’s password in the MicroPython REPL. I’m not
sure about you, but seeing any kind of password written on
the screen makes me very nervous. Continuing with the
theme of providing you with good software development
practices I think this is a good time to make an
improvement on how the Wi-Fi connection is started,
because you will need to include your Wi-Fi connection
details in every application that you build from now on.
So what I’m going to ask you to do is to create a
configuration file, where all these details are going to be
stored. Once they are in the configuration, you will not
need to enter them again, you will just import them from
there. One of the simplest patterns for configuration files
in Python is to use variables defined in a separate Python
module.
As you did in the previous chapter, first create a separate
subdirectory that is going to hold the code for this chapter.
Make sure your current directory is set to micropython-
tutorial and create a chapter4 subdirectory:

(venv) $ mkdir chapter4


(venv) $ cd chapter4

Now let’s create a new file called config.py with the


following contents:
WIFI_SSID = 'your SSID'
WIFI_PASSWORD = 'your Wi-Fi password'
You can now upload this file to your controller board. Using
rshell the command is:

(venv) $ rshell --port <board serial port name> cp config.py /pyboard

Now, when you need to connect to your Wi-Fi network you


can do it without having to write your password:
>>> import config
>>> import network
>>> sta_if = network.WLAN(network.STA_IF)
>>> sta_if.connect(config.WIFI_SSID, config.WIFI_PASSWORD)
>>> sta_if.isconnected()

Similar code to the above snippet will be included in almost


all the remaining applications in this tutorial.

4.4 Sending HTTP


Requests
Now that you have a good understanding of the Wi-Fi
capabilities in your microcontroller board, it is time to
learn how to do something useful with them. In terms of
Internet access, nothing beats being able to send a web
request (more specifically called HTTP request) out to a
cloud based service, which is basically one of the core ideas
behind the Internet of Things paradigm.
When using standard Python, most people use the requests
package to send HTTP requests. MicroPython comes
preloaded with urequests, which is a simplified version of
requests.

To run the following example, make sure your


microcontroller is connected to your home Wi-Fi router
through the station interface so that it has access to the
Internet. You can leave the access point interface in the
board enabled, or if you prefer you can disable it, since it is
not needed for this.
As an example of how to send a web request, I’m going to
contact the icanhazip.com service, a simple web service that
tells you what is your public IP address. Here is how to do
it:
>>> import urequests
>>> r = urequests.get('http://icanhazip.com')
>>> r.status_code
200
>>> r.text
'56.19.70.24\n'

The urequests.get() function sends a web request using the


GET method, which is normally associated with retrieving
information from the remote server. For this example, the
only argument to the function is the URL of the service that
receives the request. More complex web services may
require additional arguments, as you will see later.
The return value of the urequests.get() function is a
response object. The status_code attribute of this response
tells you the result of the operation using the standard
numeric codes specified by the HTTP standard. Codes in
the 2xx range are all success codes. Codes in the 3xx range
are used to indicate redirects, which are not as common
with web APIs as they are in standard web applications.
Codes in the 4xx range are error codes that indicate the
request is incorrect in some way. Codes in the 5xx range
indicate that an unexpected error has occurred.
The text attribute of the response object contains the actual
data returned by the service. For this service, the data
returned is your public IP address, or in other words, the IP
address that was assigned to your Wi-Fi router by your
Internet service provider.
4.5 Setting Up a IFTTT
Webhook
Now let’s do something more “ccloudy” with web requests.
One of the standard ways in which events are
communicated in the cloud is through webhooks. A
webhook is basically a pre-configured web address that
triggers an action when it is invoked. In this section I’m
going to show you how to use the IFTTT platform to define
a custom webhook that can perform an action of your
choosing when your microcontroller board calls it. IFTTT
supports a large number of actions, from sending an email
or SMS to ordering pizza!

The name “IFTTT” is short for “If This, Then That”, a


reference to being able to create fully customized triggers
and actions. To begin, create a free account at
https://ifttt.com. As part of the new account creation you
may be asked to provide a few services that you use to then
get better recommendations. While it is not a problem if
you do that, for this tutorial you can skip that part of the
setup.
Once you are logged in to IFTTT, click on your username in
the top-right part of the page, and then select “New
Applet” from the menu. This will take you to the “if this
then that” page:

As you can probably guess, the “this” part is a trigger, and


the “that” part is the action. Both are completely
configurable and IFTTT provides dozens of integrations
with third party services to help you build what you need.
Click on the “this” to configure the trigger. You are going
to be offered lots of options from which you need to select a
service called “Webhooks”. You can type “webhook” in the
search box to filter the list of services. Once you located
the Webhooks service, click on it. You may be presented
with a screen where you need to “connect” to this service.
This is only going to appear the first time you use the
Webhooks service in your account. Click on Connect to add
it.
The Webhooks service provides a single trigger called
“Receive a web request”. Click on it to add it to the applet.
You will then need to provide an event name for this
trigger. This webhook is going to be invoked when a button
is pressed, so I called my event button_pressed. You can use
a different name if you like:

After you click the “Create trigger” button the “this” part
will be complete, and you will be back to the “if this then
that” page.
Now click on “that” to configure the action part. Once
again you will be offered lots of integrations with third
party services. My recommendation for this first example is
that you pick an “easy” one, such as Email or SMS. The
idea is that when this example is complete, when you push
a button connected to the microcontroller an email or SMS
will be sent to you. Once you have familiarized with the
IFTTT service you can browse through the tons of
integrations to see what cool ideas you can come up with to
automate your life.
In my case I’m going to use the Email integration. If you
don’t see it as an icon, type “email” in the search box to
help you find it. Click on it to configure an email action.
The only action available in the email service is “Send me
an email”. Click on it to add it to the applet.
You will now see another configuration panel where you
can define the subject and body of the email that will be
sent out to you. Note how placeholders are used to define
the variable parts.
You are free to edit the subject and body of the email
however you like. In my case I left the subject alone, but
made a small change in the body of the email. The original
format included an “Extra data” line that had three custom
values that can be provided with the webhook, represented
by placeholders called Value1, Value2 and Value3. For this
example I’m only going to use only one custom value, so I
edited the template so that this line shows only Value1 and
labels it appropriately as “Button”.
If this does not make much sense yet, do not worry, it will
all be clear in a minute once you are able to see how IFTTT
works. For now, click the “Create action” button and then
on “Finish” complete the applet.
The webhook is fully set up, so let’s give it a try. What you
need to find out now is what is the webhook URL that was
created for this applet. Click once again on your username
in the top-right part of the page, and select “Services” from
the menu. Then click on “Webhooks”, and in the Webhooks
page click on “Documentation”.
You will be taken to a page that will allow you to build a
URL for an event, including those three custom values that
can be passed. Click on the {event} box and type
button_pressed there (or the event name that you used in
your applet). Then click on the empty box to the right of
"value1" and enter micropython1 or any identifier that you
want to use with the button that you will be building soon.
This value will be associated with the {{Value1}} placeholder
that was added in the email body.
After you entered the event name and the button identifier
the bottom of the page will show an example request that
you can use to trigger the action. This example is given
using the curl utility, which is a very popular HTTP client
that you can run from the command line. You also get a
“Test It” button that you can click to invoke the webhook
with the event name and custom values you provided. So
as a first test, go ahead and click the test button, and a few
seconds later an email should arrive (or whatever action
you selected should be executed, if you didn’t use an
email).
If you verified that the webhook is working, now it’s time to
invoke this webhook from MicroPython, and for that we
need to translate the curl command into urequests.
I showed you above how to send a GET request with
urequests. For this request a POST method is used instead.
This is the -X POST option sent to curl. So instead of
urequests.get() this time it will be urequests.post().

The first argument to this function is the URL. This URL


appears in the curl command as the last argument. It starts
with https://maker.ifttt.com/trigger/... and includes your
chosen event name and a long string of alphanumeric
characters that are your webhook key. In case you are
curious, the key is what identifies your account when the
request is sent.
The curl command also includes a Content-Type header
(given with the -H option) and a body (given with the -d
option). The body is where the custom “value1” argument
is given, using a very popular format with web APIs called
JSON, which is somewhat similar to Python dictionaries. As
it turns out, the Content-Type header is there to indicate to
the web service that the request comes with JSON data in
the body, so these two bits of data are related.
In urequests, sending a JSON body is actually easy. The
Content-Type header does not need to be manually set as in
curl, and the data needs to be given as a Python dictionary.
The conversion to JSON and the header are added by
urequests.

So finally, here is how this webhook can be invoked from


MicroPython:
>>> import urequests
>>> r = urequests.post('http://maker.ifttt.com/trigger/button_pressed/...',
... json={'value1': 'micropython1'})
>>> r.status_code
200
If you want to give this a try, power up your
microcontroller, and enter the REPL. Make sure you are
connected to your Wi-Fi router, and then use the above
code as a reference in creating your own webhook request.
Make sure that the URL that you pass is the complete URL,
including the long key that identifies your account.
Note that in the above snippet I entered the urequests.post()
in two lines. When I reached the end of the URL I typed the
comma and then pressed Enter. I then entered the rest of
the call in a second line. This was entirely done by choice,
it would have been just as well to enter everything in one
line.
Did you get an email notification from the MicroPython
issued request? Great! Now that the proof of concept is
working, this needs to be made into an application!

4.6 Emulating the


Amazon Dash Button
You have probably been suspecting this all along. What I’m
working towards in this chapter is building a clone of the
Amazon Dash button. In case you are not familiar with
these devices, these are little Wi-Fi enabled buttons made
by Amazon (now discontinued) that automatically order
specific Amazon products when pressed. The button that
you will have at the end of this chapter is going to invoke
an IFTTT webhook, and that means that you will be able to
select the action that is carried out when the button is
pressed from any of the options supported under the IFTTT
platform.
I’ve already showed you most of the functionality that is
required to complete a first version of this project. The
only part that I haven’t discussed is how to implement the
actual physical button that you have to push to trigger the
webhook invocation. For the first solution I’m going to take
advantage of the reset button that is already in this
microcontroller board. What I’m going to do is to code my
application so that it sends out the webhook request when
it boots. So then each time the reset button is pressed, the
board will reboot and a new request will be sent out.
So basically, the high-level structure of this application is
going to be as follows:
connect_wifi()
call_webhook()

Simple, right? Now I just need to implement these two


functions to complete the project.
Let’s look at the connect_wifi() function first:
import time
import config

def connect_wifi():
ap_if = network.WLAN(network.AP_IF)
ap_if.active(False)
sta_if = network.WLAN(network.STA_IF)
if not sta_if.isconnected():
print('Connecting to WiFi...')
sta_if.active(True)
sta_if.connect(config.WIFI_SSID, config.WIFI_PASSWORD)
while not sta_if.isconnected():
time.sleep(1)
print('Network config:', sta_if.ifconfig())

This function is essentially a more robust version of the Wi-


Fi connection code I’ve shown above. The function first
deactivates the access point interface, since it is not used
in this example.
Then it checks if the station interface is already connected.
This is always a good idea, as that will make this function
return immediately if the connection has been already
established. If the Wi-Fi connection does not exist, then the
interface is activated and then a call to the connect()
function is made. A Wi-Fi connection can take a few
seconds to be completed, and this happens asynchronously.
To avoid returning from this function before the connection
is fully established, I’ve added a while-loop that blocks the
function until the isconnected() function returns True. Inside
the loop I just call time.sleep(1) to wait for one second
before checking the connection again. Using a while-loop
with a sleep inside is a standard way to block until a
condition is satisfied. Without the sleep the loop would be
checking the connection repeatedly as fast as it could, and
there is really no point in doing that. Checking at a rate of
once per second is sufficient.
Before I implement the call_webhook() function, I’m going to
take advantage of the configuration file that I built earlier
and add more things to it, as that will help in keeping the
code nicely organized. I’m going to add the webhook URL
and the button name to the configuration, so that they are
independent of the application, in the same way the
connect_wifi() above is independent of the specific Wi-Fi
connection details. Here is an expanded config.py file:
WIFI_SSID = 'your SSID'
WIFI_PASSWORD = 'your Wi-Fi password'
WEBHOOK_URL = 'http://maker.ifttt.com/trigger/button_pressed/...'
BUTTON_ID = 'micropython1'

Of course, you should set these four configuration settings


as appropriate for you. In particular, note how I simplified
the webhook URL, which in reality is going to be much
longer as it includes the key that identifies your IFTTT
account.
Now I can show you a first implementation of the
call_webhook() function:
def call_webhook():
print('Invoking webhook')
response = urequests.post(config.WEBHOOK_URL,
json={'value1': config.BUTTON_ID})
if response is not None and response.status_code < 400:
print('Webhook invoked')
else:
print('Webhook failed')

This function considers any response that comes back with


a status code of 400 or higher as an error. Note how I’m
adding print() statements that provide information about
what’s going on in the application. These are obviously not
going to be seen anywhere when you are running the
device standalone, but the point of having them there is
that you can also run these functions from the REPL, and in
that case you will want to know what happens.
Here is an almost complete version of main.py with this
application:
import network
import time
import urequests
import config

def connect_wifi():
ap_if = network.WLAN(network.AP_IF)
ap_if.active(False)
sta_if = network.WLAN(network.STA_IF)
if not sta_if.isconnected():
print('Connecting to WiFi...')
sta_if.active(True)
sta_if.connect(config.WIFI_SSID, config.WIFI_PASSWORD)
while not sta_if.isconnected():
time.sleep(1)
print('Network config:', sta_if.ifconfig())

def call_webhook():
print('Invoking webhook')
response = urequests.post(config.WEBHOOK_URL,
json={'value1': config.BUTTON_ID})
if response.status_code < 400:
print('Webhook invoked')
else:
print('Webhook failed')

The only thing I haven’t added yet to main.py is the code


that invokes these two functions, and the reason for that is
that first I want to test these functions by hand from the
REPL to make sure they are working. So go ahead and
copy main.py and config.py to your microcontroller using
rshell:

(venv) $ rshell --port <board serial port name> cp main.py config.py /pyboard

Then to start fresh you may want to unplug the


microcontroller and plug it again. This is so that the Wi-Fi
connection goes away. Now start a REPL session to test the
two functions:
>>> import main
>>> main.connect_wifi()
Network config: ('192.168.0.45', '255.255.255.0', '192.168.0.1', '192.168.0.1')
>>> main.call_webhook()
Invoking webhook
Webhook invoked
>>> _

Since everything is working as expected, I can complete


the application by adding the two calls to main.py. Here is
the complete first version of my little Amazon Dash clone:
import network
import time
import urequests
import config

def connect_wifi():
ap_if = network.WLAN(network.AP_IF)
ap_if.active(False)
sta_if = network.WLAN(network.STA_IF)
if not sta_if.isconnected():
print('Connecting to WiFi...')
sta_if.active(True)
sta_if.connect(config.WIFI_SSID, config.WIFI_PASSWORD)
while not sta_if.isconnected():
time.sleep(1)
print('Network config:', sta_if.ifconfig())

def call_webhook():
print('Invoking webhook')
response = urequests.post(config.WEBHOOK_URL,
json={'value1': config.BUTTON_ID})
if response is not None and response.status_code < 400:
print('Webhook invoked')
else:
print('Webhook failed')

def run():
connect_wifi()
call_webhook()

run()

Here I added one more function called run() which


combines the two other functions. The run() function is
then invoked at the bottom from the global scope.
Copy the updated main.py to your board, and now each time
the microcontroller is plugged in or restarted, the webhook
is going to be hit. For now you can use the little reset
button that comes with the board as “the button”, and each
time you press it the board will reset and call the webhook.

4.7 Reporting Errors


I’ve mentioned above that I’ve added the print() statements
to the code so that I can see what the application is doing
when I run it from the REPL. That is all great, but when the
webhook request fails and the REPL isn’t connected, there
is currently no way to know there was a failure. Failures
when sending requests can happen for many different
reasons and are not that unlikely. For example, the IFTTT
service could be temporarily down for maintenance. Since
I haven’t incorporated a screen yet, for now the only visual
elements that I have available are (you guessed!) the
LEDs! So let’s blink the LEDs a few times to show that
there’s been an error. Here is a function that does the
blinking called show_error():
import machine

def show_error():
led = machine.Pin(config.LED_PIN, machine.Pin.OUT)
led2 = machine.Pin(config.LED2_PIN, machine.Pin.OUT)
for i in range(3):
led.on()
led2.off()
time.sleep(0.5)
led.off()
led2.on()
time.sleep(0.5)
led.on()
led2.on()

This is similar to the blinking lights application from last


chapter, but instead of using a while-loop I’m now using a
for-loop that only runs the blinking cycle 3 times. I have
also moved the LED_PIN and LED2_PIN constants to the
configuration file. Here is a new main.py version that
includes the LED error reporting:
import machine
import network
import sys
import time
import urequests
import config

def connect_wifi():
ap_if = network.WLAN(network.AP_IF)
ap_if.active(False)
sta_if = network.WLAN(network.STA_IF)
if not sta_if.isconnected():
print('Connecting to WiFi...')
sta_if.active(True)
sta_if.connect(config.WIFI_SSID, config.WIFI_PASSWORD)
while not sta_if.isconnected():
time.sleep(1)
print('Network config:', sta_if.ifconfig())

def call_webhook():
print('Invoking webhook')
response = urequests.post(config.WEBHOOK_URL,
json={'value1': config.BUTTON_ID})
if response is not None and response.status_code < 400:
print('Webhook invoked')
else:
print('Webhook failed')
raise RuntimeError('Webhook failed')

def show_error():
led = machine.Pin(config.LED_PIN, machine.Pin.OUT)
led2 = machine.Pin(config.LED2_PIN, machine.Pin.OUT)
for i in range(3):
led.on()
led2.off()
time.sleep(0.5)
led.off()
led2.on()
time.sleep(0.5)
led.on()
led2.on()
def run():
try:
connect_wifi()
call_webhook()
except Exception as exc:
sys.print_exception(exc)
show_error()

run()

The main change that I’ve made to make this application


more robust is to add a try/except block around the code in
the run() function. This is so that I can catch those
unexpected errors and handle them by calling the function
that blinks the lights. The sys.print_exception() function will
show the error message and the location of the error in the
same way errors that are not caught are shown. This is
obviously going to be useful when you run the application
manually from the REPL.
The other change is in the call_webhook() function. I’ve
added a raise RuntimeError() right after I print the error
message for a failed webhook. If you look at the code
carefully, you’ll notice that the raise statement will occur
while the code that is inside the try block is running. This
is in the call_webhook() function which is the second line
inside the try. Any errors that occur in there, including this
one that I’m self-generating, will trigger the code in the
except block to run, and that is where I make the lights
blink. If no errors occur, then the code in the except block
will be skipped.
Doing the error handling in this way is nice, because not
only I’m handling my own error condition, but I’m also
capturing any unexpected ones. I’ve mentioned above that
errors in requests come back with status codes 400 and
above, but while this is in general true, you only get these
codes when the server on the other side is running and is
able to report them. There are situations in which the
remote server cannot be contacted at all, and in those
cases urequests will also raise an exception. The try/except
block will catch my own error as well as any others that
occur while those two functions in the try block run.
I should mention that in many cases doing a “catch-all”
try/except is seen as a bad practice, because it can silence
errors that would otherwise point to bugs or other
conditions that you as developer might want to be aware
of. In this case that does not apply because I’m using
sys.print_exception() to show the error in the same way the
MicroPython interpreter would have shown it.
Here is the new config.py that includes the LED pins:
WIFI_SSID = 'your SSID'
WIFI_PASSWORD = 'your Wi-Fi password'
WEBHOOK_URL = 'http://maker.ifttt.com/trigger/button_pressed/...'
BUTTON_ID = 'micropython1'
LED_PIN = 2 # D4
LED2_PIN = 16 # D0

To test how the error reporting works you can modify the
webhook URL in config.py to be something incorrect. For
example, change it to http://maker1.ifttt.com/.... Then each
time you press the reset button, wait a couple of seconds
and then you will see the blinking lights.

4.8 The Deep Sleep State


Did you consider what happens after you press the reset
button and the webhook is invoked? The main.py script will
reach the end, so the MicroPython interpreter will just
remain idle until the reset button is pressed again, which
will restart everything. While this does not present a
technical problem, you may argue that it is wasteful to
leave the microcontroller in a fully operational state when
it is not doing anything. If you were powering the device
with a battery then keeping the device running all the time
would shorten the battery life in a considerable way.
The 8266 microcontroller has a mode called “deep sleep”,
in which it turns itself almost completely off. In particular,
the Wi-Fi subsystem and the CPU are both stopped, and
this is what makes this mode attractive, because the
amount of power needed to maintain the deep sleep mode
is minimal. The only subsystem that is kept awake is the
real-time clock (RTC), and this is because there is a
mechanism by which you can program the device to wake
up from deep sleep at a specified time (we will learn about
this particular topic in another chapter).
To make the microcontroller enter deep sleep mode you
just need to call machine.deepsleep(). Once the device is in
this mode, it can be awaken by pressing the reset button,
which is exactly the behavior that is needed for the
webhook.
There are many use cases in which it is necessary to know
if the board is awakening from deep sleep, as opposite to
being powered on for the first time. There is actually one
that affects this project, which is that there is currently no
way to distinguish a restart due to pushing the reset button
versus a first start after the device is powered on. The
machine.reset_cause() provides detailed information about
the reset cause. The possible return values are:

machine.PWRON_RESET: the device was just powered on for


the first time.
machine.HARD_RESET: the device is coming back from a
“hard” reset, such as when pressing the reset button.
machine.WDT_RESET: the watchdog timer reset the device.
This is a mechanism that ensures the device is reset if
it crashes or hangs.
machine.DEEPSLEEP_RESET: the device was reset while being
in deep sleep mode.
machine.SOFT_RESET: the device is coming back from a
“soft” reset, such as when pressing Ctrl-D when in the
REPL.

For the button application I can check that the reset cause
was machine.DEEPSLEEP_RESET, and only in that case call the
webhook.
Here is yet a new version of the run() function in main.py
that puts the device into deep sleep mode after invoking
the webhook, and also avoids the webhook for all reset
causes except when it is coming out of deep sleep:
def run():
try:
if machine.reset_cause() == machine.DEEPSLEEP_RESET:
connect_wifi()
call_webhook()
except Exception as exc:
sys.print_exception(exc)
show_error()
machine.deepsleep()

With this change, the webhook is not going to be invoked


when you first power the device, since in that situation the
reset cause is going to be machine.PWRON_RESET. In that first
running instance the microcontroller will go right into deep
sleep mode. Then each time the reset button is pressed the
device will boot and main.py will run again, and in those
cases the reset cause will be the expected one, so the
webhook will be invoked.

4.9 Adding a Debug


Mode
Did you notice a problem with the change I’ve made in the
previous section? Now the board is only awake for the
short time it sends the webhook request. At any other
times the device is almost entirely asleep, which means
that you cannot connect to it with the REPL!
If you try to connect a few times you will eventually be able
to connect, because rshell triggers a reset of the board
when it fails to connect to it. But this is a very annoying
problem that needs to be addressed. The solution I came
up with is to have a “debug mode” for this application.
When this debug mode is activated, the application will not
put the board into deep sleep mode, it will keep it awake
like before, and this will allow you to connect to it with
rshell. Using a debug mode is a useful way to alter the
behavior of the application to make your development work
on it easier or more convenient.
Remember the button implementation from the previous
chapter? That used pin D5, which was connected to GND with
a button in the middle. I used the pull-up resistor attached
to this pin to make the pin read a value of 1 when the
button was released or 0 when it was pressed. The same
concept can be used to create a debug “switch” that can
tell the application if it is okay to go into deep sleep or not.
The debug switch will work as follows:

If pin D5 reads as a 1 it means that it is not connected to


ground and the pull-up resistor is giving it the 1 value.
In this case I’m going to assume this is the “normal”
mode, and deep sleep will be invoked after the
webhoook is called.
If pin D5 reads as a 0, then it means that there is a
connection from the pin to the ground, and that will
make the application assume it is operating in “debug”
mode, which will skip the deep sleep mode and keep
the device awake at all times.

While using a button wired exactly as in the previous


chapter is fine as a debug switch, these buttons that I’m
using are not the best type for this because they don’t stay
in the pressed state. I would prefer a button that is really a
switch, so that I can leave it in the on or off position to
indicate which mode I want it to use. If you have a button
of that type, you can use it to add the debug switch. Since I
don’t have that type of button at hand, I’m going to
improvise.
To activate debug mode, I’m going to take a jumper wire
and connect one end to pin D5, and the other end to a GND
pin in the breadboard power strips. This would close the
circuit and give the GPIO pin a value of 0, which would be
the same as having the button pressed. And when I want to
stop using debug mode, I just remove the jumper wire.
Below you can see a diagram of the circuit when debug is
in use:
Now I can add another helper function that tells me if I’m
in debug mode or not that is checked before
machine.deepsleep() is invoked in run():

def is_debug():
debug = machine.Pin(config.DEBUG_PIN, machine.Pin.IN, machine.Pin.PULL_UP)
if debug.value() == 0:
print('Debug mode detected.')
return True
return False

def run():
try:
if machine.reset_cause() == machine.DEEPSLEEP_RESET:
connect_wifi()
call_webhook()
except Exception as exc:
sys.print_exception(exc)
show_error()

if not is_debug():
machine.deepsleep()

As you probably noticed, I’ve also added pin D5 to the


configuration with the name DEBUG_PIN, so this is how
config.py should look like now:

WIFI_SSID = 'your SSID'


WIFI_PASSWORD = 'your Wi-Fi password'
WEBHOOK_URL = 'http://maker.ifttt.com/trigger/button_pressed/...'
BUTTON_ID = 'micropython1'
LED_PIN = 2 # D4
LED2_PIN = 16 # D0
DEBUG_PIN = 14 # D5

Now you can upload the new versions of main.py and


config.py to the board and see how normal and debug
modes work. Note that if you do it carefully, you can insert
the debug jumper wire while the device is connected to
power, so you could go into debug mode by adding the wire
and then pressing the reset button to trigger a restart that
will not finish in deep sleep. If you connect the debug wire
on a hot device, just make sure you don’t inadvertently
touch anything with the wire connectors, as you could
cause a short circuit. If you prefer to be on the safe side,
then disconnect the device from power before adding the
wire.

4.10 Using an External


Reset Button
There is one last improvement before I call this application
complete. I thought it would be useful to wire an external
button to trigger the webhook, since the on-board reset
button is so small and difficult to reach. This button will
have to work in a different way than the button from the
previous chapter, because for this application the button
needs to be wired to the reset function of the board.
Luckily, the board has a RST pin. This pin is pulled high (i.e.
to a value of 1), just like the D5 pin I used for a button. If
the pin is connected momentarily to ground, then also like
the button from last chapter, it will go low (to a value of 0),
and this will cause the board to reset. So then, I can hook
up a button to the RST on one side, and to GND on the other.
When the button is pressed, the connection between these
two pins will be made, and that will reset the board. Here
is the breadboard diagram including an external reset
button:
Note that I removed the debug wire in this diagram. You
should add this wire whenever you want to prevent the
board from going into deep sleep.
And with this I’m going to end this chapter. Remember to
copy the latest versions of main.py and config.py to try the
latest version on your board. Below you can see two views
of my breadboard with the reset button in place:
OceanofPDF.com
Chapter 5
Temperature and Humidity
Sensor

5.1 The DHT22 Temperature


and Humidity Sensor
The DHT22 sensor is a low cost device that can obtain temperature and
humidity readings. It uses a custom data transmission protocol that is
very friendly to microcontrollers such as the Arduino and of course, the
ESP8266. There are versions of this sensor with three and four pins,
but in the four pin variant only three pins are used. The pins are
connected to voltage, ground, and an available GPIO pin. One
limitation these devices have is that to get more accurate results, you
have to wait at least two seconds between readings.
In the three-pin versions of this device the pins are VCC, OUT and GND from
left to right and looking at it from the front. The OUT pin is the data
output pin. You should verify this is the order for your own device
before you connect it. You may see the voltage pin labeled as VCC or
maybe +. The GND pin is sometimes labeled -. If you have a four-pin
version, then the order is the same, but you have to skip the third pin
which does not have any use.
Here is a diagram with the wiring of this sensor to the ESP8266 board:
To connect this sensor I’m taking advantage of the power strips in the
breadboard. The only connection that goes directly into the
microcontroller is the one for the data pin or OUT, which I connected to
pin D1, also known as GPIO5. I’ve chosen the D2 pin randomly from the
available ones. I have used D0 and D4 as output pins to control the on-
board LEDs, which I may want to still have access to. If I were to
connect the sensor on one of these pins, I would not be able to use the
corresponding LED because the pin is going to be configured as an
input pin to receive the data from the sensor, but I could have also
picked D1, D3, or any other available D pin. You can see in the diagram
that I left the external reset button from the previous chapter
connected, since it should not interfere with the sensor in any way.
Remember that if you have one of the DHT22 models with four pins the
third pin is generally the one that is unused.
It’s getting harder to show clear pictures of all the connections on the
breadboard, but if you’ve done everything correctly yours should look
more or less like mine shown below:
5.2 Obtaining Sensor
Readings
Do you want to know what’s the temperature right there where you
are? Now that the temperature sensor is hooked up to your
microcontroller you are ready to find out!
Start a MicroPython REPL on your board, and enter the following
statements to read the temperature and humidity using the sensor:
>>> import dht
>>> import machine
>>> d = dht.DHT22(machine.Pin(4))
>>> d.measure()
>>> d.temperature()
18.4
>>> d.humidity()
47.3

MicroPython comes with a few drivers for commonly used devices. The
DHT22 sensor is luckily supported, so all I needed to do is import the
corresponding class and use it. The dht package contains two
temperature sensor drivers, one for the DHT11 and another for the
DHT22. The only argument that needs to be provided to the DHT22()
class is a machine.Pin() instance for the GPIO pin connected to the data
pin of the sensor. In my case I have it connected to D2, which is GPIO4.
Once you have the object created, a call to measure() activates the
sensor and performs a reading of both temperature and humidity. This
method sends a “start” signal to the sensor through the data pin, and
this wakes the sensor from its standard dormant state. The sensor then
takes measurements of temperature and humidity and returns them,
also through the data pin. As soon as this process completes the sensor
goes back to sleep. If you are curious about the communication
protocol used in the exchanges between the microcontroller and the
sensor, you can read about it here.
After a measurement has been taken, you can use the temperature() and
humidity() methods to retrieve the results. The temperature is returned
in the Celsius scale, and the humidity is returned as a percentage, what
is usually known as relative humidity.
If you live in the United States or one of the few other countries in the
world where the Farhenheit scale is used for temperatures, then the
Celsius results are not going to mean much to you. In that case you can
take the Celsius measurement and convert it to Fahrenheit with the
following formula:
>>> d.temperature() * 9 / 5 + 32
65.12

The above calculation takes the celsius temperature and multiplies it by


1.8 (or by the fraction 9/5, which is the same thing) and then adds 32.
The result is the same temperature, but expressed in the Fahrenheit
scale.
I should note once again that the DHT22 sensor has what I consider a
minor limitation. You can’t call the measure() method more than once
every two seconds, as the sensor needs that time to reset itself and get
ready to take another measurement. If you force the sensor to measure
at a higher rate than that, then the results are not going to be
accurate. When the sensor is used correctly the accuracy is reported as
+/-0.5 degrees celsius, which is very good for such a cheap sensor.
The temperature() and humidity() methods do not have a calling rate
limitation because they just return the data that was obtained by the
last call to measure().

5.3 Weather Station


Application
Now that you know how to work with the sensor, I can show you how
this can be incorporated into an application. The final goal is to have
an application that reads the temperature and humidity at regular
intervals and uploads all those values to the cloud.
As I’ve done in previous chapters, I’m going to create a sub-directory
inside the micropython-tutorial directory where I’m going to save the
application files for this example:
(venv) $ mkdir chapter5
(venv) $ cd chapter5

I’m going to split this task in three parts. In this section I’ll show you
how to read the temperature and humidity in the context of an
application. Then in the following sections I’ll add the cloud upload
part, and finally the scheduling portion, where I’m going to configure
the microcontroller to go to sleep and wake up on its own.
To begin, let’s encapsulate the whole sensor operation logic in a
function in main.py. This function will obtain the data pin for the sensor
from the configuration. To make this function more flexible, I can also
add a setting to control which units are desired for temperature:
import dht
import machine
import config

def get_temperature_and_humidity():
dht22 = dht.DHT22(machine.Pin(config.DHT22_PIN))
dht22.measure()
temperature = dht22.temperature()
if config.FAHRENHEIT:
temperature = temperature * 9 / 5 + 32
return temperature, dht22.humidity()

You should be familiar with most of what I’ve done above, except
maybe the last line, which returns the data. This function needs to
return temperature and humidity, so I’ve chosen to return them as a
tuple. The caller of this function has two options to deal with the return
values. If it assigns the return of this function to a single variable, then
thet temperature and humidity values need to be extracted from the
tuple:
>>> result = get_temperature_and_humidity()
>>> result[0]
18.4
>>> result[1]
47.3

This may seem a little odd, because when you see result[0] in the code
it isn’t immediately clear that this refers to the temperature. A clearer
alternative that I prefer when working with tuple return values is to
break the tuple directly in the assignment:
>>> temperature, humidity = get_temperature_and_humidity()
>>> temperature
18.4
>>> humidity
47.3

As you seen above, you need to create a config.py file with the following
variables:
DHT22_PIN = 4 # D2
FAHRENHEIT = False

Of course you are free to set the value of the FAHRENHEIT variable
according to your preferences. If you want to test this so far, upload
main.py and config.py to your microcontroller and then run the following
from the REPL:
>>> import main
>>> temperature, humidity = main.get_temperature_and_humidity()
>>> temperature
19.3
>>> humidity
51.8

5.4 Logging to the Cloud


The next task I’m going to ask you do is set up an account at
thingspeak.com. This is a cloud service that stores data series,
specifically designed for Internet of Things devices. The free tier
offered by this service is more than enough for the needs of this
tutorial.

As part of the account creation you will be asked to create a second


account at mathworks.com, and for this you can use the same email
address. Once you complete the account set up and verify your email
address, click the “Channels” dropdown in the top navigation bar, and
then select “My Channels”.
At this point you don’t have any channels, so this page is going to be
mostly empty, with just a “New Channel” button. Click this button to
create a channel where you’ll upload your sensor data. In ThingSpeak,
a channel is a data structure that can hold timed series. Each channel
can have up to eight fields, but for this exercise you need just two to
hold the temperature and humidity series. See below how I configured
my channel:
The remaining options in the New Channel screen can be left alone.
Once you added the two fields, click the “Save Channel” button at the
bottom.
Now you should be in the new channel’s page. The channel has its own
navigation bar, and you are seeing the “Private View” page, where
there are still empty charts for the two fields that you created in this
channel. Click on “API Keys” on this navigation bar to access the page
that has the information on how to upload data to the channel.
The API Keys page shows the two keys that were assigned to your
channel, one for writing data to it and another for reading. On the right
sidebar there is a section titled “API Requests”, which shows a few
example HTTP requests that you can send for different tasks. The one
that is interesting at this point is the “Upload a Channel Feed”, which
shows the GET request that needs to be sent to write some data into the
channel. It should be similar to this, but with a real API key:
GET https://api.thingspeak.com/update?api_key=XXXX&field1=0

Even though this is a webhook URL in many ways similar to that of the
IFTTT service you used in the previous chapter, there are two
differences. First, ThingSpeak uses a GET request for data submission,
which is not very conventional because requests of this type cannot
carry a payload. In general a POST request is used when submitting data
to a web service, as you saw with IFTTT, where a JSON payload was
attached to the request. Some services bend the rules a little bit and
use a GET request for data submission, because in general it is easier to
send a GET request than a POST request.
But if GET requests cannot carry data with them, how is the data
submitted? Look at the above URL, in particular to the part that
follows after the ? sign. Let me repeat that part here with spaces
added for clarity:
? api_key=XXXX & field1=0

The HTTP protocol calls this the query string part of the URL. It starts
with a question mark, and is followed by one or more parameter
assignments. If multiple parameters are included, they are separated
with an ampersand sign. The thingspeak URL uses a parameter called
api_key for the write API key, which is what identifies your channel.
Then a second parameter called field1 passes a value for the first field
in the channel. So as you see, uploading data to ThingSpeak implies
generating this URL that includes parameters and then sending a GET
request with it. Since your channel has two fields, the URL is going to
have one more parameter in it:
GET https://api.thingspeak.com/update?api_key=XXXX&field1={t}&field2={h}

In this URL, I used {t} and {h} to represent the values of temperature
and humidity read from the sensor. These will have to be incorporated
into the URL before the request is made.
In the previous chapter I used a WEBHOOK_URL configuration variable to
define the webhook URL. For this application I’m going to do the same,
but this time I’m going to need to insert the two variable parameters in
it. To do this, select the upload URL shown in the API Keys page of
your channel and paste it on your config.py file. Then edit it so that it
matches the following format:
DHT22_PIN = 4 # D2
FAHRENHEIT = False
WEBHOOK_URL = 'https://api.thingspeak.com/update?api_key=XXXX&field1={temperature}&field2={humidity}'

When you do this, make sure that the value of the api_key parameter is
your channel’s write key. Also make sure you write the values for the
temperature and humidity exactly as {temperature} and {humidity}, as
these will be replaced with the actual values before the request is sent
to ThingSpeak.
The function that calls the webhook is surprisingly similar to the one
from the previous chapter:
import urequests
import config

def log_data(temperature, humidity):


print('Invoking log webhook')
url = config.WEBHOOK_URL.format(temperature=temperature,
humidity=humidity)
response = urequests.get(url)
if response.status_code < 400:
print('Webhook invoked')
else:
print('Webhook failed')
raise RuntimeError('Webhook failed')

The url is generated with the format() method of the Python strings. It
basically replaces any occurrences of {variable} with the value of a
keyword argument of the same name. It’s a very convenient function to
quickly render templates for URLs. You may also be familiar with the
newer f-strings introduced in the Python language in version 3.6.
Unfortunately MicroPython does not have f-strings, so the format()
method is the best option at this time.
After the URL is generated, the function is identical to that of the
previous chapter. The RuntimeError() is raised if the webhook returns
any 400 or above status code, which are all indicative of a problem. If
you recall, in the previous chapter any raised exception were handled
in the run() function by blinking the LEDs three times.
So far your main.py file has the get_temperature_and_humidity() and
log_data() functions. Since this application is in many ways similar to
the one from the previous chapter, some of the code can be copied
over. This includes the connect_wifi(), show_error(), is_debug() and run()
functions. The only function that needs to be adapted is run(), the
others work for this new application exactly as they are.
Here you have a complete first version of main.py for the temperature
and humidity cloud uploader application:
import dht
import machine
import network
import sys
import time
import urequests

import config

def connect_wifi():
ap_if = network.WLAN(network.AP_IF)
ap_if.active(False)
sta_if = network.WLAN(network.STA_IF)
if not sta_if.isconnected():
print('Connecting to WiFi...')
sta_if.active(True)
sta_if.connect(config.WIFI_SSID, config.WIFI_PASSWORD)
while not sta_if.isconnected():
time.sleep(1)
print('Network config:', sta_if.ifconfig())
def show_error():
led = machine.Pin(config.LED_PIN, machine.Pin.OUT)
led2 = machine.Pin(config.LED2_PIN, machine.Pin.OUT)
for i in range(3):
led.on()
led2.off()
time.sleep(0.5)
led.off()
led2.on()
time.sleep(0.5)
led.on()
led2.on()

def is_debug():
debug = machine.Pin(config.DEBUG_PIN, machine.Pin.IN, machine.Pin.PULL_UP)
if debug.value() == 0:
print('Debug mode detected.')
return True
return False

def get_temperature_and_humidity():
dht22 = dht.DHT22(machine.Pin(config.DHT22_PIN))
dht22.measure()
temperature = dht22.temperature()
if config.FAHRENHEIT:
temperature = temperature * 9 / 5 + 32
return temperature, dht22.humidity()

def log_data(temperature, humidity):


print('Invoking log webhook')
url = config.WEBHOOK_URL.format(temperature=temperature,
humidity=humidity)
response = urequests.get(url)
if response.status_code < 400:
print('Webhook invoked')
else:
print('Webhook failed')
raise RuntimeError('Webhook failed')

def run():
try:
connect_wifi()
temperature, humidity = get_temperature_and_humidity()
log_data(temperature, humidity)
except Exception as exc:
sys.print_exception(exc)
show_error()

if not is_debug():
machine.deepsleep()

run()

This should be mostly familiar to you because most of the code came
from the previous chapter. There is one minor change in the run()
function however that I want to point your attention to. The webhook
of the previous chapter was only invoked when the device was reset. To
distinguish a restart due to a reset from other conditions I used the
machine.reset_cause() function. For this application I thought it made
more sense to capture and log data for all the different restart
conditions, so I removed the check for the restart cause.
The config.py file that goes with this version of the application is shown
below:
WIFI_SSID = 'your SSID'
WIFI_PASSWORD = 'your Wi-Fi password'
LED_PIN = 2 # D4
LED2_PIN = 16 # D0
DEBUG_PIN = 14 # D5
DHT22_PIN = 4 # D2
FAHRENHEIT = False
WEBHOOK_URL = 'https://api.thingspeak.com/update?api_key=XXXX&field1={temperature}&field2={humidity}'

Of course you will need to customize this configuration to your


situation, including entering your Wi-Fi details and the correct
webhook for your ThingSpeak channel.
If you upload main.py and config.py to your microcontroller, each time
you power it or reset it a record of the current temperature and
humidity will be uploaded to your channel on thingspeak.com.

5.5 Deep Sleep with a Wake


Up Alarm
There is one last important piece in this application to complete it. The
application currently logs temperature and humidity only when the
board is powered on or reset. This was a good option for the
application that emulated Amazon Dash buttons in the previous
chapter, but for a data logger the most convenient implementation is
for data to be logged automatically at regular intervals without any
intervention. So basically, the requirement is to schedule the board to
reset after a specified interval.
The ESP8266 microcontroller has a real-time clock or RTC. Besides
keeping time, this component provides an alarm that can be
programmed to go off after a specified number of milliseconds have
passed. Interestingly, the only component that is not powered off when
the microcontroller enters deep sleep mode is the RTC, so it is possible
to program the alarm to fire after some amount of time has passed and
then put the device to sleep to let the alarm bring it back from sleep.
The following statements configure the ESP8266 alarm, which is
referenced as ALARM0 in MicroPython, to wake up the device when it
fires:
>>> rtc = machine.RTC()
>>> rtc.irq(trigger=rtc.ALARM0, wake=machine.DEEPSLEEP)
The next step is to configure when the alarm should go off. For
example, to program the alarm to fire after one minute, you have to do
this:
>>> rtc.alarm(rtc.ALARM0, 60 * 1000)

In this example the 60 * 1000 expression calculates the equivalent of one


minute in milliseconds, which are the units the alarm needs. I like to
use this expression instead of the calculated value to make the code a
little bit more readable.
There is one more detail that is going to seem very strange to you. The
method the alarm uses to wake the device is indirect. All the alarm
does is keep pin D0 high (i.e. with a value of 1) until the alarm fires, at
which point it changes it to low (0 in other words). If you go back to
the pinout diagram for the microcontroller you will see that another
alias for pin D0 is WAKE.
What does pin D0 have to do with a reset you may ask? Really nothing,
changing pin D0 from 1 to 0 does not trigger a reset on its own, and in
fact, you already know that all this does is turn one of the on-board
LEDs on! The key to force the board to reset is to connect this pin to
the RST pin. Recall from last chapter that the RST pin triggers a reset of
the board when it is set low. The button that you used to trigger a reset
connected RST to GND when it was pressed, which forces a low state. If
pin D0 is connected to RST while it is pulled high, nothing will happen,
but when the alarm triggers, pin D0 will change to a low value and that
will force RST to also go low and thus reset the board. Ingenious, right?
Unfortunately pin RST is connected to the external reset button. I did
not want to try to force two jumper wire pins into the one available hole
that connects to RST in the breadboard, so to avoid complications I
disconnected and removed the button, and then made the connection
between D0 and RST instead. I left the button on the breadboard, and I
also left the ground connection for this button, because I’m not giving
up on having the reset button working, I’ll show you how to add it back
in the next section. Here is the current connection diagram:
There is another problem that I indirectly mentioned already. I was
using pin D0 to blink the second on-board LED in error situations. But
now pin D0 is connected to RST, so turning the LED on by setting the pin
to 0 is also going to make RST low and consequently reset the board! To
avoid this problem I decided to simplify my error reporting. Instead of
blinking the two LEDs, from now on I’m going to blink just the one
connected to pin D4.
Once the alarm is configured to go off at the desired time, and pin D0 is
connected to pin RST, all that is left to do is to put the board to sleep:
>>> machine.deepsleep()

To incorporate this functionality into the application, I’m going to start


by adding one more configuration variable to config.py that will hold the
data logging interval, which I’m going to set to one minute. For this I
decided to use seconds as units:
LOG_INTERVAL = 60

Now I can consolidate the alarm handling code into a new function in
main.py:

def deepsleep():
print('Going into deepsleep for {seconds} seconds...'.format(
seconds=config.LOG_INTERVAL))
rtc = machine.RTC()
rtc.irq(trigger=rtc.ALARM0, wake=machine.DEEPSLEEP)
rtc.alarm(rtc.ALARM0, config.LOG_INTERVAL * 1000)
machine.deepsleep()

And then in run() instead of calling machine.deepsleep(), which sleeps


until a manual reset occurs, I call the above function to sleep for the
configured time. Once the board wakes up, it will reboot and run the
application one more time, just like when the board is reset, so another
sample will be logged to ThingSpeak, and then the cycle will repeat.
Here is the complete version of the application for your reference:
import dht
import machine
import network
import sys
import time
import urequests

import config

def connect_wifi():
ap_if = network.WLAN(network.AP_IF)
ap_if.active(False)
sta_if = network.WLAN(network.STA_IF)
if not sta_if.isconnected():
print('Connecting to WiFi...')
sta_if.active(True)
sta_if.connect(config.WIFI_SSID, config.WIFI_PASSWORD)
while not sta_if.isconnected():
time.sleep(1)
print('Network config:', sta_if.ifconfig())

def show_error():
led = machine.Pin(config.LED_PIN, machine.Pin.OUT)
for i in range(3):
led.on()
time.sleep(0.5)
led.off()
time.sleep(0.5)
led.on()

def is_debug():
debug = machine.Pin(config.DEBUG_PIN, machine.Pin.IN, machine.Pin.PULL_UP)
if debug.value() == 0:
print('Debug mode detected.')
return True
return False

def get_temperature_and_humidity():
dht22 = dht.DHT22(machine.Pin(config.DHT22_PIN))
dht22.measure()
temperature = dht22.temperature()
if config.FAHRENHEIT:
temperature = temperature * 9 / 5 + 32
return temperature, dht22.humidity()

def log_data(temperature, humidity):


print('Invoking log webhook')
url = config.WEBHOOK_URL.format(temperature=temperature,
humidity=humidity)
response = urequests.get(url)
if response.status_code < 400:
print('Webhook invoked')
else:
print('Webhook failed')
raise RuntimeError('Webhook failed')
def deepsleep():
print('Going into deepsleep for {seconds} seconds...'.format(
seconds=config.LOG_INTERVAL))
rtc = machine.RTC()
rtc.irq(trigger=rtc.ALARM0, wake=machine.DEEPSLEEP)
rtc.alarm(rtc.ALARM0, config.LOG_INTERVAL * 1000)
machine.deepsleep()

def run():
try:
temperature, humidity = get_temperature_and_humidity()
print('Temperature = {temperature}, Humidity = {humidity}'.format(
temperature=temperature, humidity=humidity))
connect_wifi()
temperature, humidity = get_temperature_and_humidity()
log_data(temperature, humidity)
except Exception as exc:
sys.print_exception(exc)
show_error()

if not is_debug():
deepsleep()

run()

And here is the config.py that goes with it:


WIFI_SSID = 'your SSID'
WIFI_PASSWORD = 'your Wi-Fi password'
LED_PIN = 2 # D4
DEBUG_PIN = 14 # D5
DHT22_PIN = 4 # D2
FAHRENHEIT = False
WEBHOOK_URL = 'https://api.thingspeak.com/update?api_key=XXXX&field1={temperature}&field2={humidity}'
LOG_INTERVAL = 60

Note how I simplified the show_error() function to use only one LED, and
also the removal of LED2_PIN from config.py since it is not going to be
used anymore.
Now you can upload main.py and config.py to your board and then leave
it to run for a few hours, logging temperature and humidity data every
minute. The channel page on the ThingSpeak website will update the
charts as new measurements are uploaded, and without the need for
you to refresh the page. Here is what I see on my channel after
running my microcontroller for a while:
5.6 Extending the RST Pin
In the previous section I had to disconnect the external reset button to
make space to connect the wake up alarm pin on the RST pin of the
microcontroller board. In many cases not having an external reset
button for this type of application is going to be perfectly fine. This is
now an embedded device that is self-sufficient, it knows how to go to
sleep and wake up on its own.
But reconnecting the reset button back is I think a good example to
demonstrate how you can do this with a little bit of creativity. The
problem is that the size of the microcontroller board with respect to the
breadboard leaves a single available hole to make connections to each
of the board’s pins, and in this case there are two wires that need to
connect to RST. If you had a bigger breadboard that had two open holes
per ESP8266 pin, then there would be no problem. But how to make it
work with this breadboard, and without forcing two pins into a single
hole?
Let’s revisit the concept of power strips in the breadboard. The two top
and bottom rows function as extensions for the 3V3 and GND pins. Simply
by wiring a pin to any of the holes in a power strip row, all the other
holes become extensions of that pin. The solution is, then, to create an
extension for the RST pin as well!
One way to do this would be to use one of the power strips that is
currently unused. For example, the bottom row is currently a GND
extension, but it isn’t used for anything. The wire that connects GND
with a hole in this bottom row could be moved to the RST pin and then
that row would work as an extension to RST. I think this approach can
be confusing, because the power strips by convention are used for
power and ground.
A better approach in my opinion is to build a mini-power strip in an
unused part of the breadboard. Recall that the holes in the middle
sections of the breadboard are grouped in fives, so I can take any
unused column of five holes and build my RST extension there. To do
this I picked a column of five holes in the lower half of the breadboard,
close to the border of the microcontroller. I connected a jumper wire
between RST and one of these five holes, and then used two of the
remaining four holes to connect to pin D0 and the to right side of the
external reset button. Here is the final diagram including this change:
And now the board can log sensor data every minute, but you can
override the schedule and trigger an update at any time by pushing the
reset button.
OceanofPDF.com
Chapter 6
Working with a Screen

6.1 The SSD1306 OLED


Screen
The SSD1306 is a very popular low cost screen that can be
easily connected to most microcontrollers. As with most
components in these open-source hardware times, there
are many variants and manufacturers including
monochrome and color models of different sizes and form
factors.
For this tutorial, I recommended the 128x64 monochrome
display, because it is fairly cheap and small enough to fit in
a breadboard along with other components.
In addition to the visible differences among the many
models, these screens can be divided in two large groups
based on the communication protocol that they implement,
which can be either SPI or I2C. These two protocols are
standard ways of communication between devices that are
used not only in screens but in a lot of other components.
MicroPython includes support classes for both protocols in
its standard library.
If your screen has four connection pins, then it uses the I2C
protocol, which needs two data wires plus voltage and
ground. If you see more than four pins, then your screen
supports the SPI protocol, but very likely also supports I2C
as an option. For this tutorial I have decided to use the I2C
protocol, simply because it requires less data pins. If
you’ve got one of the SPI models with more than four pins,
check the documentation of your screen to learn how to
configure it to use I2C, and which four pins you should use.
Besides the familiar VCC and GND pins, devices that
implement the I2C protocol use two data lines called SDA or
“Serial Data” and SCL or “Serial Clock”. These need to be
connected to two data pins on the microcontroller. Below
you can see a breadboard diagram showing the wiring of
the screen:

Here is another diagram that shows the screen integrated


with the other components as in the previous chapter:
The order of the four pins in my screen is GND, VCC, SCL and
SDA. Read the labels on your screen to make sure the order
is the same and adjust the connections appropriately if it is
not. I’ve seen some screens that have the order of GND and
VCC reversed.

I installed the screen in the bottom-left corner of the


breadboard, being very careful that the four pins land on
columns that are unused in between the button and the
microcontroller board. The button uses the first and third
columns of holes on the left side, so I aligned the left-most
pin of the screen in the fourth column counting from the
left. I connected the screen’s GND and VCC pins to holes in
the bottom power strips. Then I connected SCL to D3, and SDA
to D6. The choice of D pins to use is, as before, somewhat
random. I just picked two pins that I haven’t used for
anything so far.

6.2 Controlling the


Screen from MicroPython
Let’s give the new screen a try using the MicroPython
REPL. The first step is to create an object that represents
the I2C connection to the screen:
>>> import machine
>>> i2c = machine.I2C(scl=machine.Pin(0), sda=machine.Pin(12))

As you see, this is all done with the machine package. The
machine.I2C class includes the support to communicate with
the screen using the I2C protocol. The two arguments that
this object needs are the pins that are connected to the SCL
and SDA terminals. As always this is done with GPIO
numbers, and D3 is GPIO0 and D6 is GPIO12, so the pins are
instantiated as 0 and 12 respectively.
The I2C protocol supports multiple devices connected on
the same data pins, and there is a mechanism to “discover”
connected devices. If your screen is correctly connected,
you can now check if it is detected using the scan()
function:
>>> i2c.scan()
[60]

This function returns a list of device IDs that were


recognized. This model of screen comes preconfigured
with an id of 60, so when you see [60] as output you know
your screen if working. If i2c.scan() returns [] (an empty
list), then that means that it was not recognized. You need
to check your connections and make sure all the pins are
nicely seated in the breadboard holes.
When the screen is recognized, you can instantiate an
object to control it. Since these screens are so common,
MicroPython also includes a class for them:
>>> import ssd1306
>>> display = ssd1306.SSD1306_I2C(128, 64, i2c)

The arguments to the ssd1306.SSD1306() class are the screen


width and height in pixels, and the i2c instance created
previously. This object provides a good number of drawing
primitives that you can use. Of particular interest are the
fill() method which can be used to clear the screen, the
text() method that can be used to write characters, and the
show() method which updates the screen after making
changes. Here is an example:
>>> display.fill(0)
>>> display.text('Hello', 0, 0)
>>> display.text('from', 0, 16)
>>> display.text('MicroPython!', 0, 32)
>>> display.show()

The fill() call clears the screen, or more accurately, sets


all pixels in the screen to color 0, which is black. The text()
calls write some text. The arguments are the text to write,
and then the X and Y coordinates where the text should
start. The X argument refers to the horizontal position. A
value of 0 means the left border, and a value of 127 means
the right border. The Y argument is the vertical position
and it goes from 0 for the top of the screen to 63 for the
bottom.
When you were entering these statements, you surely
noticed that nothing shows on the screen until you issue
the display.show() call at the end. The reason is that the
SSD1306() driver uses buffering. When you draw or write
text to the screen those changes are temporarily written to
a memory image of the screen. Once you are done with
your changes a call to show() sends the data from the
memory buffer to the device over the I2C connection.
After running the above code, your device should like mine,
shown below:

Obviously I don’t expect you to identify the individual


connections anymore, there are too many cables for a
picture to be a clear representation, but it should give you
an idea of what you’ll have if you haven’t built your project
yet.

6.3 Displaying
Temperature and Humidity
on the Screen
I hope you agree that the next logical step is to write an
application that displays the temperature and the humidity
on the screen. But before I forget and to keep things
organized, I’m going to create a new subdirectory called
chapter6 inside micropython-tutorial to hold the project files
for this chapter:
(venv) $ mkdir chapter6
(venv) $ cd chapter6

After experimenting in the REPL to decide on the best way


to format the temperature and humidity values, I wrote this
function to encapsulate the control of the screen:
import machine
import ssd1306

def display_temperature_and_humidity(temperature, humidity):


i2c = machine.I2C(scl=machine.Pin(config.DISPLAY_SCL_PIN),
sda=machine.Pin(config.DISPLAY_SDA_PIN))
if 60 not in i2c.scan():
raise RuntimeError('Cannot find display.')

display = ssd1306.SSD1306_I2C(128, 64, i2c)


display.fill(0)

display.text('{:^16s}'.format('Temperature:'), 0, 0)
display.text('{:^16s}'.format(str(temperature) + \
('F' if config.FAHRENHEIT else 'C')), 0, 16)

display.text('{:^16s}'.format('Humidity:'), 0, 32)
display.text('{:^16s}'.format(str(humidity) + '%'), 0, 48)

display.show()
time.sleep(10)
display.poweroff()

A lot of what you see in this function should be familiar to


you, but there are a few new things. The screen is
initialized as you saw in the previous section. I’m making
sure that the ID 60 is present when I scan the I2C bus and
if I don’t find it then I raise a RuntimeError, since it is obvious
that the screen is not going to work. The exception is going
to be handled in the run() function I’m yet to write, and I’ll
just blink the LED as I did in previous applications.
You may find the syntax inside the the display.text() calls
strange. I’m using some the format() function of string
objects, which has some not very known options to center
text. The ’{:16s}’ string means that I want to show the
string given as an argument centered in a field of 16
characters. I used this formatting for the four lines that I’m
displaying.
Since I have four lines of text and 64 vertical pixels, I’m
assigning 16 pixels for each line. This is why you see the Y
coordinate on these four lines set to 0, 16, 32, and 48
respectively.
Since the temperatures can be displayed in the Celsius or
Fahrenheit scales, I’ve also added a little bit of logic to
display a C or an F after the number depending on the
selected scale. I did this using an inline conditional, which
has the format ’F’ if config.FAHRENHEIT else ’C’. This type of
expression is great to express simple decisions in a concise
manner.
After I call show() to make the text visible, I sleep for 10
seconds, and then use display.poweroff() to turn the display
off. The idea here is that the temperature and humidity
data will be visible during those 10 seconds, and then the
device will go back to sleep.
If you are wondering how this looks, here is a closeup of
the screen while showing my temperature and humidity:
The rest of the code can be assembled from parts of the
previous chapter’s application including the support for
reading the temperature and humidity from the sensor,
going into deep sleep and using a blinking light to report
unexpected errors. Here is the complete code for your
reference:
import dht
import machine
import ssd1306
import sys
import time

import config

def show_error():
led = machine.Pin(config.LED_PIN, machine.Pin.OUT)
for i in range(3):
led.on()
time.sleep(0.5)
led.off()
time.sleep(0.5)
led.on()

def is_debug():
debug = machine.Pin(config.DEBUG_PIN, machine.Pin.IN, machine.Pin.PULL_UP)
if debug.value() == 0:
print('Debug mode detected.')
return True
return False

def get_temperature_and_humidity():
dht22 = dht.DHT22(machine.Pin(config.DHT22_PIN))
dht22.measure()
temperature = dht22.temperature()
if config.FAHRENHEIT:
temperature = temperature * 9 / 5 + 32
return temperature, dht22.humidity()

def display_temperature_and_humidity(temperature, humidity):


i2c = machine.I2C(scl=machine.Pin(config.DISPLAY_SCL_PIN),
sda=machine.Pin(config.DISPLAY_SDA_PIN))
if 60 not in i2c.scan():
raise RuntimeError('Cannot find display.')

display = ssd1306.SSD1306_I2C(128, 64, i2c)


display.fill(0)

display.text('{:^16s}'.format('Temperature:'), 0, 0)
display.text('{:^16s}'.format(str(temperature) + \
('F' if config.FAHRENHEIT else 'C')), 0, 16)

display.text('{:^16s}'.format('Humidity:'), 0, 32)
display.text('{:^16s}'.format(str(humidity) + '%'), 0, 48)

display.show()
time.sleep(10)
display.poweroff()

def run():
try:
temperature, humidity = get_temperature_and_humidity()
print('Temperature = {temperature}, Humidity = {humidity}'.format(
temperature=temperature, humidity=humidity))
display_temperature_and_humidity(temperature, humidity)
except Exception as exc:
sys.print_exception(exc)
show_error()

if not is_debug():
machine.deepsleep()

run()

The configuration values needed for this application are in


this config.py:
LED_PIN = 2 # D4
DEBUG_PIN = 14 # D5
DHT22_PIN = 4 # D2
DISPLAY_SCL_PIN = 0 # D3
DISPLAY_SDA_PIN = 12 # D6
FAHRENHEIT = True
Activating the application when the button is pressed also
works as in the previous chapter, with the external reset
button wired to connect RST to GND when pressed, triggering
a reset of the board, even while it is in a deep sleep state.
You can see in the breadboard wiring diagram above that I
left the wiring from D0 to RST that I used to trigger a wake-
up alarm. That is not used by this application, but leaving
the wires in place does not affect it at all, and it would only
be a problem if I wanted to use pin D0 for another purpose,
such as bringing back the second LED.
I also left the is_debug() function, which I have used on
several examples to configure the board to optionally not
go into deep sleep, so that it is easier to connect to it and
open a REPL session. To enable the debug option you need
to connect a jumper wire between the D5 pin and GND.

6.4 Using Drawing


Primitives
While it is nice that MicroPython makes it easy to print text
on the SSD1306 screen, I’m sure you agree with me that
just showing white text on a black screen is a little boring.
In this section I want to show you some of the drawing
primitives provided by the SSD1306 driver that can be used
to complement text.
Let’s start another REPL session. Remember that you will
likely need to add the debugging jumper wire to your
circuit to prevent the board from going to sleep. In case
you don’t remember, here are the instructions to create a
screen object and clear the screen:
>>> import machine
>>> i2c = machine.I2C(scl=machine.Pin(0), sda=machine.Pin(12))
>>> i2c.scan()
[60]
>>> import ssd1306
>>> display = ssd1306.SSD1306_I2C(128, 64, i2c)
>>> display.fill(0)

One of the drawing primitives is the rect() function, which


as I’m sure you can guess, draws a rectangle:
>>> display.rect(0, 0, 128, 64, 1)
>>> display.show()

The arguments to display.rect() are the left and top


coordinates, the width and height, and the color of the
rectangle, using 1 for white and 0 for black. The above
example draws a rectangle around the entire surface of the
screen. A variant of rect() is fill_rect() which takes the
same arguments but draws a solid rectangle instead of the
outline.
Another set of drawing primitives are line():
>>> display.line(64, 0, 64, 64, 1)
>>> display.show()

The arguments to line() are the coordinates of the two


ends of the line and the color. Each line end is given as two
values, the X and Y coordinates. In the example above I’m
drawing a vertical line through the middle of the screen,
which is X=64 for a screen of 128 pixels wide. There are
two line drawing variants, vline() and hline() which are
specialized cases for vertical or horizontal lines.
While maybe a little less useful, there is also a pixel()
method which can be used to set an individual pixel by
passing the X and Y location and the color:
>>> display.pixel(5, 10, 1)
>>> display.show()
This function can be used to remove a pixel from the screen
when passing 0 as the color:
>>> display.pixel(5, 10, 0)
>>> display.show()

If you only pass the X and Y coordinates to the pixel()


function without include a pixel color, then the function
returns the current color of that pixel:
>>> display.pixel(0, 0)
1
>>> display.pixel(5, 10)
0

6.5 Drawing Images


Even though by any standard this is a very limited display,
there is always an opportunity to enhance the appearance
of your application with images or icons, as little as they
will need to be to fit!
MicroPython doesn’t offer any support for loading images
from files, but it supports an operation known as bit blit,
which is short for bit block transfer. The bit blit operation
is a common algorithm to copy an image stored in a
memory buffer onto another, possibly larger memory buffer,
such as the one used by the SSD1306 driver.
So the big problem that needs to be solved to be able to
display images on the screen is to figure out a way to load
them into a memory buffer. Once that is done, the blit()
method provided by the screen object will display it.
The memory buffers used by MicroPython are objects
created from class framebuf.FrameBuffer. Let’s take a look at
how these objects are created:
class framebuf.FrameBuffer(buffer, width, height, format, stride=width)

The buffer argument is where the binary data for the image
is stored. Since the images are going to be black and
white, each pixel uses one bit of information. Since this is a
binary representation, each byte in this buffer will store the
state of eight pixels. In most cases this is going to be an
instance of the Python’s bytearray class.
The width and height arguments specify the size of the
image in pixels. Since each byte in the buffer stores eight
pixels, a lot of things are simplified with the restriction that
images need to have a width that is a multiple of 8, because
that means that a row of pixels from the image fits into an
exact number of bytes, so this is the case where the stride
argument is equal to width. This is a restriction I’m totally
fine with.
The format specifies how the pixels are stored in the
memory buffer. The documentation lists the following list
of values for this argument:

framebuf.MONO_VLSB: Monochrome (1-bit) color format This


defines a mapping where the bits in a byte are
vertically mapped with bit 0 being nearest the top of
the screen. Consequently each byte occupies 8 vertical
pixels. Subsequent bytes appear at successive
horizontal locations until the rightmost edge is
reached. Further bytes are rendered at locations
starting at the leftmost edge, 8 pixels lower.
framebuf.MONO_HLSB: Monochrome (1-bit) color format This
defines a mapping where the bits in a byte are
horizontally mapped. Each byte occupies 8 horizontal
pixels with bit 0 being the leftmost. Subsequent bytes
appear at successive horizontal locations until the
rightmost edge is reached. Further bytes are rendered
on the next row, one pixel lower.
framebuf.MONO_HMSB: Monochrome (1-bit) color format This
defines a mapping where the bits in a byte are
horizontally mapped. Each byte occupies 8 horizontal
pixels with bit 7 being the leftmost. Subsequent bytes
appear at successive horizontal locations until the
rightmost edge is reached. Further bytes are rendered
on the next row, one pixel lower.
framebuf.RGB565: Red Green Blue (16-bit, 5+6+5) color
format
framebuf.GS2_HMSB: Grayscale (2-bit) color format
framebuf.GS4_HMSB: Grayscale (4-bit) color format
framebuf.GS8: Grayscale (8-bit) color format

Since the screen is black and white only the monochrome


options make sense, so have to pick a format from the first
three options. I’m not going to go into a lot of detail on
what the differences are, so I’ll just say that the second
option, framebuf.MONO_HLSB, is the one that I selected because
after looking at a few image formats I found one that uses
this exact format.
The image format that is a close match to this structure is
the binary variant of the Portable Bitmap format. You can
use the free Gimp image editor if you are interested in
creating your own images in this format.
Below you can see a few black & white icons that I drew
with Gimp:
One interesting detail is that in the PBM format black
pixels are written as a 1, and white pixels as a 0. This is
exactly the reverse of what MicroPython’s screen object
does, so I drew my icons in black on a white background.
The black pixels in these icons are going to render as white
on the screen, and viceversa.
The structure of a binary PBM file is as follows:
P4
# Description
<width> <height>
<binary image data>

The first line has an identifier for the format. The P4 type
indicates that this is a binary PBM file. The second line is
just a description of the image. The third line contains the
dimensions in pixels, and then after that the file includes
the binary data with the definition of all the pixels.
To read this image into a FrameBuffer object, I need to open
the .pbm file, skip the first two lines, read the width and
height from the third line, and then read the rest of the file
as a bytearray object. Once I have everything loaded I can
create the FrameBuffer object. I wrote a little function that
does this:
import framebuf

def load_image(filename):
with open(filename, 'rb') as f:
f.readline()
f.readline()
width, height = [int(v) for v in f.readline().split()]
data = bytearray(f.read())
return framebuf.FrameBuffer(data, width, height, framebuf.MONO_HLSB)

The open() function works like in standard Python. Here I’m


opening the file using the with ... as ... syntax, which is
known as a context manager. With this construct the object
that represents the file is assigned to the f variable inside
the block of code under the with statement. When this
block ends the file is automatically closed.
Inside the with block I skip the first two lines in the file by
calling readline() and ignoring the return value. On the
third line I parse the width and the height. This requires a
few steps, so to help you understand what I’ve done I’m
going to decompose that line into all those steps:

f.readline().split() reads the line from the file, and


then creates a list of substrings by splitting the line at
each space. Since this line has the format <width>
<height> the split has the effect of creating a list with
the format [’<width>’, ’<height>’]. For example, if this
line is 8 16, the result of this is [’8’, ’16’].
The problem is that the numbers are parsed as strings
by the split() call, but I need them as integers. To
convert them, I use a list comprehension, which
basically applies a transformation to every element in
the list. For the [’8’, ’16’] example, the resulting list is
[8, 16], which is exactly what I want.
Since having these two numbers in a list is a bit
inconvenient, I break the list into the two elements
using a tuple assignment. So continuing with the
example, width, height = [8, 16] would assign width = 8
and height = 16.
At this point I’m done reading the header data, so the rest
of the file is the binary packed pixels. By calling f.read() I
get the rest of the file starting from the current file
position. Since the FrameBuffer class requires this data to be
in a bytearray object, I transform it on the fly with the
bytearray(f.read()) expression.

The last line in the function creates the FrameBuffer object


passing the data, width and height values plus the MONO_HLSB
format. The stride parameter is not included because for
the images I’m going to work with it is going to be the
same as the image width.
The procedure to draw an image on the screen is then as
follows:
image = load_image('filename.pbm')
display.blit(image, x, y)

The blit() method of the screen object takes the frame


buffer with the image data as first argument, and then the
X and Y coordinates where the image should be drawn.
Here you can see a practical application of this image
drawing technique. I’m going to rewrite the
display_temperature_and_percentage() function to render a
much nicer screen. Below you can see a first attempt that
includes some nice borders and the images:
def display_temperature_and_humidity(temperature, humidity):
i2c = machine.I2C(scl=machine.Pin(config.DISPLAY_SCL_PIN),
sda=machine.Pin(config.DISPLAY_SDA_PIN))
if 60 not in i2c.scan():
raise RuntimeError('Cannot find display.')

display = ssd1306.SSD1306_I2C(128, 64, i2c)

temperature_pbm = load_image('temperature.pbm')
units_pbm = load_image('fahrenheit.pbm') if config.FAHRENHEIT \
else load_image('celsius.pbm')
humidity_pbm = load_image('humidity.pbm')
percent_pbm = load_image('percent.pbm')

display.fill(0)
display.rect(0, 0, 128, 64, 1)
display.line(64, 0, 64, 64, 1)
display.blit(temperature_pbm, 24, 4)
display.blit(humidity_pbm, 88, 4)
display.blit(units_pbm, 28, 52)
display.blit(percent_pbm, 92, 52)

display.show()
time.sleep(10)
display.poweroff()

This is not a complete function, I’m still missing the actual


temperature and humidity values, which I’m going to show
you how to render in a custom font in the next section. But
for now, this is how the new screen design looks:

Much nicer, right? If you are wondering how I aligned the


images neatly on the screen, the answer is simple: trail and
error. The temperature.pbm and humidity.pbm are 16x16 pixels,
and the other images are 8x8. Knowing their sizes and the
dimensions of the screen (128x64) makes it easy to
calculate where the images should go to be centered, or
left/right aligned, so I spent some time in the REPL trying
out different designs until I arrived at this layout which I
quite like. You can probably guess that I’m saving the
space in the middle for the numbers.
If you want to test this code with my images, here are
download links for them:

temperature.pbm
celsius.pbm
fahrenheit.pbm
humidity.pbm
percent.pbm

You have to copy them to your microcontroller board along


with the updated main.py after you replace the old
display_temperature_and_humidity() function with the new code
and add the load_image() function defined above.

6.6 Custom Fonts


The text rendering function in the SSD1306 class is fairly
basic. One of the more limiting aspects is that it does not
support custom fonts, there is only one font that is
hardcoded into the function. Unfortunately for a lot of
purposes that font is too small and unreadable unless you
have very good vision. To end this chapter I wanted to
show you how to render text using custom fonts.
The idea that I had was to treat a custom font as a
collection of images. Writing text in a custom font then is
simple, all you need to do is call the blit() method of the
display once per character and moving the X position to the
right each time. The only complication is in generating the
images for each character in the chosen font.
And it turns out that somebody has already done this and
released the code as open source. A search on GitHub led
me to the micropython-font-to-py repository by Peter
Hinch. In this repository, where you can find:

A font_to_py.py script that converts a font from your


computer into a format suitable to use by MicroPython.
This runs on your computer and generates a .py file
with all the font data inside.
Several font renderer classes for MicroPython that use
the font files generated by the font_to_py.py script.
A few example font files, ready to be used.
Documentation that explains the font format.

The freesans20.py font file from Peter’s repository seemed


like the perfect size for my needs. The “20” in the name is
the height of the characters in pixels. That is about the size
of the space I have in my screen to add the numbers.
From the font rendering options in Peter’s repository, I
settled on his simplest version, which he called
writer_minimal.py. I renamed writer_minimal.py to writer.py in
my project and also made a few minor changes to it.
You can download the font and the render code below:

freesans20.py
writer.py

The way font rendering with this code works is as follows:


# import the font and the writer code
import freesans20
import writer

# create a writer instance


font_writer = writer.Writer(display, freesans20)

# set writing position


font_writer.set_textpos(0, 0)

# write some text!


font_writer.printstring("hello")

# calculate how many pixels wide a text is


len = font_writer.stringlen("hello")

If you are wondering what is the point of the last function,


that is necessary if you want to center or right align some
text. With the default font this is easy because all
characters have an 8x8 size, but these custom fonts are not
proportionally spaced, so for example the number “1” uses
less horizontal space than the number “3”. The only way to
know how many pixels a string of text will take on the
screen is to use this function to calculate it, and then once
you know that number you can calculate where the text
needs to be displayed to appear centered or right-aligned.
Here is the snippet of code that renders the temperature
and humidity values in the correct place:
text = '{:.1f}'.format(temperature)
textlen = font_writer.stringlen(text)
font_writer.set_textpos((64 - textlen) // 2, 30)
font_writer.printstring(text)

text = '{:.1f}'.format(humidity)
textlen = font_writer.stringlen(text)
font_writer.set_textpos(64 + (64 - textlen) // 2, 30)
font_writer.printstring(text)

I first convert the numbers into strings, because this Writer


class that I’m using only knows how to print strings. To do
this conversion I use the format() method of the string
object once again. The {:.1f} format specifier requests that
the number is rendered with no more than one decimal. If
the number has more decimals, then it will be rounded to
one in the string version.
Next I use the textlen() method to get the width in pixels of
the string. This is so that I can center these numbers in
their areas.
Based on experimentation in the REPL, I decided that the Y
coordinate for these numbers is going to be Y=30. This
font has 20 pixels of height, so I will be using the screen
rows 30 to 50 for this text. If you look in the image
rendering code above, the degrees and percent images are
displayed starting at Y=52.
The temperature needs to be centered in the left side box,
which has 64 characters wide. To center it, I take 64 and
substract the width in pixels of the string. This is going to
be the amount of white space to the left and to the right of
the string, so I divide it by two and that leaves me with the
number of pixels on the left that need to be skipped. Note
that I used the // to divide here, because I want the result
to be an integer. I can then set the text rendering position
to this number on X, and 30 on Y. For the humidity the
calculation is similar, but I need to add 64 pixels because
the left border of the humidity box starts at X=64.
Once the text position is set, I call the printstring() method
to render the text. Below you can see the complete, now
final version of main.py:
import dht
import framebuf
import machine
import ssd1306
import sys
import time

import config
import freesans20
import writer

def show_error():
led = machine.Pin(config.LED_PIN, machine.Pin.OUT)
for i in range(3):
led.on()
time.sleep(0.5)
led.off()
time.sleep(0.5)
led.on()

def is_debug():
debug = machine.Pin(config.DEBUG_PIN, machine.Pin.IN, machine.Pin.PULL_UP)
if debug.value() == 0:
print('Debug mode detected.')
return True
return False
def get_temperature_and_humidity():
dht22 = dht.DHT22(machine.Pin(config.DHT22_PIN))
dht22.measure()
temperature = dht22.temperature()
if config.FAHRENHEIT:
temperature = temperature * 9 / 5 + 32
return temperature, dht22.humidity()

def load_image(filename):
with open(filename, 'rb') as f:
f.readline()
f.readline()
width, height = [int(v) for v in f.readline().split()]
data = bytearray(f.read())
return framebuf.FrameBuffer(data, width, height, framebuf.MONO_HLSB)

def display_temperature_and_humidity(temperature, humidity):


i2c = machine.I2C(scl=machine.Pin(config.DISPLAY_SCL_PIN),
sda=machine.Pin(config.DISPLAY_SDA_PIN))
if 60 not in i2c.scan():
raise RuntimeError('Cannot find display.')

display = ssd1306.SSD1306_I2C(128, 64, i2c)


font_writer = writer.Writer(display, freesans20)

temperature_pbm = load_image('temperature.pbm')
units_pbm = load_image('fahrenheit.pbm') if config.FAHRENHEIT \
else load_image('celsius.pbm')
humidity_pbm = load_image('humidity.pbm')
percent_pbm = load_image('percent.pbm')

display.fill(0)
display.rect(0, 0, 128, 64, 1)
display.line(64, 0, 64, 64, 1)
display.blit(temperature_pbm, 24, 4)
display.blit(humidity_pbm, 88, 4)
display.blit(units_pbm, 28, 52)
display.blit(percent_pbm, 92, 52)

text = '{:.1f}'.format(temperature)
textlen = font_writer.stringlen(text)
font_writer.set_textpos((64 - textlen) // 2, 30)
font_writer.printstring(text)

text = '{:.1f}'.format(humidity)
textlen = font_writer.stringlen(text)
font_writer.set_textpos(64 + (64 - textlen) // 2, 30)
font_writer.printstring(text)

display.show()
time.sleep(10)
display.poweroff()

def run():
try:
temperature, humidity = get_temperature_and_humidity()
print('Temperature = {temperature}, Humidity = {humidity}'.format(
temperature=temperature, humidity=humidity))
display_temperature_and_humidity(temperature, humidity)
except Exception as exc:
sys.print_exception(exc)
show_error()

if not is_debug():
machine.deepsleep()

run()

To try this application you need to upload main.py, config.py,


freesans20.py and writer.py to your board, along with the five
.pbm images. Here is an example of how the screen looks
with this final version of the application:

6.7 The End


With this example you have reached the end of my
MicroPython tutorial. Congratulations!
Before I go, I wanted to leave a few thoughts on continuing
on your MicroPython learning adventure, as there are a few
areas I haven’t touched in this tutorial that you may want
to research.

6.7.1 Building Custom MicroPython


firmwares
In this tutorial I have used a stock build of MicroPython
from https://micropython.org. This is a convenient
approach that works well for most projects. One
disadvantage of the stock build is that it does not allow the
inclusion of “frozen” Python code.
Consider that your .py files need to be read by the
MicroPython interpreter running inside the board and
compiled into the internal representation that the
interpreter uses to run the code, and this identical process
is repeated every time the application runs. An
optimization would be to compile the code in your
computer and then upload the compiled (or frozen) version
to the board. This saves the microcontroller from having to
compile your files every time.
For small projects the compilation step is small so it isn’t a
problem, but if you are working with a larger application
you may find that the MicroPython interpreter runs out of
memory before it can compile all your code. So in this case
working with frozen modules is the only solution.
Freezing modules is a process that occurs as part of
building the MicroPython firmware. The frozen modules
are incorporated into the firmware in their final compiled
format. The library packages that I have showed you in this
tutorial such as urequests are stored in their frozen format
in the stock firmware to save space and compilation time.
The MicroPython documentation is lacking in terms of
describing the build process and how to configure modules
to be frozen. I have learned how to do this by reading lots
of forum posts and GitHub projects from other developers,
and then I have created a GitHub project if my own that
attempts to simplify the task of building the firmware
specifically for the ESP8266 microcontroller:
https://github.com/miguelgrinberg/micropython-esp8266-
vagrant.

6.7.2 Installing Packages from PyPI


The Python package repository (or PyPI) accepts packages
for MicroPython. The MicroPython interpreter ships with
upip, a simpler version of the pip installer that can be used
from inside the MicroPython REPL, provided that you
connect your board to your Internet router. I have decided
to not discuss upip in this tutorial for two reasons:

Running upip in your board works only when installing


small packages. If you try to install something that is
moderately complex then upip will run out of memory.
This is related to the issues with compiling large source
files in the microcontroller board that I discussed
above. The upip tool performs a source install, which is
inefficient due to the compile step requirement.
Some of the packages available on PyPI for
MicroPython are incompatible with current releases of
MicroPython. This is an unfortunate side effect of
disagreements between the creator of MicroPython and
one of its core developers who now left the project.
Many of the MicroPython libraries on PyPI are
currently controlled by this developer and not by the
official project.
The approach that I have taken when I needed to install a
third party package is to copy the source files into my
project manually, a practice generally known as vendoring
the dependencies. You saw me do this with the custom font
rendering code in this chapter.

6.7.3 Implementing a Web Server


An interesting project to build with MicroPython is a web
server. This could be used for configuring your device, for
example. You would connect to the IP address used by your
microcontroller’s Wi-Fi interface with your browser from
your computer and interact with the board through a
website. I have decided to not cover writing a web server
with MicroPython simply because there is currently a lack
of web development tools for this platform.
If you are wondering if standard Python microframeworks
such as Bottle or Flask can work, the answer is no, because
as odd as it sounds, these frameworks are too big to fit in
the restricted environment of a microcontroller.
I have started my own MicroPython web framework
modeled after Flask called microdot that I hope will
address this hole in the ecosystem. Feel free to give it a
try!

6.7.4 Using Other Microcontroller


Boards
There are a few other boards that are currently supported
by MicroPython, and the list is likely to grow over time. I
chose the ESP8266 because of its low cost and popularity,
but you should definitely look at other boards if the
ESP8266 is too small (or too big!) for your needs.
You also have other options within the ESP8266 family. The
ESP-12E model that I used in this tutorial is the high-level
version which includes the micro-USB port for powering
and programming. Often this is thought of as a prototyping
board because of how easy it is to work with it. But once
you have your application built on this board, you may
consider building a production version using one of the
more basic models, such as the also popular ESP-01:

Note that you will need to learn how to solder to work with
this board!

6.7.5 Using MicroPython Derivatives


Most successful open source projects have “forks”, and
MicroPython is no exception. If you find that MicroPython
is limiting you in some way, you may want to see if there is
a derived project that works better for you. In particular, I
would like to mention CircuitPython as worthy of your
attention. The folks at Adafruit have done a fantastic job in
creating and maintaining their fork, along with lots of
additional device drivers.

6.7.6 Learning Electronics and


Digital Circuits
I took a software centric approach when I designed this
tutorial. If you want to create more complex projects, you
may want to invest some time in learning electronics and
digital circuit concepts. I have found the Electrical
Engineering videos from Kahn Academy very good to learn
the basics.

6.7.7 Running MicroPython on your


Computer
There are builds of MicroPython that can run on Windows,
Mac and Linux computers, so as long as you don’t need to
interact with the hardware, such as the machine, network, dht
or ssd1306 modules, you can write MicroPython code and
run it on your computer. For some of my projects I have
created emulated versions of some of these hardware
specific modules, and this allows me to work on my
applications directly on my computer without having to
constantly upload my source code to the ESP8266 board.
OceanofPDF.com

You might also like