Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/ota updater #384

Merged
merged 4 commits into from
Aug 20, 2017
Merged
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
38 changes: 28 additions & 10 deletions scripts/ota_updater/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,33 @@ This will allow you to send an OTA update to your device.
## Usage

```bash
python ./ota_update.py \
--broker-host "127.0.0.1" \
--broker-port "1883" \
--broker-username "" \
--broker-password "" \
--base-topic "homie/" \
--device-id "my-device-id" \
~/my_firmware.bin
> scripts/ota_updater/ota_updater.py -h
usage: ota_updater.py [-h] -l BROKER_HOST -p BROKER_PORT [-u BROKER_USERNAME]
[-d BROKER_PASSWORD] [-t BASE_TOPIC] -i DEVICE_ID
firmware

ota firmware update scirpt for ESP8226 implemenation of the Homie mqtt IoT
convention.

positional arguments:
firmware path to the firmware to be sent to the device

arguments:
-h, --help show this help message and exit
-l BROKER_HOST, --broker-host BROKER_HOST
host name or ip address of the mqtt broker
-p BROKER_PORT, --broker-port BROKER_PORT
port of the mqtt broker
-u BROKER_USERNAME, --broker-username BROKER_USERNAME
username used to authenticate with the mqtt broker
-d BROKER_PASSWORD, --broker-password BROKER_PASSWORD
password used to authenticate with the mqtt broker
-t BASE_TOPIC, --base-topic BASE_TOPIC
base topic of the homie devices on the broker
-i DEVICE_ID, --device-id DEVICE_ID
homie device id
```

All parameters are optional, except `--device-id` and the firmware path.
Default values are the one above.
* `BROKER_HOST` and `BROKER_PORT` defaults to 127.0.0.1 and 1883 respectively if not set.
* `BROKER_USERNAME` and `BROKER_PASSWORD` are optional.
* `BASE_TOPIC` defaults to `homie/` if not set
152 changes: 152 additions & 0 deletions scripts/ota_updater/ota_updater.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1 +1,153 @@
#!/usr/bin/env python

from __future__ import division, print_function
import paho.mqtt.client as mqtt
import base64, sys, math
from hashlib import md5

# The callback for when the client receives a CONNACK response from the server.
def on_connect(client, userdata, flags, rc):
if rc != 0:
print("Connection Failed with result code {}".format(rc))
client.disconnect()
else:
print("Connected with result code {}".format(rc))

# calcluate firmware md5
firmware_md5 = md5(userdata['firmware']).hexdigest()
userdata.update({'md5': firmware_md5})

# Subscribing in on_connect() means that if we lose the connection and
# reconnect then subscriptions will be renewed.
client.subscribe("{base_topic}{device_id}/$implementation/ota/status".format(**userdata))
client.subscribe("{base_topic}{device_id}/$implementation/ota/enabled".format(**userdata))
client.subscribe("{base_topic}{device_id}/$fw/#".format(**userdata))

# Wait for device info to come in and invoke the on_message callback where update will continue
print("Waiting for device info...")


# The callback for when a PUBLISH message is received from the server.
def on_message(client, userdata, msg):
# decode string for python2/3 compatiblity
msg.payload = msg.payload.decode()

if msg.topic.endswith('$implementation/ota/status'):
status = int(msg.payload.split()[0])

if userdata.get("published"):
if status == 206: # in progress
# state in progress, print progress bar
progress, total = [int(x) for x in msg.payload.split()[1].split('/')]
bar_width = 30
bar = int(bar_width*(progress/total))
print("\r[", '+'*bar, ' '*(bar_width-bar), "] ", msg.payload.split()[1], end='', sep='')
if (progress == total):
print()
sys.stdout.flush()
elif status == 304: # not modified
print("Device firmware already up to date with md5 checksum: {}".format(userdata.get('md5')))
client.disconnect()
elif status == 403: # forbidden
print("Device ota disabled, aborting...")
client.disconnect()

elif msg.topic.endswith('$fw/checksum'):
checksum = msg.payload

if userdata.get("published"):
if checksum == userdata.get('md5'):
print("Device back online. Update Successful!")
else:
print("Expecting checksum {}, got {}, update failed!".format(userdata.get('md5'), checksum))
client.disconnect()
else:
if checksum != userdata.get('md5'): # save old md5 for comparison with new firmware
userdata.update({'old_md5': checksum})
else:
print("Device firmware already up to date with md5 checksum: {}".format(checksum))
client.disconnect()

elif msg.topic.endswith('ota/enabled'):
if msg.payload == 'true':
userdata.update({'ota_enabled': True})
else:
print("Device ota disabled, aborting...")
client.disconnect()

if ( not userdata.get("published") ) and ( userdata.get('ota_enabled') ) and \
( 'old_md5' in userdata.keys() ) and ( userdata.get('md5') != userdata.get('old_md5') ):
# push the firmware binary
userdata.update({"published": True})
topic = "{base_topic}{device_id}/$implementation/ota/firmware/{md5}".format(**userdata)
print("Publishing new firmware with checksum {}".format(userdata.get('md5')))
client.publish(topic, userdata['firmware'])


def main(broker_host, broker_port, broker_username, broker_password, base_topic, device_id, firmware):
# initialise mqtt client and register callbacks
client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message

# set username and password if given
if broker_username and broker_password:
client.username_pw_set(broker_username, broker_password)

# save data to be used in the callbacks
client.user_data_set({
"base_topic": base_topic,
"device_id": device_id,
"firmware": firmware
})

# start connection
print("Connecting to mqtt broker {} on port {}".format(broker_host, broker_port))
client.connect(broker_host, broker_port, 60)

# Blocking call that processes network traffic, dispatches callbacks and handles reconnecting.
client.loop_forever()


if __name__ == '__main__':
import argparse

parser = argparse.ArgumentParser(
description='ota firmware update scirpt for ESP8226 implemenation of the Homie mqtt IoT convention.')

# ensure base topic always ends with a '/'
def base_topic_arg(s):
s = str(s)
if not s.endswith('/'):
s = s + '/'
return s

# specify arguments
parser.add_argument('-l', '--broker-host', type=str, required=False,
help='host name or ip address of the mqtt broker', default="127.0.0.1")
parser.add_argument('-p', '--broker-port', type=int, required=False,
help='port of the mqtt broker', default=1883)
parser.add_argument('-u', '--broker-username', type=str, required=False,
help='username used to authenticate with the mqtt broker')
parser.add_argument('-d', '--broker-password', type=str, required=False,
help='password used to authenticate with the mqtt broker')
parser.add_argument('-t', '--base-topic', type=base_topic_arg, required=False,
help='base topic of the homie devices on the broker', default="homie/")
parser.add_argument('-i', '--device-id', type=str, required=True,
help='homie device id')
parser.add_argument('firmware', type=argparse.FileType('rb'),
help='path to the firmware to be sent to the device')

# workaround for http://bugs.python.org/issue9694
parser._optionals.title = "arguments"

# get and validate arguments
args = parser.parse_args()

# read the contents of firmware into buffer
firmware = args.firmware.read()
args.firmware.close()

# Invoke the business logic
main(args.broker_host, args.broker_port, args.broker_username,
args.broker_password, args.base_topic, args.device_id, firmware)
2 changes: 1 addition & 1 deletion scripts/ota_updater/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
paho-mqtt==1.2.3
paho-mqtt >1.2.3,<=1.3.0