diff --git a/examples/OTA/1.0.2/flash/config.py b/examples/OTA/1.0.2/flash/config.py new file mode 100644 index 0000000..f1d932a --- /dev/null +++ b/examples/OTA/1.0.2/flash/config.py @@ -0,0 +1,3 @@ +WIFI_SSID = "ENTER_ME" +WIFI_PW = "ENTER_ME" +SERVER_HOST = "ENTER_ME" diff --git a/examples/OTA/1.0.2/flash/get_id.py b/examples/OTA/1.0.2/flash/get_id.py new file mode 100644 index 0000000..fa40144 --- /dev/null +++ b/examples/OTA/1.0.2/flash/get_id.py @@ -0,0 +1,4 @@ +from network import LoRa +import binascii +lora = LoRa(mode=LoRa.LORAWAN) +print(binascii.hexlify(lora.mac()).upper().decode('utf-8')) diff --git a/examples/OTA/1.0.2/flash/lib/OTA.py b/examples/OTA/1.0.2/flash/lib/OTA.py new file mode 100644 index 0000000..8b261fb --- /dev/null +++ b/examples/OTA/1.0.2/flash/lib/OTA.py @@ -0,0 +1,256 @@ +import network +import socket +import ssl +import machine +import ujson +import uhashlib +import ubinascii +import gc +import pycom +import os +import machine + +# Try to get version number +try: + from OTA_VERSION import VERSION +except ImportError: + VERSION = '1.0.0' + + +class OTA(): + # The following two methods need to be implemented in a subclass for the + # specific transport mechanism e.g. WiFi + + def connect(self): + raise NotImplementedError() + + def get_data(self, req, dest_path=None, hash=False): + raise NotImplementedError() + + # OTA methods + + def get_current_version(self): + return VERSION + + def get_update_manifest(self): + req = "manifest.json?current_ver={}".format(self.get_current_version()) + manifest_data = self.get_data(req).decode() + manifest = ujson.loads(manifest_data) + gc.collect() + return manifest + + def update(self): + manifest = self.get_update_manifest() + if manifest is None: + print("Already on the latest version") + return + + # Download new files and verify hashes + for f in manifest['new'] + manifest['update']: + # Upto 5 retries + for _ in range(5): + try: + self.get_file(f) + break + except Exception as e: + print(e) + print("Error downloading `{}` retrying...".format(f['URL'])) + else: + raise Exception("Failed to download `{}`".format(f['URL'])) + + # Backup old files + # only once all files have been successfully downloaded + for f in manifest['update']: + self.backup_file(f) + + # Rename new files to proper name + for f in manifest['new'] + manifest['update']: + new_path = "{}.new".format(f['dst_path']) + dest_path = "{}".format(f['dst_path']) + + os.rename(new_path, dest_path) + + # `Delete` files no longer required + # This actually makes a backup of the files incase we need to roll back + for f in manifest['delete']: + self.delete_file(f) + + # Flash firmware + if "firmware" in manifest: + self.write_firmware(manifest['firmware']) + + # Save version number + try: + self.backup_file({"dst_path": "/flash/OTA_VERSION.py"}) + except OSError: + pass # There isnt a previous file to backup + with open("/flash/OTA_VERSION.py", 'w') as fp: + fp.write("VERSION = '{}'".format(manifest['version'])) + from OTA_VERSION import VERSION + + # Reboot the device to run the new decode + machine.reset() + + def get_file(self, f): + new_path = "{}.new".format(f['dst_path']) + + # If a .new file exists from a previously failed update delete it + try: + os.remove(new_path) + except OSError: + pass # The file didnt exist + + # Download new file with a .new extension to not overwrite the existing + # file until the hash is verified. + hash = self.get_data(f['URL'].split("/", 3)[-1], + dest_path=new_path, + hash=True) + + # Hash mismatch + if hash != f['hash']: + print(hash, f['hash']) + msg = "Downloaded file's hash does not match expected hash" + raise Exception(msg) + + def backup_file(self, f): + bak_path = "{}.bak".format(f['dst_path']) + dest_path = "{}".format(f['dst_path']) + + # Delete previous backup if it exists + try: + os.remove(bak_path) + except OSError: + pass # There isnt a previous backup + + # Backup current file + os.rename(dest_path, bak_path) + + def delete_file(self, f): + bak_path = "/{}.bak_del".format(f) + dest_path = "/{}".format(f) + + # Delete previous delete backup if it exists + try: + os.remove(bak_path) + except OSError: + pass # There isnt a previous delete backup + + # Backup current file + os.rename(dest_path, bak_path) + + def write_firmware(self, f): + hash = self.get_data(f['URL'].split("/", 3)[-1], + hash=True, + firmware=True) + # TODO: Add verification when released in future firmware + + +class WiFiOTA(OTA): + def __init__(self, ssid_name, ssid_password, host, port=443): + self.ssid_name = ssid_name + self.ssid_password = ssid_password + self.host = host + self.port = port + + def connect(self): + self.wlan = network.WLAN(mode=network.WLAN.STA) + if not self.wlan.isconnected() or self.wlan.ssid() != self.ssid_name: + for net in self.wlan.scan(): + if net.ssid == self.ssid_name: + self.wlan.connect(self.ssid_name, auth=(network.WLAN.WPA2, + self.ssid_password)) + while not self.wlan.isconnected(): + machine.idle() # save power while waiting + break + else: + raise Exception("Cannot find network '{}'".format(SSID)) + else: + # Already connected to the correct WiFi + pass + + def _http_get(self, path, host): + req_fmt = 'GET /{} HTTP/1.0\r\nHost: {}\r\n\r\n' + req = bytes(req_fmt.format(path, host), 'utf8') + return req + + def get_socket(self, host, port=80): + ai = socket.getaddrinfo(host, port) + addr = ai[0][4] + s = socket.socket() + s.connect(addr) + + if port in (443, 8443): + s = ssl.wrap_socket(s) + + return s + + def get_data(self, req, dest_path=None, hash=False, firmware=False): + # Connect to server + print("Requesting: {}".format(req)) + + # open a new socket + s = self.get_socket(self.host, self.port) + + # Request File + s.sendall(self._http_get(req, "{}:{}".format(self.host, self.port))) + + try: + content = bytearray() + fp = None + if dest_path is not None: + if firmware: + raise Exception("Cannot write firmware to a file") + fp = open(dest_path, 'wb') + + if firmware: + pycom.ota_start() + + h = uhashlib.sha1() + + # Get data from server + result = s.recv(100) + + start_writing = False + while (len(result) > 0): + # Ignore the HTTP headers + if not start_writing: + if "\r\n\r\n" in result: + start_writing = True + result = result.decode().split("\r\n\r\n")[1].encode() + + if start_writing: + if firmware: + pycom.ota_write(result) + elif fp is None: + content.extend(result) + else: + fp.write(result) + + if hash: + h.update(result) + + result = s.recv(100) + + s.close() + + if fp is not None: + fp.close() + if firmware: + pycom.ota_finish() + + except Exception as e: + # Since only one hash operation is allowed at Once + # ensure we close it if there is an error + if h is not None: + h.digest() + raise e + + hash_val = ubinascii.hexlify(h.digest()).decode() + + if dest_path is None: + if hash: + return (bytes(content), hash_val) + else: + return bytes(content) + elif hash: + return hash_val diff --git a/examples/OTA/1.0.2/flash/main.py b/examples/OTA/1.0.2/flash/main.py new file mode 100644 index 0000000..9c32e5e --- /dev/null +++ b/examples/OTA/1.0.2/flash/main.py @@ -0,0 +1,67 @@ +from network import LoRa, WLAN +import socket +import time +from OTA import WiFiOTA +from time import sleep +import pycom +import binascii + +from config import WIFI_SSID, WIFI_PW, SERVER_HOST + +# Turn on GREEN LED +pycom.heartbeat(False) +pycom.rgbled(0xff) + +# Setup OTA +ota = WiFiOTA(WIFI_SSID, + WIFI_PW, + SERVER_HOST, # Update server address + 8000) # Update server port + +# Turn off WiFi to save power +w = WLAN() +w.deinit() + +# Initialize LoRa in LORAWAN mode. +lora = LoRa(mode=LoRa.LORAWAN, region=LoRa.EU868) + +app_eui = binascii.unhexlify('ENTER_ME') +app_key = binascii.unhexlify('ENTER_ME') + +# join a network using OTAA (Over the Air Activation) +lora.join(activation=LoRa.OTAA, auth=(app_eui, app_key), timeout=0) + +# wait until the module has joined the network +while not lora.has_joined(): + time.sleep(2.5) + print('Not yet joined...') + +# create a LoRa socket +s = socket.socket(socket.AF_LORA, socket.SOCK_RAW) + +# set the LoRaWAN data rate +s.setsockopt(socket.SOL_LORA, socket.SO_DR, 5) + +# make the socket blocking +# (waits for the data to be sent and for the 2 receive windows to expire) +s.setblocking(True) + +while True: + # send some data + s.send(bytes([0x04, 0x05, 0x06])) + + # make the socket non-blocking + # (because if there's no data received it will block forever...) + s.setblocking(False) + + # get any data received (if any...) + data = s.recv(64) + + # Some sort of OTA trigger + if data == bytes([0x01, 0x02, 0x03]): + print("Performing OTA") + # Perform OTA + ota.connect() + ota.update() + + sleep(5)