Skip to content

Commit

Permalink
✨ Add OTA updater script (#384)
Browse files Browse the repository at this point in the history
* 🐍Add python ota updater script

* 💼 Update documentation of ota update script

* 😑 Add comments to ota updater script

* 🔮 Use 127.0.0.1:1883 as default broker setting

For the ota updater script
  • Loading branch information
LiyouZhou authored and marvinroger committed Aug 20, 2017
1 parent ec03fd5 commit e11b456
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 11 deletions.
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

0 comments on commit e11b456

Please sign in to comment.