Raspberry Pi Pico OTA Update Guide (MicroPython)

This tutorial explains how to update Raspberry Pi Pico Over-The-Air (OTA) and flash MicroPython firmware remotely. Code stored on GitHub is downloaded to Pico when an update is initiated.

OTA updates can be beneficial for devices like the Raspberry Pi Pico because they simplify the process of deploying firmware updates remotely without needing physical access to the device. For projects where multiple Raspberry Pi Picos are deployed (e.g., IoT devices, sensor networks), OTA updates makes it scalable as you don’t need to manually update each one.

Prerequisites

Your Raspberry Pi Pico needs to be flashed with a MicroPython UF2 file to program it in MicroPython. You can read our getting started guide for Raspberry Pi Pico where we show all the steps required to start programming Raspberry Pi Pico & Pico W.

If you are using macOS, follow our guide to program Raspberry Pi Pico on macOS using Thonny IDE.

Raspberry Pi Pico needs internet access for the OTA update process explained in this guide. Follow our guide-How to connect Raspberry Pi Pico W to Internet using Wi-Fi to learn more.

How the OTA Update Works

The main code in Raspberry Pi Pico handles OTA updates as well as tasks. When an update is requested from Pico over the internet, files are fetched from a GitHub repository and copied to the internal memory of Pico. These new files will overwrite earlier files on Pico. The updates can be scheduled to run at a definite time, on boot, or user command.

For demonstration, we shall first save a MicroPython script to Pico that will blink its onboard LED at 1 Hertz (500ms ON, then 500ms OFF). After the OTA firmware update process completes, the LED will start blinking 5 times faster at 5 Hertz.

GitHub Setup

Creating a GitHub repository allows you to manage your code in a powerful way. You can easily track changes or collaborate with others. The repository can also be used for version control and easy rollback if needed.

Create a GitHub Repository

Create a GitHub repository to host the code to be downloaded during an OTA update process.

To create a new repository on GitHub:

  • Go to github.com and sign in to your account.
  • Locate the “+” icon in the top right corner of the page and click it.
  • Select “New repository” from the dropdown menu
  • Fill in repository details:
    • Repository name: Choose a unique name. I set mine as “pico-ota-micropython”.
    • Description: Optional, but helpful.
    • Choose Public or Private visibility. I set mine as Public for this tutorial.
    • You can set .gitignore and License as None for now.
  • Finally, click the “Create repository” button.

Add Files to the GitHub Repository

  • After you’ve created a new repository, click on creating a new file to add a new file to the repository. Alternatively, you can also navigate to Add file > Create new file on the repository page.
  • You can add multiple files and folders to this repository, which shall be downloaded to Raspberry Pi Pico during an OTA update. Here, I will create a single file for demonstration.
  • Copy and paste the following code into the new file you created.
import ugit
from machine import Pin
import time

pin = Pin(0,Pin.IN,Pin.PULL_UP)
if pin.value() is 0:
    ugit.pull_all()
    
#main code here
TIME_MS=100
LED = Pin("LED", Pin.OUT)
while True:
    LED.off()
    time.sleep_ms(TIME_MS)
    LED.on()
    time.sleep_ms(TIME_MS)
Code language: PHP (php)

This MicroPython script will blink the onboard LED on the Raspberry Pi Pico at 5 Hz (5 times per second). The code will be explained later in this article.

  • Save the code as main.py and click on Commit changes.
  • Select a suitable commit message and click on Commit changes.

MicroPython OTA Update Library

To download firmware wirelessly to Raspberry Pi Pico, I found the ugit library on GitHub to be helpful. But I have made some changes to this code for robust WiFi connectivity and added comments to improve readability.

Here is the code I used that you need to copy over to Raspberry Pi Pico:

import os
import urequests
import json
import hashlib
import binascii
import machine
import time
import network
from machine import Pin

# Global variable to hold the internal file structure of the device
global internal_tree

# Wi-Fi credentials (change as needed)
ssid = "YOUR_SSID"
password = "YOUR_PASSWORD"

# --- GitHub Repository Configuration ---
# Set your GitHub username and repository name here
# Repository must be public unless a personal access token is provided
user = 'YOUR_GITHUB_USERNAME'
repository = 'YOUR_REPO_NAME'
token = ''
# Default branch to be used (e.g., 'main' or 'master')
default_branch = 'main'
# Files to exclude from update or deletion
# Do not remove 'ugit.py' from this list unless you know what you are doing
ignore_files = ['/ugit.py', '/init.py']
ignore = ignore_files

# --- Static URLs for GitHub API and Raw Content Access ---
giturl = f'https://github.com/{user}/{repository}'
call_trees_url = f'https://api.github.com/repos/{user}/{repository}/git/trees/{default_branch}?recursive=1'
raw = f'https://raw.githubusercontent.com/{user}/{repository}/master/'

