├── .gitignore ├── test_ota.py ├── .github └── workflows │ └── python-publish.yml ├── README.md └── ota.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .venv 3 | __pycache__ 4 | *.pyc 5 | WIFI_CONFIG.py -------------------------------------------------------------------------------- /test_ota.py: -------------------------------------------------------------------------------- 1 | from ota import OTAUpdater 2 | from WIFI_CONFIG import SSID, PASSWORD 3 | 4 | firmware_url = "https://github.com/kevinmcaleer/ota_test/main/" 5 | 6 | ota_updater = OTAUpdater(SSID, PASSWORD, firmware_url, "test_ota.py") 7 | 8 | ota_updater.download_and_install_update_if_available() 9 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MicroPython Over-the-Air updater 2 | 3 | This library enables you to update your MicroPython projects over the air, at start-up, or whenever you choose. 4 | 5 | --- 6 | 7 | To use this code: 8 | 9 | 1. Add the `ota.py` to your MicroPython device 10 | 11 | 1. Create a file named `WIFI_CONFIG.py` on your MicroPython device, which contains two variables: `SSID` and `PASSWORD`: 12 | 13 | ```python 14 | SSID = "my wifi hotspot name" 15 | PASSWORD = "wifi password" 16 | ``` 17 | 18 | 1. Add this to your main program code: 19 | 20 | ```python 21 | from ota import OTAUpdater 22 | from WIFI_CONFIG import SSID, PASSWORD 23 | 24 | firmware_url = "https://raw.githubusercontent.com///" 25 | 26 | ``` 27 | 28 | where `` is your github username, `` is the name of the repository to check for updates and `` is the name of the branch to monitor. 29 | 30 | 1. Add this to your main program code: 31 | 32 | ```python 33 | ota_updater = OTAUpdater(SSID, PASSWORD, firmware_url, "test.py") 34 | ota_updater.download_and_install_update_if_available() 35 | 36 | ``` 37 | 1. On your GitHub repository, add a `version.json` file, and add a `version` element to the JSON file, with a version number: 38 | 39 | ```json 40 | { 41 | "version":3 42 | } 43 | ``` 44 | 45 | --- 46 | 47 | The `OTAUpdater` will connect to github over wifi using your provided wifi credentials, check what the most up-to-date version of the firmware is, compare this to a local file present on the device named `version.json`, which contains the version number of the current on device firmware. 48 | 49 | If the local file is not present it will create one with a version number of `0`. If the Github version is newer, it will download the latest file and overwrite the file on the device with the same name, then restart the MicroPython board. 50 | 51 | --- 52 | 53 | If you find this useful, please let me know on our discord server: 54 | -------------------------------------------------------------------------------- /ota.py: -------------------------------------------------------------------------------- 1 | import network 2 | import urequests 3 | import os 4 | import json 5 | import machine 6 | from time import sleep 7 | 8 | class OTAUpdater: 9 | """ This class handles OTA updates. It connects to the Wi-Fi, checks for updates, downloads and installs them.""" 10 | def __init__(self, ssid, password, repo_url, filename): 11 | self.filename = filename 12 | self.ssid = ssid 13 | self.password = password 14 | self.repo_url = repo_url 15 | if "www.github.com" in self.repo_url : 16 | print(f"Updating {repo_url} to raw.githubusercontent") 17 | self.repo_url = self.repo_url.replace("www.github","raw.githubusercontent") 18 | elif "github.com" in self.repo_url: 19 | print(f"Updating {repo_url} to raw.githubusercontent'") 20 | self.repo_url = self.repo_url.replace("github","raw.githubusercontent") 21 | self.version_url = self.repo_url + 'main/version.json' 22 | print(f"version url is: {self.version_url}") 23 | self.firmware_url = self.repo_url + 'main/' + filename 24 | 25 | # get the current version (stored in version.json) 26 | if 'version.json' in os.listdir(): 27 | with open('version.json') as f: 28 | self.current_version = int(json.load(f)['version']) 29 | print(f"Current device firmware version is '{self.current_version}'") 30 | 31 | else: 32 | self.current_version = 0 33 | # save the current version 34 | with open('version.json', 'w') as f: 35 | json.dump({'version': self.current_version}, f) 36 | 37 | def connect_wifi(self): 38 | """ Connect to Wi-Fi.""" 39 | 40 | sta_if = network.WLAN(network.STA_IF) 41 | sta_if.active(True) 42 | sta_if.connect(self.ssid, self.password) 43 | while not sta_if.isconnected(): 44 | print('.', end="") 45 | sleep(0.25) 46 | print(f'Connected to WiFi, IP is: {sta_if.ifconfig()[0]}') 47 | 48 | def fetch_latest_code(self)->bool: 49 | """ Fetch the latest code from the repo, returns False if not found.""" 50 | 51 | # Fetch the latest code from the repo. 52 | response = urequests.get(self.firmware_url) 53 | if response.status_code == 200: 54 | print(f'Fetched latest firmware code, status: {response.status_code}, - {response.text}') 55 | 56 | # Save the fetched code to memory 57 | self.latest_code = response.text 58 | return True 59 | 60 | elif response.status_code == 404: 61 | print(f'Firmware not found - {self.firmware_url}.') 62 | return False 63 | 64 | def update_no_reset(self): 65 | """ Update the code without resetting the device.""" 66 | 67 | # Save the fetched code and update the version file to latest version. 68 | with open('latest_code.py', 'w') as f: 69 | f.write(self.latest_code) 70 | 71 | # update the version in memory 72 | self.current_version = self.latest_version 73 | 74 | # save the current version 75 | with open('version.json', 'w') as f: 76 | json.dump({'version': self.current_version}, f) 77 | 78 | # free up some memory 79 | self.latest_code = None 80 | 81 | # Overwrite the old code. 82 | # os.rename('latest_code.py', self.filename) 83 | 84 | def update_and_reset(self): 85 | """ Update the code and reset the device.""" 86 | 87 | print(f"Updating device... (Renaming latest_code.py to {self.filename})", end="") 88 | 89 | # Overwrite the old code. 90 | os.rename('latest_code.py', self.filename) 91 | 92 | # Restart the device to run the new code. 93 | print('Restarting device...') 94 | machine.reset() # Reset the device to run the new code. 95 | 96 | def check_for_updates(self): 97 | """ Check if updates are available.""" 98 | 99 | # Connect to Wi-Fi 100 | self.connect_wifi() 101 | 102 | print(f'Checking for latest version... on {self.version_url}') 103 | response = urequests.get(self.version_url) 104 | 105 | data = json.loads(response.text) 106 | 107 | print(f"data is: {data}, url is: {self.version_url}") 108 | print(f"data version is: {data['version']}") 109 | # Turn list to dict using dictionary comprehension 110 | # my_dict = {data[i]: data[i + 1] for i in range(0, len(data), 2)} 111 | 112 | self.latest_version = int(data['version']) 113 | print(f'latest version is: {self.latest_version}') 114 | 115 | # compare versions 116 | newer_version_available = True if self.current_version < self.latest_version else False 117 | 118 | print(f'Newer version available: {newer_version_available}') 119 | return newer_version_available 120 | 121 | def download_and_install_update_if_available(self): 122 | """ Check for updates, download and install them.""" 123 | if self.check_for_updates(): 124 | if self.fetch_latest_code(): 125 | self.update_no_reset() 126 | self.update_and_reset() 127 | else: 128 | print('No new updates available.') 129 | --------------------------------------------------------------------------------