├── .gitignore ├── LICENSE.md ├── README.md ├── __init__.py ├── app ├── __init__.py ├── httpclient.py ├── ota_updater.py └── start.py ├── main.py └── pymakr.conf /.gitignore: -------------------------------------------------------------------------------- 1 | secrets.py 2 | pymakr.conf -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Joshua Bellamy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | The MIT License (MIT) 23 | 24 | Copyright (c) 2013, 2014 micropython-lib contributors 25 | 26 | Permission is hereby granted, free of charge, to any person obtaining a copy 27 | of this software and associated documentation files (the "Software"), to deal 28 | in the Software without restriction, including without limitation the rights 29 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 30 | copies of the Software, and to permit persons to whom the Software is 31 | furnished to do so, subject to the following conditions: 32 | 33 | The above copyright notice and this permission notice shall be included in 34 | all copies or substantial portions of the Software. 35 | 36 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 37 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 38 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 39 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 40 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 41 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 42 | THE SOFTWARE. 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MicroPython OTA Updater 2 | 3 | This micropython module allows for automatic updating of your code on Microcontrollers using github releases. It allows you to update devices in the field with ease. 4 | 5 | > Note: due to a bug in the SSL library of ESP8266 devices, micropython-ota-updater cannot be used on these devices. See https://github.com/rdehuyss/micropython-ota-updater/issues/6 and https://github.com/micropython/micropython/issues/6737 6 | 7 | ## History 8 | - 2018/07/19 - First public release 9 | - 2020/12/23 - Major rewrite adding support for M5Stack and low memory devices (I now can upgrade big projects with it on devices like M5Stack Core 1 which are very memory constraint) and it now also supports secrets files (which are kept during upgrades) 10 | 11 | 12 | ## Workflow 13 | The workflow is as follows: 14 | * You have a github repo where you host your micropython project 15 | * In this project, you include all your source code in a certain folder (e.g. `app`) 16 | * You also include the ota_updater.py (https://github.com/rdehuyss/micropython-ota-updater) 17 | * You control your releases with GitHub releases (if you want to deploy a new version, create a new GitHub release) 18 | 19 | There are now two different ways to update your code: 20 | 21 | ### Install new version immediately after boot 22 | You can choose to install a new version yourself. Note that due to memory limitations, this must happen first thing after boot. 23 | 24 | To do so, make sure you have an active internet connection and use the following code at startup: 25 | ```python 26 | @staticmethod 27 | def _otaUpdate(): 28 | ulogging.info('Checking for Updates...') 29 | from .ota_updater import OTAUpdater 30 | otaUpdater = OTAUpdater('https://github.com/rdehuyss/chicken-shed-mgr', github_src_dir='src', main_dir='app', secrets_file="secrets.py") 31 | otaUpdater.install_update_if_available() 32 | del(otaUpdater) 33 | ``` 34 | 35 | > Do not forget to do a machine.reset() after the code above. 36 | 37 | 38 | ### Let the OTAUpdater decide when to do the update 39 | * whenever you feel fit, you ask the OTAUpdater (on my project this is after a hardware interrupt which starts up the WLAN) to check for a new version using `ota_updater.check_for_update_to_install_during_next_reboot()` 40 | * if a new version is present, the OTAUpdater generate a `next` folder and within that folder a file called `.version_on_reboot`. After that, you do a `machine.reset()` to kill the WIFI connection. 41 | * You use the following code in your `main.py`: 42 | ```python 43 | from ota_update.main.ota_updater import OTAUpdater 44 | 45 | 46 | def download_and_install_update_if_available(): 47 | o = OTAUpdater('url-to-your-github-project') 48 | o.install_update_if_available_after_boot('wifi-ssid', 'wifi-password') 49 | 50 | 51 | def start(): 52 | # your custom code goes here. Something like this: ... 53 | # from main.x import YourProject 54 | # project = YourProject() 55 | # ... 56 | 57 | 58 | def boot(): 59 | download_and_install_update_if_available() 60 | start() 61 | 62 | 63 | boot() 64 | ``` 65 | * the OTAUpdater will check if there is a file called `next/.version_on_reboot`. 66 | * If so, it will initialize the WIFI connection, download the latest code, move it to the `app` folder. You then need to do a `machine.reset()`. On reboot, the latest code will be in the `app` folder and you will be running the latest version. 67 | * If not, it will NOT initialize the WIFI connection and just start the existing code in the `app` folder 68 | 69 | ## Extra features 70 | ### Support for Private Repositories 71 | This module also adds support for private repositories. To do so, use it as follows: 72 | 73 | ```python 74 | token='YOUR_GITHUB_TOKEN' 75 | updater = OTAUpdater('https://github.com/sergiuszm/cae_fipy', headers={'Authorization': 'token {}'.format(token)}) 76 | ``` 77 | 78 | ### Support for secrets file 79 | MicroPython OTA updater now also supports a secret file (which is added to .gitignore). This secrets file must be installed initially (e.g. using USB) and will always be kept when downloading newer versions. In my case, it contains the WIFI credentials and other secret stuff. A secrets file can be used as follows: 80 | 81 | ```python 82 | WIFI_SSID='your-ssid' 83 | WIFI_PASSWORD='your-password' 84 | ``` 85 | See [main.py](https://github.com/rdehuyss/micropython-ota-updater/blob/master/main.py) on how it is then used. 86 | 87 | 88 | ## More info? 89 | See the [article on Medium](https://medium.com/@ronald.dehuysser/micropython-ota-updates-and-github-a-match-made-in-heaven-45fde670d4eb). 90 | 91 | ## Example 92 | - [Showerloop](https://github.com/rdehuyss/showerloop/blob/master/main.py) uses the micropython-ota-updater to get the latest release. 93 | - [Chicken Shed Mgr](https://github.com/rdehuyss/chicken-shed-mgr/blob/main/src/main.py) uses the micropython-ota-updater to get the latest release. 94 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdehuyss/micropython-ota-updater/a512e8bf8fb0ad5a107098c07a61feb60b5f11dc/__init__.py -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdehuyss/micropython-ota-updater/a512e8bf8fb0ad5a107098c07a61feb60b5f11dc/app/__init__.py -------------------------------------------------------------------------------- /app/httpclient.py: -------------------------------------------------------------------------------- 1 | import usocket, os, gc 2 | class Response: 3 | 4 | def __init__(self, socket, saveToFile=None): 5 | self._socket = socket 6 | self._saveToFile = saveToFile 7 | self._encoding = 'utf-8' 8 | if saveToFile is not None: 9 | CHUNK_SIZE = 512 # bytes 10 | with open(saveToFile, 'w') as outfile: 11 | data = self._socket.read(CHUNK_SIZE) 12 | while data: 13 | outfile.write(data) 14 | data = self._socket.read(CHUNK_SIZE) 15 | outfile.close() 16 | 17 | self.close() 18 | 19 | def close(self): 20 | if self._socket: 21 | self._socket.close() 22 | self._socket = None 23 | 24 | @property 25 | def content(self): 26 | if self._saveToFile is not None: 27 | raise SystemError('You cannot get the content from the response as you decided to save it in {}'.format(self._saveToFile)) 28 | 29 | try: 30 | result = self._socket.read() 31 | return result 32 | finally: 33 | self.close() 34 | 35 | @property 36 | def text(self): 37 | return str(self.content, self._encoding) 38 | 39 | def json(self): 40 | try: 41 | import ujson 42 | result = ujson.load(self._socket) 43 | return result 44 | finally: 45 | self.close() 46 | 47 | 48 | class HttpClient: 49 | 50 | def __init__(self, headers={}): 51 | self._headers = headers 52 | 53 | def is_chunked_data(data): 54 | return getattr(data, "__iter__", None) and not getattr(data, "__len__", None) 55 | 56 | def request(self, method, url, data=None, json=None, file=None, custom=None, saveToFile=None, headers={}, stream=None): 57 | chunked = data and self.is_chunked_data(data) 58 | redirect = None #redirection url, None means no redirection 59 | def _write_headers(sock, _headers): 60 | for k in _headers: 61 | sock.write(b'{}: {}\r\n'.format(k, _headers[k])) 62 | 63 | try: 64 | proto, dummy, host, path = url.split('/', 3) 65 | except ValueError: 66 | proto, dummy, host = url.split('/', 2) 67 | path = '' 68 | if proto == 'http:': 69 | port = 80 70 | elif proto == 'https:': 71 | import ussl 72 | port = 443 73 | else: 74 | raise ValueError('Unsupported protocol: ' + proto) 75 | 76 | if ':' in host: 77 | host, port = host.split(':', 1) 78 | port = int(port) 79 | 80 | ai = usocket.getaddrinfo(host, port, 0, usocket.SOCK_STREAM) 81 | if len(ai) < 1: 82 | raise ValueError('You are not connected to the internet...') 83 | ai = ai[0] 84 | 85 | s = usocket.socket(ai[0], ai[1], ai[2]) 86 | try: 87 | s.connect(ai[-1]) 88 | if proto == 'https:': 89 | gc.collect() 90 | s = ussl.wrap_socket(s, server_hostname=host) 91 | s.write(b'%s /%s HTTP/1.0\r\n' % (method, path)) 92 | if not 'Host' in headers: 93 | s.write(b'Host: %s\r\n' % host) 94 | # Iterate over keys to avoid tuple alloc 95 | _write_headers(s, self._headers) 96 | _write_headers(s, headers) 97 | 98 | # add user agent 99 | s.write(b'User-Agent: MicroPython Client\r\n') 100 | if json is not None: 101 | assert data is None 102 | import ujson 103 | data = ujson.dumps(json) 104 | s.write(b'Content-Type: application/json\r\n') 105 | 106 | if data: 107 | if chunked: 108 | s.write(b"Transfer-Encoding: chunked\r\n") 109 | else: 110 | s.write(b"Content-Length: %d\r\n" % len(data)) 111 | s.write(b"\r\n") 112 | if data: 113 | if chunked: 114 | for chunk in data: 115 | s.write(b"%x\r\n" % len(chunk)) 116 | s.write(chunk) 117 | s.write(b"\r\n") 118 | s.write("0\r\n\r\n") 119 | else: 120 | s.write(data) 121 | elif file: 122 | s.write(b'Content-Length: %d\r\n' % os.stat(file)[6]) 123 | s.write(b'\r\n') 124 | with open(file, 'r') as file_object: 125 | for line in file_object: 126 | s.write(line + '\n') 127 | elif custom: 128 | custom(s) 129 | else: 130 | s.write(b'\r\n') 131 | 132 | l = s.readline() 133 | #print('l: ', l) 134 | l = l.split(None, 2) 135 | status = int(l[1]) 136 | reason = '' 137 | if len(l) > 2: 138 | reason = l[2].rstrip() 139 | while True: 140 | l = s.readline() 141 | if not l or l == b'\r\n': 142 | break 143 | #print('l: ', l) 144 | if l.startswith(b'Transfer-Encoding:'): 145 | if b'chunked' in l: 146 | raise ValueError('Unsupported ' + l) 147 | elif l.startswith(b'Location:') and not 200 <= status <= 299: 148 | if status in [301, 302, 303, 307, 308]: 149 | redirect = l[10:-2].decode() 150 | else: 151 | raise NotImplementedError("Redirect {} not yet supported".format(status)) 152 | except OSError: 153 | s.close() 154 | raise 155 | 156 | if redirect: 157 | s.close() 158 | if status in [301, 302, 303]: 159 | return self.request('GET', url=redirect, **kw) 160 | else: 161 | return self.request(method, redirect, **kw) 162 | else: 163 | resp = Response(s,saveToFile) 164 | resp.status_code = status 165 | resp.reason = reason 166 | return resp 167 | 168 | def head(self, url, **kw): 169 | return self.request('HEAD', url, **kw) 170 | 171 | def get(self, url, **kw): 172 | return self.request('GET', url, **kw) 173 | 174 | def post(self, url, **kw): 175 | return self.request('POST', url, **kw) 176 | 177 | def put(self, url, **kw): 178 | return self.request('PUT', url, **kw) 179 | 180 | def patch(self, url, **kw): 181 | return self.request('PATCH', url, **kw) 182 | 183 | def delete(self, url, **kw): 184 | return self.request('DELETE', url, **kw) -------------------------------------------------------------------------------- /app/ota_updater.py: -------------------------------------------------------------------------------- 1 | import os, gc 2 | from .httpclient import HttpClient 3 | 4 | class OTAUpdater: 5 | """ 6 | A class to update your MicroController with the latest version from a GitHub tagged release, 7 | optimized for low power usage. 8 | """ 9 | 10 | def __init__(self, github_repo, github_src_dir='', module='', main_dir='main', new_version_dir='next', secrets_file=None, headers={}): 11 | self.http_client = HttpClient(headers=headers) 12 | self.github_repo = github_repo.rstrip('/').replace('https://github.com/', '') 13 | self.github_src_dir = '' if len(github_src_dir) < 1 else github_src_dir.rstrip('/') + '/' 14 | self.module = module.rstrip('/') 15 | self.main_dir = main_dir 16 | self.new_version_dir = new_version_dir 17 | self.secrets_file = secrets_file 18 | 19 | def __del__(self): 20 | self.http_client = None 21 | 22 | def check_for_update_to_install_during_next_reboot(self) -> bool: 23 | """Function which will check the GitHub repo if there is a newer version available. 24 | 25 | This method expects an active internet connection and will compare the current 26 | version with the latest version available on GitHub. 27 | If a newer version is available, the file 'next/.version' will be created 28 | and you need to call machine.reset(). A reset is needed as the installation process 29 | takes up a lot of memory (mostly due to the http stack) 30 | 31 | Returns 32 | ------- 33 | bool: true if a new version is available, false otherwise 34 | """ 35 | 36 | (current_version, latest_version) = self._check_for_new_version() 37 | if latest_version > current_version: 38 | print('New version available, will download and install on next reboot') 39 | self._create_new_version_file(latest_version) 40 | return True 41 | 42 | return False 43 | 44 | def install_update_if_available_after_boot(self, ssid, password) -> bool: 45 | """This method will install the latest version if out-of-date after boot. 46 | 47 | This method, which should be called first thing after booting, will check if the 48 | next/.version' file exists. 49 | 50 | - If yes, it initializes the WIFI connection, downloads the latest version and installs it 51 | - If no, the WIFI connection is not initialized as no new known version is available 52 | """ 53 | 54 | if self.new_version_dir in os.listdir(self.module): 55 | if '.version' in os.listdir(self.modulepath(self.new_version_dir)): 56 | latest_version = self.get_version(self.modulepath(self.new_version_dir), '.version') 57 | print('New update found: ', latest_version) 58 | OTAUpdater._using_network(ssid, password) 59 | self.install_update_if_available() 60 | return True 61 | 62 | print('No new updates found...') 63 | return False 64 | 65 | def install_update_if_available(self) -> bool: 66 | """This method will immediately install the latest version if out-of-date. 67 | 68 | This method expects an active internet connection and allows you to decide yourself 69 | if you want to install the latest version. It is necessary to run it directly after boot 70 | (for memory reasons) and you need to restart the microcontroller if a new version is found. 71 | 72 | Returns 73 | ------- 74 | bool: true if a new version is available, false otherwise 75 | """ 76 | 77 | (current_version, latest_version) = self._check_for_new_version() 78 | if latest_version > current_version: 79 | print('Updating to version {}...'.format(latest_version)) 80 | self._create_new_version_file(latest_version) 81 | self._download_new_version(latest_version) 82 | self._copy_secrets_file() 83 | self._delete_old_version() 84 | self._install_new_version() 85 | return True 86 | 87 | return False 88 | 89 | 90 | @staticmethod 91 | def _using_network(ssid, password): 92 | import network 93 | sta_if = network.WLAN(network.STA_IF) 94 | if not sta_if.isconnected(): 95 | print('connecting to network...') 96 | sta_if.active(True) 97 | sta_if.connect(ssid, password) 98 | while not sta_if.isconnected(): 99 | pass 100 | print('network config:', sta_if.ifconfig()) 101 | 102 | def _check_for_new_version(self): 103 | current_version = self.get_version(self.modulepath(self.main_dir)) 104 | latest_version = self.get_latest_version() 105 | 106 | print('Checking version... ') 107 | print('\tCurrent version: ', current_version) 108 | print('\tLatest version: ', latest_version) 109 | return (current_version, latest_version) 110 | 111 | def _create_new_version_file(self, latest_version): 112 | self.mkdir(self.modulepath(self.new_version_dir)) 113 | with open(self.modulepath(self.new_version_dir + '/.version'), 'w') as versionfile: 114 | versionfile.write(latest_version) 115 | versionfile.close() 116 | 117 | def get_version(self, directory, version_file_name='.version'): 118 | if version_file_name in os.listdir(directory): 119 | with open(directory + '/' + version_file_name) as f: 120 | version = f.read() 121 | return version 122 | return '0.0' 123 | 124 | def get_latest_version(self): 125 | latest_release = self.http_client.get('https://api.github.com/repos/{}/releases/latest'.format(self.github_repo)) 126 | gh_json = latest_release.json() 127 | try: 128 | version = gh_json['tag_name'] 129 | except KeyError as e: 130 | raise ValueError( 131 | "Release not found: \n", 132 | "Please ensure release as marked as 'latest', rather than pre-release \n", 133 | "github api message: \n {} \n ".format(gh_json) 134 | ) 135 | latest_release.close() 136 | return version 137 | 138 | def _download_new_version(self, version): 139 | print('Downloading version {}'.format(version)) 140 | self._download_all_files(version) 141 | print('Version {} downloaded to {}'.format(version, self.modulepath(self.new_version_dir))) 142 | 143 | def _download_all_files(self, version, sub_dir=''): 144 | url = 'https://api.github.com/repos/{}/contents{}{}{}?ref=refs/tags/{}'.format(self.github_repo, self.github_src_dir, self.main_dir, sub_dir, version) 145 | gc.collect() 146 | file_list = self.http_client.get(url) 147 | file_list_json = file_list.json() 148 | for file in file_list_json: 149 | path = self.modulepath(self.new_version_dir + '/' + file['path'].replace(self.main_dir + '/', '').replace(self.github_src_dir, '')) 150 | if file['type'] == 'file': 151 | gitPath = file['path'] 152 | print('\tDownloading: ', gitPath, 'to', path) 153 | self._download_file(version, gitPath, path) 154 | elif file['type'] == 'dir': 155 | print('Creating dir', path) 156 | self.mkdir(path) 157 | self._download_all_files(version, sub_dir + '/' + file['name']) 158 | gc.collect() 159 | 160 | file_list.close() 161 | 162 | def _download_file(self, version, gitPath, path): 163 | self.http_client.get('https://raw.githubusercontent.com/{}/{}/{}'.format(self.github_repo, version, gitPath), saveToFile=path) 164 | 165 | def _copy_secrets_file(self): 166 | if self.secrets_file: 167 | fromPath = self.modulepath(self.main_dir + '/' + self.secrets_file) 168 | toPath = self.modulepath(self.new_version_dir + '/' + self.secrets_file) 169 | print('Copying secrets file from {} to {}'.format(fromPath, toPath)) 170 | self._copy_file(fromPath, toPath) 171 | print('Copied secrets file from {} to {}'.format(fromPath, toPath)) 172 | 173 | def _delete_old_version(self): 174 | print('Deleting old version at {} ...'.format(self.modulepath(self.main_dir))) 175 | self._rmtree(self.modulepath(self.main_dir)) 176 | print('Deleted old version at {} ...'.format(self.modulepath(self.main_dir))) 177 | 178 | def _install_new_version(self): 179 | print('Installing new version at {} ...'.format(self.modulepath(self.main_dir))) 180 | if self._os_supports_rename(): 181 | os.rename(self.modulepath(self.new_version_dir), self.modulepath(self.main_dir)) 182 | else: 183 | self._copy_directory(self.modulepath(self.new_version_dir), self.modulepath(self.main_dir)) 184 | self._rmtree(self.modulepath(self.new_version_dir)) 185 | print('Update installed, please reboot now') 186 | 187 | def _rmtree(self, directory): 188 | for entry in os.ilistdir(directory): 189 | is_dir = entry[1] == 0x4000 190 | if is_dir: 191 | self._rmtree(directory + '/' + entry[0]) 192 | else: 193 | os.remove(directory + '/' + entry[0]) 194 | os.rmdir(directory) 195 | 196 | def _os_supports_rename(self) -> bool: 197 | self._mk_dirs('otaUpdater/osRenameTest') 198 | os.rename('otaUpdater', 'otaUpdated') 199 | result = len(os.listdir('otaUpdated')) > 0 200 | self._rmtree('otaUpdated') 201 | return result 202 | 203 | def _copy_directory(self, fromPath, toPath): 204 | if not self._exists_dir(toPath): 205 | self._mk_dirs(toPath) 206 | 207 | for entry in os.ilistdir(fromPath): 208 | is_dir = entry[1] == 0x4000 209 | if is_dir: 210 | self._copy_directory(fromPath + '/' + entry[0], toPath + '/' + entry[0]) 211 | else: 212 | self._copy_file(fromPath + '/' + entry[0], toPath + '/' + entry[0]) 213 | 214 | def _copy_file(self, fromPath, toPath): 215 | with open(fromPath) as fromFile: 216 | with open(toPath, 'w') as toFile: 217 | CHUNK_SIZE = 512 # bytes 218 | data = fromFile.read(CHUNK_SIZE) 219 | while data: 220 | toFile.write(data) 221 | data = fromFile.read(CHUNK_SIZE) 222 | toFile.close() 223 | fromFile.close() 224 | 225 | def _exists_dir(self, path) -> bool: 226 | try: 227 | os.listdir(path) 228 | return True 229 | except: 230 | return False 231 | 232 | def _mk_dirs(self, path:str): 233 | paths = path.split('/') 234 | 235 | pathToCreate = '' 236 | for x in paths: 237 | self.mkdir(pathToCreate + x) 238 | pathToCreate = pathToCreate + x + '/' 239 | 240 | # different micropython versions act differently when directory already exists 241 | def mkdir(self, path:str): 242 | try: 243 | os.mkdir(path) 244 | except OSError as exc: 245 | if exc.args[0] == 17: 246 | pass 247 | 248 | 249 | def modulepath(self, path): 250 | return self.module + '/' + path if self.module else path -------------------------------------------------------------------------------- /app/start.py: -------------------------------------------------------------------------------- 1 | print('Version 2 installed using USB') -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def connectToWifiAndUpdate(): 4 | import time, machine, network, gc, app.secrets as secrets 5 | time.sleep(1) 6 | print('Memory free', gc.mem_free()) 7 | 8 | from app.ota_updater import OTAUpdater 9 | 10 | sta_if = network.WLAN(network.STA_IF) 11 | if not sta_if.isconnected(): 12 | print('connecting to network...') 13 | sta_if.active(True) 14 | sta_if.connect(secrets.WIFI_SSID, secrets.WIFI_PASSWORD) 15 | while not sta_if.isconnected(): 16 | pass 17 | print('network config:', sta_if.ifconfig()) 18 | otaUpdater = OTAUpdater('https://github.com/rdehuyss/micropython-ota-updater', main_dir='app', secrets_file="secrets.py") 19 | hasUpdated = otaUpdater.install_update_if_available() 20 | if hasUpdated: 21 | machine.reset() 22 | else: 23 | del(otaUpdater) 24 | gc.collect() 25 | 26 | def startApp(): 27 | import app.start 28 | 29 | 30 | connectToWifiAndUpdate() 31 | startApp() -------------------------------------------------------------------------------- /pymakr.conf: -------------------------------------------------------------------------------- 1 | { 2 | "address": "/dev/ttyUSB0", 3 | "username": "micro", 4 | "password": "python", 5 | "sync_folder": "", 6 | "open_on_start": true, 7 | "safe_boot_on_upload": false, 8 | "py_ignore": [ 9 | "pymakr.conf", 10 | ".vscode", 11 | ".gitignore", 12 | ".git", 13 | "project.pymakr", 14 | "env", 15 | "venv", 16 | "M5Stack_MicroPython", 17 | "__pycache__", 18 | "py" 19 | ], 20 | "fast_upload": false 21 | } --------------------------------------------------------------------------------