- #!/usr/bin/env python
- # coding=utf-8
- # -------------------------------------------------------------------------------
- # ESPurna OTA manager
- # xose.perez@gmail.com
- #
- # Requires PlatformIO Core
- # -------------------------------------------------------------------------------
- from __future__ import print_function
-
- import argparse
- import os
- import re
- import shutil
- import socket
- import subprocess
- import sys
- import time
-
- from zeroconf import ServiceBrowser, ServiceStateChange, Zeroconf
-
- try:
- # noinspection PyUnresolvedReferences
- input = raw_input # Python2 *!! redefining build-in input.
- except NameError:
- pass # Python3
-
- # -------------------------------------------------------------------------------
-
- DISCOVER_TIMEOUT = 2
-
- description = "ESPurna OTA Manager v0.3"
- devices = []
- discover_last = 0
-
-
- # -------------------------------------------------------------------------------
-
- def on_service_state_change(zeroconf, service_type, name, state_change):
- """
- Callback that adds discovered devices to "devices" list
- """
-
- global discover_last
-
- if state_change is ServiceStateChange.Added:
- discover_last = time.time()
- info = zeroconf.get_service_info(service_type, name)
- if info:
-
- hostname = info.server.split(".")[0]
- device = {
- 'hostname': hostname.upper(),
- 'ip': socket.inet_ntoa(info.address),
- 'mac': '',
- 'app_name': '',
- 'app_version': '',
- 'build_date': '',
- 'target_board': '',
- 'mem_size': 0,
- 'sdk_size': 0,
- 'free_space': 0,
- }
-
- for key, item in info.properties.items():
- device[key.decode('UTF-8')] = item.decode('UTF-8')
-
- # rename fields (needed for sorting by name)
- device['app'] = device['app_name']
- device['device'] = device['target_board']
- device['version'] = device['app_version']
-
- devices.append(device)
-
-
- def list_devices():
- """
- Shows the list of discovered devices
- """
- output_format = "{:>3} {:<14} {:<15} {:<17} {:<12} {:<12} {:<20} {:<25} {:<8} {:<8} {:<10}"
- print(output_format.format(
- "#",
- "HOSTNAME",
- "IP",
- "MAC",
- "APP",
- "VERSION",
- "BUILD_DATE",
- "DEVICE",
- "MEM_SIZE",
- "SDK_SIZE",
- "FREE_SPACE"
- ))
- print("-" * 164)
-
- index = 0
- for device in devices:
- index += 1
- print(output_format.format(
- index,
- device.get('hostname', ''),
- device.get('ip', ''),
- device.get('mac', ''),
- device.get('app_name', ''),
- device.get('app_version', ''),
- device.get('build_date', ''),
- device.get('target_board', ''),
- device.get('mem_size', 0),
- device.get('sdk_size', 0),
- device.get('free_space', 0),
- ))
-
- print()
-
-
- def get_boards():
- """
- Grabs board types fro hardware.h file
- """
- boards = []
- for line in open("espurna/config/hardware.h"):
- m = re.search(r'defined\((\w*)\)', line)
- if m:
- boards.append(m.group(1))
- return sorted(boards)
-
-
- def get_device_size(device):
- if device.get('mem_size', 0) == device.get('sdk_size', 0):
- return int(device.get('mem_size', 0)) / 1024
- return 0
-
-
- def get_empty_board():
- """
- Returns the empty structure of a board to flash
- """
- board = {'board': '', 'ip': '', 'size': 0, 'auth': '', 'flags': ''}
- return board
-
-
- def get_board_by_index(index):
- """
- Returns the required data to flash a given board
- """
- board = {}
- if 1 <= index <= len(devices):
- device = devices[index - 1]
- board['hostname'] = device.get('hostname')
- board['board'] = device.get('target_board', '')
- board['ip'] = device.get('ip', '')
- board['size'] = get_device_size(device)
- return board
-
-
- def get_board_by_mac(mac):
- """
- Returns the required data to flash a given board
- """
- for device in devices:
- if device.get('mac', '').lower() == mac:
- board = {}
- board['hostname'] = device.get('hostname')
- board['board'] = device.get('device')
- board['ip'] = device.get('ip')
- board['size'] = get_device_size(device)
- if not board['board'] or not board['ip'] or board['size'] == 0:
- return None
- return board
- return None
-
-
- def get_board_by_hostname(hostname):
- """
- Returns the required data to flash a given board
- """
- hostname = hostname.lower()
- for device in devices:
- if device.get('hostname', '').lower() == hostname:
- board = {}
- board['hostname'] = device.get('hostname')
- board['board'] = device.get('target_board')
- board['ip'] = device.get('ip')
- board['size'] = get_device_size(device)
- if not board['board'] or not board['ip'] or board['size'] == 0:
- return None
- return board
- return None
-
-
- def input_board():
- """
- Grabs info from the user about what device to flash
- """
-
- # Choose the board
- try:
- index = int(input("Choose the board you want to flash (empty if none of these): "))
- except ValueError:
- index = 0
- if index < 0 or len(devices) < index:
- print("Board number must be between 1 and {}\n".format(str(len(devices))))
- return None
-
- board = get_board_by_index(index)
-
- # Choose board type if none before
- if len(board.get('board', '')) == 0:
-
- print()
- count = 1
- boards = get_boards()
- for name in boards:
- print("{:3d}\t{}".format(count, name))
- count += 1
- print()
- try:
- index = int(input("Choose the board type you want to flash: "))
- except ValueError:
- index = 0
- if index < 1 or len(boards) < index:
- print("Board number must be between 1 and {}\n".format(str(len(boards))))
- return None
- board['board'] = boards[index - 1]
-
- # Choose board size of none before
- if board.get('size', 0) == 0:
- try:
- board['size'] = int(input("Board memory size (1 for 1M, 2 for 2M, 4 for 4M): "))
- except ValueError:
- print("Wrong memory size")
- return None
-
- # Choose IP of none before
- if len(board.get('ip', '')) == 0:
- board['ip'] = input("IP of the device to flash (empty for 192.168.4.1): ") or "192.168.4.1"
-
- return board
-
-
- def boardname(board):
- return board.get('hostname', board['ip'])
-
-
- def store(device, env):
- source = ".pio/build/{}/firmware.elf".format(env)
- destination = ".pio/build/elfs/{}.elf".format(boardname(device).lower())
-
- dst_dir = os.path.dirname(destination)
- if not os.path.exists(dst_dir):
- os.mkdir(dst_dir)
-
- shutil.move(source, destination)
-
-
- def run(device, env):
- print("Building and flashing image over-the-air...")
- environ = os.environ.copy()
- environ["ESPURNA_IP"] = device["ip"]
- environ["ESPURNA_BOARD"] = device["board"]
- environ["ESPURNA_AUTH"] = device["auth"]
- environ["ESPURNA_FLAGS"] = device["flags"]
- environ["ESPURNA_PIO_SHARED_LIBRARIES"] = "y"
-
- command = ("platformio", "run", "--silent", "--environment", env, "-t", "upload")
- subprocess.check_call(command, env=environ)
-
- store(device, env)
-
-
- # -------------------------------------------------------------------------------
-
- if __name__ == '__main__':
-
- # Parse command line options
- parser = argparse.ArgumentParser(description=description)
- parser.add_argument("-c", "--core", help="flash ESPurna core", default=0, action='count')
- parser.add_argument("-f", "--flash", help="flash device", default=0, action='count')
- parser.add_argument("-o", "--flags", help="extra flags", default='')
- parser.add_argument("-p", "--password", help="auth password", default='')
- parser.add_argument("-s", "--sort", help="sort devices list by field", default='hostname')
- parser.add_argument("-y", "--yes", help="do not ask for confirmation", default=0, action='count')
- parser.add_argument("hostnames", nargs='*', help="Hostnames to update")
- args = parser.parse_args()
-
- print()
- print(description)
- print()
-
- # Look for services
- zeroconf = Zeroconf()
- browser = ServiceBrowser(zeroconf, "_arduino._tcp.local.", handlers=[on_service_state_change])
- discover_last = time.time()
- while time.time() < discover_last + DISCOVER_TIMEOUT:
- pass
- # zeroconf.close()
-
- if len(devices) == 0:
- print("Nothing found!\n")
- sys.exit(0)
-
- # Sort list
- field = args.sort.lower()
- if field not in devices[0]:
- print("Unknown field '{}'\n".format(field))
- sys.exit(1)
- devices = sorted(devices, key=lambda device: device.get(field, ''))
-
- # List devices
- list_devices()
-
- # Flash device
- if args.flash > 0:
-
- # Board(s) to flash
- queue = []
-
- # Check if hostnames
- for hostname in args.hostnames:
- board = get_board_by_hostname(hostname)
- if board:
- board['auth'] = args.password
- board['flags'] = args.flags
- queue.append(board)
-
- # If no boards ask the user
- if len(queue) == 0:
- board = input_board()
- if board:
- board['auth'] = args.password or input("Authorization key of the device to flash: ")
- board['flags'] = args.flags or input("Extra flags for the build: ")
- queue.append(board)
-
- # If still no boards quit
- if len(queue) == 0:
- sys.exit(0)
-
- queue = sorted(queue, key=lambda device: device.get('board', ''))
-
- # Flash each board
- for board in queue:
-
- # Flash core version?
- if args.core > 0:
- board['flags'] = "-DESPURNA_CORE " + board['flags']
-
- env = "esp8266-{:d}m-ota".format(board['size'])
-
- # Summary
- print()
- print("HOST = {}".format(boardname(board)))
- print("IP = {}".format(board['ip']))
- print("BOARD = {}".format(board['board']))
- print("AUTH = {}".format(board['auth']))
- print("FLAGS = {}".format(board['flags']))
- print("ENV = {}".format(env))
-
- response = True
- if args.yes == 0:
- response = (input("\nAre these values right [y/N]: ") == "y")
- if response:
- print()
- run(board, env)
|