led = Pin("LED", Pin.OUT)

def pull(f_path, raw_url):
    """
    Downloads and saves a file from GitHub to the local file system.
    """
    print(f'Pulling {f_path} from GitHub')
    headers = {'User-Agent': 'electrocredible-ota-pico'}
    if len(token) > 0:
        headers['authorization'] = f"bearer {token}"
    r = urequests.get(raw_url, headers=headers)
    try:
        new_file = open(f_path, 'w')
        new_file.write(r.content.decode('utf-8'))
        new_file.close()
    except:
        print('Decode failed. Consider adding non-code files to ignore list.')
        try:
            new_file.close()
        except:
            print('Attempted to close file to save memory during raw file decode.')

def pull_all(tree=call_trees_url, raw=raw, ignore=ignore, isconnected=False):
    """
    Pulls all files from the GitHub repository and syncs them with the device.
    Deletes local files not found in the GitHub repository.
    """
    if not isconnected:
        wlan = wificonnect()
    os.chdir('/')
    tree = pull_git_tree()
    internal_tree = build_internal_tree()
    internal_tree = remove_ignore(internal_tree)
    print('Ignore list applied. Updated file list:')
    print(internal_tree)
    log = []
    for i in tree['tree']:
        if i['type'] == 'tree':
            try:
                os.mkdir(i['path'])
            except:
                print(f'Failed to create directory {i["path"]}, it may already exist.')
        elif i['path'] not in ignore:
            try:
                os.remove(i['path'])
                log.append(f'{i["path"]} removed from internal memory')
                internal_tree = remove_item(i['path'], internal_tree)
            except:
                log.append(f'{i["path"]} delete failed from internal memory')
                print('Failed to delete existing file')
            try:
                pull(i['path'], raw + i['path'])
                log.append(f'{i["path"]} updated')
            except:
                log.append(f'{i["path"]} failed to pull')
    if len(internal_tree) > 0:
        print('Leftover files not in GitHub tree:')
        print(internal_tree)
        for i in internal_tree:
            os.remove(i)
            log.append(f'{i} removed from internal memory')
    logfile = open('ugit_log.py', 'w')
    logfile.write(str(log))
    logfile.close()  
    print('Resetting device in 5 seconds...')
    time.sleep(5)
    machine.reset()

def wificonnect(ssid=ssid, password=password):
    """
    Connects to a Wi-Fi network using provided SSID and password.
    """
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    wlan.connect(ssid, password)
    # Wait for connection to establish
    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)
    # Manage connection errors
    if wlan.status() != 3:
        print('Network Connection has failed')
    else:
        print('connected')
        status = wlan.ifconfig()
        print( 'ip = ' + status[0] )
        #blink led 3 times after WiFi connected successfully
        for _ in range(3):
            led.on()
            time.sleep(0.1)     
            led.off()
            time.sleep(0.1) 
    return wlan

def build_internal_tree():
    """
    Recursively builds a list of all files on the device and their SHA-1 hashes.
    """
    global internal_tree
    internal_tree = []
    os.chdir('/')
    for i in os.listdir():
        add_to_tree(i)
    return internal_tree

def add_to_tree(dir_item):
    """
    Helper function to recursively add files and directories to the internal tree.
    """
    global internal_tree
    if is_directory(dir_item) and len(os.listdir(dir_item)) >= 1:
        os.chdir(dir_item)
        for i in os.listdir():
            add_to_tree(i)
        os.chdir('..')
    else:
        if os.getcwd() != '/':
            subfile_path = os.getcwd() + '/' + dir_item
        else:
            subfile_path = os.getcwd() + dir_item
        try:
            internal_tree.append([subfile_path, get_hash(subfile_path)])
        except OSError:
            print(f'{dir_item} could not be added to tree')

def get_hash(file):
    """
    Computes SHA-1 hash of a given file.
    """
    o_file = open(file)
    r_file = o_file.read()
    sha1obj = hashlib.sha1(r_file)
    return binascii.hexlify(sha1obj.digest())

def get_data_hash(data):
    """
    Computes SHA-1 hash of given string data.
    """
    sha1obj = hashlib.sha1(data)
    return binascii.hexlify(sha1obj.digest())

def is_directory(file):
    """
    Checks if a given path is a directory.
    """
    try:
        return os.stat(file)[8] == 0
    except:
        return False

def pull_git_tree(tree_url=call_trees_url, raw=raw):
    """
    Fetches the Git tree structure from the GitHub API.
    """
    headers = {'User-Agent': 'electrocredible-ota-pico'}
    if len(token) > 0:
        headers['authorization'] = f"bearer {token}"
    r = urequests.get(tree_url, headers=headers)
    data = json.loads(r.content.decode('utf-8'))
    if 'tree' not in data:
        raise Exception(f'Default branch "{default_branch}" not found.')
    return data

