Compare commits

...

No commits in common. "v2.1" and "main" have entirely different histories.
v2.1 ... main

58 changed files with 1577 additions and 11297 deletions

10
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: "daily"

144
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,144 @@
name: Build Workflow
on:
push:
branches: [main]
pull_request:
jobs:
build-windows:
runs-on: windows-2019
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Python
uses: actions/setup-python@v3.1.0
with:
python-version: '3.9'
architecture: 'x64'
- name: Install requirements
run: |
pip install -r requirements.txt -r requirements_build.txt
pip install -e .
- name: Run PyInstaller
run: |
python -m PyInstaller.__main__ -F -w -n ESPHome-Flasher -i icon.ico esphomeflasher\__main__.py
- uses: actions/upload-artifact@v2
with:
name: Windows
path: dist/ESPHome-Flasher.exe
build-windows-x86:
runs-on: windows-2019
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Python
uses: actions/setup-python@v3.1.0
with:
python-version: '3.9'
architecture: 'x86'
- name: Install requirements
run: |
pip install -r requirements.txt -r requirements_build.txt
pip install -e .
- name: Run PyInstaller
run: |
python -m PyInstaller.__main__ -F -w -n ESPHome-Flasher -i icon.ico esphomeflasher\__main__.py
- name: See dist directory
run: ls dist
- uses: actions/upload-artifact@v2
with:
name: Windows-x86
path: dist/ESPHome-Flasher.exe
build-ubuntu:
runs-on: ubuntu-18.04
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Python
uses: actions/setup-python@v3.1.0
with:
python-version: '3.9'
- name: Install dependencies
run: |
sudo apt update
sudo apt install libgtk-3-dev libnotify-dev libsdl2-dev
pip install -U \
-f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-18.04 \
wxPython
- name: Install requirements
run: |
pip install -r requirements.txt -r requirements_build.txt
pip install -e .
- name: Run PyInstaller
run: |
python -m PyInstaller.__main__ -F -w -n ESPHome-Flasher -i icon.ico esphomeflasher/__main__.py
- name: See dist directory
run: ls dist
- uses: actions/upload-artifact@v2
with:
name: Ubuntu
path: dist/ESPHome-Flasher
build-macos:
runs-on: macOS-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Python
uses: actions/setup-python@v3.1.0
with:
python-version: '3.9'
- name: Install requirements
run: |
pip install -r requirements.txt -r requirements_build.txt
pip install -e .
- name: Run PyInstaller
run: |
python -m PyInstaller.__main__ -F -w -n ESPHome-Flasher -i icon.icns esphomeflasher/__main__.py
- name: See dist directory
run: ls dist
- name: Tar files
run: |
tar -C dist -cvf dist.tar ESPHome-Flasher.app
- name: 'Upload Artifact'
uses: actions/upload-artifact@v2
with:
name: macOS
path: dist.tar
build-pypi:
runs-on: ubuntu-18.04
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Python
uses: actions/setup-python@v3.1.0
with:
python-version: '3.9'
- name: Install dependencies
run: |
sudo apt update
sudo apt install libgtk-3-dev libnotify-dev libsdl2-dev
pip install -U \
-f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-18.04 \
wxPython
- name: Install requirements
run: |
pip install -r requirements.txt -r requirements_build.txt
pip install -e .
- name: Run sdist
run: python setup.py sdist bdist_wheel
- name: See dist directory
run: ls dist
- uses: actions/upload-artifact@v2
with:
name: sdist
path: dist/esphomeflasher-*.tar.gz
- uses: actions/upload-artifact@v2
with:
name: bdist_wheel
path: dist/esphomeflasher-*.whl

71
.github/workflows/lint.yml vendored Normal file
View File

@ -0,0 +1,71 @@
name: Lint workflow
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
jobs:
ci:
name: ${{ matrix.name }}
runs-on: ubuntu-18.04
strategy:
fail-fast: false
matrix:
include:
- id: flake8
name: Lint with flake8
- id: pylint
name: Lint with pylint
- id: black
name: Check formatting with black
- id: isort
name: Check import order with isort
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3.1.0
id: python
with:
python-version: '3.7'
- name: Install apt dependencies
run: |
sudo apt update
sudo apt install libgtk-3-dev libnotify-dev libsdl2-dev
- name: Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
- name: Restore PIP cache
uses: actions/cache@v3.0.1
with:
path: ${{ steps.pip-cache.outputs.dir }}
key: pip-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt', 'requirements_test.txt') }}
restore-keys: |
pip-${{ steps.python.outputs.python-version }}-
- name: Set up Python environment
run: |
pip3 install -U \
-f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-18.04 \
wxPython
pip3 install -r requirements.txt -r requirements_test.txt
pip3 install -e .
- name: Register problem matchers
run: |
echo "::add-matcher::.github/workflows/matchers/flake8.json"
echo "::add-matcher::.github/workflows/matchers/pylint.json"
echo "::add-matcher::.github/workflows/matchers/isort.json"
- run: flake8 esphomeflasher
if: ${{ matrix.id == 'flake8' }}
- run: pylint esphomeflasher
if: ${{ matrix.id == 'pylint' }}
- run: black --check --diff --color esphomeflasher
if: ${{ matrix.id == 'black' }}
- run: isort --check --diff esphomeflasher
if: ${{ matrix.id == 'isort' }}

27
.github/workflows/lock.yml vendored Normal file
View File

@ -0,0 +1,27 @@
name: Lock
on:
schedule:
- cron: '30 0 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
concurrency:
group: lock
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v3
with:
pr-inactive-days: "7"
pr-lock-reason: ""
exclude-any-pr-labels: keep-open
issue-inactive-days: "7"
issue-lock-reason: ""
exclude-any-issue-labels: keep-open

30
.github/workflows/matchers/flake8.json vendored Normal file
View File

@ -0,0 +1,30 @@
{
"problemMatcher": [
{
"owner": "flake8-error",
"severity": "error",
"pattern": [
{
"regexp": "^(.*):(\\d+):(\\d+):\\s([EF]\\d{3}\\s.*)$",
"file": 1,
"line": 2,
"column": 3,
"message": 4
}
]
},
{
"owner": "flake8-warning",
"severity": "warning",
"pattern": [
{
"regexp": "^(.*):(\\d+):(\\d+):\\s([CDNW]\\d{3}\\s.*)$",
"file": 1,
"line": 2,
"column": 3,
"message": 4
}
]
}
]
}

14
.github/workflows/matchers/isort.json vendored Normal file
View File

@ -0,0 +1,14 @@
{
"problemMatcher": [
{
"owner": "isort",
"pattern": [
{
"regexp": "^ERROR:\\s+(.+)\\s+(.+)$",
"file": 1,
"message": 2
}
]
}
]
}

