Skip to content
This repository was archived by the owner on Sep 10, 2024. It is now read-only.

Update OTA lib to support domains #69

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/OTA/1.0.2/flash/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
WIFI_SSID = "ENTER_ME"
WIFI_PW = "ENTER_ME"
SERVER_HOST = "ENTER_ME"
4 changes: 4 additions & 0 deletions examples/OTA/1.0.2/flash/get_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from network import LoRa
import binascii
lora = LoRa(mode=LoRa.LORAWAN)
print(binascii.hexlify(lora.mac()).upper().decode('utf-8'))
256 changes: 256 additions & 0 deletions examples/OTA/1.0.2/flash/lib/OTA.py
Original file line number Diff line number Diff line change
@@ -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
67 changes: 67 additions & 0 deletions examples/OTA/1.0.2/flash/main.py
Original file line number Diff line number Diff line change
@@ -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)