OceanofPDF - Com MicroPython and The Internet of Things - Miguel Grinberg
OceanofPDF - Com MicroPython and The Internet of Things - Miguel Grinberg
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.
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
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.
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
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.
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 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
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...
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 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> _
/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!
>>> _
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.
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
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
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:
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_PIN = 2 # D4
LED2_PIN = 16 # D0
BUTTON_PIN = 14 # D5
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()
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()
Or activate it again:
>>> ap_if.active(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')
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().
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 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')
(venv) $ rshell --port <board serial port name> cp main.py config.py /pyboard
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()
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 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()
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.
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()
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()
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
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
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
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 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}'
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()
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 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()
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
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]
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
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()
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()
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 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:
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)
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()
temperature.pbm
celsius.pbm
fahrenheit.pbm
humidity.pbm
percent.pbm
freesans20.py
writer.py
text = '{:.1f}'.format(humidity)
textlen = font_writer.stringlen(text)
font_writer.set_textpos(64 + (64 - textlen) // 2, 30)
font_writer.printstring(text)
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)
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()
Note that you will need to learn how to solder to work with
this board!