commit 6cf760fc3c680f2e7d43be1b8e1dc97829fac07c Author: Otto Winter Date: Wed Nov 7 21:24:25 2018 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..499c484 --- /dev/null +++ b/.gitignore @@ -0,0 +1,106 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +config/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3d66460 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Marcel Stör, Otto Winter + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d15f6cd --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# esphomeflasher + +esphomeflasher is a utility app for the [esphomelib](https://esphomelib.com/esphomeyaml/index.html) +framework and is designed to make flashing ESPs with esphomelib as simple as possible by: + + * Having pre-built binaries for most operating systems. + * Hiding all non-essential options for flashing. All necessary options for flashing + (bootloader, flash mode) are automatically extracted from the binary. + +This project was originally intended to be a simple command-line tool, +but then I decided that a GUI would be nice. As I don't like writing graphical +front end code, the GUI largely is based on the +[NodeMCU PyFlasher](https://github.com/marcelstoer/nodemcu-pyflasher) +project. + +The flashing process is done using the [esptool](https://github.com/espressif/esptool) +library by espressif. + +## Installation + +Es doesn't have to be installed, just double-click it and it'll start. +Check the [releases section](https://github.com/OttoWinter/esphomeflasher/releases) +for downloads for your platform. + +## Installation Using `pip` + +If you want to install this application from `pip`: + +- Install Python 3.x +- Install [wxPython 4.x](https://wxpython.org/) manually or run `pip3 install wxpython` +- Install this project using `pip3 install esphomeflasher` +- Start the GUI using `esphomeflasher`. Alternatively, you can use the command line interface ( + type `esphomeflasher -h` for info) + +## Build it yourself + +If you want to build this application yourself you need to: + +- Install Python 3.x +- Install [wxPython 4.x](https://wxpython.org/) manually or run `pip3 install wxpython` +- Download this project and run `pip3 install -e .` in the project's root. +- Start the GUI using `esphomeflasher`. Alternatively, you can use the command line interface ( + type `esphomeflasher -h` for info) + +## License + +[MIT](http://opensource.org/licenses/MIT) © Marcel Stör, Otto Winter diff --git a/esphomeflasher/__init__.py b/esphomeflasher/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/esphomeflasher/__main__.py b/esphomeflasher/__main__.py new file mode 100644 index 0000000..bf185d2 --- /dev/null +++ b/esphomeflasher/__main__.py @@ -0,0 +1,156 @@ +from __future__ import print_function + +import argparse +from datetime import datetime +import re +import sys + +import esptool +import serial + +from esphomeflasher import const +from esphomeflasher.common import ESP32ChipInfo, EsphomeflasherError, chip_run_stub, \ + configure_write_flash_args, detect_chip, detect_flash_size, read_chip_info +from esphomeflasher.const import ESP32_DEFAULT_BOOTLOADER_FORMAT, ESP32_DEFAULT_OTA_DATA, \ + ESP32_DEFAULT_PARTITIONS +from esphomeflasher.helpers import list_serial_ports + + +def parse_args(argv): + parser = argparse.ArgumentParser(prog='esphomeflasher {}'.format(const.__version__)) + parser.add_argument('-p', '--port', + help="Select the USB/COM port for uploading.") + group = parser.add_mutually_exclusive_group(required=False) + group.add_argument('--esp8266', action='store_true') + group.add_argument('--esp32', action='store_true') + parser.add_argument('--bootloader', + help="(ESP32-only) The bootloader to flash.", + default=ESP32_DEFAULT_BOOTLOADER_FORMAT) + parser.add_argument('--partitions', + help="(ESP32-only) The partitions to flash.", + default=ESP32_DEFAULT_PARTITIONS) + parser.add_argument('--otadata', + help="(ESP32-only) The otadata file to flash.", + default=ESP32_DEFAULT_OTA_DATA) + parser.add_argument('--no-erase', + help="Do not erase flash before flashing", + action='store_true') + parser.add_argument('binary', help="The binary image to flash.") + + return parser.parse_args(argv[1:]) + + +def select_port(args): + if args.port is not None: + print(u"Using '{}' as serial port.".format(args.port)) + return args.port + ports = list_serial_ports() + if not ports: + raise EsphomeflasherError("No serial port found!") + if len(ports) != 1: + print("Found more than one serial port:") + for port, desc in ports: + print(u" * {} ({})".format(port, desc)) + print("Please choose one with the --port argument.") + raise EsphomeflasherError + print(u"Auto-detected serial port: {}".format(ports[0][0])) + return ports[0][0] + + +ANSI_REGEX = re.compile(r"\033\[[0-9;]*m") + + +def run_esphomeflasher(argv): + args = parse_args(argv) + try: + firmware = open(args.binary, 'rb') + except IOError as err: + raise EsphomeflasherError("Error opening binary: {}".format(err)) + port = select_port(args) + chip = detect_chip(port, args.esp8266, args.esp32) + info = read_chip_info(chip) + + print() + print("Chip Info:") + print(" - Chip Family: {}".format(info.family)) + print(" - Chip Model: {}".format(info.model)) + if isinstance(info, ESP32ChipInfo): + print(" - Number of Cores: {}".format(info.num_cores)) + print(" - Max CPU Frequency: {}".format(info.cpu_frequency)) + print(" - Has Bluetooth: {}".format('YES' if info.has_bluetooth else 'NO')) + print(" - Has Embedded Flash: {}".format('YES' if info.has_embedded_flash else 'NO')) + print(" - Has Factory-Calibrated ADC: {}".format( + 'YES' if info.has_factory_calibrated_adc else 'NO')) + else: + print(" - Chip ID: {:08X}".format(info.chip_id)) + + print(" - MAC Address: {}".format(info.mac)) + + stub_chip = chip_run_stub(chip) + + flash_size = detect_flash_size(stub_chip) + print(" - Flash Size: {}".format(flash_size)) + + try: + stub_chip.flash_set_parameters(esptool.flash_size_bytes(flash_size)) + except esptool.FatalError as err: + raise EsphomeflasherError("Error setting flash parameters: {}".format(err)) + + mock_args = configure_write_flash_args(info, firmware, flash_size, + args.bootloader, args.partitions, + args.otadata) + + if not args.no_erase: + try: + esptool.erase_flash(stub_chip, mock_args) + except esptool.FatalError as err: + raise EsphomeflasherError("Error while erasing flash: {}".format(err)) + + try: + esptool.write_flash(stub_chip, mock_args) + except esptool.FatalError as err: + raise EsphomeflasherError("Error while writing flash: {}".format(err)) + + print("Hard Resetting...") + stub_chip.hard_reset() + + print("Done! Flashing is complete!") + print() + + print("Showing logs:") + with stub_chip._port as ser: + while True: + try: + raw = ser.readline() + except serial.SerialException: + print("Serial port closed!") + return + text = raw.decode(errors='ignore') + ANSI_REGEX.sub('', text) + line = text.replace('\r', '').replace('\n', '') + time = datetime.now().time().strftime('[%H:%M:%S]') + message = time + line + try: + print(message) + except UnicodeEncodeError: + print(message.encode('ascii', 'backslashreplace')) + + +def main(): + try: + if len(sys.argv) <= 1: + from esphomeflasher import gui + + return gui.main() or 0 + return run_esphomeflasher(sys.argv) or 0 + except EsphomeflasherError as err: + msg = str(err) + if msg: + print(msg) + return 1 + except KeyboardInterrupt: + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/esphomeflasher/common.py b/esphomeflasher/common.py new file mode 100644 index 0000000..011ccee --- /dev/null +++ b/esphomeflasher/common.py @@ -0,0 +1,201 @@ +import io +import struct + +import esptool + +from esphomeflasher.const import HTTP_REGEX +from esphomeflasher.helpers import prevent_print + + +class EsphomeflasherError(Exception): + pass + + +class MockEsptoolArgs(object): + def __init__(self, flash_size, addr_filename): + self.compress = True + self.no_compress = False + self.flash_size = flash_size + self.addr_filename = addr_filename + self.flash_mode = 'keep' + self.flash_freq = 'keep' + self.no_stub = False + self.verify = False + + +class ChipInfo(object): + def __init__(self, family, model, mac): + self.family = family + self.model = model + self.mac = mac + self.is_esp32 = None + + def as_dict(self): + return { + 'family': self.family, + 'model': self.model, + 'mac': self.mac, + 'is_esp32': self.is_esp32, + } + + +class ESP32ChipInfo(ChipInfo): + def __init__(self, model, mac, num_cores, cpu_frequency, has_bluetooth, has_embedded_flash, + has_factory_calibrated_adc): + super(ESP32ChipInfo, self).__init__("ESP32", model, mac) + self.num_cores = num_cores + self.cpu_frequency = cpu_frequency + self.has_bluetooth = has_bluetooth + self.has_embedded_flash = has_embedded_flash + self.has_factory_calibrated_adc = has_factory_calibrated_adc + + def as_dict(self): + data = ChipInfo.as_dict(self) + data.update({ + 'num_cores': self.num_cores, + 'cpu_frequency': self.cpu_frequency, + 'has_bluetooth': self.has_bluetooth, + 'has_embedded_flash': self.has_embedded_flash, + 'has_factory_calibrated_adc': self.has_factory_calibrated_adc, + }) + return data + + +class ESP8266ChipInfo(ChipInfo): + def __init__(self, model, mac, chip_id): + super(ESP8266ChipInfo, self).__init__("ESP8266", model, mac) + self.chip_id = chip_id + + def as_dict(self): + data = ChipInfo.as_dict(self) + data.update({ + 'chip_id': self.chip_id, + }) + return data + + +def read_chip_property(func, *args, **kwargs): + try: + return prevent_print(func, *args, **kwargs) + except esptool.FatalError as err: + raise EsphomeflasherError("Reading chip details failed: {}".format(err)) + + +def read_chip_info(chip): + mac = ':'.join('{:02X}'.format(x) for x in read_chip_property(chip.read_mac)) + if isinstance(chip, esptool.ESP32ROM): + model = read_chip_property(chip.get_chip_description) + features = read_chip_property(chip.get_chip_features) + num_cores = 2 if 'Dual Core' in features else 1 + frequency = next((x for x in ('160MHz', '240MHz') if x in features), '80MHz') + has_bluetooth = 'BT' in features + has_embedded_flash = 'Embedded Flash' in features + has_factory_calibrated_adc = 'VRef calibration in efuse' in features + return ESP32ChipInfo(model, mac, num_cores, frequency, has_bluetooth, + has_embedded_flash, has_factory_calibrated_adc) + elif isinstance(chip, esptool.ESP8266ROM): + model = read_chip_property(chip.get_chip_description) + chip_id = read_chip_property(chip.chip_id) + return ESP8266ChipInfo(model, mac, chip_id) + raise EsphomeflasherError("Unknown chip type {}".format(type(chip))) + + +def chip_run_stub(chip): + try: + return chip.run_stub() + except esptool.FatalError as err: + raise EsphomeflasherError("Error putting ESP in stub flash mode: {}".format(err)) + + +def detect_flash_size(stub_chip): + flash_id = read_chip_property(stub_chip.flash_id) + return esptool.DETECTED_FLASH_SIZES.get(flash_id >> 16, '4MB') + + +def read_firmware_info(firmware): + header = firmware.read(4) + firmware.seek(0) + + magic, _, flash_mode_raw, flash_size_freq = struct.unpack("BBBB", header) + if magic != esptool.ESPLoader.ESP_IMAGE_MAGIC: + raise EsphomeflasherError( + "The firmware binary is invalid (magic byte={:02X}, should be {:02X})" + "".format(magic, esptool.ESPLoader.ESP_IMAGE_MAGIC)) + flash_freq_raw = flash_size_freq & 0x0F + flash_mode = {0: 'qio', 1: 'qout', 2: 'dio', 3: 'dout'}.get(flash_mode_raw) + flash_freq = {0: '40m', 1: '26m', 2: '20m', 0xF: '80m'}.get(flash_freq_raw) + return flash_mode, flash_freq + + +def open_downloadable_binary(path): + if hasattr(path, 'seek'): + path.seek(0) + return path + + if HTTP_REGEX.match(path) is not None: + import requests + + try: + response = requests.get(path) + response.raise_for_status() + except requests.exceptions.Timeout as err: + raise EsphomeflasherError( + "Timeout while retrieving firmware file '{}': {}".format(path, err)) + except requests.exceptions.RequestException as err: + raise EsphomeflasherError( + "Error while retrieving firmware file '{}': {}".format(path, err)) + + binary = io.BytesIO() + binary.write(response.content) + binary.seek(0) + return binary + + try: + return open(path, 'rb') + except IOError as err: + raise EsphomeflasherError("Error opening binary '{}': {}".format(path, err)) + + +def format_bootloader_path(path, flash_mode, flash_freq): + return path.replace('$FLASH_MODE$', flash_mode).replace('$FLASH_FREQ$', flash_freq) + + +def configure_write_flash_args(info, firmware_path, flash_size, + bootloader_path, partitions_path, otadata_path): + addr_filename = [] + firmware = open_downloadable_binary(firmware_path) + flash_mode, flash_freq = read_firmware_info(firmware) + if isinstance(info, ESP32ChipInfo): + if flash_freq in ('26m', '20m'): + raise EsphomeflasherError( + "No bootloader available for flash frequency {}".format(flash_freq)) + bootloader = open_downloadable_binary( + format_bootloader_path(bootloader_path, flash_mode, flash_freq)) + partitions = open_downloadable_binary(partitions_path) + otadata = open_downloadable_binary(otadata_path) + + addr_filename.append((0x1000, bootloader)) + addr_filename.append((0x8000, partitions)) + addr_filename.append((0xE000, otadata)) + addr_filename.append((0x10000, firmware)) + else: + addr_filename.append((0x0, firmware)) + return MockEsptoolArgs(flash_size, addr_filename) + + +def detect_chip(port, force_esp8266=False, force_esp32=False): + if force_esp8266 or force_esp32: + klass = esptool.ESP32ROM if force_esp32 else esptool.ESP8266ROM + chip = klass(port) + else: + try: + chip = esptool.ESPLoader.detect_chip(port) + except esptool.FatalError as err: + raise EsphomeflasherError("ESP Chip Auto-Detection failed: {}".format(err)) + + try: + chip.connect() + except esptool.FatalError as err: + raise EsphomeflasherError("Error connecting to ESP: {}".format(err)) + + return chip diff --git a/esphomeflasher/const.py b/esphomeflasher/const.py new file mode 100644 index 0000000..85e06b0 --- /dev/null +++ b/esphomeflasher/const.py @@ -0,0 +1,14 @@ +import re + +__version__ = "1.0.0" + +ESP32_DEFAULT_OTA_DATA = 'https://github.com/espressif/arduino-esp32/blob/1.0.0/tools/partitions/boot_app0.bin' +# The latest bootloader image seems to work with older firmwares, but the "stable" bootloader +# doesn't work with firmwares generated with the latest esp-idf +ESP32_DEFAULT_BOOTLOADER_FORMAT = 'https://github.com/espressif/arduino-esp32/raw/' \ + '96822d783f3ab6a56a69b227ba4d1a1a36c66268/tools/sdk/' \ + 'bin/bootloader_$FLASH_MODE$_$FLASH_FREQ$.bin' +ESP32_DEFAULT_PARTITIONS = 'https://github.com/OttoWinter/esphomeflasher/blob/master/partitions.bin' + +# https://stackoverflow.com/a/3809435/8924614 +HTTP_REGEX = re.compile(r'https?://(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&/=]*)') diff --git a/esphomeflasher/gui.py b/esphomeflasher/gui.py new file mode 100644 index 0000000..89f5a4c --- /dev/null +++ b/esphomeflasher/gui.py @@ -0,0 +1,232 @@ +# This GUI is a fork of the brilliant https://github.com/marcelstoer/nodemcu-pyflasher + +import sys +import threading + +import wx +import wx.adv +from wx.lib.embeddedimage import PyEmbeddedImage +import wx.lib.inspection +import wx.lib.mixins.inspection + +from esphomeflasher.helpers import list_serial_ports + + +# See discussion at http://stackoverflow.com/q/41101897/131929 +class RedirectText: + def __init__(self, text_ctrl): + self._out = text_ctrl + + def write(self, string): + if string.startswith("\r"): + # carriage return -> remove last line i.e. reset position to start of last line + current_value = self._out.GetValue() + last_newline = current_value.rfind("\n") + new_value = current_value[:last_newline + 1] # preserve \n + new_value += string[1:] # chop off leading \r + wx.CallAfter(self._out.SetValue, new_value) + else: + wx.CallAfter(self._out.AppendText, string) + + def flush(self): + pass + + +class FlashingThread(threading.Thread): + def __init__(self, parent, firmware, port): + threading.Thread.__init__(self) + self.daemon = True + self._parent = parent + self._firmware = firmware + self._port = port + + def run(self): + try: + from esphomeflasher.__main__ import run_esphomeflasher + + argv = ['esphomeflasher', '--port', self._port, self._firmware] + run_esphomeflasher(argv) + except Exception as e: + print("Unexpected error: {}".format(e)) + raise + + +class MainFrame(wx.Frame): + def __init__(self, parent, title): + wx.Frame.__init__(self, parent, -1, title, size=(700, 650), + style=wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE) + + self._firmware = None + self._port = None + + self._build_status_bar() + self._build_menu_bar() + self._init_ui() + + sys.stdout = RedirectText(self.console_ctrl) + + self.SetMinSize((640, 480)) + self.Centre(wx.BOTH) + self.Show(True) + + def _init_ui(self): + def on_reload(event): + self.choice.SetItems(self._get_serial_ports()) + + def on_clicked(event): + self.console_ctrl.SetValue("") + worker = FlashingThread(self, self._firmware, self._port) + worker.start() + + def on_select_port(event): + choice = event.GetEventObject() + self._port = choice.GetString(choice.GetSelection()) + + def on_pick_file(event): + self._firmware = event.GetPath().replace("'", "") + + panel = wx.Panel(self) + + hbox = wx.BoxSizer(wx.HORIZONTAL) + + fgs = wx.FlexGridSizer(7, 2, 10, 10) + + self.choice = wx.Choice(panel, choices=self._get_serial_ports()) + self.choice.Bind(wx.EVT_CHOICE, on_select_port) + bmp = Reload.GetBitmap() + reload_button = wx.BitmapButton(panel, id=wx.ID_ANY, bitmap=bmp, + size=(bmp.GetWidth() + 7, bmp.GetHeight() + 7)) + reload_button.Bind(wx.EVT_BUTTON, on_reload) + reload_button.SetToolTip("Reload serial device list") + + file_picker = wx.FilePickerCtrl(panel, style=wx.FLP_USE_TEXTCTRL) + file_picker.Bind(wx.EVT_FILEPICKER_CHANGED, on_pick_file) + + serial_boxsizer = wx.BoxSizer(wx.HORIZONTAL) + serial_boxsizer.Add(self.choice, 1, wx.EXPAND) + serial_boxsizer.AddStretchSpacer(0) + serial_boxsizer.Add(reload_button, 0, wx.ALIGN_RIGHT, 20) + + button = wx.Button(panel, -1, "Flash ESP") + button.Bind(wx.EVT_BUTTON, on_clicked) + + self.console_ctrl = wx.TextCtrl(panel, style=wx.TE_MULTILINE | wx.TE_READONLY | wx.HSCROLL) + self.console_ctrl.SetFont( + wx.Font(13, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)) + self.console_ctrl.SetBackgroundColour(wx.BLACK) + self.console_ctrl.SetForegroundColour(wx.RED) + self.console_ctrl.SetDefaultStyle(wx.TextAttr(wx.RED)) + + port_label = wx.StaticText(panel, label="Serial port") + file_label = wx.StaticText(panel, label="Firmware") + + console_label = wx.StaticText(panel, label="Console") + + fgs.AddMany([ + port_label, (serial_boxsizer, 1, wx.EXPAND), + file_label, (file_picker, 1, wx.EXPAND), + (wx.StaticText(panel, label="")), (button, 1, wx.EXPAND), + (console_label, 1, wx.EXPAND), (self.console_ctrl, 1, wx.EXPAND)]) + fgs.AddGrowableRow(3, 1) + fgs.AddGrowableCol(1, 1) + hbox.Add(fgs, proportion=2, flag=wx.ALL | wx.EXPAND, border=15) + panel.SetSizer(hbox) + + def _get_serial_ports(self): + ports = [] + for port, desc in list_serial_ports(): + ports.append(port) + if not self._port and ports: + self._port = ports[0] + if not ports: + ports.append("") + return ports + + def _build_status_bar(self): + self.statusBar = self.CreateStatusBar(2, wx.STB_SIZEGRIP) + self.statusBar.SetStatusWidths([-2, -1]) + status_text = "Welcome to esphomeflasher (based on PyFlasher)" + self.statusBar.SetStatusText(status_text, 0) + + def _build_menu_bar(self): + self.menuBar = wx.MenuBar() + + # File menu + file_menu = wx.Menu() + wx.App.SetMacExitMenuItemId(wx.ID_EXIT) + exit_item = file_menu.Append(wx.ID_EXIT, "E&xit\tCtrl-Q", "Exit esphomeflasher") + exit_item.SetBitmap(Exit.GetBitmap()) + self.Bind(wx.EVT_MENU, self._on_exit_app, exit_item) + self.menuBar.Append(file_menu, "&File") + + # Help menu + help_menu = wx.Menu() + help_menu.Append(wx.ID_ABOUT, '&About esphomeflasher', 'About') + self.menuBar.Append(help_menu, '&Help') + + self.SetMenuBar(self.menuBar) + + # Menu methods + def _on_exit_app(self, event): + self.Close(True) + + def log_message(self, message): + self.console_ctrl.AppendText(message) + + +class App(wx.App, wx.lib.mixins.inspection.InspectionMixin): + def OnInit(self): + wx.SystemOptions.SetOption("mac.window-plain-transition", 1) + self.SetAppName("esphomeflasher") + + frame = MainFrame(None, "esphomeflasher") + frame.Show() + + return True + + +Exit = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0" + "RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAN1SURBVHjaYvz//z8DJQAggFhA" + "xEpGRgaQMX+B+A8DgwYLM1M+r4K8P4+8vMi/P38Y3j18+O7Fs+fbvv7+0w9Uc/kHVG070HKA" + "AGJBNg0omC5jZtynnpfHJeHkzPDmxQuGf6/eMIj+/yP+9MD+xFPrN8Reu3W3Gqi0D2IXAwNA" + "AIEN+A/hpWuEBMwwmj6TgUVEjOHTo0cM9y9dZfj76ycDCysrg4K5FYMUvyAL7+pVnYfOXwJp" + "6wIRAAHECAqDJYyMWpLmpmftN2/mYBEVZ3h38SLD9wcPGP6LioIN/7Z+PQM3UB3vv/8MXB/f" + "MSzdvv3vpecvzfr+/z8HEEBMYFMYGXM0iwrAmu+sXcvw4OxZhqenTjEwAv3P9OsXw+unTxne" + "6Osz3Ll3l+HvyzcMVlLSzMBwqgTpBQggsAG8MuKB4r9eM7zfv5PhHxMzg4qLCwPD0ycMDL9/" + "MzD+/cvw/8kTBgUbGwbB1DSGe1cuMbD8+8EgwMPjCtILEEDgMOCSkhT+t20Nw4v7nxkkNuxm" + "eLNmFYO0sCgDCwcHAwMzM4Pkl68MLzs7GGS6uhmOCwgxcD2+x8DLysID0gsQQGAD/gH99vPL" + "dwZGDjaG/0An/z19goHp/z+Gn9dvgoP4/7dPDD9OnGD4+/0bA5uCAsPPW8DA5eACxxxAAIEN" + "+PDuw/ufirJizE9fMzALCjD8efOO4dHObQx/d29k+PObgeHr268MQta2DCw8fAz/X75k+M/I" + "xPDh1+9vIL0AAQQOg9dPX2x7w8TDwPL2FcOvI8cYxFs7GFjFpRl+PP/K8O3NVwZuIREGpe5u" + "hp83rjF8u3iO4RsnO8OzHz8PgvQCBBA4GrsZGfUUtNXPWiuLsny59YxBch3Qdl4uhq/rNzP8" + "BwYin58PAysbG8MFLy+Gnw9uM5xkYPp38fNX22X//x8DCCAmqD8u3bh6s+Lssy8MrCLcDC/8" + "3Rl+LVvOwG1syMBrYcbwfetmhmsOdgy/795iuMXEwnDh89c2oJ7jIL0AAQR2wQRgXvgKNAfo" + "qRIlJfk2NR42Rj5gEmb5+4/h35+/DJ+/fmd4DUyNN4B+v/DlWwcwcTWzA9PXQqBegACCGwAK" + "ERD+zsBgwszOXirEwe7OzvCP5y/QCx/+/v/26vfv/R///O0GOvkII1AdKxCDDAAIIEZKszNA" + "gAEA1sFjF+2KokIAAAAASUVORK5CYII=") + +Reload = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABGdBTUEAALGOfPtRkwAAACBj" + "SFJNAAB6JQAAgIMAAPn/AACA6AAAdTAAAOpgAAA6lwAAF2+XqZnUAAAABmJLR0QA/wD/AP+g" + "vaeTAAAACXBIWXMAAABIAAAASABGyWs+AAAACXZwQWcAAAAYAAAAGAB4TKWmAAACZUlEQVRI" + "x7XVT4iXRRgH8M/8Mv9tUFgRZiBESRIhbFAo8kJ0EYoOwtJBokvTxUtBQnUokIjAoCi6+HiR" + "CNKoU4GHOvQieygMJKRDEUiahC4UtGkb63TY+cnb6/rb3276vQwzzzPf5/9MKqW4kRj8n8s5" + "53U55y03xEDOeRu+xe5ReqtWQDzAC3gTa3D7KP20nBrknDfhMB7vHH+Dj3AWxyPitxUZyDnv" + "xsElPL6MT/BiRJwbaaBN6eamlH9yzmvxPp5bRibPYDIizg96pIM2pak2pSexGiLiEr7H3DIM" + "3IMP/hNBm9It+BDzmGp6oeWcd+BIvdzFRZzGvUOnOtg6qOTrcRxP4ZVmkbxFxDQm8WVPtDMi" + "tmIDPu7JJocpehnb8F1Tyo/XijsizmMX9teCwq1VNlvrdKFzZeOgTelOvFQPfurV5NE2pc09" + "I/MR8TqewAxu68hmMd1RPzXAw1hXD9b3nL4bJ9qUdi0SzbF699ee6K9ObU6swoMd4Y42pYmm" + "lNm6/91C33/RpvQG9jelzHeMnK4F7uK+ur49bNNzHeEdONSmNFH3f9R1gNdwrKZ0UeSc77fQ" + "CCfxFqSveQA/9HTn8DM2d9I3xBk83ZQy3SNPFqb4JjwTEX9S56BN6SimjI857GtKea+ST+Cx" + "6synETHssCuv6V5sd/UQXQur8VCb0tqmlEuYi4jPF1PsTvJGvFMjGfVPzOD5ppTPxvHkqseu" + "Teku7MQm7MEjHfFXeLYp5ey4uRz5XLcpHbAwhH/jVbzblHJ5TG4s/aPN4BT2NKWcXA7xuBFs" + "wS9NKRdXQr6kgeuBfwEbWdzTvan9igAAADV0RVh0Y29tbWVudABSZWZyZXNoIGZyb20gSWNv" + "biBHYWxsZXJ5IGh0dHA6Ly9pY29uZ2FsLmNvbS/RLzdIAAAAJXRFWHRkYXRlOmNyZWF0ZQAy" + "MDExLTA4LTIxVDE0OjAxOjU2LTA2OjAwdNJAnQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxMS0w" + "OC0yMVQxNDowMTo1Ni0wNjowMAWP+CEAAAAASUVORK5CYII=") + + +def main(): + app = App(False) + app.MainLoop() diff --git a/esphomeflasher/helpers.py b/esphomeflasher/helpers.py new file mode 100644 index 0000000..f1973d8 --- /dev/null +++ b/esphomeflasher/helpers.py @@ -0,0 +1,37 @@ +from __future__ import print_function + +import os +import sys + +import serial + +DEVNULL = open(os.devnull, 'w') + + +def list_serial_ports(): + # from https://github.com/pyserial/pyserial/blob/master/serial/tools/list_ports.py + from serial.tools.list_ports import comports + result = [] + for port, desc, info in comports(): + if not port or "VID:PID" not in info: + continue + split_desc = desc.split(' - ') + if len(split_desc) == 2 and split_desc[0] == split_desc[1]: + desc = split_desc[0] + result.append((port, desc)) + result.sort() + return result + + +def prevent_print(func, *args, **kwargs): + orig_sys_stdout = sys.stdout + sys.stdout = DEVNULL + try: + return func(*args, **kwargs) + except serial.SerialException as err: + from esphomeflasher.common import EsphomeflasherError + + raise EsphomeflasherError("Serial port closed: {}".format(err)) + finally: + sys.stdout = orig_sys_stdout + pass diff --git a/partitions.bin b/partitions.bin new file mode 100644 index 0000000..e648fa3 Binary files /dev/null and b/partitions.bin differ diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..b79b419 --- /dev/null +++ b/setup.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +"""esphomeflasher setup script.""" +from setuptools import setup, find_packages + +from esphomeflasher import const + +PROJECT_NAME = 'esphomeflasher' +PROJECT_PACKAGE_NAME = 'esphomeflasher' +PROJECT_LICENSE = 'MIT' +PROJECT_AUTHOR = 'Otto Winter' +PROJECT_COPYRIGHT = '2018, Otto Winter' +PROJECT_URL = 'https://esphomelib.com/esphomeyaml/guides/esphomeflasher.html' +PROJECT_EMAIL = 'contact@otto-winter.com' + +PROJECT_GITHUB_USERNAME = 'OttoWinter' +PROJECT_GITHUB_REPOSITORY = 'esphomeflasher' + +PYPI_URL = 'https://pypi.python.org/pypi/{}'.format(PROJECT_PACKAGE_NAME) +GITHUB_PATH = '{}/{}'.format(PROJECT_GITHUB_USERNAME, PROJECT_GITHUB_REPOSITORY) +GITHUB_URL = 'https://github.com/{}'.format(GITHUB_PATH) + +DOWNLOAD_URL = '{}/archive/{}.zip'.format(GITHUB_URL, const.__version__) + +REQUIRES = [ + 'esptool>=2.3.1', + 'requests', +] + +setup( + name=PROJECT_PACKAGE_NAME, + version=const.__version__, + license=PROJECT_LICENSE, + url=GITHUB_URL, + download_url=DOWNLOAD_URL, + author=PROJECT_AUTHOR, + author_email=PROJECT_EMAIL, + description="ESP8266/ESP32 firmware flasher for esphomelib", + include_package_data=True, + zip_safe=False, + platforms='any', + test_suite='tests', + python_requires='>=3.5', + install_requires=REQUIRES, + keywords=['home', 'automation'], + entry_points={ + 'console_scripts': [ + 'esphomeflasher = esphomeflasher.__main__:main' + ] + }, + packages=find_packages() +)