2016-12-28 23:52:35 +01:00
|
|
|
#!/usr/bin/env python
|
|
|
|
|
|
|
|
import wx
|
|
|
|
import wx.lib.inspection
|
|
|
|
import wx.lib.mixins.inspection
|
2017-02-28 22:25:58 +01:00
|
|
|
import sys, os
|
2016-12-28 23:52:35 +01:00
|
|
|
import esptool
|
|
|
|
import threading
|
2017-02-28 22:25:58 +01:00
|
|
|
import json
|
2016-12-29 22:55:41 +01:00
|
|
|
import images as images
|
2016-12-28 23:52:35 +01:00
|
|
|
from serial.tools import list_ports
|
|
|
|
from esptool import ESPROM
|
|
|
|
from argparse import Namespace
|
|
|
|
|
2017-04-16 21:13:15 +02:00
|
|
|
__version__ = "1.0.1"
|
2017-01-14 23:09:36 +01:00
|
|
|
__supported_baud_rates__ = [9600, 57600, 74880, 115200, 230400, 460800, 921600]
|
2016-12-28 23:52:35 +01:00
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
# See discussion at http://stackoverflow.com/q/41101897/131929
|
|
|
|
class RedirectText:
|
|
|
|
def __init__(self, text_ctrl):
|
|
|
|
self.__out = text_ctrl
|
|
|
|
self.__pending_backspaces = 0
|
|
|
|
|
|
|
|
def write(self, string):
|
|
|
|
new_string = ""
|
|
|
|
number_of_backspaces = 0
|
|
|
|
for c in string:
|
|
|
|
if c == "\b":
|
|
|
|
number_of_backspaces += 1
|
|
|
|
else:
|
|
|
|
new_string += c
|
|
|
|
|
|
|
|
if self.__pending_backspaces > 0:
|
|
|
|
# current value minus pending backspaces plus new string
|
|
|
|
new_value = self.__out.GetValue()[:-1 * self.__pending_backspaces] + new_string
|
|
|
|
wx.CallAfter(self.__out.SetValue, new_value)
|
|
|
|
else:
|
|
|
|
wx.CallAfter(self.__out.AppendText, new_string)
|
|
|
|
|
|
|
|
self.__pending_backspaces = number_of_backspaces
|
|
|
|
|
|
|
|
def flush(self):
|
|
|
|
None
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class FlashingThread(threading.Thread):
|
|
|
|
def __init__(self, parent, config):
|
|
|
|
threading.Thread.__init__(self)
|
|
|
|
self.daemon = True
|
2017-01-14 23:36:28 +01:00
|
|
|
self._parent = parent
|
|
|
|
self._config = config
|
2016-12-28 23:52:35 +01:00
|
|
|
|
|
|
|
def run(self):
|
2017-01-14 23:36:28 +01:00
|
|
|
esp = ESPROM(port=self._config.port)
|
2016-12-28 23:52:35 +01:00
|
|
|
args = Namespace()
|
|
|
|
args.flash_size = "detect"
|
2017-01-14 23:36:28 +01:00
|
|
|
args.flash_mode = self._config.mode
|
2016-12-28 23:52:35 +01:00
|
|
|
args.flash_freq = "40m"
|
|
|
|
args.no_progress = False
|
|
|
|
args.verify = True
|
2017-01-14 23:36:28 +01:00
|
|
|
args.baud = self._config.baud
|
|
|
|
args.addr_filename = [[int("0x00000", 0), open(self._config.firmware_path, 'rb')]]
|
2016-12-28 23:52:35 +01:00
|
|
|
# needs connect() before each operation, see https://github.com/espressif/esptool/issues/157
|
2017-01-14 23:36:28 +01:00
|
|
|
if self._config.erase_before_flash:
|
2016-12-28 23:52:35 +01:00
|
|
|
esp.connect()
|
|
|
|
esptool.erase_flash(esp, args)
|
|
|
|
esp.connect()
|
|
|
|
esptool.write_flash(esp, args)
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# DTO between GUI and flashing thread
|
|
|
|
class FlashConfig:
|
|
|
|
def __init__(self):
|
|
|
|
self.baud = 115200
|
|
|
|
self.erase_before_flash = False
|
|
|
|
self.mode = "qio"
|
2017-01-14 23:09:36 +01:00
|
|
|
self.firmware_path = None
|
2016-12-28 23:52:35 +01:00
|
|
|
self.port = None
|
|
|
|
|
2017-02-28 22:25:58 +01:00
|
|
|
@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)
|
|
|
|
|
2016-12-28 23:52:35 +01:00
|
|
|
def is_complete(self):
|
2017-01-14 23:09:36 +01:00
|
|
|
return self.firmware_path is not None and self.port is not None
|
2016-12-28 23:52:35 +01:00
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
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)
|
2017-02-28 22:25:58 +01:00
|
|
|
self._config = FlashConfig.load(self._get_config_file_path())
|
2016-12-28 23:52:35 +01:00
|
|
|
|
2017-01-14 23:36:28 +01:00
|
|
|
self._build_status_bar()
|
|
|
|
self._set_icons()
|
|
|
|
self._build_menu_bar()
|
|
|
|
self._init_ui()
|
2016-12-28 23:52:35 +01:00
|
|
|
|
|
|
|
sys.stdout = RedirectText(self.console_ctrl)
|
|
|
|
|
|
|
|
self.SetMinSize((640, 480))
|
|
|
|
self.Centre(wx.BOTH)
|
|
|
|
self.Show(True)
|
|
|
|
|
2017-01-14 23:36:28 +01:00
|
|
|
def _init_ui(self):
|
2017-01-14 23:09:36 +01:00
|
|
|
def on_reload(event):
|
2017-01-14 23:36:28 +01:00
|
|
|
self.choice.SetItems(self._get_serial_ports())
|
2017-01-14 23:09:36 +01:00
|
|
|
|
|
|
|
def on_baud_changed(event):
|
|
|
|
radio_button = event.GetEventObject()
|
|
|
|
|
|
|
|
if radio_button.GetValue():
|
2017-01-14 23:36:28 +01:00
|
|
|
self._config.baud = radio_button.rate
|
2017-01-14 23:09:36 +01:00
|
|
|
|
|
|
|
def on_mode_changed(event):
|
|
|
|
radio_button = event.GetEventObject()
|
|
|
|
|
|
|
|
if radio_button.GetValue():
|
2017-01-14 23:36:28 +01:00
|
|
|
self._config.mode = radio_button.mode
|
2017-01-14 23:09:36 +01:00
|
|
|
|
|
|
|
def on_erase_changed(event):
|
|
|
|
radio_button = event.GetEventObject()
|
|
|
|
|
|
|
|
if radio_button.GetValue():
|
2017-01-14 23:36:28 +01:00
|
|
|
self._config.erase_before_flash = radio_button.erase
|
2017-01-14 23:09:36 +01:00
|
|
|
|
|
|
|
def on_clicked(event):
|
|
|
|
self.console_ctrl.SetValue("")
|
2017-01-14 23:36:28 +01:00
|
|
|
worker = FlashingThread(self, self._config)
|
2017-01-14 23:09:36 +01:00
|
|
|
worker.start()
|
|
|
|
|
|
|
|
def on_select_port(event):
|
|
|
|
choice = event.GetEventObject()
|
2017-01-14 23:36:28 +01:00
|
|
|
self._config.port = choice.GetString(choice.GetSelection())
|
2017-01-14 23:09:36 +01:00
|
|
|
|
|
|
|
def on_pick_file(event):
|
2017-01-14 23:36:28 +01:00
|
|
|
self._config.firmware_path = event.GetPath().replace("'", "")
|
2017-01-14 23:09:36 +01:00
|
|
|
|
2016-12-28 23:52:35 +01:00
|
|
|
panel = wx.Panel(self)
|
|
|
|
|
|
|
|
hbox = wx.BoxSizer(wx.HORIZONTAL)
|
|
|
|
|
|
|
|
fgs = wx.FlexGridSizer(7, 2, 10, 10)
|
|
|
|
|
2017-01-14 23:36:28 +01:00
|
|
|
self.choice = wx.Choice(panel, choices=self._get_serial_ports())
|
2017-01-14 23:09:36 +01:00
|
|
|
self.choice.Bind(wx.EVT_CHOICE, on_select_port)
|
2017-02-28 22:25:58 +01:00
|
|
|
self._select_configured_port()
|
2016-12-28 23:52:35 +01:00
|
|
|
bmp = images.Reload.GetBitmap()
|
|
|
|
reload_button = wx.BitmapButton(panel, id=wx.ID_ANY, bitmap=bmp,
|
|
|
|
size=(bmp.GetWidth() + 7, bmp.GetHeight() + 7))
|
2017-01-14 23:09:36 +01:00
|
|
|
reload_button.Bind(wx.EVT_BUTTON, on_reload)
|
2016-12-28 23:52:35 +01:00
|
|
|
|
|
|
|
file_picker = wx.FilePickerCtrl(panel, style=wx.FLP_USE_TEXTCTRL)
|
2017-01-14 23:09:36 +01:00
|
|
|
file_picker.Bind(wx.EVT_FILEPICKER_CHANGED, on_pick_file)
|
2016-12-28 23:52:35 +01:00
|
|
|
|
|
|
|
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)
|
2017-01-14 23:09:36 +01:00
|
|
|
|
|
|
|
def add_baud_radio_button(sizer, idx, rate):
|
|
|
|
style = wx.RB_GROUP if idx == 0 else 0
|
|
|
|
radio_button = wx.RadioButton(panel, name="baud-%d" % rate, label="%d" % rate, style=style)
|
|
|
|
radio_button.rate = rate
|
|
|
|
# sets default value
|
2017-01-14 23:36:28 +01:00
|
|
|
radio_button.SetValue(rate == self._config.baud)
|
2017-01-14 23:09:36 +01:00
|
|
|
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)
|
2016-12-28 23:52:35 +01:00
|
|
|
|
|
|
|
flashmode_boxsizer = wx.BoxSizer(wx.HORIZONTAL)
|
2017-02-28 22:25:58 +01:00
|
|
|
|
|
|
|
def add_flash_mode_radio_button(sizer, idx, mode, label):
|
|
|
|
style = wx.RB_GROUP if idx == 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 Flash I/O (qio)")
|
|
|
|
add_flash_mode_radio_button(flashmode_boxsizer, 1, "dio", "Dual Flash I/O (dio), usually for >=4MB flash chips")
|
2016-12-28 23:52:35 +01:00
|
|
|
|
|
|
|
erase_boxsizer = wx.BoxSizer(wx.HORIZONTAL)
|
2017-02-28 22:25:58 +01:00
|
|
|
|
|
|
|
def add_erase_radio_button(sizer, idx, erase_before_flash, label, value):
|
|
|
|
style = wx.RB_GROUP if idx == 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)
|
2017-01-14 23:09:36 +01:00
|
|
|
|
|
|
|
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))
|
2016-12-28 23:52:35 +01:00
|
|
|
|
2017-01-14 23:09:36 +01:00
|
|
|
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")
|
|
|
|
erase_label = wx.StaticText(panel, label="Erase flash")
|
|
|
|
console_label = wx.StaticText(panel, label="Console")
|
2016-12-28 23:52:35 +01:00
|
|
|
|
|
|
|
fgs.AddMany([
|
|
|
|
port_label, (serial_boxsizer, 1, wx.EXPAND),
|
|
|
|
file_label, (file_picker, 1, wx.EXPAND),
|
|
|
|
baud_label, baud_boxsizer,
|
|
|
|
flashmode_label, 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)
|
|
|
|
|
2017-02-28 22:25:58 +01:00
|
|
|
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
|
|
|
|
|
2017-01-14 23:36:28 +01:00
|
|
|
def _get_serial_ports(self):
|
2016-12-28 23:52:35 +01:00
|
|
|
ports = [""]
|
|
|
|
for port, desc, hwid in sorted(list_ports.comports()):
|
|
|
|
ports.append(port)
|
|
|
|
return ports
|
|
|
|
|
2017-01-14 23:36:28 +01:00
|
|
|
def _set_icons(self):
|
2016-12-28 23:52:35 +01:00
|
|
|
self.SetIcon(images.Icon.GetIcon())
|
|
|
|
|
2017-01-14 23:36:28 +01:00
|
|
|
def _build_status_bar(self):
|
2016-12-28 23:52:35 +01:00
|
|
|
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)
|
|
|
|
|
2017-01-14 23:36:28 +01:00
|
|
|
def _build_menu_bar(self):
|
2017-01-14 23:09:36 +01:00
|
|
|
self.menuBar = wx.MenuBar()
|
2016-12-28 23:52:35 +01:00
|
|
|
|
|
|
|
# File menu
|
2017-01-14 23:09:36 +01:00
|
|
|
file_menu = wx.Menu()
|
2016-12-28 23:52:35 +01:00
|
|
|
wx.App.SetMacExitMenuItemId(wx.ID_EXIT)
|
2017-01-14 23:09:36 +01:00
|
|
|
exit_item = file_menu.Append(wx.ID_EXIT, "E&xit\tCtrl-Q", "Exit NodeMCU PyFlasher")
|
2016-12-28 23:52:35 +01:00
|
|
|
exit_item.SetBitmap(images.Exit.GetBitmap())
|
2017-01-14 23:36:28 +01:00
|
|
|
self.Bind(wx.EVT_MENU, self._on_exit_app, exit_item)
|
2017-01-14 23:09:36 +01:00
|
|
|
self.menuBar.Append(file_menu, "&File")
|
2016-12-28 23:52:35 +01:00
|
|
|
|
|
|
|
# Help menu
|
2017-01-14 23:09:36 +01:00
|
|
|
help_menu = wx.Menu()
|
|
|
|
help_item = help_menu.Append(wx.ID_ABOUT, '&About NodeMCU PyFlasher', 'About')
|
2017-01-14 23:36:28 +01:00
|
|
|
self.Bind(wx.EVT_MENU, self._on_help_about, help_item)
|
2017-01-14 23:09:36 +01:00
|
|
|
self.menuBar.Append(help_menu, '&Help')
|
2016-12-28 23:52:35 +01:00
|
|
|
|
|
|
|
self.SetMenuBar(self.menuBar)
|
|
|
|
|
2017-02-28 22:25:58 +01:00
|
|
|
def _get_config_file_path(self):
|
|
|
|
return wx.StandardPaths.Get().GetUserConfigDir() + "/nodemcu-pyflasher.json"
|
|
|
|
|
2016-12-28 23:52:35 +01:00
|
|
|
# Menu methods
|
2017-01-14 23:36:28 +01:00
|
|
|
def _on_exit_app(self, event):
|
2017-02-28 22:25:58 +01:00
|
|
|
self._config.safe(self._get_config_file_path())
|
2016-12-28 23:52:35 +01:00
|
|
|
self.Close(True)
|
|
|
|
|
2017-01-14 23:36:28 +01:00
|
|
|
def _on_help_about(self, event):
|
2017-02-18 22:18:43 +01:00
|
|
|
from About import AboutDlg
|
|
|
|
about = AboutDlg(self)
|
2016-12-28 23:52:35 +01:00
|
|
|
about.ShowModal()
|
|
|
|
about.Destroy()
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
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)
|
2017-01-14 23:36:28 +01:00
|
|
|
self.Bind(wx.EVT_CLOSE, self._on_close)
|
|
|
|
self.__fc = wx.FutureCall(2000, self._show_main)
|
2016-12-28 23:52:35 +01:00
|
|
|
|
2017-01-14 23:36:28 +01:00
|
|
|
def _on_close(self, evt):
|
2016-12-28 23:52:35 +01:00
|
|
|
# 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()
|
2017-01-14 23:36:28 +01:00
|
|
|
self._show_main()
|
2016-12-28 23:52:35 +01:00
|
|
|
|
2017-01-14 23:36:28 +01:00
|
|
|
def _show_main(self):
|
2016-12-28 23:52:35 +01:00
|
|
|
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()
|