Eight MicroPython / Python Experiments for the ESP32

Categories ESP32, IoT, MicroPython, Python

MicroPython Experiments

Python is an incredibly productive language and when applied to tiny systems like the ESP32, it is a real joy to work with. MicroPython is a reimplementation of the Python language for constrained systems, to be exact. In the following detailed article, we see how to get MicroPython up and running and then we go on to explore support it has for various hardware peripherals on the ESP32 in a series of simple “experiments” that build on one another. Although I’ve provided schematics separately for every experiment, all the schematics can be made into one whole so that everything works together. It is just that having a simple schematic for a given experiment make the understanding a lot easier.

We will start with how to get MicroPython up and running on the ESP32, see how to blink an LED, program the capacitive touch sensors, use MicroPython threads, program a DHT11 temperature and humidity sensor, connect to wifi, use hardware timers, use external interrupts, and finally put everything we’ve learnt into a final project that uses Picoweb to serve a lot of information into a nice web/HTML UI.

ESP32 Picoweb based UI

Some notes on serial port setup

You program the ESP32 and also get print messages from it via the ESP32’s serial port, which you typically connect to your host computer via a micro-USB cable. I don’t want to go into the details of how to successfully connect to your ESP32 from your computer. If your ESP32 board has any documentation at all, you’ll need to figure out which serial port chip your board comes with, install the driver to support it if necessary on your operating system, and also figure out the serial device which shows up on your computer, so that you can configure it in your serial terminal emulator of choice. In my case, the serial chip on my ESP32 board seems to be a Silabs CP2102 for which Mac drivers are available from the vendor’s website. It shows up as /dev/SLAB_USBtoUART, which is also picked up by CoolTerm, a serial console program I’m using now. It it initialized to 115200 baud. You need to figure out the serial chip used on your board and make it work with your operating system as the very first step.

Erase ESP32’s Flash

Let’s start by erasing the ESP32’s internal flash memory before we go ahead and install MicroPython.

$ /usr/local/bin/esptool.py --port /dev/tty.SLAB_USBtoUART erase_flash                                           2 ↵
esptool.py v2.5.0
Serial port /dev/tty.SLAB_USBtoUART
Connecting........_____....._____....._____....._____....._____....._____....._
Detecting chip type... ESP32
Chip is ESP32D0WDQ6 (revision 1)
Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse
MAC: 24:0a:c4:97:11:0c
Uploading stub...
Running stub...
Stub running...
Erasing flash (this may take a while)...
Chip erase completed successfully in 7.5s
Hard resetting via RTS pin...

Write the downloaded Micro Python bin file to the ESP32

You can download the latest release of MicroPython for the ESP32 and then flash it with the esptool.py utility. Installing esptool it is a matter of running pip install esptool on your host computer.

$ esptool.py --port /dev/tty.SLAB_USBtoUART --baud 460800 write_flash --flash_size=detect 0 ~/Desktop/esp32-20180923-v1.9.4-568-g4df194394.bin
esptool.py v2.5.0
Serial port /dev/tty.SLAB_USBtoUART
Connecting........_
Detecting chip type... ESP32
Chip is ESP32D0WDQ6 (revision 1)
Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse
MAC: 24:0a:c4:97:11:0c
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 460800
Changed.
Configuring flash size...
Auto-detected Flash size: 4MB
Compressed 1053296 bytes to 663038...
Wrote 1053296 bytes (663038 compressed) at 0x00000000 in 15.4 seconds (effective 546.6 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...

Playing with Python on the ESP32

Getting to the Python REPL shell

In the serial console screenshot above, I’ve split the output into 4 color bands so I can explain what’s going on easily. The output highlighted in the first, sky-blue band is from the ESP32 booting MicroPython. You are left in a Python REPL shell you should be familiar with if you’ve dealt with Python before.

I then use the print statement to print the customary “Hello world” in the next band. In the green band after that, I import os and call the uname() function that returns operating system / environment information. Finally, I want to show you the boot.py file, which has special meaning in MicroPython. If you put your code in there, MicroPython will run it automatically every time the ESP32 boots up. In all the experiments we’ll do in this article and video, we’ll be putting our code into boot.py.

Experiment #1: Blinking an LED

Blinking an LED is the “Hello world” of the embedded systems world.

import machine
import utime

pin2 = machine.Pin(2, machine.Pin.OUT)
while True:
    pin2.value(1)
    utime.sleep_ms(500)
    pin2.value(0)
    utime.sleep_ms(500)

This example is fairly simple to understand. You can find code for this in the 01_blink/ directory.

Blinking LED with ESP32

Copying files into the ESP32

With MicroPython, there is a simple file system available. You can list, read and write files like how you’d normally do with Python. For our blink example to work, we need to replace MicroPython’s boot.py with the version we have so that even when the ESP32’s power is turned off and back on, our program always runs automatically. You can install Adafruit’s AmPy utility. It is a Python utility that runs on your development/host system and you can install it with pip like any other Python module. Instructions are available on AmPy’s Github page.

$ ampy -d 1.0 -p /dev/tty.SLAB_USBtoUART put boot.py

Once ampy is installed, you can invoke it as above to put a file into your ESP32. You can also get a file, make a directory, remove files, run python scripts on the ESP32 board with ampy. Check out the help text it prints for all options it supports. It is an interesting utility by itself. While I’m in the process of mentioning ampy, I’d also like to mention rshell for the sake of completion. Check it out. You might find it useful in your future MicroPython projects.

Some notes on blink

Now, back to our blink example. Once you copy the file into the ESP32, it should start running.

The blink example is pretty self-explanatory, but there are a couple of things I’d like to mention. The imported machine module abstracts all the hardware related functionality in MicroPython. The machine.Pin function lets you setup GPIO. In this case, we’re setting up pin D2 to be an output capable pin. In the infinite loop that follows, we turn the LED on and off by setting its value to 1 and 0 interspersed with 500ms delays.

Experiment #2: Programming the touch sensors

The ESP32 has 10 capacitive touch sensors. You can read all about those and the ESP32’s other on-chip peripherals in its reference manual. We will be using an RGB LED (which is basically a single LED package that has 3 built-in red, green and blue LEDs) to detect 3 different touch inputs. We will assign one touch input to the red, green and blue LEDs each.

import machine
import utime

r_led = machine.Pin(13, machine.Pin.OUT)
g_led = machine.Pin(12, machine.Pin.OUT)
b_led = machine.Pin(14, machine.Pin.OUT)

touch7 = machine.TouchPad(machine.Pin(27))
touch8 = machine.TouchPad(machine.Pin(33))
touch9 = machine.TouchPad(machine.Pin(32))

while True:
    utime.sleep_ms(500)

    t7 = touch7.read()
    if t7 < 100:
        r_led.value(1)
    else:
        r_led.value(0)

    t8 = touch8.read()
    if t8 < 100:
        g_led.value(1)
    else:
        g_led.value(0)

    t9 = touch9.read()
    if t9 < 100:
        b_led.value(1)
    else:
        b_led.value(0)

    print('Touch R: %d G: %d B:%d ' % (t7, t8, t9))
ESP32 Capacitive Touch Sensors Programming

MicroPython has the machine.TouchPad class that lets you create TouchPad objects for any supported pin. You can then use the TouchPad objects’s read() method to read the capacitive touch pad’s value. In my case, a firm touch reliably resulted in a value of around 60 or less being read. So, in the code, I check if the value is 100 or less. If it is, I turn the respective LED on or off. When the pin is not being touched, values of anywhere between 550 and 620 are read. So, when a pin is touched, there is a 5x reduction in the value read, which you can reliably use a detect touch. We could do the reads and setting the LED state in a tight loop, but since there is a print call, I’ve added a delay/sleep of 500ms so that print messages don’t fill up the serial terminal.

Experiment #3: Threads!

Threads are among MicroPython’s coolest features. Threads let you organize your code better into distinct tasks. We’re generally used to using threads in full-fledged operating systems and I was really excited to try them out in such a tiny computer such as the ESP32 and that too, with Python! I found one issue with threads and few other MicroPython features: they don’t play well with tools like ampy. With ampy unable to transfer files once threads are running, the only way to add any more files is to erase the board, reinstall MicroPython and then use ampy to transfer any more files on the board. I am yet to find a way around this. It is just annoying and takes time, but it’ll let you start over with a fresh program.

import machine
import utime
import _thread


r_led = machine.Pin(13, machine.Pin.OUT)
g_led = machine.Pin(12, machine.Pin.OUT)
b_led = machine.Pin(14, machine.Pin.OUT)

touch7 = machine.TouchPad(machine.Pin(27))
touch8 = machine.TouchPad(machine.Pin(33))
touch9 = machine.TouchPad(machine.Pin(32))

def blinkerThread():
    pin2 = machine.Pin(2, machine.Pin.OUT)
    while True:
        pin2.value(1)
        utime.sleep_ms(500)
        pin2.value(0)
        utime.sleep_ms(500)

_thread.start_new_thread(blinkerThread, ())

while True:
    t7 = touch7.read()
    if t7 < 100:
        r_led.value(1)
    else:
        r_led.value(0)

    t8 = touch8.read()
    if t8 < 100:
        g_led.value(1)
    else:
        g_led.value(0)

    t9 = touch9.read()
    if t9 < 100:
        b_led.value(1)
    else:
        b_led.value(0)

    print('Touch R: %d G: %d B:%d ' % (t7, t8, t9))

In this example, in the main thread, we are running the touch detection code from experiment #2 and in a newly created thread, we run the blink example from experiment #1. So, we’ve combined two programs into one and they are independently running in two threads. I know that we created only one thread explicitly, but in general, the main portion of your program is called the “main thread”. So, I’ll just go with the same parlance here as well.

Experiment #4: Temperature sensor

We’ll use the venerable DHT11 for this experiment. MicroPython has built-in support for the DHT11 and for us, it is just a simple matter of making the right API calls to read temperature and humidity data from our DHT11 once we connect it to the ESP32.

import machine
import utime
import _thread
import dht

r_led = machine.Pin(13, machine.Pin.OUT)
g_led = machine.Pin(12, machine.Pin.OUT)
b_led = machine.Pin(14, machine.Pin.OUT)

touch7 = machine.TouchPad(machine.Pin(27))
touch8 = machine.TouchPad(machine.Pin(33))
touch9 = machine.TouchPad(machine.Pin(32))

def blinkerThread():
    pin2 = machine.Pin(2, machine.Pin.OUT)
    while True:
        pin2.value(1)
        utime.sleep_ms(500)
        pin2.value(0)
        utime.sleep_ms(500)

def tempReaderThread():
    d = dht.DHT11(machine.Pin(23))
    while True:
        d.measure()
        print('Temperature: %d Humidity: %d' % (d.temperature(), d.humidity()))
        utime.sleep_ms(5000)

_thread.start_new_thread(blinkerThread, ())
_thread.start_new_thread(tempReaderThread, ())

while True:
    t7 = touch7.read()
    if t7 < 100:
        r_led.value(1)
    else:
        r_led.value(0)

    t8 = touch8.read()
    if t8 < 100:
        g_led.value(1)
    else:
        g_led.value(0)

    t9 = touch9.read()
    if t9 < 100:
        b_led.value(1)
    else:
        b_led.value(0)

    utime.sleep_ms(500)
    print('Touch R: %d G: %d B:%d ' % (t7, t8, t9))
ESP32 temperature and humidity sensor with the DHT11

We continue as in the previous example by building on previous examples. In this experiment, we have the previous features of the blinking LED and the capacitive touch sensors working. We add another thread, where we handle the DHT11 temperature and humidity sensor. We create a DHT11 object using MicroPython’s dht module. We use the DHT11 class and specify a machine.Pin in the constructor, specifying the pin where the DHT11 is connected to our ESP32. We store the object in d. Calling d.measure() measure the temperature and humidity. We can then read both those values as integers by calling d.temperature() and d.humidity(). This is what we do exactly in the tempReaderThread. It is a simple while loop that wakes up every 5 seconds and prints out the temperature and humidity.

Experiment #5: Connecting to Wifi

This is not much of an experiment really, but we learn how to connect to your wifi network. We will also need this in the next experiment, our grand finale where we need to install Python packages on the ESP32 from the internet. We need the ESP32 to have a working internet connection for us to be able to do that.

import network

def do_connect():
    sta_if = network.WLAN(network.STA_IF)
    if not sta_if.isconnected():
        print('connecting to network...')
        sta_if.active(True)
        sta_if.connect('your-wifi-ssid', 'your-wifi-password')
        while not sta_if.isconnected():
            pass
    print('network config:', sta_if.ifconfig())


do_connect()

Copy this file over to the ESP32 using the ampy utility. In the next experiment we’ll see how to use this to connect to your wifi. Once the connection is made, you can see that we’re calling sta_if.ifconfig() which returns details about the wifi interface, which we then print to the serial console. The most important bit of information there is the IP address, which we will need, if we are to connect to the ESP32, as we will need in our next experiment.

Experiment #6: Web server with Picoweb

In our final experiment, we will create a web server with Picoweb, but we will also introduce a few other concepts. Since those concepts are pretty much standalone topics themselves, I’ll be splitting them up into their own experiments. As part of experiment #6, we shall take a look at Picoweb, an excellent and light web server framework for MicroPython clearly inspired by the Flask Python web framework. Picoweb is a framework with minimal fuss and works as one might expect. There are very few things to learn and you’ll be up to speed in not time at all.

For this experiment, we’ll be using Picoweb for only 2 things:

  1. Serve the index.html page
  2. Serve JSON data via 3 different calls
    • /get_temp_history serves JSON data containing temperature and humidity every minute for the past one hour
    • /get_touch_states serves JSON data containing the current state of the R, G & B LEDs which are triggered by the capacitive touch pins
    • /get_ext_int_count serves JSON data that has the count of the external interrupts that occurred via a switch we’ve wired up to the ESP32

In total, there are 4 different calls.

import machine
import utime
import dht
import picoweb

ip_address = None
temp_history = {"temperature": [], "humidity": []}
temp_minute_counter = 0
r_led_state = False
g_led_state = False
b_led_state = False
btn_press_counter = 0

r_led = machine.Pin(13, machine.Pin.OUT)
g_led = machine.Pin(12, machine.Pin.OUT)
b_led = machine.Pin(14, machine.Pin.OUT)

touch7 = machine.TouchPad(machine.Pin(27))
touch8 = machine.TouchPad(machine.Pin(33))
touch9 = machine.TouchPad(machine.Pin(32))

def do_connect():
    import network
    global ip_address
    sta_if = network.WLAN(network.STA_IF)
    if not sta_if.isconnected():
        print('connecting to network...')
        sta_if.active(True)
        sta_if.connect('your-wifi-ssid', 'your-wifi-password')
        while not sta_if.isconnected():
            pass
    print('network config:', sta_if.ifconfig())
    ip_address = sta_if.ifconfig() [0]


def extIntHandler(pin):
  global btn_press_counter
  btn_press_counter += 1

def timerIntHandler_temperature(timer):
    global temp_minute_counter, temp_history

    if temp_minute_counter == 60:
        temp_minute_counter = 0
        d.measure()
        temp_history['temperature'].append(d.temperature())
        temp_history['humidity'].append(d.humidity())
        if len(temp_history['temperature']) &gt; 60:
            del temp_history['temperature'][0]
            del temp_history['humidity'][0]
    
    temp_minute_counter += 1

def timerIntHandler_touch(timer):
    global r_led_state, g_led_state, b_led_state

    t7 = touch7.read()
    if t7 < 100:
        r_led.value(1)
        r_led_state = True
    else:
        r_led.value(0)
        r_led_state = False

    t8 = touch8.read()
    if t8 < 100:
        g_led.value(1)
        g_led_state = True
    else:
        g_led.value(0)
        g_led_state = False

    t9 = touch9.read()
    if t9 < 100:
        b_led.value(1)
        b_led_state = True
    else:
        b_led.value(0)
        b_led_state = False

do_connect()

d = dht.DHT11(machine.Pin(23))

dht11_timer = machine.Timer(0)
dht11_timer.init(period=1000, mode=machine.Timer.PERIODIC, callback=timerIntHandler_temperature)

touch_timer = machine.Timer(1)
touch_timer.init(period=500, mode=machine.Timer.PERIODIC, callback=timerIntHandler_touch)

p22 = machine.Pin(22, machine.Pin.IN, machine.Pin.PULL_UP)
p22.irq(trigger=machine.Pin.IRQ_FALLING, handler=extIntHandler)

app = picoweb.WebApp(__name__)

@app.route("/")
def send_index(req, resp):
    yield from app.sendfile(resp, 'index.html')

@app.route("/get_temp_history")
def get_temp_history(req, resp):
    global temp_history
    yield from picoweb.jsonify(resp, temp_history)

@app.route("/get_ext_int_count")
def get_ext_int_count(req, resp):
    global btn_press_counter
    yield from picoweb.jsonify(resp, {'btn_press_counter': btn_press_counter})

@app.route("/get_touch_states")
def get_touch_states(req, resp):
    touch_states = {"r_led_state": r_led_state, 
                        "g_led_state": g_led_state, 
                        "b_led_state": b_led_state}
    yield from picoweb.jsonify(resp, touch_states)

app.run(debug=1, host=ip_address, port=80)

Do not be intimidated by the length of this file. We’re simply doing many things via timers (read the next section) and then we’re serving the relevant data via JSON when requested.

Setup packages before running Picoweb

In order to run Picoweb, we need two MicroPython packages installed on the ESP32. On a normal computer, you can use the pip utility to install Python packages and surprisingly, MicroPython has upip. It is not a script you can run, however. It is a MicroPython module. It is super simple to use. Before you can use upip though, you’ll need to be connected to the internet. This is where Experiment #5 comes in handy. Let’s run upip to install the packages we need on the ESP32 to get our Picoweb based program up and running.

$ ampy -d 1.0 -p /dev/tty.SLAB_USBtoUART put ../05_wifi_connect/conn_wifi.py

In the serial console, you can now just import conn_wifi and it’ll then connect to your wifi. Note that it also prints various network details, including the IP address of the ESP32 allotted by your wifi router. We’ll need that later. We now switch to the serial console and install the required picoweb and micropython-logging MicroPython packages. See the following session to get an idea about how this works:

>>> import conn_wifi
I (72416) wifi: wifi driver task: 3ffcada8, prio:23, stack:3584, core=0
I (72416) wifi: wifi firmware version: 7aac1f9
I (72416) wifi: config NVS flash: enabled
I (72416) wifi: config nano formating: disabled
.[0;32mI (72416) system_api: Base MAC address is not set, read default base MAC address from BLK0 of EFUSE.[0m
.[0;32mI (72426) system_api: Base MAC address is not set, read default base MAC address from BLK0 of EFUSE.[0m
I (72446) wifi: Init dynamic tx buffer num: 32
I (72446) wifi: Init data frame dynamic rx buffer num: 64
I (72446) wifi: Init management frame dynamic rx buffer num: 64
I (72456) wifi: Init static rx buffer size: 1600
I (72456) wifi: Init static rx buffer num: 10
I (72466) wifi: Init dynamic rx buffer num: 0
connecting to network...
.[0;33mW (72466) phy_init: failed to load RF calibration data (0x1102), falling back to full calibration.[0m
.[0;32mI (72636) phy: phy_version: 3960, 5211945, Jul 18 2018, 10:40:07, 0, 2.[0m
I (72636) wifi: mode : sta (24:0a:c4:97:11:0c)
.[0;32mI (72636) wifi: STA_START.[0m
I (72766) wifi: n:1 1, o:1 0, ap:255 255, sta:1 1, prof:1
I (73326) wifi: state: init -&gt; auth (b0)
I (73336) wifi: state: auth -&gt; assoc (0)
I (73336) wifi: state: assoc -&gt; run (10)
I (73346) wifi: connected with santorini, channel 1
I (73346) wifi: pm start, type: 1

.[0;32mI (73356) network: CONNECTED.[0m
.[0;32mI (73996) event: sta ip: 10.0.0.158, mask: 255.255.255.0, gw: 10.0.0.1.[0m
.[0;32mI (73996) network: GOT_IP.[0m
network config: ('10.0.0.158', '255.255.255.0', '10.0.0.1', '10.0.0.1')
>>> import upip
>>> upip.install('picoweb')
Installing to: /lib/
Warning: pypi.org SSL certificate is not validated
Installing picoweb 1.4.1 from https://files.pythonhosted.org/packages/1f/4e/4f66ea16b069cba5f91a244592d2c5148b5599540cc8941afde91fc8ad2d/picoweb-1.4.1.tar.gz
Installing micropython-uasyncio 2.1 from https://files.pythonhosted.org/packages/3d/4e/aa4345eb0fc10d911d179b33126c0a0d4c09558246c8f84b9f99898008a4/micropython-uasyncio-2.1.tar.gz
Installing micropython-pkg_resources 0.2.1 from https://files.pythonhosted.org/packages/58/87/5acc262e0f642186891ef8d944bd727989000ce8d9263514f73b8feb7fdc/micropython-pkg_resources-0.2.1.tar.gz
Installing micropython-uasyncio.core 2.0.1 from https://files.pythonhosted.org/packages/39/9f/619bbe7454419b72073e61b3d9abbc5dfeb54cc4062c2a2d21ad4984db85/micropython-uasyncio.core-2.0.1.tar.gz
>>> upip.install('micropython-logging')
Installing to: /lib/
Installing micropython-logging 0.3 from https://files.pythonhosted.org/packages/01/dd/e2ae81443b92aec79d5f940b2af1e219fbc141bd69ec712d3d95cc6ab018/micropython-logging-0.3.tar.gz
>>> 

Great! Now we’re all set to run our Picoweb based program. You can now copy over index.html and boot.py using ampy on to the ESP32. Check the serial console for the ESP32’s IP address. We will need that to enter in a browser. From the output above, you can see that, in my case, the ESP32’s IP address is 10.0.0.158.

How we’re using Picoweb

The code in conn_wifi.py is part of our boot.py file which contains the Picoweb based app. We call do_connect(), which tries to connect to wifi and prints out the ESP32’s IP address. The, we call app = picoweb.WebApp(__name__) which initiailizes the web framework. The app object is then used to define various callbacks that will be called in response to the HTTP calls we need and then finally, app.run() is called where we pass in our IP address and also port 80, which is the default port for HTTP.

If you don’t understand Python decorators or yield from, you can always read about them online. There are plenty of excellent articles and videos available. Our callback functions are decorated with @app.route() and they get called when the corresponding HTTP call comes in. For the / route, we simply send the index.html file with the help of the app.sendfile() method. For other calls where we need to send JSON data, we use picoweb.jsonify(). This function converts Python objects into JSON strings. It is incredibly useful since most web services almost always use JSON to communicate with clients. The code in our Picoweb callback functions are very simple to understand.

The index.html file we serve reaches the browser and starts running the Javascript code contained in there. It makes further calls to our ESP32 to get temperature & humidity data, external interrupt counts and touch pin LED statuses. We poll every second for the touch state data whereas other data, we poll less frequently. Take a look at the Javascript code to see how we fetch data from the ESP32 via the Picoweb calls we’ve defined.

    function update_ints_count() {
        $.ajax({
                url: '/get_ext_int_count',
                dataType: 'json'
            })
            .done(function(data) {
                console.log(data);
                $('#ext-ints-count').html(data.btn_press_counter);
                setTimeout(update_ints_count, 2000);
            });
    }

In the example above, the update_ints_count() function makes a call get_ext_int_count to the ESP32, which results in the get_ext_int_count() callback running. It sends over some JSON data, using which our Javascript updates the HTML element with ID ext-ints-count. It when sets up the same function to be fired again in 2 seconds using the setTimeout function. Other Javascript code is similar and you should be able to make out what is going on.

Experiment #7: Hardware Timers

The ESP32 has 4 hardware timers. These can be used very easily from MicroPython. In the previous examples, we used threads to keep some logic separate and running in its own thread of execution. Now, we’ll be using hardware timers in the ESP32 to do the same. Hardware timers essentially trigger some callback function. For our use case, we want the timer callback function to be called periodically. Typically the way this happens is that the hardware timer simply counts down an integer at a specified clock speed. When the value of this integer reaches 0, the timer has said to have “expired”. When this happens, a callback is triggered. In periodic mode, the original integer count down value in the timer register is restored so that it can start counting down again. But with MicroPython, we’re spared of all the clock and integer value calculations, since all we need to do is specify a period after which the callback should be triggered.

dht11_timer = machine.Timer(0)
dht11_timer.init(period=1000, mode=machine.Timer.PERIODIC, callback=timerIntHandler_temperature)

touch_timer = machine.Timer(1)
touch_timer.init(period=500, mode=machine.Timer.PERIODIC, callback=timerIntHandler_touch)

In the code above, we’re setting up two different timers 0 and 1. We are specifying three other things apart from the hardware timer (0 and 1). The period, the type of timer and the callback function to call. Here, we set the timer type to Timer.PERIODIC. The other option we have is Timer.ONE_SHOT, meaning the callback will occur only once. This is useful if we want to execute some logic in a future after a short while. You can read about hardware timers in MicroPython in the documentation. In the callback that is being called every second, we’re measuring the temperature and humidity, and every 60 seconds, we’re storing a value in an array. The idea is to store temperature and humidity values for every minute for the past one hour in two separate arrays. Please note that you always need to be aware of the amount of RAM you’ll use. In our case, we are storing, worst-case, 120 integers split in two separate arrays. So, we should be fine. Also, we have logic to remove the oldest value from the array once we have 60 integers, so that the arrays don’t keep growing out of control.

 

Experiment #8: Handling External Interrupts

An Interrupt Service Routine (ISR) in any CPU is nothing but a special function that is called when an interrupt occurs. System timers are also interrupts. When an interrupt occurs, depending on the system architecture, other interrupts are disabled (or maybe not; sometimes there is a hierarchical system), the current thread details are saved and then the CPU begins executing the designated ISR. It has to be noted that the ISR must be very short. You should not attempt to do things that may take a long time, like for example communicating over the serial port, etc. In our ISR, which we’ve setup to trigger when pin D23 goes low, simply increments an integer and does nothing else. Elsewhere from a Picoweb callback, we read that counter.

p22 = machine.Pin(22, machine.Pin.IN, machine.Pin.PULL_UP)
p22.irq(trigger=machine.Pin.IRQ_FALLING, handler=extIntHandler)

We’re setting up pin 22 as IN, meaning we are intending to read something rather than control an LED, in which case, we’d have set it up as an OUT. For the external interrupt, we bring pin D22 low by connecting it to ground via a push-button switch. When the switch is pressed, D22 is brought low and the external interrupt handler is called. We increment a count variable there.

ESP32 external interrupt handling

You need to pay special attention when dealing with complex variable types like arrays and dictionaries or changing the values of variables that could also be potentially access by an ISR. Let’s consider that an ISR adds items to a list and from the main thread, we need to remove those items, you need to surround the code in the main thread that removes items from the array with code the disables and then enables interrupts. Because, removing items from an array it not an atomic operation. Meaning that although you see a single Python statement, eventually, it is several machine instructions. When, in the middle of executing those instructions, the ISR runs and adds something into the array, it might lead to an inconsistent state and ultimately to data corruption. See the code below on how to set up a critical section where you can safely manipulate variables that are also accessed by ISRs by disabling interrupts.

state = machine.disable_irq()
interruptCounter = interruptCounter-1
machine.enable_irq(state)

Please note that critical sections have to be really short and must include the exact operation that is critical. You should not include other code there. The CPU and the operating system use timer interrupts to get several things done. When system interrupts are disabled for a long time, performance suffers.

Notes on debouncing

If you played around with the external interrupt example, you might have noticed that the external interrupt counter increments quite a bit even if you pressed the button only once. The is the effect of bouncing. Even the slowest of CPUs are really fast compared to us human. When we press the button, the button’s internal metal contacts bounce about before settling down. This is called “bouncing”. Programmers generally add what is know as “debouncing” logic to ensure that only one button press event is registered. Take a moment to thank the person who wrote debouncing logic in the computer keyboard you use everyday. I have a technique of dealing with debouncing with timers, but that is a topic for a separate article. In the interest of simplicity, we are not going to add any debouncing. And like many authors love to say, it is left as an exercise to you, the reader.

HTML/Javascript Source

I know HTML/Javascript isn’t usually a strong area of someone involved in IoT or embedded systems, but since it has become the default way to build a UI, it might be a great idea to put in the effort to learn it.

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>
        ESP32 Project!
    </title>
    <style type="text/css">
        #title {
            font-size: 64px;
        }
        
        #led-container {
            height: 150px;
        }
        
        #red-led {
            background-color: #330000;
        }
        
        #blue-led {
            background-color: #000033;
        }
        
        #green-led {
            background-color: #003300;
        }
        
        .led {
            width: 150px;
            height: 100%;
            border-radius: 50%;
            margin: 10px;
            float: left;
        }
        
        .table-header {
            text-transform: uppercase;
            font-weight: bold;
        }
        
        .table-value {
            font-size: 160px;
            color: #999;
        }
    </style>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js" type="text/javascript">
    </script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.min.js" type="text/javascript">
    </script>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
    <script type="text/javascript">
        $(document).ready(function() {
            var ctx = document.getElementById('tempHistoryChart').getContext('2d');
            var chart = new Chart(ctx, {
                // The type of chart we want to create
                type: 'line',

                // The data for our dataset
                data: {
                    labels: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59],
                    datasets: [{
                        label: "Temperature",
                        borderColor: 'rgb(255, 99, 132)',
                        data: [],
                    }, {
                        label: "Humidity",
                        borderColor: 'rgb(54, 162, 235)',
                        data: [],
                    }]
                },

                // Configuration options go here
                options: {
                    scales: {
                        yAxes: [{
                            ticks: {
                                beginAtZero: true
                            }
                        }]
                    }
                }
            });

            update_touch_states();
            update_temp_chart();
            update_ints_count();

            function update_touch_states() {
                $.ajax({
                        url: '/get_touch_states',
                        dataType: 'json'
                    })
                    .done(function(data) {
                        console.log(data);
                        if (data.r_led_state) {
                            $('#red-led').css('background-color', 'ee0000');
                        } else {
                            $('#red-led').css('background-color', '550000');
                        }
                        if (data.g_led_state) {
                            $('#green-led').css('background-color', '00ee00');
                        } else {
                            $('#green-led').css('background-color', '005500');
                        }
                        if (data.b_led_state) {
                            $('#blue-led').css('background-color', '0000ee');
                        } else {
                            $('#blue-led').css('background-color', '000055');
                        }
                        setTimeout(update_touch_states, 1000);
                    });
            }

            function update_temp_chart() {
                $.ajax({
                        url: '/get_temp_history',
                        dataType: 'json'
                    })
                    .done(function(data) {
                        console.log(data);
                        chart.data.datasets[0].data = data.temperature;
                        chart.data.datasets[1].data = data.humidity;
                        chart.update();
                        /* Get the latest readings and set their values in the respective elements */
                        if (data.temperature.length && data.humidity.length) {
                            const c_temp = data.temperature[data.temperature.length - 1];
                            const c_humi = data.humidity[data.humidity.length - 1];
                            $('#current-temp').html(c_temp);
                            $('#current-humidity').html(c_humi);
                        }
                        setTimeout(update_temp_chart, 1000 * 60);
                    });
            }

            function update_ints_count() {
                $.ajax({
                        url: '/get_ext_int_count',
                        dataType: 'json'
                    })
                    .done(function(data) {
                        console.log(data);
                        $('#ext-ints-count').html(data.btn_press_counter);
                        setTimeout(update_ints_count, 2000);
                    });
            }
        });
    </script>
</head>

<body>

    <div class="container">
        <div class="row">
            <div class="col-12">
                <h1 class="text-center" id="title">
                    ITYWIK ESP32 Micropython Tutorial
                </h1>
            </div>
        </div>

        <div class="row mt-5">
            <div class="col-6">
                <h2 id="temperature-title" class="text-primary">Current Temperature</h2>
                <hr>
                <table>
                    <tr>
                        <td class="table-header">temperature &deg; C</td>
                        <td class="table-header" style="padding-left: 30px;">humidity</td>
                    </tr>
                    <tr>
                        <td class="table-value" id="current-temp">...</td>
                        <td class="table-value" style="padding-left: 30px;" id="current-humidity">...</td>
                    </tr>
                </table>
            </div>

            <div class="col-6">
                <h2 class="text-primary">Touch Control LED States</h2>
                <hr>
                <div id="led-container">
                    <div class="led" id="red-led">
                    </div>
                    <div class="led" id="green-led">
                    </div>
                    <div class="led" id="blue-led">
                    </div>
                </div>
            </div>
        </div>


        <div class="row mt-4">
            <div class="col-8">
                <h2 class="text-primary">Temperature and humidity in the past 1 hour</h2>
                <hr>
                <canvas id="tempHistoryChart"></canvas>
            </div>
            <div class="col-4">
                <h2 class="text-primary">External interrupts</h2>
                <hr>
                <div class="table-value" id="ext-ints-count">
                    00
                </div>
            </div>
        </div>

    </div>
</body>

</html>