mirror of
https://github.com/esphome/esphome-flasher.git
synced 2024-11-24 12:16:45 +01:00
Initial commit
This commit is contained in:
commit
6cf760fc3c
106
.gitignore
vendored
Normal file
106
.gitignore
vendored
Normal file
@ -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/
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -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.
|
47
README.md
Normal file
47
README.md
Normal file
@ -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
|
0
esphomeflasher/__init__.py
Normal file
0
esphomeflasher/__init__.py
Normal file
156
esphomeflasher/__main__.py
Normal file
156
esphomeflasher/__main__.py
Normal file
@ -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())
|
201
esphomeflasher/common.py
Normal file
201
esphomeflasher/common.py
Normal file
@ -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
|
14
esphomeflasher/const.py
Normal file
14
esphomeflasher/const.py
Normal file
@ -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@:%_+.~#?&/=]*)')
|
232
esphomeflasher/gui.py
Normal file
232
esphomeflasher/gui.py
Normal file
@ -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()
|
37
esphomeflasher/helpers.py
Normal file
37
esphomeflasher/helpers.py
Normal file
@ -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
|
BIN
partitions.bin
Normal file
BIN
partitions.bin
Normal file
Binary file not shown.
51
setup.py
Normal file
51
setup.py
Normal file
@ -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()
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user