Pico OTAUpdate

In building a Raspberry Pi Pico W project that has a web interface and is deployed remotely running on a battery that is charged by solar power, it became more than a hassle to update the code on the Pico. This typically involved connecting my laptop to the device using a USB cable.

There has to be a better way.

Enter OTAUpdate. Over The Air Updating.

I started with this tutorial: (https://www.pythontutorials.net/blog/micropython-ota/) , but added a reasonable amount including expanding the versions.json file to include individual entries for each module (or other file) on your pico. ota_update.py and check_versions.py are pulled from that tutorial and modified.

I found that some developers use Github to store their code and “git” the code to the Pico. (https://github.com/rdehuyss/micropython-ota-updater/blob/master/app/ota_updater.py). This works and may be a good solution for a production product, although there are significant downsides to using Github or even a private git server. The first downside to using Github is that you store your code on a public repository. A deal killer for proprietary projects. The second downside is this is not very useful for rapid development. GitHub updates can sometimes take many minutes. Not friendly to rapid development and testing.

You can deploy your own private git server and push and pull data from it. However, most full featured git servers have a lot of overhead. I tested gitlab as a VM and it ran out of memory just running with no access with 4GB of RAM (3GB Physical + 1GB swap) and there was a constant background CPU usage doing who knows what. gitlab reconfigure can take 2 minutes. A lot of overhead for a simple operation.

A simple nginx web server on a basic linux install can run on less than 100MB of RAM and takes literally zero CPU until you make a request. I am running mine on 512MB VM running Debian 13.

In the future I will look at using a bare bones cli git server. But this OTA solution uses any web server.

This is config.py. Some developers use a json file to store their contents, but I have found that using a .py module makes referencing the constants from any other module very simple.

PORT=80
WIFI_SSID='SSID'
WIFI_PASSWORD='PWD'
FLASH_MS=250
# Check version this many milliseconds (for test - 15 seconds,  for production every hour? or every day?)
VERSION_MS=15000
# Web site base URL
BASE_URL="http://192.168.0.123/blink/"
# Versions file stored at BASE_URL
VERSIONS_FILE="versions.json"

The second module is check_version.py. This is the module that does the comparison of the versions currently installed with the version on the web server. ota_update is a module called by check_version.py when an update is required.

import urequests
import json
import config
import os
import machine
from ota_update import ota_update

def check_versions():
    try:
        response = urequests.get(config.BASE_URL + config.VERSIONS_FILE)
        if response.status_code == 200:
            versions_remote = json.loads(response.text)
    except Exception as e:
        print("Error:", e)

    directory = "/"
    jsonfile = "versions.json"
    jsonpath = directory + jsonfile
    # Open and read the JSON file
    try:
        # Open the file for read only
        with open(jsonpath, 'r') as file:
            # Read the json data into dictionary named versions
            versions_local = json.load(file)
    except:
        # If the local file does not exist, write out a copy of the remote file minus 1
        # so that all files are freshly downloaded.
        versions_local = {}
        with open(jsonpath, "w") as file:
            file.write(json.dumps(versions_remote))
            file.close
        for files in versions_remote:
            versions_local[files] = versions_remote[files] - 1
    
    # Start with the assumption there are no changes
    changes = False
    for file in versions_remote:
        if versions_remote[file] > versions_local[file]:
            # ota_update returns 0 if the file updated successfully
            if ota_update(config.BASE_URL, file) == 0:
                # If even one file changes, we will reboot, but after all files are checked/updated
                changes = True

    # Write out to the local file a copy of the remote versions file
    with open(jsonpath, "w") as file:
        file.write(json.dumps(versions_remote))
        file.close
    
    if changes:
        machine.reset()

This is oat_update.py, called by check_versions if a file is not the latest version. It downloads the file to temp.tmp and then renames it over the old version.

# ota_uptate(BASE_URL, FILENAME)
import urequests
import os
import machine

def ota_update(BASE_URL, FILE):
    try:
        response = urequests.get(BASE_URL + FILE)
        if response.status_code == 200:
            try:
                with open('temp.tmp', 'w') as f:
                    f.write(response.text)
                response.close()
                os.rename('temp.tmp', FILE)
                return 0
            except OSError as e:
                return e
        else:
            return response.status_code
    except Exception as e:
        print("OTA update error:", e)
        return e

This is the main.py file. It does some time set up and calls connect_wlan to initiate the wireless network. Then it loops indefinitely. Every config.VERSION_MS it runs the check_versions(), which grabs the versions.json file from the server and then updates any modules that have a new version than the one on this pico.

from time import sleep_ms, ticks_ms, ticks_diff, time
import ntptime
from flash import flash
from check_versions import check_versions
import config
from connect_wlan import connect_wlan


def main():
    boot_time = time()
    flash_ms = ticks_ms()
    version_ms = ticks_ms()
    wlan = connect_wlan()
    while True:
        sleep_ms(10)
        flash_ms = flash(flash_ms)

        if ticks_diff(ticks_ms(), version_ms) > config.VERSION_MS:
            check_versions()
            version_ms = ticks_ms()

        now = time()
        if (now % 3600 == 0 or now == boot_time + 10):
            try:
                ntptime.settime()
            except:
                print("Failed to get ntptime, will try again in 10 seconds")
                boot_time = now


main()

Here is connect_wlan.py

import network
import socket
import time
import machine
import config

def connect_wlan():
# Initialize the Wireless LAN
    wlan = network.WLAN(network.STA_IF)
    wlan.active(False)
    wlan.disconnect()
   
    wlan.active(True)
    wlan.connect(config.WIFI_SSID, config.WIFI_PASSWORD)

    max_wait = 10
    while max_wait > 0:
        if wlan.status() < 0 or wlan.status() >= 3:
            break
        max_wait -= 1
        print('waiting for connection...')
        time.sleep(1)

    if wlan.status() != 3:
        print("Failed setting up wifi, will restart in 10 seconds")
        time.sleep(10)
        machine.reset()
    
    print('connected to wifi:', config.WIFI_SSID, 'with ip = ', wlan.ifconfig()[0])
    return wlan

Finally, here is blink.py, a simple routine that blinks the LED on the pico by toggling the Pin state whenever called. main.py calls this and the frequency is set in the config file.

from machine import Pin
from time import ticks_ms
from time import ticks_diff
import config

def flash(flash_ms):
    pin = Pin("LED", Pin.OUT)
    if ticks_diff(ticks_ms(), flash_ms) > config.FLASH_MS:
        pin.toggle()
        flash_ms = ticks_ms()
    return flash_ms

This provides a very easy way to test the Over The Air. Just change the config.FLASH_MS and wait less than config.VERSION_MS milliseconds and it should grab the new config.py and reboot and the blink rate would speed up or slow down based on your change.

!


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *