├── .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 | Python 22 | NodeMCU 23 | Espressif, producers of ESP8266 et.al. 24 | wxPython, cross-platform GUI framework 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 | Donate with PayPal 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 |

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 | [![License](https://marcelstoer.github.io/nodemcu-pyflasher/images/mit-license-badge.svg)](https://github.com/marcelstoer/nodemcu-pyflasher/blob/master/LICENSE) 3 | [![Github Downloads (all assets, all releases)](https://img.shields.io/github/downloads/marcelstoer/nodemcu-pyflasher/total.svg?style=flat)](https://github.com/marcelstoer/nodemcu-pyflasher/releases) 4 | [![GitHub Downloads (all assets, latest release)](https://img.shields.io/github/downloads/marcelstoer/nodemcu-pyflasher/latest/total?style=flat)](https://github.com/marcelstoer/nodemcu-pyflasher/releases) 5 | [![PayPal Donation](https://img.shields.io/badge/donate_through-PayPal-%23009cde?logo=paypal)](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 | ![Image of NodeMCU PyFlasher GUI](images/gui.png) 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 | [![PayPal Donations](./images/paypal-256.png)](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 | ) --------------------------------------------------------------------------------