Compare commits
No commits in common. "v2.1" and "main" have entirely different histories.
|
@ -0,0 +1,10 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
|
@ -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
|
|
@ -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' }}
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"problemMatcher": [
|
||||
{
|
||||
"owner": "isort",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^ERROR:\\s+(.+)\\s+(.+)$",
|
||||
"file": 1,
|
||||
"message": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
|
@ -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.
|
|
@ -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/
|
||||
|
|
72
About.py
|
@ -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>© 2017 Marcel Stö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())
|
||||
|
||||
# ---------------------------------------------------------------------------
|
|
@ -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())
|
4
LICENSE
|
@ -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.
|
|
@ -0,0 +1,3 @@
|
|||
include LICENSE
|
||||
include README.md
|
||||
include requirements.txt
|
440
Main.py
|
@ -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()
|
94
README.md
|
@ -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
|
||||
|
|
|
@ -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.
|
|
@ -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')
|
|
@ -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')
|
|
@ -1,3 +0,0 @@
|
|||
pyinstaller --log-level=DEBUG ^
|
||||
--noconfirm ^
|
||||
build-on-win.spec
|
5
build.sh
|
@ -1,5 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
#rm -fr build dist
|
||||
pyinstaller --log-level=DEBUG \
|
||||
--noconfirm \
|
||||
build-on-mac.spec
|
|
@ -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)
|
||||
|
|
@ -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())
|
|
@ -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
|
|
@ -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@:%_+.~#?&/=]*)"
|
||||
)
|
|
@ -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()
|
|
@ -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
|
|
@ -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
Before Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 11 KiB |
BIN
images/exit.png
Before Width: | Height: | Size: 995 B |
BIN
images/gui.png
Before Width: | Height: | Size: 405 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 4.8 KiB |
BIN
images/info.png
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 953 B |
Before Width: | Height: | Size: 399 KiB |
Before Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 9.3 KiB |
|
@ -1,4 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import Main
|
||||
Main.main()
|
|
@ -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",
|
||||
]
|
|
@ -0,0 +1,3 @@
|
|||
wxpython>=4.1.1,<5.0
|
||||
esptool==3.2
|
||||
requests>=2.26.0,<3
|
|
@ -0,0 +1,2 @@
|
|||
pyinstaller>=4.5.1,<5
|
||||
wheel
|
|
@ -0,0 +1,4 @@
|
|||
pylint==2.13.4
|
||||
black==22.3.0
|
||||
flake8==4.0.1
|
||||
isort==5.10.1
|
|
@ -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,
|
||||
|
|
@ -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.*")
|
||||
)
|