32
.github/workflows/matchers/pylint.json vendored Normal file
View File

@ -0,0 +1,32 @@
{
"problemMatcher": [
{
"owner": "pylint-error",
"severity": "error",
"pattern": [
{
"regexp": "^(.+):(\\d+):(\\d+):\\s(([EF]\\d{4}):\\s.+)$",
"file": 1,
"line": 2,
"column": 3,
"message": 4,
"code": 5
}
]
},
{
"owner": "pylint-warning",
"severity": "warning",
"pattern": [
{
"regexp": "^(.+):(\\d+):(\\d+):\\s(([CRW]\\d{4}):\\s.+)$",
"file": 1,
"line": 2,
"column": 3,
"message": 4,
"code": 5
}
]
}
]
}

121
.github/workflows/release-assets.yml vendored Normal file
View File

@ -0,0 +1,121 @@
name: Release Assets Workflow
on:
release:
type: [created]
jobs:
build-windows:
runs-on: windows-2019
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Python
uses: actions/setup-python@v3.1.0
with:
python-version: '3.9'
architecture: 'x64'
- name: Install requirements
run: |
pip install -r requirements.txt -r requirements_build.txt
pip install -e .
- name: Run PyInstaller
run: |
python -m PyInstaller.__main__ -F -w -n ESPHome-Flasher -i icon.ico esphomeflasher\__main__.py
- uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: dist/ESPHome-Flasher.exe
asset_name: ESPHome-Flasher-$tag-Windows-x64.exe
tag: ${{ github.ref }}
overwrite: true
build-windows-x86:
runs-on: windows-2019
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Python
uses: actions/setup-python@v3.1.0
with:
python-version: '3.9'
architecture: 'x86'
- name: Install requirements
run: |
pip install -r requirements.txt -r requirements_build.txt
pip install -e .
- name: Run PyInstaller
run: |
python -m PyInstaller.__main__ -F -w -n ESPHome-Flasher -i icon.ico esphomeflasher\__main__.py
- name: See dist directory
run: ls dist
- uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: dist/ESPHome-Flasher.exe
asset_name: ESPHome-Flasher-$tag-Windows-x86.exe
tag: ${{ github.ref }}
overwrite: true
build-ubuntu:
runs-on: ubuntu-18.04
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Python
uses: actions/setup-python@v3.1.0
with:
python-version: '3.9'
- name: Install dependencies
run: |
sudo apt update
sudo apt install libgtk-3-dev libnotify-dev libsdl2-dev
pip install -U \
-f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-18.04 \
wxPython
- name: Install requirements
run: |
pip install -r requirements.txt -r requirements_build.txt
pip install -e .
- name: Run PyInstaller
run: |
python -m PyInstaller.__main__ -F -w -n ESPHome-Flasher -i icon.ico esphomeflasher/__main__.py
- name: See dist directory
run: ls dist
- uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: dist/ESPHome-Flasher
asset_name: ESPHome-Flasher-$tag-Ubuntu-x64.exec
tag: ${{ github.ref }}
overwrite: true
build-macos:
runs-on: macOS-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Python
uses: actions/setup-python@v3.1.0
with:
python-version: '3.9'
- name: Install requirements
run: |
pip install -r requirements.txt -r requirements_build.txt
pip install -e .
- name: Run PyInstaller
run: |
python -m PyInstaller.__main__ -F -w -n ESPHome-Flasher -i icon.icns esphomeflasher/__main__.py
- name: See dist directory
run: ls dist
- name: Tar files
run: |
tar -C dist -cvf dist.tar ESPHome-Flasher.app
- uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: dist.tar
asset_name: ESPHome-Flasher-$tag-macOS.tar
tag: ${{ github.ref }}
overwrite: true

38
.github/workflows/stale.yml vendored Normal file
View File

@ -0,0 +1,38 @@
name: Stale
on:
schedule:
- cron: '30 0 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
concurrency:
group: lock
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v4
with:
days-before-pr-stale: 90
days-before-pr-close: 7
days-before-issue-stale: 90
days-before-issue-close: 7
remove-stale-when-updated: true
stale-pr-label: "stale"
exempt-pr-labels: "no-stale"
stale-issue-label: "stale"
exempt-issue-labels: "no-stale"
stale-pr-message: >
There hasn't been any activity on this pull request recently. This
pull request has been automatically marked as stale because of that
and will be closed if no further activity occurs within 7 days.
Thank you for your contributions.
stale-issue-message: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.

47
.gitignore vendored
View File