def parse_git_tree():
    """
    Prints the directories and files in the GitHub tree.
    """
    tree = pull_git_tree()
    dirs = []
    files = []
    for i in tree['tree']:
        if i['type'] == 'tree':
            dirs.append(i['path'])
        elif i['type'] == 'blob':
            files.append([i['path'], i['sha'], i['mode']])
    print('Directories:', dirs)
    print('Files:', files)

def check_ignore(tree=call_trees_url, raw=raw, ignore=ignore):
    """
    Displays which files in the GitHub tree are ignored locally.
    """
    os.chdir('/')
    tree = pull_git_tree()
    for i in tree['tree']:
        if i['path'] not in ignore:
            print(f'{i["path"]} not in ignore')
        else:
            print(f'{i["path"]} is in ignore')

def remove_ignore(internal_tree, ignore=ignore):
    """
    Removes ignored files from the internal file list.
    """
    clean_tree = []
    int_tree = [i[0] for i in internal_tree]
    for i in int_tree:
        if i not in ignore:
            clean_tree.append(i)
    return clean_tree

def remove_item(item, tree):
    """
    Removes a specific item from a list of tracked files.
    """
    return [i for i in tree if item not in i]

def backup():
    """
    Creates a backup of the current internal files and their hashes.
    """
    int_tree = build_internal_tree()
    backup_text = "ugit Backup Version 1.0\n\n"
    for i in int_tree:
        data = open(i[0], 'r')
        backup_text += f'FN:SHA1{i[0]},{i[1]}\n'
        backup_text += '---' + data.read() + '---\n'
        data.close()
    backup = open('ugit.backup', 'w')
    backup.write(backup_text)
    backup.close()
Code language: PHP (php)

You need to make some changes before saving this code.

Replace ssid = "YOUR_SSID" and password = "YOUR_PASSWORD" with your actual Wi-Fi name and password, like ssid = "MyWiFi" and password = "abc12345". Keep the quotes. Without this change, your device won’t connect to the network.

ssid = "YOUR_SSID"
password = "YOUR_PASSWORD"Code language: JavaScript (javascript)

Replace user ='YOUR_GITHUB_USERNAME'with your actual GitHub username, repository ='YOUR_REPO_NAME' with your repository name, and token = '' with your personal access token if your repository is private. Leave the token empty if the repo is public. Keep the quotes while replacing text.

user = 'YOUT_GITHUB_USERNAME'
repository = 'YOUR_REPO_NAME'
token = ''Code language: JavaScript (javascript)

In the following line, mention the filenames that you don’t want to be deleted from Pico after an OTA update. The ugit.py file must be compulsorily included. Additional files can be specified in a list of file paths. For example, I have an init.py that I want to keep after OTA firmware update.

ignore_files = ['/ugit.py', '/init.py']Code language: JavaScript (javascript)

All other files will be deleted after an update if you do not include them in this list of file paths.

After making the necessary changes, copy and save the code to Raspberry Pi Pico as ugit.py.

If you are unfamiliar with saving files to Raspberry Pi Pico, here are the steps using the Thonny IDE:

  • Connect Pico to your computer using a USB cable. Open Thonny IDE and set the interpreter to use MicroPython on Raspberry Pi Pico.
Thonny IDE select interpreter as MicroPython Raspberry Pi Pico
  • Go to File>New in Thonny IDE to create a new project. 
  • Paste your code into the blank space.
  • Click on File>Save as and select the save location as Raspberry Pi Pico.
Thonny Save to
  • Name the code file as ugit.py and click OK to save it.

Also read: Raspberry Pi Pico Data Logger using its Flash Memory (MicroPython Code)

Backup Files

You can skip this part if you don’t have any important files in Raspberry Pi Pico. To back up files before an update, include the following code in your main.py code or boot.py code before calling ugit.pull_all() code:

ugit.backup()Code language: CSS (css)

OTA Update on Boot

A basic script that will backup existing files and then update Raspberry Pi Pico over OTA:

import ugit

ugit.backup()

ugit.pull_all()Code language: CSS (css)

If you want the OTA update to occur automatically at power up, save this code file as boot.py on Raspberry Pi Pico. The boot.py file runs first when Raspberry Pi Pico powers up. Pico will backup existing files and download OTA updates each time it is powered up.

OTA Update using REPL

Using the MicroPython REPL, you can enter the following lines one after the other to instantly download OTA updates:

import ugit
ugit.pull_all()Code language: JavaScript (javascript)

Here is a screenshot of the shell in Thonny IDE that shows the output messages while updating OTA:

OTA Update Raspberry Pi Pico using Pushbutton

