MicroPython - Iterative development process with an ESP8266

MicroPython lets you create Python-based modules that can be built on top of the frozen base Python image. You can upload python (py) files to the device's file system where they can be run separately and then called as part of the final product. Many teams can just use the existing Python capabilities and the bundled C modules without having to create c code of their own.

There is a set of problems that are not real-time constrained and a class of relatively inexpensive IoT devices that are just spacious enough to support modular software and incremental updates.  Micropython makes it easy for the average developer to build modular software and incrementally develop and update components.  

Developing software for IoT devices can be painful because there is no way to debug, step through or instrument code. The code runs from beginning to end.  It is debugged by looking at external symptoms or serial port output. Software is downloaded to the device as a monolithic unit because there is no easy way to incrementally update the device. This leads to long Edit / Download / Restart / Evaluate cycles.  Real-time systems can be even harder because race conditions can be difficult to tease apart.

This diagram shows the standard configuration where a Developer Machine is connected to the IoT board over a single serial channel. All program updates, debug output, and interactions happen over this one channel. Some systems support WIFI updates instead of serial but these OTA updates are really best suited to updating working code in the production deployment environment.

Organizing MicroPython Code

We have to pay attention to the limited amount of program and variable space in these devices. This means we have to really shrink down our code. We balance this against the fact that modular code is easier to update and deploy.  This gives us two options, deploy code in fewer larger files or more smaller files. In some ways, it isn't the files that matter.  Managing and modularizing the code is the important part. Isolating functionality makes it easier to test that functionality outside of the larger program it is intended to run in.
  1. Create Python Code in Functions and classes.  
  2. Put even your main loop into a function like main()
  3. Dependency inject as much as you can into the class configuration.
  4. Pull configuration constants out of the modules into a configuration class.
  5. Make it easy to run functions from the REPL

What is the REPL?
  • Read the user input (your Python commands).
  • Evaluate your code (to work out what you mean).
  • Print any results (so you can see the computer’s response).
  • Loop back to step 1 (to continue the conversation).

Youtube Talk

Toggle an IO pin example

This example is portable and testable. Everything it relies on is passed in as parameters.

import machine
def toggle_pin(pinNum, msec, times):
    """2: led or 16:relay"""
    pin = machine.Pin(pinNum, machine.Pin.OUT)
    def toggle_pin(p):
        p.value(not p.value())
    import time
    # each blink is on/off
    num_left = times * 2
    while num_left > 0:
        toggle_pin(pin)
        time.sleep_ms(msec)
        num_left -= 1


It can be easily tested from the REPL prompt.  I've removed the ">>>" REPL prompt to make it easy to copy.

from toggle import toggle_pin
toggle_pin(2, 500, 2)

Get a remote string via HTTP GET call example

This example isn't done yet.  It reads and prints the response of an HTTP GET call.  Additional work needs to be done but the code is testable outside of any other structure

import socket
def http_get_print(url):
    _, _, host, path = url.split("/", 3)
    addr = socket.getaddrinfo(host, 80)[0][-1]
    s = socket.socket()
    s.connect(addr)
    s.send(bytes("GET /%s HTTP/1.0\r\nHost: %s\r\n\r\n" % (path, host), "utf8"))
    while True:
        data = s.recv(100)
        if data:
            print(str(data, "utf8"), end="")
        else:
            break
    s.close()

The test code in the REPL would look something like the following.  Again the REPL ">>>" prompt was removed to make it easier to copy/paste.

from httpget import http_get_print
http_get_print("http://micropython.org/ks/test.html"

Structure the main.py for testing

MicroPython runs main.py on startup.  A lot of the sample programs put all the code in the body of the main.py.  This makes it difficult to test pieces independently.  I like this structure because I can run main() from the REPL prompt while still supporting the main.py standard for startup modules.

from config import ssid, password, hostname
from connectwifi import WIFI
from webserver import WebServer
from toggle import toggle_pin
from httpget import http_get_print
def main():
    """lets us test main() without board reset"""
    conn = WIFI(ssid, password, hostname)
    conn.do_connect()

http_get_print("http://micropython.org/ks/test.html"
    toggle_pin(2, 500, 2)

    server = WebServer("LED (Pin 2)", 2, False, "RELAY (Pin 16)", 16, True)
    server.run_server()


if __name__ == "__main__":
    main()

Another nice thing about this structure is that it documents the steps required to run the various components.   The main() is simple and it is easy for me to copy/paste these few lines into the REPL prompt window to verify each step.

Developer Experience

Building testable code will add more layers and change the shape of your code.  This is a good thing that must be balanced against the space on your device.

Organize your code

  1. Put configuration in its own file that can be used across modules
  2. Organize code into functions and classes. 
  3. Write functions and classes to be easy to run from the REPL
  4. Organize code so that the main.py is only a few lines long.
  5. Configure startup to call a function in main.py

The loop

We have a limitation that we can only use the serial port for one thing at a time. We can either be in the REPL shell or we can manipulate the file system. The currently running program is killed every time we drop into REPL.
  1. Work on a single module at a time.
    1. Edit the module/code, in an IDE.
    2. Open an rshell prompt from a terminal sitting in your code directory.
    3. Upload the module to the device /pyboard partition using rshell
    4. Drop into REPL
    5. Run any initialization code required.  EX: Bring up the WIFI connection.
    6. Run the module standalone to verify functionality
    7. Stop the module if it is in a loop with control-c
    8. Drop out of REPL to upload the next source file with control-x
  2. Repeat this process one module at a time
  3. Update the main loop to invoke the developed modules in sequence.

rshell and Windows Environment Variables

I use rshell for all my command line interactions with a MicroPython board. rshell is a python program that gets installed in Python's scripts directory.  The installer will tell you if rshell will be on your command line path and will give you the path if it is not. You should add that path to your user path.  This will let you run python modules as Windows executables without having to do any python -m ... nonsense.  The user's path variable can be set via the Environment Variables control panel.


Related Content

Revision History

Created 2022 11

Comments

Popular posts from this blog

Installing the RNDIS driver on Windows 11 to use USB Raspberry Pi as network attached

Understanding your WSL2 RAM and swap - Changing the default 50%-25%

Almost PaaS Document Parsing with Tika and AWS Elastic Beanstalk