├── .gitignore
├── About.py
├── HtmlPopupTransientWindow.py
├── LICENSE
├── Main.py
├── README.md
├── build-on-mac.spec
├── build-on-win.spec
├── build.bat
├── build.sh
├── encode-bitmaps.py
├── images.py
├── images
├── NodeMCU-icon-org.png
├── espressif-256.png
├── espressif-64.png
├── exit.png
├── gui.png
├── icon-256.icns
├── icon-256.ico
├── icon-256.png
├── icon-64.png
├── info.png
├── paypal-256.png
├── python-256.png
├── python-64.png
├── reload.png
├── splash.png
├── wxpython-256.png
└── wxpython-64.png
├── nodemcu-pyflasher.py
├── requirements.txt
├── windows-metadata.yaml
└── windows-version-info.txt
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .DS_Store
3 |
4 | # Byte-compiled / optimized / DLL files
5 | __pycache__/
6 | *.py[cod]
7 | *$py.class
8 |
9 | # C extensions
10 | *.so
11 |
12 | # Distribution / packaging
13 | .Python
14 | env/
15 | build/
16 | develop-eggs/
17 | dist/
18 | downloads/
19 | eggs/
20 | .eggs/
21 | lib/
22 | lib64/
23 | parts/
24 | sdist/
25 | var/
26 | *.egg-info/
27 | .installed.cfg
28 | *.egg
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | #*.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *,cover
49 | .hypothesis/
50 |
51 | # Translations
52 | *.mo
53 | *.pot
54 |
55 | # Django stuff:
56 | *.log
57 | local_settings.py
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # IPython Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # dotenv
82 | .env
83 |
84 | # virtualenv
85 | venv/
86 | ENV/
87 | .venv
88 |
89 | # Spyder project settings
90 | .spyderproject
91 |
92 | # Rope project settings
93 | .ropeproject
--------------------------------------------------------------------------------
/About.py:
--------------------------------------------------------------------------------
1 |
2 | # coding=utf-8
3 |
4 | import sys
5 | import datetime
6 | import os
7 | import wx
8 | import wx.html
9 | import wx.lib.wxpTag
10 | import webbrowser
11 | from Main import __version__
12 |
13 | # ---------------------------------------------------------------------------
14 |
15 |
16 | class AboutDlg(wx.Dialog):
17 | text = '''
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | NodeMCU PyFlasher
27 |
28 | Version {1}
29 |
30 | Fork the project on
31 | GitHub and help improve it for all!
32 |
33 |
34 | As with everything I offer for free, this is donation-ware.
35 |
36 |
37 |
38 |
39 |
40 | © {2} Marcel Stör. Licensed under MIT.
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | '''
52 |
53 | def __init__(self, parent):
54 | wx.Dialog.__init__(self, parent, wx.ID_ANY, "About NodeMCU PyFlasher")
55 | html = HtmlWindow(self, wx.ID_ANY, size=(420, -1))
56 | if "gtk2" in wx.PlatformInfo or "gtk3" in wx.PlatformInfo:
57 | html.SetStandardFonts()
58 | txt = self.text.format(self._get_bundle_dir(), __version__, datetime.datetime.now().year)
59 | html.SetPage(txt)
60 | ir = html.GetInternalRepresentation()
61 | html.SetSize((ir.GetWidth() + 25, ir.GetHeight() + 25))
62 | self.SetClientSize(html.GetSize())
63 | self.CentreOnParent(wx.BOTH)
64 |
65 | @staticmethod
66 | def _get_bundle_dir():
67 | # set by PyInstaller, see http://pyinstaller.readthedocs.io/en/v3.2/runtime-information.html
68 | if getattr(sys, 'frozen', False):
69 | # noinspection PyUnresolvedReferences,PyProtectedMember
70 | return sys._MEIPASS
71 | else:
72 | return os.path.dirname(os.path.abspath(__file__))
73 |
74 |
75 | class HtmlWindow(wx.html.HtmlWindow):
76 | def OnLinkClicked(self, link):
77 | webbrowser.open(link.GetHref())
78 |
79 | # ---------------------------------------------------------------------------
80 |
--------------------------------------------------------------------------------
/HtmlPopupTransientWindow.py:
--------------------------------------------------------------------------------
1 |
2 | # coding=utf-8
3 |
4 | import wx
5 | import wx.html
6 | import webbrowser
7 |
8 |
9 | class HtmlPopupTransientWindow(wx.PopupTransientWindow):
10 | def __init__(self, parent, style, html_body_content, bgcolor, size):
11 | wx.PopupTransientWindow.__init__(self, parent, style)
12 | panel = wx.Panel(self)
13 | panel.SetBackgroundColour(bgcolor)
14 |
15 | html_window = self.HtmlWindow(panel, wx.ID_ANY, size=size)
16 | html_window.SetPage('' + html_body_content + '')
17 |
18 | sizer = wx.BoxSizer(wx.VERTICAL)
19 | sizer.Add(html_window, 0, wx.ALL, 5)
20 | panel.SetSizer(sizer)
21 |
22 | sizer.Fit(panel)
23 | sizer.Fit(self)
24 | self.Layout()
25 |
26 | class HtmlWindow(wx.html.HtmlWindow):
27 | def OnLinkClicked(self, link):
28 | # get a hold of the PopupTransientWindow to close it
29 | self.GetParent().GetParent().Dismiss()
30 | webbrowser.open(link.GetHref())
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Marcel Stör
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Main.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import wx
4 | import wx.adv
5 | import wx.lib.inspection
6 | import wx.lib.mixins.inspection
7 |
8 | import sys
9 | import os
10 | import esptool
11 | import threading
12 | import json
13 | import images as images
14 | from serial import SerialException
15 | from serial.tools import list_ports
16 | import locale
17 |
18 | # see https://discuss.wxpython.org/t/wxpython4-1-1-python3-8-locale-wxassertionerror/35168
19 | locale.setlocale(locale.LC_ALL, 'C')
20 |
21 | __version__ = "5.1.0"
22 | __flash_help__ = '''
23 | This setting is highly dependent on your device!
24 |
25 | Details at http://bit.ly/2v5Rd32
27 | and in the esptool
28 | documentation
29 |
30 | - Most ESP32 and ESP8266 ESP-12 use DIO.
31 | - Most ESP8266 ESP-01/07 use QIO.
32 | - ESP8285 requires DOUT.
33 |
34 |
35 | '''
36 | __auto_select__ = "Auto-select"
37 | __auto_select_explanation__ = "(first port with Espressif device)"
38 | __supported_baud_rates__ = [9600, 57600, 74880, 115200, 230400, 460800, 921600]
39 |
40 | # ---------------------------------------------------------------------------
41 |
42 |
43 | # See discussion at http://stackoverflow.com/q/41101897/131929
44 | class RedirectText:
45 | def __init__(self, text_ctrl):
46 | self.__out = text_ctrl
47 |
48 | def write(self, string):
49 | if string.startswith("\r"):
50 | # carriage return -> remove last line i.e. reset position to start of last line
51 | current_value = self.__out.GetValue()
52 | last_newline = current_value.rfind("\n")
53 | new_value = current_value[:last_newline + 1] # preserve \n
54 | new_value += string[1:] # chop off leading \r
55 | wx.CallAfter(self.__out.SetValue, new_value)
56 | else:
57 | wx.CallAfter(self.__out.AppendText, string)
58 |
59 | # noinspection PyMethodMayBeStatic
60 | def flush(self):
61 | # noinspection PyStatementEffect
62 | None
63 |
64 | # esptool >=3 handles output differently of the output stream is not a TTY
65 | # noinspection PyMethodMayBeStatic
66 | def isatty(self):
67 | return True
68 |
69 | # ---------------------------------------------------------------------------
70 |
71 |
72 | # ---------------------------------------------------------------------------
73 | class FlashingThread(threading.Thread):
74 | def __init__(self, parent, config):
75 | threading.Thread.__init__(self)
76 | self.daemon = True
77 | self._parent = parent
78 | self._config = config
79 |
80 | def run(self):
81 | try:
82 | command = []
83 |
84 | if not self._config.port.startswith(__auto_select__):
85 | command.append("--port")
86 | command.append(self._config.port)
87 |
88 | command.extend(["--baud", str(self._config.baud),
89 | "--after", "no_reset",
90 | "write_flash",
91 | # https://github.com/espressif/esptool/issues/599
92 | "--flash_size", "detect",
93 | "--flash_mode", self._config.mode,
94 | "0x00000", self._config.firmware_path])
95 |
96 | if self._config.erase_before_flash:
97 | command.append("--erase-all")
98 |
99 | print("Command: esptool.py %s\n" % " ".join(command))
100 |
101 | esptool.main(command)
102 |
103 | # The last line printed by esptool is "Staying in bootloader." -> some indication that the process is
104 | # done is needed
105 | print("\nFirmware successfully flashed. Unplug/replug or reset device \nto switch back to normal boot "
106 | "mode.")
107 | except SerialException as e:
108 | self._parent.report_error(e.strerror)
109 | raise e
110 |
111 |
112 | # ---------------------------------------------------------------------------
113 |
114 |
115 | # ---------------------------------------------------------------------------
116 | # DTO between GUI and flashing thread
117 | class FlashConfig:
118 | def __init__(self):
119 | self.baud = 115200
120 | self.erase_before_flash = False
121 | self.mode = "dio"
122 | self.firmware_path = None
123 | self.port = None
124 |
125 | @classmethod
126 | def load(cls, file_path):
127 | conf = cls()
128 | if os.path.exists(file_path):
129 | with open(file_path, 'r') as f:
130 | data = json.load(f)
131 | conf.port = data['port']
132 | conf.baud = data['baud']
133 | conf.mode = data['mode']
134 | conf.erase_before_flash = data['erase']
135 | return conf
136 |
137 | def safe(self, file_path):
138 | data = {
139 | 'port': self.port,
140 | 'baud': self.baud,
141 | 'mode': self.mode,
142 | 'erase': self.erase_before_flash,
143 | }
144 | with open(file_path, 'w') as f:
145 | json.dump(data, f)
146 |
147 | def is_complete(self):
148 | return self.firmware_path is not None and self.port is not None
149 |
150 | # ---------------------------------------------------------------------------
151 |
152 |
153 | # ---------------------------------------------------------------------------
154 | class NodeMcuFlasher(wx.Frame):
155 |
156 | def __init__(self, parent, title):
157 | wx.Frame.__init__(self, parent, -1, title, size=(725, 650),
158 | style=wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE)
159 | self._config = FlashConfig.load(self._get_config_file_path())
160 |
161 | self._build_status_bar()
162 | self._set_icons()
163 | self._build_menu_bar()
164 | self._init_ui()
165 |
166 | sys.stdout = RedirectText(self.console_ctrl)
167 |
168 | self.Centre(wx.BOTH)
169 | self.Show(True)
170 | print("Connect your device")
171 | print("\nIf you chose the serial port auto-select feature you might need to ")
172 | print("turn off Bluetooth")
173 |
174 | def _init_ui(self):
175 | def on_reload(event):
176 | self.choice.SetItems(self._get_serial_ports())
177 |
178 | def on_baud_changed(event):
179 | radio_button = event.GetEventObject()
180 |
181 | if radio_button.GetValue():
182 | self._config.baud = radio_button.rate
183 |
184 | def on_mode_changed(event):
185 | radio_button = event.GetEventObject()
186 |
187 | if radio_button.GetValue():
188 | self._config.mode = radio_button.mode
189 |
190 | def on_erase_changed(event):
191 | radio_button = event.GetEventObject()
192 |
193 | if radio_button.GetValue():
194 | self._config.erase_before_flash = radio_button.erase
195 |
196 | def on_clicked(event):
197 | self.console_ctrl.SetValue("")
198 | worker = FlashingThread(self, self._config)
199 | worker.start()
200 |
201 | def on_select_port(event):
202 | choice = event.GetEventObject()
203 | self._config.port = choice.GetString(choice.GetSelection())
204 |
205 | def on_pick_file(event):
206 | self._config.firmware_path = event.GetPath().replace("'", "")
207 |
208 | panel = wx.Panel(self)
209 |
210 | # Fix popup that never goes away.
211 | def onHover(event):
212 | global hovered
213 | if(len(hovered) != 0 ):
214 | hovered[0].Dismiss()
215 | hovered = []
216 |
217 | panel.Bind(wx.EVT_MOTION,onHover)
218 |
219 | hbox = wx.BoxSizer(wx.HORIZONTAL)
220 |
221 | fgs = wx.FlexGridSizer(7, 2, 10, 10)
222 |
223 | self.choice = wx.Choice(panel, choices=self._get_serial_ports())
224 | self.choice.Bind(wx.EVT_CHOICE, on_select_port)
225 | self._select_configured_port()
226 |
227 | reload_button = wx.Button(panel, label="Reload")
228 | reload_button.Bind(wx.EVT_BUTTON, on_reload)
229 | reload_button.SetToolTip("Reload serial device list")
230 |
231 | file_picker = wx.FilePickerCtrl(panel, style=wx.FLP_USE_TEXTCTRL)
232 | file_picker.Bind(wx.EVT_FILEPICKER_CHANGED, on_pick_file)
233 |
234 | serial_boxsizer = wx.BoxSizer(wx.HORIZONTAL)
235 | serial_boxsizer.Add(self.choice, 1, wx.EXPAND)
236 | serial_boxsizer.Add(reload_button, flag=wx.LEFT, border=10)
237 |
238 | baud_boxsizer = wx.BoxSizer(wx.HORIZONTAL)
239 |
240 | def add_baud_radio_button(sizer, index, baud_rate):
241 | style = wx.RB_GROUP if index == 0 else 0
242 | radio_button = wx.RadioButton(panel, name="baud-%d" % baud_rate, label="%d" % baud_rate, style=style)
243 | radio_button.rate = baud_rate
244 | # sets default value
245 | radio_button.SetValue(baud_rate == self._config.baud)
246 | radio_button.Bind(wx.EVT_RADIOBUTTON, on_baud_changed)
247 | sizer.Add(radio_button)
248 | sizer.AddSpacer(10)
249 |
250 | for idx, rate in enumerate(__supported_baud_rates__):
251 | add_baud_radio_button(baud_boxsizer, idx, rate)
252 |
253 | flashmode_boxsizer = wx.BoxSizer(wx.HORIZONTAL)
254 |
255 | def add_flash_mode_radio_button(sizer, index, mode, label):
256 | style = wx.RB_GROUP if index == 0 else 0
257 | radio_button = wx.RadioButton(panel, name="mode-%s" % mode, label="%s" % label, style=style)
258 | radio_button.Bind(wx.EVT_RADIOBUTTON, on_mode_changed)
259 | radio_button.mode = mode
260 | radio_button.SetValue(mode == self._config.mode)
261 | sizer.Add(radio_button)
262 | sizer.AddSpacer(10)
263 |
264 | add_flash_mode_radio_button(flashmode_boxsizer, 0, "qio", "Quad I/O (QIO)")
265 | add_flash_mode_radio_button(flashmode_boxsizer, 1, "dio", "Dual I/O (DIO)")
266 | add_flash_mode_radio_button(flashmode_boxsizer, 2, "dout", "Dual Output (DOUT)")
267 |
268 | erase_boxsizer = wx.BoxSizer(wx.HORIZONTAL)
269 |
270 | def add_erase_radio_button(sizer, index, erase_before_flash, label, value):
271 | style = wx.RB_GROUP if index == 0 else 0
272 | radio_button = wx.RadioButton(panel, name="erase-%s" % erase_before_flash, label="%s" % label, style=style)
273 | radio_button.Bind(wx.EVT_RADIOBUTTON, on_erase_changed)
274 | radio_button.erase = erase_before_flash
275 | radio_button.SetValue(value)
276 | sizer.Add(radio_button)
277 | sizer.AddSpacer(10)
278 |
279 | erase = self._config.erase_before_flash
280 | add_erase_radio_button(erase_boxsizer, 0, False, "no", erase is False)
281 | add_erase_radio_button(erase_boxsizer, 1, True, "yes, wipes all data", erase is True)
282 |
283 | button = wx.Button(panel, -1, "Flash NodeMCU")
284 | button.Bind(wx.EVT_BUTTON, on_clicked)
285 |
286 | self.console_ctrl = wx.TextCtrl(panel, style=wx.TE_MULTILINE | wx.TE_READONLY | wx.HSCROLL)
287 | self.console_ctrl.SetFont(wx.Font((0, 13), wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL,
288 | wx.FONTWEIGHT_NORMAL))
289 | self.console_ctrl.SetBackgroundColour(wx.WHITE)
290 | self.console_ctrl.SetForegroundColour(wx.BLUE)
291 | self.console_ctrl.SetDefaultStyle(wx.TextAttr(wx.BLUE))
292 |
293 | port_label = wx.StaticText(panel, label="Serial port")
294 | file_label = wx.StaticText(panel, label="NodeMCU firmware")
295 | baud_label = wx.StaticText(panel, label="Baud rate")
296 | flashmode_label = wx.StaticText(panel, label="Flash mode")
297 |
298 | def on_info_hover(event):
299 | global hovered
300 | if(len(hovered) == 0):
301 | from HtmlPopupTransientWindow import HtmlPopupTransientWindow
302 | win = HtmlPopupTransientWindow(self, wx.SIMPLE_BORDER, __flash_help__, "#FFB6C1", (410, 140))
303 |
304 | image = event.GetEventObject()
305 | image_position = image.ClientToScreen((0, 0))
306 | image_size = image.GetSize()
307 | win.Position(image_position, (0, image_size[1]))
308 |
309 | win.Popup()
310 | hovered = [win]
311 |
312 |
313 | icon = wx.StaticBitmap(panel, wx.ID_ANY, images.Info.GetBitmap())
314 | icon.Bind(wx.EVT_MOTION, on_info_hover)
315 |
316 | flashmode_label_boxsizer = wx.BoxSizer(wx.HORIZONTAL)
317 | flashmode_label_boxsizer.Add(flashmode_label, 1, wx.EXPAND)
318 | flashmode_label_boxsizer.AddStretchSpacer(0)
319 | flashmode_label_boxsizer.Add(icon)
320 |
321 | erase_label = wx.StaticText(panel, label="Erase flash")
322 | console_label = wx.StaticText(panel, label="Console")
323 |
324 | fgs.AddMany([
325 | port_label, (serial_boxsizer, 1, wx.EXPAND),
326 | file_label, (file_picker, 1, wx.EXPAND),
327 | baud_label, baud_boxsizer,
328 | flashmode_label_boxsizer, flashmode_boxsizer,
329 | erase_label, erase_boxsizer,
330 | (wx.StaticText(panel, label="")), (button, 1, wx.EXPAND),
331 | (console_label, 1, wx.EXPAND), (self.console_ctrl, 1, wx.EXPAND)])
332 | fgs.AddGrowableRow(6, 1)
333 | fgs.AddGrowableCol(1, 1)
334 | hbox.Add(fgs, proportion=2, flag=wx.ALL | wx.EXPAND, border=15)
335 | panel.SetSizer(hbox)
336 |
337 | def _select_configured_port(self):
338 | count = 0
339 | for item in self.choice.GetItems():
340 | if item == self._config.port:
341 | self.choice.Select(count)
342 | break
343 | count += 1
344 |
345 | @staticmethod
346 | def _get_serial_ports():
347 | ports = [__auto_select__ + " " + __auto_select_explanation__]
348 | for port, desc, hwid in sorted(list_ports.comports()):
349 | ports.append(port)
350 | return ports
351 |
352 | def _set_icons(self):
353 | self.SetIcon(images.Icon.GetIcon())
354 |
355 | def _build_status_bar(self):
356 | self.statusBar = self.CreateStatusBar(2, wx.STB_SIZEGRIP)
357 | self.statusBar.SetStatusWidths([-2, -1])
358 | status_text = "Welcome to NodeMCU PyFlasher %s" % __version__
359 | self.statusBar.SetStatusText(status_text, 0)
360 |
361 | def _build_menu_bar(self):
362 | self.menuBar = wx.MenuBar()
363 |
364 | # File menu
365 | file_menu = wx.Menu()
366 | wx.App.SetMacExitMenuItemId(wx.ID_EXIT)
367 | exit_item = file_menu.Append(wx.ID_EXIT, "E&xit\tCtrl-Q", "Exit NodeMCU PyFlasher")
368 | exit_item.SetBitmap(images.Exit.GetBitmap())
369 | self.Bind(wx.EVT_MENU, self._on_exit_app, exit_item)
370 | self.menuBar.Append(file_menu, "&File")
371 |
372 | # Help menu
373 | help_menu = wx.Menu()
374 | help_item = help_menu.Append(wx.ID_ABOUT, '&About NodeMCU PyFlasher', 'About')
375 | self.Bind(wx.EVT_MENU, self._on_help_about, help_item)
376 | self.menuBar.Append(help_menu, '&Help')
377 |
378 | self.SetMenuBar(self.menuBar)
379 |
380 | @staticmethod
381 | def _get_config_file_path():
382 | return wx.StandardPaths.Get().GetUserConfigDir() + "/nodemcu-pyflasher.json"
383 |
384 | # Menu methods
385 | def _on_exit_app(self, event):
386 | self._config.safe(self._get_config_file_path())
387 | self.Close(True)
388 |
389 | def _on_help_about(self, event):
390 | from About import AboutDlg
391 | about = AboutDlg(self)
392 | about.ShowModal()
393 | about.Destroy()
394 |
395 | def report_error(self, message):
396 | self.console_ctrl.SetValue(message)
397 |
398 | def log_message(self, message):
399 | self.console_ctrl.AppendText(message)
400 |
401 | # ---------------------------------------------------------------------------
402 |
403 |
404 | # ---------------------------------------------------------------------------
405 | class MySplashScreen(wx.adv.SplashScreen):
406 | def __init__(self):
407 | global hovered
408 | hovered = []
409 | wx.adv.SplashScreen.__init__(self, images.Splash.GetBitmap(),
410 | wx.adv.SPLASH_CENTRE_ON_SCREEN | wx.adv.SPLASH_TIMEOUT, 2500, None, -1)
411 | self.Bind(wx.EVT_CLOSE, self._on_close)
412 | self.__fc = wx.CallLater(2000, self._show_main)
413 |
414 | def _on_close(self, evt):
415 | # Make sure the default handler runs too so this window gets
416 | # destroyed
417 | evt.Skip()
418 | self.Hide()
419 |
420 | # if the timer is still running then go ahead and show the
421 | # main frame now
422 | if self.__fc.IsRunning():
423 | self.__fc.Stop()
424 | self._show_main()
425 |
426 | def _show_main(self):
427 | frame = NodeMcuFlasher(None, "NodeMCU PyFlasher")
428 | frame.Show()
429 | if self.__fc.IsRunning():
430 | self.Raise()
431 |
432 | # ---------------------------------------------------------------------------
433 |
434 |
435 | # ----------------------------------------------------------------------------
436 | class App(wx.App, wx.lib.mixins.inspection.InspectionMixin):
437 | def OnInit(self):
438 | # see https://discuss.wxpython.org/t/wxpython4-1-1-python3-8-locale-wxassertionerror/35168
439 | self.ResetLocale()
440 | wx.SystemOptions.SetOption("mac.window-plain-transition", 1)
441 | self.SetAppName("NodeMCU PyFlasher")
442 |
443 | # Create and show the splash screen. It will then create and
444 | # show the main frame when it is time to do so. Normally when
445 | # using a SplashScreen you would create it, show it and then
446 | # continue on with the application's initialization, finally
447 | # creating and showing the main application window(s). In
448 | # this case we have nothing else to do so we'll delay showing
449 | # the main frame until later (see ShowMain above) so the users
450 | # can see the SplashScreen effect.
451 | splash = MySplashScreen()
452 | splash.Show()
453 |
454 | return True
455 |
456 |
457 | # ---------------------------------------------------------------------------
458 | def main():
459 | app = App(False)
460 | app.MainLoop()
461 | # ---------------------------------------------------------------------------
462 |
463 |
464 | if __name__ == '__main__':
465 | __name__ = 'Main'
466 | main()
467 |
468 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NodeMCU PyFlasher
2 | [](https://github.com/marcelstoer/nodemcu-pyflasher/blob/master/LICENSE)
3 | [](https://github.com/marcelstoer/nodemcu-pyflasher/releases)
4 | [](https://github.com/marcelstoer/nodemcu-pyflasher/releases)
5 | [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=HFN4ZMET5XS2Q)
6 |
7 | 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/).
8 |
9 | 
10 |
11 | ## Installation
12 | 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. Starting from 3.0 there's also a .dmg for macOS.
13 |
14 | ## Status
15 | Scan the [list of open issues](https://github.com/marcelstoer/nodemcu-pyflasher/issues) for bugs and pending features.
16 |
17 | **Note**
18 |
19 | This is my first Python project. If you have constructive feedback as for how to improve the code please do reach out to me.
20 |
21 | ## Getting help
22 | 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).
23 |
24 | ## Donationware
25 | 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.
26 |
27 | [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=HFN4ZMET5XS2Q)
28 |
29 | ## Build it yourself
30 | If you want to build this application yourself you need to:
31 |
32 | - Install [Python 3.x](https://www.python.org/downloads/) and [Pip](https://pip.pypa.io/en/stable/installing/) (it comes with Python if installed from `python.org`).
33 | - Create a virtual environment with `python -m venv venv`
34 | - Activate the virtual environment with `. venv/bin/activate` (`. venv/Scripts/activate` if you are on Windows with [Cygwin](https://www.cygwin.com/) or [Mingw](http://mingw.org/))
35 | - Run `pip install -r requirements.txt`
36 |
37 | **A note on Linux:** As described on the [downloads section of `wxPython`](https://www.wxpython.org/pages/downloads/), wheels for Linux are complicated and may require you to run something like this to install `wxPython` correctly:
38 |
39 | ```bash
40 | # Assuming you are running it on Ubuntu 18.04 LTS with GTK3
41 | pip install -U \
42 | -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-18.04 \
43 | wxPython
44 | ```
45 |
46 | ## Why this project exists
47 |
48 | ### Motivation
49 | This addresses an issue the NodeMCU community touched on several times in the past, most recently at
50 | [#1500 (comment)](https://github.com/nodemcu/nodemcu-firmware/pull/1500#issuecomment-247884981).
51 |
52 | I stated that based on my experience doing NodeMCU user support it should be a lot simpler to flash NodeMCU for Windows users.
53 |
54 | - 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)).
55 | - 70% of the users of my [nodemcu-build.com](https://nodemcu-build.com) service are on Windows.
56 | - BUT Windows doesn't come with Python installed - which is required for esptool.py.
57 | - BUT Windows users in general are more reluctant to use the CLI than Linux/Mac users - which is required for esptool.py.
58 |
59 | To conclude: this is not a comfortable situation for NodeMCU's largest user group.
60 |
61 | ### The plan
62 | 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.
63 |
64 | ### Implementation
65 | - Uses the cross-platform wxPython GUI framework. I also tried PyForms/PyQt4 but settled for wxPython.
66 | - Requires absolutely minimal user input.
67 | - The esptool.py "console" output is redirected to text control on the GUI.
68 | - 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.
69 |
70 | ## License
71 | [MIT](http://opensource.org/licenses/MIT) © Marcel Stör
72 |
--------------------------------------------------------------------------------
/build-on-mac.spec:
--------------------------------------------------------------------------------
1 | # -*- mode: python ; coding: utf-8 -*-
2 |
3 | import os
4 |
5 | # We need to add the flasher stub JSON files explicitly: https://github.com/espressif/esptool/issues/1059
6 | venv_python_folder_name = next(d for d in os.listdir('./.venv/lib') if d.startswith('python') and os.path.isdir(os.path.join('./.venv/lib', d)))
7 | local_stub_flasher_path = "./.venv/lib/{}/site-packages/esptool/targets/stub_flasher".format(venv_python_folder_name)
8 |
9 | a = Analysis(
10 | ['nodemcu-pyflasher.py'],
11 | pathex=[],
12 | binaries=[],
13 | datas=[
14 | ("images", "images"),
15 | ("{}/1".format(local_stub_flasher_path), "./esptool/targets/stub_flasher/1"),
16 | ("{}/2".format(local_stub_flasher_path), "./esptool/targets/stub_flasher/2")
17 | ],
18 | hiddenimports=[],
19 | hookspath=[],
20 | hooksconfig={},
21 | runtime_hooks=[],
22 | excludes=[],
23 | noarchive=False,
24 | optimize=0,
25 | )
26 | pyz = PYZ(a.pure)
27 |
28 | exe = EXE(
29 | pyz,
30 | a.scripts,
31 | [],
32 | exclude_binaries=True,
33 | name='NodeMCU PyFlasher',
34 | debug=False,
35 | bootloader_ignore_signals=False,
36 | strip=False,
37 | upx=True,
38 | console=False,
39 | disable_windowed_traceback=False,
40 | argv_emulation=False,
41 | target_arch=None,
42 | codesign_identity=None,
43 | entitlements_file=None,
44 | )
45 | coll = COLLECT(
46 | exe,
47 | a.binaries,
48 | a.datas,
49 | strip=False,
50 | upx=True,
51 | upx_exclude=[],
52 | name='NodeMCU PyFlasher',
53 | icon='images/icon-256.icns'
54 | )
55 | app = BUNDLE(
56 | coll,
57 | name='NodeMCU PyFlasher.app',
58 | version='5.1.0',
59 | icon='images/icon-256.icns',
60 | bundle_identifier='com.frightanic.nodemcu-pyflasher')
61 |
--------------------------------------------------------------------------------
/build-on-win.spec:
--------------------------------------------------------------------------------
1 | # -*- mode: python ; coding: utf-8 -*-
2 |
3 | # We need to add the flasher stub JSON files explicitly: https://github.com/espressif/esptool/issues/1059
4 | local_stub_flasher_path = "./.venv/Lib/site-packages/esptool/targets/stub_flasher"
5 |
6 | a = Analysis(
7 | ['nodemcu-pyflasher.py'],
8 | pathex=[],
9 | binaries=[],
10 | datas=[
11 | ("images", "images"),
12 | ("{}/1".format(local_stub_flasher_path), "./esptool/targets/stub_flasher/1"),
13 | ("{}/2".format(local_stub_flasher_path), "./esptool/targets/stub_flasher/2")
14 | ],
15 | hiddenimports=[],
16 | hookspath=[],
17 | hooksconfig={},
18 | runtime_hooks=[],
19 | excludes=[],
20 | noarchive=False,
21 | optimize=0,
22 | )
23 | pyz = PYZ(a.pure)
24 |
25 | exe = EXE(
26 | pyz,
27 | a.scripts,
28 | a.binaries,
29 | a.datas,
30 | [],
31 | name='NodeMCU-PyFlasher',
32 | version='windows-version-info.txt',
33 | debug=False,
34 | bootloader_ignore_signals=False,
35 | strip=False,
36 | upx=True,
37 | upx_exclude=[],
38 | runtime_tmpdir=None,
39 | console=False,
40 | icon='images\\icon-256.ico',
41 | disable_windowed_traceback=False,
42 | argv_emulation=False,
43 | target_arch=None,
44 | codesign_identity=None,
45 | entitlements_file=None,
46 | )
47 |
--------------------------------------------------------------------------------
/build.bat:
--------------------------------------------------------------------------------
1 | pyinstaller --log-level=DEBUG ^
2 | --noconfirm ^
3 | build-on-win.spec
4 |
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # rm -fr build dist
3 | VERSION=5.1.0
4 | NAME="NodeMCU PyFlasher"
5 | DIST_NAME="NodeMCU-PyFlasher"
6 |
7 | pyinstaller --log-level=DEBUG \
8 | --noconfirm \
9 | build-on-mac.spec
10 |
11 | # https://github.com/sindresorhus/create-dmg
12 | create-dmg "dist/$NAME.app"
13 | mv "$NAME $VERSION.dmg" "dist/$DIST_NAME.dmg"
14 |
--------------------------------------------------------------------------------
/encode-bitmaps.py:
--------------------------------------------------------------------------------
1 |
2 | """
3 | This is a way to save the startup time when running img2py on lots of
4 | files...
5 | """
6 |
7 | from wx.tools import img2py
8 |
9 | command_lines = [
10 | "-F -n Exit images/exit.png images.py",
11 | "-a -F -n Reload images/reload.png images.py",
12 | "-a -F -n Splash images/splash.png images.py",
13 | "-a -F -n Info images/info.png images.py",
14 | "-a -F -i -n Icon images/icon-256.png images.py",
15 | ]
16 |
17 | if __name__ == "__main__":
18 | for line in command_lines:
19 | args = line.split()
20 | img2py.main(args)
21 |
22 |
--------------------------------------------------------------------------------
/images/NodeMCU-icon-org.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcelstoer/nodemcu-pyflasher/0f1b1099b6f3426d2d641339144c4dcda17396c2/images/NodeMCU-icon-org.png
--------------------------------------------------------------------------------
/images/espressif-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcelstoer/nodemcu-pyflasher/0f1b1099b6f3426d2d641339144c4dcda17396c2/images/espressif-256.png
--------------------------------------------------------------------------------
/images/espressif-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcelstoer/nodemcu-pyflasher/0f1b1099b6f3426d2d641339144c4dcda17396c2/images/espressif-64.png
--------------------------------------------------------------------------------
/images/exit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcelstoer/nodemcu-pyflasher/0f1b1099b6f3426d2d641339144c4dcda17396c2/images/exit.png
--------------------------------------------------------------------------------
/images/gui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcelstoer/nodemcu-pyflasher/0f1b1099b6f3426d2d641339144c4dcda17396c2/images/gui.png
--------------------------------------------------------------------------------
/images/icon-256.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcelstoer/nodemcu-pyflasher/0f1b1099b6f3426d2d641339144c4dcda17396c2/images/icon-256.icns
--------------------------------------------------------------------------------
/images/icon-256.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcelstoer/nodemcu-pyflasher/0f1b1099b6f3426d2d641339144c4dcda17396c2/images/icon-256.ico
--------------------------------------------------------------------------------
/images/icon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcelstoer/nodemcu-pyflasher/0f1b1099b6f3426d2d641339144c4dcda17396c2/images/icon-256.png
--------------------------------------------------------------------------------
/images/icon-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcelstoer/nodemcu-pyflasher/0f1b1099b6f3426d2d641339144c4dcda17396c2/images/icon-64.png
--------------------------------------------------------------------------------
/images/info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcelstoer/nodemcu-pyflasher/0f1b1099b6f3426d2d641339144c4dcda17396c2/images/info.png
--------------------------------------------------------------------------------
/images/paypal-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcelstoer/nodemcu-pyflasher/0f1b1099b6f3426d2d641339144c4dcda17396c2/images/paypal-256.png
--------------------------------------------------------------------------------
/images/python-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcelstoer/nodemcu-pyflasher/0f1b1099b6f3426d2d641339144c4dcda17396c2/images/python-256.png
--------------------------------------------------------------------------------
/images/python-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcelstoer/nodemcu-pyflasher/0f1b1099b6f3426d2d641339144c4dcda17396c2/images/python-64.png
--------------------------------------------------------------------------------
/images/reload.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcelstoer/nodemcu-pyflasher/0f1b1099b6f3426d2d641339144c4dcda17396c2/images/reload.png
--------------------------------------------------------------------------------
/images/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcelstoer/nodemcu-pyflasher/0f1b1099b6f3426d2d641339144c4dcda17396c2/images/splash.png
--------------------------------------------------------------------------------
/images/wxpython-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcelstoer/nodemcu-pyflasher/0f1b1099b6f3426d2d641339144c4dcda17396c2/images/wxpython-256.png
--------------------------------------------------------------------------------
/images/wxpython-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcelstoer/nodemcu-pyflasher/0f1b1099b6f3426d2d641339144c4dcda17396c2/images/wxpython-64.png
--------------------------------------------------------------------------------
/nodemcu-pyflasher.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import Main
4 | Main.main()
5 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | esptool==4.8.1
2 | pyserial~=3.5
3 | wxPython==4.2.2
4 | PyInstaller==6.11.1
5 | httplib2>=0.18.1
6 | pyinstaller-versionfile>=2.0.0
7 |
--------------------------------------------------------------------------------
/windows-metadata.yaml:
--------------------------------------------------------------------------------
1 | # https://github.com/DudeNr33/pyinstaller-versionfile
2 | # create-version-file windows-metadata.yaml --outfile windows-version-info.txt
3 | Version: 5.1.0
4 | CompanyName: Marcel Stör
5 | FileDescription: NodeMCU PyFlasher
6 | InternalName: NodeMCU PyFlasher
7 | LegalCopyright: © Marcel Stör. All rights reserved.
8 | OriginalFilename: NodeMCU-PyFlasher.exe
9 | ProductName: NodeMCU PyFlasher
--------------------------------------------------------------------------------
/windows-version-info.txt:
--------------------------------------------------------------------------------
1 | # GENERATED FILE. DO NOT EDIT. Created by running create-version-file windows-metadata.yaml --outfile windows-version-info.txt
2 | #
3 | # UTF-8
4 | #
5 | # For more details about fixed file info 'ffi' see:
6 | # http://msdn.microsoft.com/en-us/library/ms646997.aspx
7 |
8 | VSVersionInfo(
9 | ffi=FixedFileInfo(
10 | # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
11 | # Set not needed items to zero 0. Must always contain 4 elements.
12 | filevers=(5,1,0,0),
13 | prodvers=(5,1,0,0),
14 | # Contains a bitmask that specifies the valid bits 'flags'r
15 | mask=0x3f,
16 | # Contains a bitmask that specifies the Boolean attributes of the file.
17 | flags=0x0,
18 | # The operating system for which this file was designed.
19 | # 0x4 - NT and there is no need to change it.
20 | OS=0x40004,
21 | # The general type of file.
22 | # 0x1 - the file is an application.
23 | fileType=0x1,
24 | # The function of the file.
25 | # 0x0 - the function is not defined for this fileType
26 | subtype=0x0,
27 | # Creation date and time stamp.
28 | date=(0, 0)
29 | ),
30 | kids=[
31 | StringFileInfo(
32 | [
33 | StringTable(
34 | u'040904B0',
35 | [StringStruct(u'CompanyName', u'Marcel Stör'),
36 | StringStruct(u'FileDescription', u'NodeMCU PyFlasher'),
37 | StringStruct(u'FileVersion', u'5.1.0.0'),
38 | StringStruct(u'InternalName', u'NodeMCU PyFlasher'),
39 | StringStruct(u'LegalCopyright', u'© Marcel Stör. All rights reserved.'),
40 | StringStruct(u'OriginalFilename', u'NodeMCU-PyFlasher.exe'),
41 | StringStruct(u'ProductName', u'NodeMCU PyFlasher'),
42 | StringStruct(u'ProductVersion', u'5.1.0.0')])
43 | ]),
44 | VarFileInfo([VarStruct(u'Translation', [1033, 1200])])
45 | ]
46 | )
--------------------------------------------------------------------------------