@ -1,5 +1,3 @@
.idea/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@ -10,7 +8,6 @@ __pycache__/
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
@ -22,15 +19,17 @@ 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
*.spec
# Installer logs
pip-log.txt
@ -44,8 +43,9 @@ htmlcov/
.cache
nosetests.xml
coverage.xml
*,cover
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
@ -54,6 +54,7 @@ coverage.xml
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
@ -68,7 +69,7 @@ docs/_build/
# PyBuilder
target/
# IPython Notebook
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
@ -77,15 +78,41 @@ target/
# celery beat schedule file
celerybeat-schedule
# dotenv
.env
# SageMath parsed files
*.sage.py
# virtualenv
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
config/
.DS_Store
/.idea/
.vscode/*
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/

View File

@ -1,72 +0,0 @@
# coding=utf-8
import sys, os, wx
import wx.html
import wx.lib.wxpTag
import webbrowser
from Main import __version__
# ---------------------------------------------------------------------------
class AboutDlg(wx.Dialog):
text = '''
<html>
<body bgcolor="#DCDCDC" style="font-family: Arial; background-color: #DCDCDC;">
<center>
<img src="{0}/images/python-64.png" width="64" height="64" alt="Python">
<img src="{0}/images/icon-64.png" width="64" height="64" alt="NodeMCU">
<img src="{0}/images/espressif-64.png" width="64" height="64" alt="Espressif, producers of ESP8266 et.al.">
<img src="{0}/images/wxpython-64.png" width="64" height="43" alt="wxPython, cross-platform GUI framework">
<h1>NodeMCU PyFlasher</h1>
<p>Version {1}</p>
<p>Fork the <a style="color: #004CE5;" href="https://github.com/marcelstoer/nodemcu-pyflasher">project on
GitHub</a> and help improve it for all!</p>
<p>
As with everything I offer for free, this is donation-ware.
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=HFN4ZMET5XS2Q"><img src="{0}/images/paypal-256.png" width="256" height="88" alt="Donate with PayPal"></a>
</p>
<p>&copy; 2017 Marcel St&ouml;r. Licensed under MIT.</p>
<p>
<wxp module="wx" class="Button">
<param name="label" value="Close">
<param name="id" value="ID_OK">
</wxp>
</p>
</center>
</body>
</html>
'''
def __init__(self, parent):
wx.Dialog.__init__(self, parent, wx.ID_ANY, "About NodeMCU PyFlasher")
html = HtmlWindow(self, wx.ID_ANY, size=(420, -1))
if "gtk2" in wx.PlatformInfo or "gtk3" in wx.PlatformInfo:
html.SetStandardFonts()
txt = self.text.format(self._get_bundle_dir(), __version__)
html.SetPage(txt)
ir = html.GetInternalRepresentation()
html.SetSize((ir.GetWidth() + 25, ir.GetHeight() + 25))
self.SetClientSize(html.GetSize())
self.CentreOnParent(wx.BOTH)
def _get_bundle_dir(self):
# set by PyInstaller, see http://pyinstaller.readthedocs.io/en/v3.2/runtime-information.html
if getattr(sys, 'frozen', False):
return sys._MEIPASS
else:
return os.path.dirname(os.path.abspath(__file__))
class HtmlWindow(wx.html.HtmlWindow):
def OnLinkClicked(self, link):
webbrowser.open(link.GetHref())
# ---------------------------------------------------------------------------

View File

@ -1,30 +0,0 @@
# coding=utf-8
import wx
import wx.html
import webbrowser
class HtmlPopupTransientWindow(wx.PopupTransientWindow):
def __init__(self, parent, style, html_body_content, bgcolor, size):
wx.PopupTransientWindow.__init__(self, parent, style)
panel = wx.Panel(self)
panel.SetBackgroundColour(bgcolor)
html_window = self.HtmlWindow(panel, wx.ID_ANY, size=size)
html_window.SetPage('<body bgcolor="' + bgcolor + '">' + html_body_content + '</body>')
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(html_window, 0, wx.ALL, 5)
panel.SetSizer(sizer)
sizer.Fit(panel)
sizer.Fit(self)
self.Layout()
class HtmlWindow(wx.html.HtmlWindow):
def OnLinkClicked(self, link):
# get a hold of the PopupTransientWindow to close it
self.GetParent().GetParent().Dismiss()
webbrowser.open(link.GetHref())

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2016 Marcel Stör
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
@ -18,4 +18,4 @@ 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.
SOFTWARE.

3
MANIFEST.in Normal file
View File

@ -0,0 +1,3 @@
include LICENSE
include README.md
include requirements.txt

440
Main.py
View File

@ -1,440 +0,0 @@
#!/usr/bin/env python
import wx
import wx.lib.inspection
import wx.lib.mixins.inspection
import sys, os
import esptool
import threading
import json
import images as images
from serial import SerialException
from serial.tools import list_ports
from esptool import ESPLoader
from esptool import NotImplementedInROMError
from argparse import Namespace
__version__ = "2.1"
__flash_help__ = '''
<p>This setting is highly dependent on your device!<p>
<p>
Details at <a style="color: #004CE5;"
href="https://www.esp32.com/viewtopic.php?p=5523&sid=08ef44e13610ecf2a2a33bb173b0fd5c#p5523">http://bit.ly/2v5Rd32</a>
and in the <a style="color: #004CE5;" href="https://github.com/espressif/esptool/#flash-modes">esptool
documentation</a>
<ul>
<li>Most ESP32 and ESP8266 ESP-12 use 'dio'.</li>
<li>Most ESP8266 ESP-01/07 use 'qio'.</li>
<li>ESP8285 requires 'dout'.</li>
</ul>
</p>
'''
__supported_baud_rates__ = [9600, 57600, 74880, 115200, 230400, 460800, 921600]
# ---------------------------------------------------------------------------
# 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):
None
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
class FlashingThread(threading.Thread):
def __init__(self, parent, config):
threading.Thread.__init__(self)
self.daemon = True
self._parent = parent
self._config = config
def run(self):
try:
initial_baud = min(ESPLoader.ESP_ROM_BAUD, self._config.baud)
esp = ESPLoader.detect_chip(self._config.port, initial_baud)
print("Chip is %s" % (esp.get_chip_description()))
esp = esp.run_stub()
if self._config.baud > initial_baud:
try:
esp.change_baud(self._config.baud)
except NotImplementedInROMError:
print("WARNING: ROM doesn't support changing baud rate. Keeping initial baud rate %d." %
initial_baud)
args = Namespace()
args.flash_size = "detect"
args.flash_mode = self._config.mode
args.flash_freq = "40m"
args.no_progress = False
args.no_stub = False
args.verify = False # TRUE is deprecated
args.compress = True
args.addr_filename = [[int("0x00000", 0), open(self._config.firmware_path, 'rb')]]
print("Configuring flash size...")
esptool.detect_flash_size(esp, args)
esp.flash_set_parameters(esptool.flash_size_bytes(args.flash_size))
if self._config.erase_before_flash:
esptool.erase_flash(esp, args)
esptool.write_flash(esp, args)
# The last line printed by esptool is "Leaving..." -> some indication that the process is done is needed
print("\nDone.")
except SerialException as e:
self._parent.report_error(e.strerror)
raise e
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# DTO between GUI and flashing thread
class FlashConfig:
def __init__(self):
self.baud = 115200
self.erase_before_flash = False
self.mode = "dio"
self.firmware_path = None
self.port = None
@classmethod
def load(cls, file_path):
conf = cls()
if os.path.exists(file_path):
with open(file_path, 'r') as f:
data = json.load(f)
conf.port = data['port']
conf.baud = data['baud']
conf.mode = data['mode']
conf.erase_before_flash = data['erase']
return conf
def safe(self, file_path):
data = {
'port': self.port,
'baud': self.baud,
'mode': self.mode,
'erase': self.erase_before_flash,
}
with open(file_path, 'w') as f:
json.dump(data, f)
def is_complete(self):
return self.firmware_path is not None and self.port is not None
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
class NodeMcuFlasher(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._config = FlashConfig.load(self._get_config_file_path())
self._build_status_bar()
self._set_icons()
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_baud_changed(event):
radio_button = event.GetEventObject()
if radio_button.GetValue():
self._config.baud = radio_button.rate
def on_mode_changed(event):
radio_button = event.GetEventObject()
if radio_button.GetValue():
self._config.mode = radio_button.mode
def on_erase_changed(event):
radio_button = event.GetEventObject()
if radio_button.GetValue():
self._config.erase_before_flash = radio_button.erase
def on_clicked(event):
self.console_ctrl.SetValue("")
worker = FlashingThread(self, self._config)
worker.start()
def on_select_port(event):
choice = event.GetEventObject()
self._config.port = choice.GetString(choice.GetSelection())
def on_pick_file(event):
self._config.firmware_path = 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)
self._select_configured_port()
bmp = images.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.SetToolTipString("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)
baud_boxsizer = wx.BoxSizer(wx.HORIZONTAL)
def add_baud_radio_button(sizer, index, baud_rate):
style = wx.RB_GROUP if index == 0 else 0
radio_button = wx.RadioButton(panel, name="baud-%d" % baud_rate, label="%d" % baud_rate, style=style)
radio_button.rate = baud_rate
# sets default value
radio_button.SetValue(baud_rate == self._config.baud)
radio_button.Bind(wx.EVT_RADIOBUTTON, on_baud_changed)
sizer.Add(radio_button)
sizer.AddSpacer(10)
for idx, rate in enumerate(__supported_baud_rates__):
add_baud_radio_button(baud_boxsizer, idx, rate)
flashmode_boxsizer = wx.BoxSizer(wx.HORIZONTAL)
def add_flash_mode_radio_button(sizer, index, mode, label):
style = wx.RB_GROUP if index == 0 else 0
radio_button = wx.RadioButton(panel, name="mode-%s" % mode, label="%s" % label, style=style)
radio_button.Bind(wx.EVT_RADIOBUTTON, on_mode_changed)
radio_button.mode = mode
radio_button.SetValue(mode == self._config.mode)
sizer.Add(radio_button)
sizer.AddSpacer(10)
add_flash_mode_radio_button(flashmode_boxsizer, 0, "qio", "Quad I/O (qio)")
add_flash_mode_radio_button(flashmode_boxsizer, 1, "dio", "Dual I/O (dio)")
add_flash_mode_radio_button(flashmode_boxsizer, 2, "dout", "Dual Output (dout)")
erase_boxsizer = wx.BoxSizer(wx.HORIZONTAL)
def add_erase_radio_button(sizer, index, erase_before_flash, label, value):
style = wx.RB_GROUP if index == 0 else 0
radio_button = wx.RadioButton(panel, name="erase-%s" % erase_before_flash, label="%s" % label, style=style)
radio_button.Bind(wx.EVT_RADIOBUTTON, on_erase_changed)
radio_button.erase = erase_before_flash
radio_button.SetValue(value)
sizer.Add(radio_button)
sizer.AddSpacer(10)
erase = self._config.erase_before_flash
add_erase_radio_button(erase_boxsizer, 0, False, "no", erase is False)
add_erase_radio_button(erase_boxsizer, 1, True, "yes, wipes all data", erase is True)
button = wx.Button(panel, -1, "Flash NodeMCU")
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="NodeMCU firmware")
baud_label = wx.StaticText(panel, label="Baud rate")
flashmode_label = wx.StaticText(panel, label="Flash mode")
def on_info_hover(event):
from HtmlPopupTransientWindow import HtmlPopupTransientWindow
win = HtmlPopupTransientWindow(self, wx.SIMPLE_BORDER, __flash_help__, "#FFB6C1", (410, 140))
image = event.GetEventObject()
image_position = image.ClientToScreen((0, 0))
image_size = image.GetSize()
win.Position(image_position, (0, image_size[1]))
win.Popup()
icon = wx.StaticBitmap(panel, wx.ID_ANY, images.Info.GetBitmap())
icon.Bind(wx.EVT_MOTION, on_info_hover)
flashmode_label_boxsizer = wx.BoxSizer(wx.HORIZONTAL)
flashmode_label_boxsizer.Add(flashmode_label, 1, wx.EXPAND)
flashmode_label_boxsizer.AddStretchSpacer(0)
flashmode_label_boxsizer.Add(icon, 0, wx.ALIGN_RIGHT, 20)
erase_label = wx.StaticText(panel, label="Erase flash")
console_label = wx.StaticText(panel, label="Console")
fgs.AddMany([
port_label, (serial_boxsizer, 1, wx.EXPAND),
file_label, (file_picker, 1, wx.EXPAND),
baud_label, baud_boxsizer,
flashmode_label_boxsizer, flashmode_boxsizer,
erase_label, erase_boxsizer,
(wx.StaticText(panel, label="")), (button, 1, wx.EXPAND),
(console_label, 1, wx.EXPAND), (self.console_ctrl, 1, wx.EXPAND)])
fgs.AddGrowableRow(6, 1)
fgs.AddGrowableCol(1, 1)
hbox.Add(fgs, proportion=2, flag=wx.ALL | wx.EXPAND, border=15)
panel.SetSizer(hbox)
def _select_configured_port(self):
count = 0
for item in self.choice.GetItems():
if item == self._config.port:
self.choice.Select(count)
break
count += 1
def _get_serial_ports(self):
ports = [""]
for port, desc, hwid in sorted(list_ports.comports()):
ports.append(port)
return ports
def _set_icons(self):
self.SetIcon(images.Icon.GetIcon())
def _build_status_bar(self):
self.statusBar = self.CreateStatusBar(2, wx.ST_SIZEGRIP)
self.statusBar.SetStatusWidths([-2, -1])
status_text = "Welcome to NodeMCU PyFlasher %s" % __version__
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 NodeMCU PyFlasher")
exit_item.SetBitmap(images.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_item = help_menu.Append(wx.ID_ABOUT, '&About NodeMCU PyFlasher', 'About')
self.Bind(wx.EVT_MENU, self._on_help_about, help_item)
self.menuBar.Append(help_menu, '&Help')
self.SetMenuBar(self.menuBar)
def _get_config_file_path(self):
return wx.StandardPaths.Get().GetUserConfigDir() + "/nodemcu-pyflasher.json"
# Menu methods
def _on_exit_app(self, event):
self._config.safe(self._get_config_file_path())
self.Close(True)
def _on_help_about(self, event):
from About import AboutDlg
about = AboutDlg(self)
about.ShowModal()
about.Destroy()
def report_error(self, message):
self.console_ctrl.SetValue(message)
def log_message(self, message):
self.console_ctrl.AppendText(message)
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
class MySplashScreen(wx.SplashScreen):
def __init__(self):
wx.SplashScreen.__init__(self, images.Splash.GetBitmap(),
wx.SPLASH_CENTRE_ON_SCREEN | wx.SPLASH_TIMEOUT,
2500, None, -1)
self.Bind(wx.EVT_CLOSE, self._on_close)
self.__fc = wx.FutureCall(2000, self._show_main)
def _on_close(self, evt):
# Make sure the default handler runs too so this window gets
# destroyed
evt.Skip()
self.Hide()
# if the timer is still running then go ahead and show the
# main frame now
if self.__fc.IsRunning():
self.__fc.Stop()
self._show_main()
def _show_main(self):
frame = NodeMcuFlasher(None, "NodeMCU PyFlasher")
frame.Show()
if self.__fc.IsRunning():
self.Raise()
# ---------------------------------------------------------------------------
# ----------------------------------------------------------------------------
class App(wx.App, wx.lib.mixins.inspection.InspectionMixin):
def OnInit(self):
wx.SystemOptions.SetOptionInt("mac.window-plain-transition", 1)
self.SetAppName("NodeMCU PyFlasher")
# Create and show the splash screen. It will then create and
# show the main frame when it is time to do so. Normally when
# using a SplashScreen you would create it, show it and then
# continue on with the application's initialization, finally
# creating and showing the main application window(s). In
# this case we have nothing else to do so we'll delay showing
# the main frame until later (see ShowMain above) so the users
# can see the SplashScreen effect.
splash = MySplashScreen()
splash.Show()
return True
# ---------------------------------------------------------------------------
def main():
app = App(False)
app.MainLoop()
# ---------------------------------------------------------------------------
if __name__ == '__main__':
__name__ = 'Main'
main()

View File

@ -1,64 +1,66 @@
# NodeMCU PyFlasher
[![License](https://marcelstoer.github.io/nodemcu-pyflasher/images/mit-license-badge.svg)](https://github.com/marcelstoer/nodemcu-pyflasher/blob/master/LICENSE)
[![Github Releases](https://img.shields.io/github/downloads/marcelstoer/nodemcu-pyflasher/total.svg?style=flat)](https://github.com/marcelstoer/nodemcu-pyflasher/releases)
[![PayPal Donation](https://marcelstoer.github.io/nodemcu-pyflasher/images/donate-paypal-badge.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=HFN4ZMET5XS2Q)
[![Twitter URL](https://marcelstoer.github.io/nodemcu-pyflasher/images/twitter-badge.svg)](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fmarcelstoer%2Fnodemcu-pyflasher)
[![Facebook URL](https://marcelstoer.github.io/nodemcu-pyflasher/images/facebook-badge.svg)](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fgithub.com%2Fmarcelstoer%2Fnodemcu-pyflasher)
# Deprecated
Self-contained [NodeMCU](https://github.com/nodemcu/nodemcu-firmware) flasher with GUI based on [esptool.py](https://github.com/espressif/esptool) and [wxPython](https://www.wxpython.org/).
This project is deprecated in favour of browser based flashing with [ESP Web Tools](https://github.com/esphome/esp-web-tools). For example: https://web.esphome.io
![Image of NodeMCU PyFlasher GUI](images/gui.png)
# ESPHome-Flasher
ESPHome-Flasher is a utility app for the [ESPHome](https://esphome.io/)
framework and is designed to make flashing ESPs with ESPHome 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
NodeMCU PyFlasher doesn't have to be installed, just double-click it and it'll start. Check the [releases section](https://github.com/marcelstoer/nodemcu-pyflasher/releases) for downloads for your platform. For every release there's at least a .exe file for Windows.
## Status
Scan the [list of open issues](https://github.com/marcelstoer/nodemcu-pyflasher/issues) for bugs and pending features.
It doesn't have to be installed, just double-click it and it'll start.
Check the [releases section](https://github.com/esphome/esphome-flasher/releases)
for downloads for your platform.
- Due to [pyinstaller/pyinstaller#2355](https://github.com/pyinstaller/pyinstaller/issues/2355) I can't provide an app bundle for macOS yet. The PyInstaller `.spec` file and the build script are ready, though. Of course you can still *run* the application on macOS. Clone this repo and then do `python nodemcu-pyflasher.py` (see "[Build it yourself](#build-it-yourself)" for pre-requisits).
## Installation Using `pip`
**Note**
If you want to install this application from `pip`:
This is my first Python project. If you have constructive feedback as for how to improve the code please do reach out to me.
## Getting help
In the unlikely event that you're stuck with this simple tool the best way to get help is to turn to the ["Tools and IDE" subforum on esp8266.com](http://www.esp8266.com/viewforum.php?f=22).
## Donationware
All open-source development by the author is donationware. Show your love and support for open-source development by donating to the good cause through PayPal.
[![PayPal Donations](./images/paypal-256.png)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=HFN4ZMET5XS2Q)
- Install Python 3.x
- Install [wxPython 4.x](https://wxpython.org/) manually or run `pip3 install wxpython` (see also linux notes below)
- 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
- Install [wxPython 3.x](https://sourceforge.net/projects/wxpython/files/wxPython/) (not 4.0 betas!)
- Install [esptool.py](https://github.com/espressif/esptool#easy-installation) which brings pySerial or install pySerial standalone
- 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)
## Why this project exists
### Motivation
This addresses an issue the NodeMCU community touched on several times in the past, most recently at
[#1500 (comment)](https://github.com/nodemcu/nodemcu-firmware/pull/1500#issuecomment-247884981).
## Linux Notes
I stated that based on my experience doing NodeMCU user support it should be a lot simpler to flash NodeMCU for Windows users.
Installing wxpython for linux can be a bit challenging (especially when you don't want to install from source).
You can use the following command to install a wxpython suitable with your OS:
- A number of flashing tools are available but only two are actively maintained: esptool-ck and esptool.py. Only one is endorsed by Espressif: [esptool.py](https://github.com/espressif/esptool) (they hired the developer(s)).
- 70% of the users of my [nodemcu-build.com](https://nodemcu-build.com) service are on Windows.
- BUT Windows doesn't come with Python installed - which is required for esptool.py.
- BUT Windows users in general are more reluctant to use the CLI than Linux/Mac users - which is required for esptool.py.
To conclude: this is not a comfortable situation for NodeMCU's largest user group.
### The plan
For quite a while I planned to write a self-contained GUI tool which would use esptool.py in the background. It should primarily target Windows users but since I'm on Mac it should be cross-platform. Even though I had never used Python before I felt confident to pull this off.
### Implementation
- Uses the cross-platform wxPython GUI framework. I also tried PyForms/PyQt4 but settled for wxPython.
- Requires absolutely minimal user input.
- The esptool.py "console" output is redirected to text control on the GUI.
- Uses [PyInstaller](https://github.com/pyinstaller/pyinstaller) to create self-contained executable for Windows and Mac. The packaged app can run standalone i.e. without installing itself, a Python interpreter or any modules.
```bash
# Go to https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ and select the correct OS type
# here, we assume ubuntu 18.03 bionic
pip3 install -U \
-f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-18.04 \
wxPython
```
## License
[MIT](http://opensource.org/licenses/MIT) © Marcel Stör
[MIT](http://opensource.org/licenses/MIT) © Marcel Stör, Otto Winter

13
build-instructions.md Normal file
View File

@ -0,0 +1,13 @@
# macOS
`pyinstaller -F -w -n ESPHome-Flasher -i icon.icns esphomeflasher/__main__.py`
# Windows
1. Start up VM
2. Install Python (3) from App Store
3. Download esphome-flasher from GitHub
4. `pip install -e.` and `pip install pyinstaller`
5. Check with `python -m esphomeflasher.__main__`
6. `python -m PyInstaller.__main__ -F -w -n ESPHome-Flasher -i icon.ico esphomeflasher\__main__.py`
7. Go to `dist` folder, check ESPHome-Flasher.exe works.

View File

@ -1,31 +0,0 @@
# -*- mode: python -*-
block_cipher = None
a = Analysis(['nodemcu-pyflasher.py'],
binaries=None,
datas=[("images", "images")],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name='NodeMCU-PyFlasher',
debug=False,
strip=False,
upx=True,
console=False , icon='images/icon-256.icns')
app = BUNDLE(exe,
name='NodeMCU-PyFlasher.app',
icon='./images/icon-256.icns',
bundle_identifier='com.frightanic.nodemcu-pyflasher')

View File

@ -1,26 +0,0 @@
# -*- mode: python -*-
block_cipher = None
a = Analysis(['nodemcu-pyflasher.py'],
binaries=[],
datas=[("images", "images")],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name='NodeMCU-PyFlasher',
debug=False,
strip=False,
upx=True,
console=False , icon='images\\icon-256.ico')

View File

@ -1,3 +0,0 @@
pyinstaller --log-level=DEBUG ^
--noconfirm ^
build-on-win.spec

View File

@ -1,5 +0,0 @@
#!/usr/bin/env bash
#rm -fr build dist
pyinstaller --log-level=DEBUG \
--noconfirm \
build-on-mac.spec

View File

@ -1,21 +0,0 @@
"""
This is a way to save the startup time when running img2py on lots of
files...
"""
from wx.tools import img2py
command_lines = [
"-F -n Exit images/exit.png images.py",
"-a -F -n Reload images/reload.png images.py",
"-a -F -n Splash images/splash.png images.py",
"-a -F -n Info images/info.png images.py",
"-a -F -i -n Icon images/icon-256.png images.py",
]
if __name__ == "__main__":
for line in command_lines:
args = line.split()
img2py.main(args)

View File

221
esphomeflasher/__main__.py Normal file
View File

@ -0,0 +1,221 @@
from __future__ import print_function
import argparse
import sys
import time
from datetime import datetime
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=f"esphomeflasher {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")
group.add_argument(
"--upload-baud-rate",
type=int,
default=460800,
help="Baud rate to upload with (not for logging)",
)
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("--show-logs", help="Only show logs", 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(f"Using '{args.port}' as serial 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(f" * {port} ({desc})")
print("Please choose one with the --port argument.")
raise EsphomeflasherError
print(f"Auto-detected serial port: {ports[0][0]}")
return ports[0][0]
def show_logs(serial_port):
print("Showing logs:")
with serial_port:
while True:
try:
raw = serial_port.readline()
except serial.SerialException:
print("Serial port closed!")
return
text = raw.decode(errors="ignore")
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 run_esphomeflasher(argv):
args = parse_args(argv)
port = select_port(args)
if args.show_logs:
serial_port = serial.Serial(port, baudrate=115200)
show_logs(serial_port)
return
try:
# pylint: disable=consider-using-with
firmware = open(args.binary, "rb")
except IOError as err:
raise EsphomeflasherError(f"Error opening binary: {err}") from err
chip = detect_chip(port, args.esp8266, args.esp32)
info = read_chip_info(chip)
print()
print("Chip Info:")
print(f" - Chip Family: {info.family}")
print(f" - Chip Model: {info.model}")
if isinstance(info, ESP32ChipInfo):
print(f" - Number of Cores: {info.num_cores}")
print(f" - Max CPU Frequency: {info.cpu_frequency}")
print(f" - Has Bluetooth: {'YES' if info.has_bluetooth else 'NO'}")
print(f" - Has Embedded Flash: {'YES' if info.has_embedded_flash else 'NO'}")
print(
f" - Has Factory-Calibrated ADC: {'YES' if info.has_factory_calibrated_adc else 'NO'}"
)
else:
print(f" - Chip ID: {info.chip_id:08X}")
print(f" - MAC Address: {info.mac}")
stub_chip = chip_run_stub(chip)
flash_size = None
if args.upload_baud_rate != 115200:
try:
stub_chip.change_baud(args.upload_baud_rate)
except esptool.FatalError as err:
raise EsphomeflasherError(
f"Error changing ESP upload baud rate: {err}"
) from err
# Check if the higher baud rate works
try:
flash_size = detect_flash_size(stub_chip)
except EsphomeflasherError:
# Go back to old baud rate by recreating chip instance
print(
f"Chip does not support baud rate {args.upload_baud_rate}, changing to 115200"
)
# pylint: disable=protected-access
stub_chip._port.close()
chip = detect_chip(port, args.esp8266, args.esp32)
stub_chip = chip_run_stub(chip)
if flash_size is None:
flash_size = detect_flash_size(stub_chip)
print(f" - Flash Size: {flash_size}")
mock_args = configure_write_flash_args(
info, firmware, flash_size, args.bootloader, args.partitions, args.otadata
)
print(f" - Flash Mode: {mock_args.flash_mode}")
print(f" - Flash Frequency: {mock_args.flash_freq.upper()}Hz")
try:
stub_chip.flash_set_parameters(esptool.flash_size_bytes(flash_size))
except esptool.FatalError as err:
raise EsphomeflasherError(f"Error setting flash parameters: {err}") from err
if not args.no_erase:
try:
esptool.erase_flash(stub_chip, mock_args)
except esptool.FatalError as err:
raise EsphomeflasherError(f"Error while erasing flash: {err}") from err
try:
esptool.write_flash(stub_chip, mock_args)
except esptool.FatalError as err:
raise EsphomeflasherError(f"Error while writing flash: {err}") from err
print("Hard Resetting...")
stub_chip.hard_reset()
print("Done! Flashing is complete!")
print()
if args.upload_baud_rate != 115200:
# pylint: disable=protected-access
stub_chip._port.baudrate = 115200
time.sleep(0.05) # get rid of crap sent during baud rate change
# pylint: disable=protected-access
stub_chip._port.flushInput()
# pylint: disable=protected-access
show_logs(stub_chip._port)
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())

234
esphomeflasher/common.py Normal file
View File

@ -0,0 +1,234 @@
import io
import struct
import esptool
from esphomeflasher.const import HTTP_REGEX
from esphomeflasher.helpers import prevent_print
class EsphomeflasherError(Exception):
pass
class MockEsptoolArgs:
def __init__(self, flash_size, addr_filename, flash_mode, flash_freq):
self.compress = True
self.no_compress = False
self.flash_size = flash_size
self.addr_filename = addr_filename
self.flash_mode = flash_mode
self.flash_freq = flash_freq
self.no_stub = False
self.verify = False
self.erase_all = False
self.encrypt = False
self.encrypt_files = None
class ChipInfo:
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().__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().__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(f"Reading chip details failed: {err}") from err
def read_chip_info(chip):
mac = ":".join(f"{x:02X}" 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,
)
if 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(f"Unknown chip type {type(chip)}")
def chip_run_stub(chip):
try:
return chip.run_stub()
except esptool.FatalError as err:
raise EsphomeflasherError(
f"Error putting ESP in stub flash mode: {err}"
) from 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(
f"The firmware binary is invalid (magic byte={magic:02X}, should be {esptool.ESPLoader.ESP_IMAGE_MAGIC:02X})"
)
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(
f"Timeout while retrieving firmware file '{path}': {err}"
) from err
except requests.exceptions.RequestException as err:
raise EsphomeflasherError(
f"Error while retrieving firmware file '{path}': {err}"
) from err
binary = io.BytesIO()
binary.write(response.content)
binary.seek(0)
return binary
try:
return open(path, "rb")
except IOError as err:
raise EsphomeflasherError(f"Error opening binary '{path}': {err}") from 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(
f"No bootloader available for flash frequency {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, flash_mode, flash_freq)
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:
if "Wrong boot mode detected" in str(err):
msg = "ESP is not in flash boot mode. If your board has a flashing pin, try again while keeping it pressed."
else:
msg = f"ESP Chip Auto-Detection failed: {err}"
raise EsphomeflasherError(msg) from err
try:
chip.connect()
except esptool.FatalError as err:
raise EsphomeflasherError(f"Error connecting to ESP: {err}") from err
return chip

17
esphomeflasher/const.py Normal file
View File

@ -0,0 +1,17 @@
import re
__version__ = "1.4.0"
ESP32_DEFAULT_OTA_DATA = "https://raw.githubusercontent.com/espressif/arduino-esp32/1.0.0/tools/partitions/boot_app0.bin"
ESP32_DEFAULT_BOOTLOADER_FORMAT = (
"https://raw.githubusercontent.com/espressif/arduino-esp32/"
"1.0.4/tools/sdk/bin/bootloader_$FLASH_MODE$_$FLASH_FREQ$.bin"
)
ESP32_DEFAULT_PARTITIONS = (
"https://raw.githubusercontent.com/esphome/esphomeflasher/main/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@:%_+.~#?&/=]*)"
)

371
esphomeflasher/gui.py Normal file
View File

@ -0,0 +1,371 @@
# This GUI is a fork of the brilliant https://github.com/marcelstoer/nodemcu-pyflasher
import re
import sys
import threading
from io import TextIOBase
import wx
import wx.adv
import wx.lib.inspection
import wx.lib.mixins.inspection
from wx.lib.embeddedimage import PyEmbeddedImage
from esphomeflasher.helpers import list_serial_ports
# pylint: disable=no-member
COLOR_RE = re.compile(r"(?:\033)(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))")
COLORS = {
"black": wx.BLACK,
"red": wx.RED,
"green": wx.GREEN,
"yellow": wx.YELLOW,
"blue": wx.BLUE,
"magenta": wx.Colour(255, 0, 255),
"cyan": wx.CYAN,
"white": wx.WHITE,
}
FORE_COLORS = {**COLORS, None: wx.WHITE}
BACK_COLORS = {**COLORS, None: wx.BLACK}
# See discussion at http://stackoverflow.com/q/41101897/131929
class RedirectText(TextIOBase):
def __init__(self, text_ctrl):
super().__init__()
self._out = text_ctrl
self._i = 0
self._line = ""
self._bold = False
self._italic = False
self._underline = False
self._foreground = None
self._background = None
self._secret = False
def _add_content(self, value):
attr = wx.TextAttr()
if self._bold:
attr.SetFontWeight(wx.FONTWEIGHT_BOLD)
attr.SetTextColour(FORE_COLORS[self._foreground])
attr.SetBackgroundColour(BACK_COLORS[self._background])
wx.CallAfter(self._out.SetDefaultStyle, attr)
wx.CallAfter(self._out.AppendText, value)
def _write_line(self):
pos = 0
while True:
match = COLOR_RE.search(self._line, pos)
if match is None:
break
j = match.start()
self._add_content(self._line[pos:j])
pos = match.end()
for code in match.group(1).split(";"):
code = int(code)
if code == 0:
self._bold = False
self._italic = False
self._underline = False
self._foreground = None
self._background = None
self._secret = False
elif code == 1:
self._bold = True
elif code == 3:
self._italic = True
elif code == 4:
self._underline = True
elif code == 5:
self._secret = True
elif code == 6:
self._secret = False
elif code == 22:
self._bold = False
elif code == 23:
self._italic = False
elif code == 24:
self._underline = False
elif code == 30:
self._foreground = "black"
elif code == 31:
self._foreground = "red"
elif code == 32:
self._foreground = "green"
elif code == 33:
self._foreground = "yellow"
elif code == 34:
self._foreground = "blue"
elif code == 35:
self._foreground = "magenta"
elif code == 36:
self._foreground = "cyan"
elif code == 37:
self._foreground = "white"
elif code == 39:
self._foreground = None
elif code == 40:
self._background = "black"
elif code == 41:
self._background = "red"
elif code == 42:
self._background = "green"
elif code == 43:
self._background = "yellow"
elif code == 44:
self._background = "blue"
elif code == 45:
self._background = "magenta"
elif code == 46:
self._background = "cyan"
elif code == 47:
self._background = "white"
elif code == 49:
self._background = None
self._add_content(self._line[pos:])
def write(self, string):
# pylint: disable=invalid-name
for s in string:
if s == "\r":
current_value = self._out.GetValue()
last_newline = current_value.rfind("\n")
wx.CallAfter(self._out.Remove, last_newline + 1, len(current_value))
# self._line += '\n'
self._write_line()
self._line = ""
continue
self._line += s
if s == "\n":
self._write_line()
self._line = ""
continue
def writable(self):
return True
def isatty(self) -> bool:
return True
class FlashingThread(threading.Thread):
def __init__(self, parent, firmware, port, show_logs=False):
threading.Thread.__init__(self)
self.daemon = True
self._parent = parent
self._firmware = firmware
self._port = port
self._show_logs = show_logs
def run(self):
try:
from esphomeflasher.__main__ import run_esphomeflasher
argv = ["esphomeflasher", "--port", self._port, self._firmware]
if self._show_logs:
argv.append("--show-logs")
run_esphomeflasher(argv)
except Exception as err:
print(f"Unexpected error: {err}")
raise
class MainFrame(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(
self,
parent,
-1,
title,
size=(725, 650),
style=wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE,
)
self._firmware = None
self._port = None
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_logs_clicked(event):
self.console_ctrl.SetValue("")
worker = FlashingThread(self, "dummy", self._port, show_logs=True)
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_NOT, 20)
button = wx.Button(panel, -1, "Flash ESP")
button.Bind(wx.EVT_BUTTON, on_clicked)
logs_button = wx.Button(panel, -1, "View Logs")
logs_button.Bind(wx.EVT_BUTTON, on_logs_clicked)
self.console_ctrl = wx.TextCtrl(
panel, style=wx.TE_MULTILINE | wx.TE_READONLY | wx.HSCROLL
)
self.console_ctrl.SetFont(
wx.Font(
(0, 13),
wx.FONTFAMILY_TELETYPE,
wx.FONTSTYLE_NORMAL,
wx.FONTWEIGHT_NORMAL,
)
)
self.console_ctrl.SetBackgroundColour(wx.BLACK)
self.console_ctrl.SetForegroundColour(wx.WHITE)
self.console_ctrl.SetDefaultStyle(wx.TextAttr(wx.WHITE))
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 selection row
port_label,
(serial_boxsizer, 1, wx.EXPAND),
# Firmware selection row (growable)
file_label,
(file_picker, 1, wx.EXPAND),
# Flash ESP button
(wx.StaticText(panel, label="")),
(button, 1, wx.EXPAND),
# View Logs button
(wx.StaticText(panel, label="")),
(logs_button, 1, wx.EXPAND),
# Console View (growable)
(console_label, 1, wx.EXPAND),
(self.console_ctrl, 1, wx.EXPAND),
]
)
fgs.AddGrowableRow(4, 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, _ in list_serial_ports():
ports.append(port)
if not self._port and ports:
self._port = ports[0]
if not ports:
ports.append("")
return ports
# 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):
# pylint: disable=invalid-name
def OnInit(self):
wx.SystemOptions.SetOption("mac.window-plain-transition", 1)
self.SetAppName("esphome-flasher (Based on NodeMCU PyFlasher)")
frame = MainFrame(None, "esphome-flasher (Based on NodeMCU PyFlasher)")
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()

38
esphomeflasher/helpers.py Normal file
View File

@ -0,0 +1,38 @@
from __future__ import print_function
import os
import sys
import serial
# pylint: disable=unspecified-encoding,consider-using-with
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(f"Serial port closed: {err}") from err
finally:
sys.stdout = orig_sys_stdout

View File

@ -1,12 +0,0 @@
If esptool.py is installed using python setup.py` from a checked out version it creates something like the following
in the Python environment
esptool.py file containing
#!/usr/local/opt/python/bin/python2.7
# EASY-INSTALL-SCRIPT: 'esptool==1.3.dev0','esptool.py'
__requires__ = 'esptool==1.3.dev0'
__import__('pkg_resources').run_script('esptool==1.3.dev0', 'esptool.py')
PyInstaller (and cx_Freeze) doesn't support pkg_resources and complains about 'ImportError: "No module named
pkg_resources"'. This can be avoided if the application maintains a local copy of esptool.py.

2412
esptool.py

File diff suppressed because it is too large Load Diff

BIN
icon.icns Normal file

Binary file not shown.

BIN
icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

8183
images.py

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 995 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 953 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

View File

@ -1,4 +0,0 @@
#!/usr/bin/env python
import Main
Main.main()

BIN
partitions.bin Normal file

Binary file not shown.

24
pyproject.toml Normal file
View File

@ -0,0 +1,24 @@
[tool.isort]
profile = "black"
multi_line_output = 3
[tool.black]
target-version = ['py37']
[tool.pylint.MASTER]
reports = 'no'
disable = [
"too-many-branches",
"missing-function-docstring",
"missing-module-docstring",
"too-many-statements",
"import-outside-toplevel",
"line-too-long",
"missing-class-docstring",
"too-few-public-methods",
"too-many-arguments",
"too-many-instance-attributes",
"too-many-locals",
"unused-argument",
"cyclic-import",
]

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
wxpython>=4.1.1,<5.0
esptool==3.2
requests>=2.26.0,<3

2
requirements_build.txt Normal file
View File

@ -0,0 +1,2 @@
pyinstaller>=4.5.1,<5
wheel

4
requirements_test.txt Normal file
View File

@ -0,0 +1,4 @@
pylint==2.13.4
black==22.3.0
flake8==4.0.1
isort==5.10.1

14
setup.cfg Normal file
View File

@ -0,0 +1,14 @@
[flake8]
max-line-length = 120
# Following 4 for black compatibility
# E501: line too long
# W503: Line break occurred before a binary operator
# E203: Whitespace before ':'
# D202 No blank lines allowed after function docstring
ignore =
E501,
W503,
E203,
D202,

59
setup.py Normal file
View File

@ -0,0 +1,59 @@
#!/usr/bin/env python
"""esphomeflasher setup script."""
import os
from setuptools import setup, find_packages
from esphomeflasher import const
PROJECT_NAME = 'esphomeflasher'
PROJECT_PACKAGE_NAME = 'esphomeflasher'
PROJECT_LICENSE = 'MIT'
PROJECT_AUTHOR = 'ESPHome'
PROJECT_COPYRIGHT = '2021, ESPHome'
PROJECT_URL = 'https://esphome.io/guides/faq.html'
PROJECT_EMAIL = 'contact@esphome.io'
PROJECT_GITHUB_USERNAME = 'esphome'
PROJECT_GITHUB_REPOSITORY = 'esphome-flasher'
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__)
here = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(here, 'requirements.txt')) as requirements_txt:
REQUIRES = requirements_txt.read().splitlines()
with open(os.path.join(here, 'README.md')) as readme:
LONG_DESCRIPTION = readme.read()
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 ESPHome",
include_package_data=True,
zip_safe=False,
platforms='any',
test_suite='tests',
python_requires='>=3.7,<4.0',
install_requires=REQUIRES,
long_description=LONG_DESCRIPTION,
long_description_content_type='text/markdown',
keywords=['home', 'automation'],
entry_points={
'console_scripts': [
'esphomeflasher = esphomeflasher.__main__:main'
]
},
packages=find_packages(include="esphomerelease.*")
)