In this method, Pico will download OTA updates when the user connects a specific GPIO pin to the ground, typically using a pushbutton. Here is how it works:

  • Each time Raspberry Pi Pico resets or boots up, the main MicroPython script will check if a pushbutton is pressed by the user.
  • If yes, the script begins the update sequence. All files in the GitHub repository that you created earlier will be downloaded to Pico.
  • Raspberry Pi Pico will reset automatically when the update process is complete.

Connect a pushbutton between GPIO 0 and GND in Raspberry Pi Pico as shown in the schematic below.

Raspberry Pi Pico W pushbutton wiring

You can also select any other GPIO pin as an input. Read – Raspberry Pi Pico & Pico W Pinout Guide – All Pins Explained. Change the code accordingly if your GPIO pin differs. The pushbutton is wired such that pressing it will pull the GPIO 0 pin to the ground.

Now, upload the following main script to Raspberry Pi Pico:

import ugit
from machine import Pin
import time

pin = Pin(0,Pin.IN,Pin.PULL_UP)
if pin.value() is 0:
    ugit.pull_all() 

#main code here
TIME_MS=500
LED = Pin("LED", Pin.OUT)
while True:
    LED.off()
    time.sleep_ms(TIME_MS)
    LED.on()
    time.sleep_ms(TIME_MS)Code language: PHP (php)

Save the script as main.py to Raspberry Pi Pico. Code file saved to Pico as main.py runs after boot automatically. The main code consists of a function that downloads OTA updates as well as tasks that should run on Pico.

You can also include a boot.py file if you wish to separate the OTA update code from the rest of the program.

Main Code Explanation

Import the ugit library to update/download code from GitHub, the Pin class from the machine module to control GPIO pins, and the time module to use delays.

import ugit
from machine import Pin
import timeCode language: JavaScript (javascript)

Set up a GPIO 0 pin as an input with an internal pull-up resistor enabled.

pin = Pin(0, Pin.IN, Pin.PULL_UP)

Check if the pin is connected to ground. If yes, then pull code from GitHub using ugit.pull_all().

if pin.value() is 0:
    ugit.pull_all()Code language: CSS (css)

After this, write the main function that your microcontroller must perform. Here, I have used an LED blinking script for demonstration.

A constant called TIME_MS is set to 500 milliseconds for the LED blink timing.

TIME_MS=500

Set up the built-in LED as an output pin.

LED = Pin("LED", Pin.OUT)Code language: JavaScript (javascript)

Start an infinite loop that will keep blinking the onboard LED with a delay of 500ms between ON and OFF states.

while True:
    LED.off()
    time.sleep_ms(TIME_MS)
    LED.on()
    time.sleep_ms(TIME_MS)Code language: CSS (css)

Demonstration

After you upload the main code explained above, run the code or reset Pico once. Your Raspberry Pi Pico’s onboard LED should start blinking at 1 Hz, i.e., it stays ON for 500ms followed by 500ms OFF.

Raspberry Pi Pico W onboard LED blinking

Checklist before OTA update:

  • ugit.py and main.py code should be stored in Pico.
  • Another main.py file is saved to a GitHub repository that differs from the main code running on Pico.
  • You have backed up your code if required, and you have edited the ugit.py code to exclude certain files from deletion.
  • You have wired the tactile pushbutton according to schematic.

Now, to perform an OTA update:

  • Remove power from the Raspberry Pi Pico
  • While powering it back up, hold the pushbutton connected to the GPIO 0 pin for a couple of seconds and then release it. Wait for a few seconds for the update to complete.

OTA update process will download the files stored in the GitHub repository and write them to the flash memory of Pico. Previous files in Pico will be deleted or overwritten, unless they are listed as exclusions.

When the update process is finished, the onboard LED in Pico will start blinking faster. The only difference between the main code saved to Pico and the one saved on GitHub is the delay time for the blinking LED. The constant TIME_MS is set to 500 milliseconds in the code saved to Pico, while it is 200 milliseconds in the code saved in GitHub.

Also read: How to Reset Raspberry Pi Pico – 4 Easy Ways

Summary

This guide explained how we can perform MicroPython OTA updates on Raspberry Pi Pico. Here are the few steps summarized:

  • Create a GitHub repository and store code and files you wish to download over OTA.
  • Upload the OTA update library to Pico
  • Update Pico using the REPL, the boot.py file, or by including an update routine in the main code.

You can further enhance this project by including functions for version control. For scheduling OTA updates at regular intervals, you can interface an external RTC (Real Time Clock) with Pico. This article might be helpful: DS3231 with Raspberry Pi Pico Guide- Arduino & MicroPython Code

Hope you found this guide useful. Please leave your queries and thoughts in the comments section below.


Posted

in

by

Comments

Leave a Reply

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