diff --git a/configtool.py b/configtool.py index 3862d0a..ec483af 100755 --- a/configtool.py +++ b/configtool.py @@ -1,5 +1,16 @@ #!/usr/bin/env python +# Note: syntax in this file has to be kept compatible with Pyton 3, else +# Python errors in the compilation stage already, without showing +# user help about the incompatibility. To test, simply run +# +# python3 ./configtool.py +# +# This should show the explaining text rather than a SyntaxError. +# +# If you feel like porting Configtool to Python 3 compatibility altogether: +# patches are welcome! See https://docs.python.org/3/howto/pyporting.html + import sys import time if sys.version_info.major >= 3: @@ -9,621 +20,55 @@ if sys.version_info.major >= 3: time.sleep(10) sys.exit(-1) -try: - import wx -except: - print("ImportError: No module named wx\n\n" - "wxPython is not installed. This program requires wxPython to run.\n" - "See your package manager and/or http://wxpython.org/download.php.") - time.sleep(10) - sys.exit(-1) - +import getopt import os.path import inspect -from configtool.data import reHelpText -cmd_folder = os.path.realpath(os.path.abspath(os.path.split(inspect.getfile( - inspect.currentframe()))[0])) +from configtool.gui import StartGui -from configtool.decoration import Decoration -from configtool.settings import Settings, SettingsDlg -from configtool.printerpanel import PrinterPanel -from configtool.boardpanel import BoardPanel -from configtool.build import Build, Upload -from configtool.data import reInclude -ID_LOAD_PRINTER = 1000 -ID_SAVE_PRINTER = 1001 -ID_SAVE_PRINTER_AS = 1002 -ID_LOAD_BOARD = 1010 -ID_SAVE_BOARD = 1011 -ID_SAVE_BOARD_AS = 1012 -ID_LOAD_CONFIG = 1020 -ID_SAVE_CONFIG = 1021 -ID_BUILD = 1030 -ID_UPLOAD = 1031 -ID_SETTINGS = 1040 -ID_HELP = 1050 -ID_REPORT = 1051 -ID_ABOUT = 1052 +def cmdLoad(arg): + print("Want to load %s, but don't know how.\n" % arg) +def cmdHelp(): + print("""Usage: %s [options] -class ConfigFrame(wx.Frame): - def __init__(self): - wx.Frame.__init__(self, None, -1, "Teacup Configtool", size = (880, 550)) - self.Bind(wx.EVT_CLOSE, self.onClose) - self.Bind(wx.EVT_SIZE, self.onResize) +Running without any options starts the gui (normal operation). +Following options are available for command line automation: - self.deco = Decoration() + -h, --help Show this help text. - panel = wx.Panel(self, -1) - panel.SetBackgroundColour(self.deco.getBackgroundColour()) - panel.Bind(wx.EVT_PAINT, self.deco.onPaintBackground) + -l , --load= Load a specific printer config, board config + or .ini file. +""" % sys.argv[0]) - self.settings = Settings(self, cmd_folder) - self.settings.font = wx.Font(8, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, - wx.FONTWEIGHT_BOLD) - self.settings.folder = cmd_folder +def CommandLine(argv): + """ Parse and act on command line arguments. All script automation commands + result in sys.exit() (i.e. they do not return from this function). Other + options like --debug will return to allow the gui to launch. + """ + try: + opts, args = getopt.getopt(argv, "hl:", ["help", "load="]) + except getopt.GetoptError as err: + print(err) + print("Use '%s --help' to get help with command line options." % + sys.argv[0]) + sys.exit(2) - self.heaters = [] - self.savePrtEna = False - self.saveBrdEna = False - self.protPrtFile = False - self.protBrdFile = False - - sz = wx.BoxSizer(wx.HORIZONTAL) - - self.nb = wx.Notebook(panel, wx.ID_ANY, size = (880, 550), - style = wx.BK_DEFAULT) - self.nb.SetBackgroundColour(self.deco.getBackgroundColour()) - self.nb.SetFont(self.settings.font) - - self.printerFileName = None - self.printerTabDecor = "" - self.printerBaseText = "Printer" - self.pgPrinter = PrinterPanel(self, self.nb, self.settings) - self.nb.AddPage(self.pgPrinter, self.printerBaseText) - - self.boardFileName = None - self.boardTabDecor = "" - self.boardBaseText = "Board" - self.pgBoard = BoardPanel(self, self.nb, self.settings) - self.nb.AddPage(self.pgBoard, self.boardBaseText) - - panel.Fit() - self.panel = panel - - sz.Add(self.nb, 1, wx.EXPAND + wx.ALL, 5) - self.SetSizer(sz) - self.makeMenu() - - def onClose(self, evt): - if not self.pgPrinter.confirmLoseChanges("exit"): - return - - if not self.pgBoard.confirmLoseChanges("exit"): - return - - self.Destroy() - - def onResize(self, evt): - self.panel.SetSize(self.GetClientSize()) - self.Refresh() - evt.Skip(); - - def setPrinterTabFile(self, fn): - self.printerFileName = fn - self.updatePrinterTab() - - def setPrinterTabDecor(self, prefix): - self.printerTabDecor = prefix - self.updatePrinterTab() - - def updatePrinterTab(self): - txt = self.printerTabDecor + self.printerBaseText - if self.printerFileName: - txt += " <%s>" % self.printerFileName - self.nb.SetPageText(0, txt) - - def setBoardTabFile(self, fn): - self.boardFileName = fn - self.updateBoardTab() - - def setBoardTabDecor(self, prefix): - self.boardTabDecor = prefix - self.updateBoardTab() - - def updateBoardTab(self): - txt = self.boardTabDecor + self.boardBaseText - if self.boardFileName: - txt += " <%s>" % self.boardFileName - self.nb.SetPageText(1, txt) - - def setHeaters(self, ht): - self.heaters = ht - self.pgPrinter.setHeaters(ht) - - def makeMenu(self): - file_menu = wx.Menu() - - file_menu.Append(ID_LOAD_CONFIG, "Load config.h", - "Load config.h and its named printer and board files.") - self.Bind(wx.EVT_MENU, self.onLoadConfig, id = ID_LOAD_CONFIG) - file_menu.Enable(ID_LOAD_CONFIG, False) - - file_menu.Append(ID_SAVE_CONFIG, "Save config.h", "Save config.h file.") - self.Bind(wx.EVT_MENU, self.onSaveConfig, id = ID_SAVE_CONFIG) - file_menu.Enable(ID_SAVE_CONFIG, False) - - file_menu.AppendSeparator() - - file_menu.Append(ID_LOAD_PRINTER, "Load printer", - "Load a printer configuration file.") - self.Bind(wx.EVT_MENU, self.pgPrinter.onLoadConfig, id = ID_LOAD_PRINTER) - - file_menu.Append(ID_SAVE_PRINTER, "Save printer", - "Save printer configuration.") - self.Bind(wx.EVT_MENU, self.onSavePrinterConfig, id = ID_SAVE_PRINTER) - file_menu.Enable(ID_SAVE_PRINTER, False) - - file_menu.Append(ID_SAVE_PRINTER_AS, "Save printer as...", - "Save printer configuration to a new file.") - self.Bind(wx.EVT_MENU, self.onSavePrinterConfigAs, id = ID_SAVE_PRINTER_AS) - file_menu.Enable(ID_SAVE_PRINTER_AS, False) - - file_menu.AppendSeparator() - - file_menu.Append(ID_LOAD_BOARD, "Load board", - "Load a board configuration file.") - self.Bind(wx.EVT_MENU, self.pgBoard.onLoadConfig, id = ID_LOAD_BOARD) - - file_menu.Append(ID_SAVE_BOARD, "Save board", "Save board configuration.") - self.Bind(wx.EVT_MENU, self.onSaveBoardConfig, id = ID_SAVE_BOARD) - file_menu.Enable(ID_SAVE_BOARD, False) - - file_menu.Append(ID_SAVE_BOARD_AS, "Save board as...", - "Save board configuration to a new file.") - self.Bind(wx.EVT_MENU, self.onSaveBoardConfigAs, id = ID_SAVE_BOARD_AS) - file_menu.Enable(ID_SAVE_BOARD_AS, False) - - file_menu.AppendSeparator() - - file_menu.Append(wx.ID_EXIT, "E&xit", "Exit the application.") - self.Bind(wx.EVT_MENU, self.onClose, id = wx.ID_EXIT) - - self.fileMenu = file_menu - - menu_bar = wx.MenuBar() - - menu_bar.Append(file_menu, "&File") - - edit_menu = wx.Menu() - - edit_menu.Append(ID_SETTINGS, "Settings", "Change settings.") - self.Bind(wx.EVT_MENU, self.onEditSettings, id = ID_SETTINGS) - - self.editMenu = edit_menu - - menu_bar.Append(edit_menu, "&Edit") - - build_menu = wx.Menu() - - build_menu.Append(ID_BUILD, "Build", "Build the executable.") - self.Bind(wx.EVT_MENU, self.onBuild, id = ID_BUILD) - - build_menu.Append(ID_UPLOAD, "Upload", "Upload the executable.") - self.Bind(wx.EVT_MENU, self.onUpload, id = ID_UPLOAD) - - self.buildMenu = build_menu - - menu_bar.Append(build_menu, "&Build") - - help_menu = wx.Menu() - - help_menu.Append(ID_HELP, "Help", "Find help.") - self.Bind(wx.EVT_MENU, self.onHelp, id = ID_HELP) - - help_menu.Append(ID_REPORT, "Report problem", - "Report a problem to Teacup maintainers.") - self.Bind(wx.EVT_MENU, self.onReportProblem, id = ID_REPORT) - - help_menu.AppendSeparator() - - help_menu.Append(ID_ABOUT, "About Teacup") - self.Bind(wx.EVT_MENU, self.onAbout, id = ID_ABOUT) - - self.helpMenu = help_menu - - menu_bar.Append(help_menu, "&Help") - - self.SetMenuBar(menu_bar) - loadFlag = self.checkEnableLoadConfig() - self.checkEnableUpload() - if loadFlag: - self.loadConfigFile("config.h") - - def onSaveBoardConfig(self, evt): - rc = self.pgBoard.onSaveConfig(evt) - if rc: - self.checkEnableLoadConfig() - return rc - - def onSaveBoardConfigAs(self, evt): - rc = self.pgBoard.onSaveConfigAs(evt) - if rc: - self.checkEnableLoadConfig() - return rc - - def onSavePrinterConfig(self, evt): - rc = self.pgPrinter.onSaveConfig(evt) - if rc: - self.checkEnableLoadConfig() - return rc - - def onSavePrinterConfigAs(self, evt): - rc = self.pgPrinter.onSaveConfigAs(evt) - if rc: - self.checkEnableLoadConfig() - return rc - - def checkEnableLoadConfig(self): - fn = os.path.join(cmd_folder, "config.h") - if os.path.isfile(fn): - self.fileMenu.Enable(ID_LOAD_CONFIG, True) - self.buildMenu.Enable(ID_BUILD, True) - return True - else: - self.fileMenu.Enable(ID_LOAD_CONFIG, False) - self.buildMenu.Enable(ID_BUILD, False) - return False - - def checkEnableUpload(self): - fn = os.path.join(cmd_folder, "teacup.hex") - if os.path.isfile(fn): - self.buildMenu.Enable(ID_UPLOAD, True) - else: - self.buildMenu.Enable(ID_UPLOAD, False) - - def enableSavePrinter(self, saveFlag, saveAsFlag): - self.fileMenu.Enable(ID_SAVE_PRINTER, saveFlag) - self.fileMenu.Enable(ID_SAVE_PRINTER_AS, saveAsFlag) - self.savePrtEna = saveAsFlag - self.protPrtFile = not saveFlag - if self.savePrtEna and self.saveBrdEna: - self.enableSaveConfig(True) - else: - self.enableSaveConfig(False) - - def enableSaveBoard(self, saveFlag, saveAsFlag): - self.fileMenu.Enable(ID_SAVE_BOARD, saveFlag) - self.fileMenu.Enable(ID_SAVE_BOARD_AS, saveAsFlag) - self.saveBrdEna = saveAsFlag - self.protBrdFile = not saveFlag - if self.savePrtEna and self.saveBrdEna: - self.enableSaveConfig(True) - else: - self.enableSaveConfig(False) - - def enableSaveConfig(self, flag): - self.fileMenu.Enable(ID_SAVE_CONFIG, flag) - - def onLoadConfig(self, evt): - self.loadConfigFile("config.h") - - def loadConfigFile(self, fn): - if not self.pgPrinter.confirmLoseChanges("load config"): - return False - - if not self.pgBoard.confirmLoseChanges("load config"): - return False - - pfile, bfile = self.getConfigFileNames(fn) - - if not pfile: - self.message("Config file did not contain a printer file " - "include statement.", "Config error") - return False - else: - if not self.pgPrinter.loadConfigFile(pfile): - self.message("There was a problem loading the printer config file:\n%s" - % pfile, "Config error") - return False - - if not bfile: - self.message("Config file did not contain a board file " - "include statement.", "Config error") - return False - else: - if not self.pgBoard.loadConfigFile(bfile): - self.message("There was a problem loading the board config file:\n%s" - % bfile, "Config error") - return False - - return True - - def getConfigFileNames(self, fn): - pfile = None - bfile = None - path = os.path.join(cmd_folder, fn) - try: - cfgBuffer = list(open(path)) - except: - self.message("Unable to process config file %s." % fn, "File error") - return None, None - - for ln in cfgBuffer: - if not ln.lstrip().startswith("#include"): - continue - - m = reInclude.search(ln) - if m: - t = m.groups() - if len(t) == 1: - if "printer." in t[0]: - if pfile: - self.message("Multiple printer file include statements.\n" - "Ignoring %s." % ln, "Config error", - wx.OK + wx.ICON_WARNING) - else: - pfile = os.path.join(cmd_folder, t[0]) - elif "board." in t[0]: - if bfile: - self.message("Multiple board file include statements.\n" - "Ignoring %s." % ln, "Config error", - wx.OK + wx.ICON_WARNING) - else: - bfile = os.path.join(cmd_folder, t[0]) - else: - self.message("Unable to parse include statement:\n%s" % ln, - "Config error") - - return pfile, bfile - - def onSaveConfig(self, evt): - fn = os.path.join(cmd_folder, "config.h") - try: - fp = open(fn, 'w') - except: - self.message("Unable to open config.h for output.", "File error") - return False - - bfn = self.pgBoard.getFileName() - if self.pgBoard.isModified() and self.pgBoard.isValid(): - if not self.pgBoard.saveConfigFile(bfn): - return False - else: - self.pgBoard.generateTempTables() - - pfn = self.pgPrinter.getFileName() - if self.pgPrinter.isModified() and self.pgPrinter.isValid(): - if not self.pgPrinter.saveConfigFile(pfn): - return False - - prefix = cmd_folder + os.path.sep - lpfx = len(prefix) - - if bfn.startswith(prefix): - rbfn = bfn[lpfx:] - else: - rbfn = bfn - - if pfn.startswith(prefix): - rpfn = pfn[lpfx:] - else: - rpfn = pfn - - fp.write("\n") - fp.write("// Configuration for controller board.\n") - fp.write("#include \"%s\"\n" % rbfn) - fp.write("\n") - fp.write("// Configuration for printer board.\n") - fp.write("#include \"%s\"\n" % rpfn) - - fp.close() - - self.checkEnableLoadConfig() - return True - - def onBuild(self, evt): - self.onBuildorUpload(True) - - def onUpload(self, evt): - self.onBuildorUpload(False) - - def onBuildorUpload(self, buildFlag): - if not (self.pgPrinter.hasData() or self.pgBoard.hasData()): - dlg = wx.MessageDialog(self, "Data needs to be loaded. " - "Click Yes to load config.h.", - "Data missing", - wx.YES_NO | wx.NO_DEFAULT | wx.ICON_INFORMATION) - rc = dlg.ShowModal() - dlg.Destroy() - if rc != wx.ID_YES: - return - - self.loadConfigFile("config.h") - else: - if self.pgPrinter.isModified(): - dlg = wx.MessageDialog(self, "Printer data needs to be saved. Click " - "Yes to save printer configuration.", - "Changes pending", - wx.YES_NO | wx.NO_DEFAULT | wx.ICON_INFORMATION) - rc = dlg.ShowModal() - dlg.Destroy() - if rc != wx.ID_YES: - return - - if self.protPrtFile: - rc = self.onSavePrinterConfigAs(None) - else: - rc = self.onSavePrinterConfig(None) - if not rc: - return - - if self.pgBoard.isModified(): - dlg = wx.MessageDialog(self, "Board data needs to be saved. Click " - "Yes to save board configuration.", - "Changes pending", - wx.YES_NO | wx.NO_DEFAULT | wx.ICON_INFORMATION) - rc = dlg.ShowModal() - dlg.Destroy() - if rc != wx.ID_YES: - return - - if self.protBrdFile: - rc = self.onSaveBoardConfigAs(None) - else: - rc = self.onSaveBoardConfig(None) - if not rc: - return - - if not self.verifyConfigLoaded(): - dlg = wx.MessageDialog(self, "Loaded configuration does not match the " - "config.h file. Click Yes to save config.h.", - "Configuration changed", - wx.YES_NO | wx.NO_DEFAULT | wx.ICON_INFORMATION) - rc = dlg.ShowModal() - dlg.Destroy() - if rc != wx.ID_YES: - return - - if not self.onSaveConfig(None): - return - - f_cpu, cpu = self.pgBoard.getCPUInfo() - if not cpu: - dlg = wx.MessageDialog(self, "Unable to determine CPU type.", - "CPU type error", wx.OK | wx.ICON_ERROR) - dlg.ShowModal() - dlg.Destroy() - return - - if not f_cpu: - dlg = wx.MessageDialog(self, "Unable to determine CPU clock rate.", - "CPU clock rate error", wx.OK | wx.ICON_ERROR) - dlg.ShowModal() - dlg.Destroy() - return - - if buildFlag: - dlg = Build(self, self.settings, f_cpu, cpu) - dlg.ShowModal() - dlg.Destroy() - self.checkEnableUpload() - else: - dlg = Upload(self, self.settings, f_cpu, cpu) - dlg.ShowModal() - dlg.Destroy() - - def verifyConfigLoaded(self): - pfile, bfile = self.getConfigFileNames("config.h") - lpfile = self.pgPrinter.getFileName() - lbfile = self.pgBoard.getFileName() - - return ((pfile == lpfile) and (bfile == lbfile)) - - def onEditSettings(self, evt): - dlg = SettingsDlg(self, self.settings) - rc = dlg.ShowModal() - dlg.Destroy() - - def onHelp(self, evt): - self.message("Find help by hovering slowly over the buttons and text " - "fields. Tooltip should appear, explaining things.", - "Find help", style = wx.OK) - - def onReportProblem(self, evt): - import urllib - import webbrowser - import subprocess - from sys import platform - - # Testing allowed URLs up to 32 kB in size. Longer URLs are simply chopped. - mailRecipients ="reply+0004dc756da9f0641af0a3834c580ad5be469f4f6b" \ - "5d4cfc92cf00000001118c958a92a169ce051faa8c@" \ - "reply.github.com,mah@jump-ing.de" - mailSubject = "Teacup problem report" - mailBody = "Please answer these questions before hitting \"send\":\n\n" \ - "What did you try to do?\n\n\n" \ - "What did you expect to happen?\n\n\n" \ - "What happened instead?\n\n\n\n" \ - "To allow developers to help, configuration files are " \ - "attached, with help comments stripped:\n" - - for f in self.pgBoard.getFileName(), self.pgPrinter.getFileName(): - if not f: - mailBody += "\n(no file loaded)\n" - continue - - mailBody += "\n" + os.path.basename(f) + ":\n" - mailBody += "----------------------------------------------\n" - try: - fc = open(f).read() - fc = reHelpText.sub("", fc) - mailBody += fc - except: - mailBody += "(could not read this file)\n" - mailBody += "----------------------------------------------\n" - - url = "mailto:" + urllib.quote(mailRecipients) + \ - "?subject=" + urllib.quote(mailSubject) + \ - "&body=" + urllib.quote(mailBody) - - # This is a work around a bug in gvfs-open coming with (at least) Ubuntu - # 15.04. gvfs-open would open mailto:///user@example.com instead of - # the requested mailto:user@example.com. - if platform.startswith("linux"): - try: - subprocess.check_output(["gvfs-open", "--help"]) - - # Broken gvfs-open exists, so it might be used. - # Try to open the URL directly. - for urlOpener in "thunderbird", "evolution", "firefox", "mozilla", \ - "epiphany", "konqueror", "chromium-browser", \ - "google-chrome": - try: - subprocess.check_output([urlOpener, url], stderr=subprocess.STDOUT) - return - except: - pass - except: - pass - - webbrowser.open_new(url) - - def onAbout(self, evt): - # Get the contributors' top 10 with something like this: - # export B=experimental - # git log $B | grep "Author:" | sort | uniq | while \ - # read A; do N=$(git log $B | grep "$A" | wc -l); echo "$N $A"; done | \ - # sort -rn - self.message("Teacup Firmware is a 3D Printer and CNC machine controlling " - "firmware with emphasis on performance, efficiency and " - "outstanding quality. What Teacup does, shall it do very well." - "\n\n\n" - "Lots of people hard at work! Top 10 contributors:\n\n" - " Markus Hitter (542 commits)\n" - " Michael Moon (322 commits)\n" - " Phil Hord (55 commits)\n" - " Jeff Bernardis (51 commits)\n" - " Markus Amsler (47 commits)\n" - " David Forrest (27 commits)\n" - " Jim McGee (15 commits)\n" - " Ben Jackson (12 commits)\n" - " Bas Laarhoven (10 commits)\n" - " Stephan Walter (9 commits)\n" - " Roland Brochard (3 commits)\n" - " Jens Ch. Restemeier (3 commits)\n", - "About Teacup", style = wx.OK) - - def message(self, text, title, style = wx.OK + wx.ICON_ERROR): - dlg = wx.MessageDialog(self, text, title, style) - dlg.ShowModal() - dlg.Destroy() + # Check for HELP first. + for opt, arg in opts: + if opt in ("-h", "--help"): + cmdHelp() + sys.exit() + # Now parse other options. + for opt, arg in opts: + if opt in ("-l", "--load"): + cmdLoad(arg) if __name__ == '__main__': - app = wx.App(False) - frame = ConfigFrame() - frame.Show(True) - app.MainLoop() + CommandLine(sys.argv[1:]) + + cmdFolder = os.path.realpath(os.path.abspath(os.path.split(inspect.getfile( + inspect.currentframe()))[0])) + StartGui(cmdFolder) diff --git a/configtool/gui.py b/configtool/gui.py new file mode 100644 index 0000000..3420ae8 --- /dev/null +++ b/configtool/gui.py @@ -0,0 +1,622 @@ + +import sys +import time +try: + import wx +except: + print("ImportError: No module named wx\n\n" + "wxPython is not installed. This program requires wxPython to run.\n" + "See your package manager and/or http://wxpython.org/download.php.") + time.sleep(10) + sys.exit(-1) + +import os.path + +from configtool.data import reHelpText +from configtool.decoration import Decoration +from configtool.settings import Settings, SettingsDlg +from configtool.printerpanel import PrinterPanel +from configtool.boardpanel import BoardPanel +from configtool.build import Build, Upload +from configtool.data import reInclude + +ID_LOAD_PRINTER = 1000 +ID_SAVE_PRINTER = 1001 +ID_SAVE_PRINTER_AS = 1002 +ID_LOAD_BOARD = 1010 +ID_SAVE_BOARD = 1011 +ID_SAVE_BOARD_AS = 1012 +ID_LOAD_CONFIG = 1020 +ID_SAVE_CONFIG = 1021 +ID_BUILD = 1030 +ID_UPLOAD = 1031 +ID_SETTINGS = 1040 +ID_HELP = 1050 +ID_REPORT = 1051 +ID_ABOUT = 1052 + + +cmdFolder = "placeholder" + +class ConfigFrame(wx.Frame): + def __init__(self): + wx.Frame.__init__(self, None, -1, "Teacup Configtool", size = (880, 550)) + self.Bind(wx.EVT_CLOSE, self.onClose) + self.Bind(wx.EVT_SIZE, self.onResize) + + self.deco = Decoration() + + panel = wx.Panel(self, -1) + panel.SetBackgroundColour(self.deco.getBackgroundColour()) + panel.Bind(wx.EVT_PAINT, self.deco.onPaintBackground) + + self.settings = Settings(self, cmdFolder) + self.settings.font = wx.Font(8, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, + wx.FONTWEIGHT_BOLD) + self.settings.folder = cmdFolder + + self.heaters = [] + self.savePrtEna = False + self.saveBrdEna = False + self.protPrtFile = False + self.protBrdFile = False + + sz = wx.BoxSizer(wx.HORIZONTAL) + + self.nb = wx.Notebook(panel, wx.ID_ANY, size = (880, 550), + style = wx.BK_DEFAULT) + self.nb.SetBackgroundColour(self.deco.getBackgroundColour()) + self.nb.SetFont(self.settings.font) + + self.printerFileName = None + self.printerTabDecor = "" + self.printerBaseText = "Printer" + self.pgPrinter = PrinterPanel(self, self.nb, self.settings) + self.nb.AddPage(self.pgPrinter, self.printerBaseText) + + self.boardFileName = None + self.boardTabDecor = "" + self.boardBaseText = "Board" + self.pgBoard = BoardPanel(self, self.nb, self.settings) + self.nb.AddPage(self.pgBoard, self.boardBaseText) + + panel.Fit() + self.panel = panel + + sz.Add(self.nb, 1, wx.EXPAND + wx.ALL, 5) + self.SetSizer(sz) + self.makeMenu() + + def onClose(self, evt): + if not self.pgPrinter.confirmLoseChanges("exit"): + return + + if not self.pgBoard.confirmLoseChanges("exit"): + return + + self.Destroy() + + def onResize(self, evt): + self.panel.SetSize(self.GetClientSize()) + self.Refresh() + evt.Skip(); + + def setPrinterTabFile(self, fn): + self.printerFileName = fn + self.updatePrinterTab() + + def setPrinterTabDecor(self, prefix): + self.printerTabDecor = prefix + self.updatePrinterTab() + + def updatePrinterTab(self): + txt = self.printerTabDecor + self.printerBaseText + if self.printerFileName: + txt += " <%s>" % self.printerFileName + self.nb.SetPageText(0, txt) + + def setBoardTabFile(self, fn): + self.boardFileName = fn + self.updateBoardTab() + + def setBoardTabDecor(self, prefix): + self.boardTabDecor = prefix + self.updateBoardTab() + + def updateBoardTab(self): + txt = self.boardTabDecor + self.boardBaseText + if self.boardFileName: + txt += " <%s>" % self.boardFileName + self.nb.SetPageText(1, txt) + + def setHeaters(self, ht): + self.heaters = ht + self.pgPrinter.setHeaters(ht) + + def makeMenu(self): + file_menu = wx.Menu() + + file_menu.Append(ID_LOAD_CONFIG, "Load config.h", + "Load config.h and its named printer and board files.") + self.Bind(wx.EVT_MENU, self.onLoadConfig, id = ID_LOAD_CONFIG) + file_menu.Enable(ID_LOAD_CONFIG, False) + + file_menu.Append(ID_SAVE_CONFIG, "Save config.h", "Save config.h file.") + self.Bind(wx.EVT_MENU, self.onSaveConfig, id = ID_SAVE_CONFIG) + file_menu.Enable(ID_SAVE_CONFIG, False) + + file_menu.AppendSeparator() + + file_menu.Append(ID_LOAD_PRINTER, "Load printer", + "Load a printer configuration file.") + self.Bind(wx.EVT_MENU, self.pgPrinter.onLoadConfig, id = ID_LOAD_PRINTER) + + file_menu.Append(ID_SAVE_PRINTER, "Save printer", + "Save printer configuration.") + self.Bind(wx.EVT_MENU, self.onSavePrinterConfig, id = ID_SAVE_PRINTER) + file_menu.Enable(ID_SAVE_PRINTER, False) + + file_menu.Append(ID_SAVE_PRINTER_AS, "Save printer as...", + "Save printer configuration to a new file.") + self.Bind(wx.EVT_MENU, self.onSavePrinterConfigAs, id = ID_SAVE_PRINTER_AS) + file_menu.Enable(ID_SAVE_PRINTER_AS, False) + + file_menu.AppendSeparator() + + file_menu.Append(ID_LOAD_BOARD, "Load board", + "Load a board configuration file.") + self.Bind(wx.EVT_MENU, self.pgBoard.onLoadConfig, id = ID_LOAD_BOARD) + + file_menu.Append(ID_SAVE_BOARD, "Save board", "Save board configuration.") + self.Bind(wx.EVT_MENU, self.onSaveBoardConfig, id = ID_SAVE_BOARD) + file_menu.Enable(ID_SAVE_BOARD, False) + + file_menu.Append(ID_SAVE_BOARD_AS, "Save board as...", + "Save board configuration to a new file.") + self.Bind(wx.EVT_MENU, self.onSaveBoardConfigAs, id = ID_SAVE_BOARD_AS) + file_menu.Enable(ID_SAVE_BOARD_AS, False) + + file_menu.AppendSeparator() + + file_menu.Append(wx.ID_EXIT, "E&xit", "Exit the application.") + self.Bind(wx.EVT_MENU, self.onClose, id = wx.ID_EXIT) + + self.fileMenu = file_menu + + menu_bar = wx.MenuBar() + + menu_bar.Append(file_menu, "&File") + + edit_menu = wx.Menu() + + edit_menu.Append(ID_SETTINGS, "Settings", "Change settings.") + self.Bind(wx.EVT_MENU, self.onEditSettings, id = ID_SETTINGS) + + self.editMenu = edit_menu + + menu_bar.Append(edit_menu, "&Edit") + + build_menu = wx.Menu() + + build_menu.Append(ID_BUILD, "Build", "Build the executable.") + self.Bind(wx.EVT_MENU, self.onBuild, id = ID_BUILD) + + build_menu.Append(ID_UPLOAD, "Upload", "Upload the executable.") + self.Bind(wx.EVT_MENU, self.onUpload, id = ID_UPLOAD) + + self.buildMenu = build_menu + + menu_bar.Append(build_menu, "&Build") + + help_menu = wx.Menu() + + help_menu.Append(ID_HELP, "Help", "Find help.") + self.Bind(wx.EVT_MENU, self.onHelp, id = ID_HELP) + + help_menu.Append(ID_REPORT, "Report problem", + "Report a problem to Teacup maintainers.") + self.Bind(wx.EVT_MENU, self.onReportProblem, id = ID_REPORT) + + help_menu.AppendSeparator() + + help_menu.Append(ID_ABOUT, "About Teacup") + self.Bind(wx.EVT_MENU, self.onAbout, id = ID_ABOUT) + + self.helpMenu = help_menu + + menu_bar.Append(help_menu, "&Help") + + self.SetMenuBar(menu_bar) + loadFlag = self.checkEnableLoadConfig() + self.checkEnableUpload() + if loadFlag: + self.loadConfigFile("config.h") + + def onSaveBoardConfig(self, evt): + rc = self.pgBoard.onSaveConfig(evt) + if rc: + self.checkEnableLoadConfig() + return rc + + def onSaveBoardConfigAs(self, evt): + rc = self.pgBoard.onSaveConfigAs(evt) + if rc: + self.checkEnableLoadConfig() + return rc + + def onSavePrinterConfig(self, evt): + rc = self.pgPrinter.onSaveConfig(evt) + if rc: + self.checkEnableLoadConfig() + return rc + + def onSavePrinterConfigAs(self, evt): + rc = self.pgPrinter.onSaveConfigAs(evt) + if rc: + self.checkEnableLoadConfig() + return rc + + def checkEnableLoadConfig(self): + fn = os.path.join(cmdFolder, "config.h") + if os.path.isfile(fn): + self.fileMenu.Enable(ID_LOAD_CONFIG, True) + self.buildMenu.Enable(ID_BUILD, True) + return True + else: + self.fileMenu.Enable(ID_LOAD_CONFIG, False) + self.buildMenu.Enable(ID_BUILD, False) + return False + + def checkEnableUpload(self): + fn = os.path.join(cmdFolder, "teacup.hex") + if os.path.isfile(fn): + self.buildMenu.Enable(ID_UPLOAD, True) + else: + self.buildMenu.Enable(ID_UPLOAD, False) + + def enableSavePrinter(self, saveFlag, saveAsFlag): + self.fileMenu.Enable(ID_SAVE_PRINTER, saveFlag) + self.fileMenu.Enable(ID_SAVE_PRINTER_AS, saveAsFlag) + self.savePrtEna = saveAsFlag + self.protPrtFile = not saveFlag + if self.savePrtEna and self.saveBrdEna: + self.enableSaveConfig(True) + else: + self.enableSaveConfig(False) + + def enableSaveBoard(self, saveFlag, saveAsFlag): + self.fileMenu.Enable(ID_SAVE_BOARD, saveFlag) + self.fileMenu.Enable(ID_SAVE_BOARD_AS, saveAsFlag) + self.saveBrdEna = saveAsFlag + self.protBrdFile = not saveFlag + if self.savePrtEna and self.saveBrdEna: + self.enableSaveConfig(True) + else: + self.enableSaveConfig(False) + + def enableSaveConfig(self, flag): + self.fileMenu.Enable(ID_SAVE_CONFIG, flag) + + def onLoadConfig(self, evt): + self.loadConfigFile("config.h") + + def loadConfigFile(self, fn): + if not self.pgPrinter.confirmLoseChanges("load config"): + return False + + if not self.pgBoard.confirmLoseChanges("load config"): + return False + + pfile, bfile = self.getConfigFileNames(fn) + + if not pfile: + self.message("Config file did not contain a printer file " + "include statement.", "Config error") + return False + else: + if not self.pgPrinter.loadConfigFile(pfile): + self.message("There was a problem loading the printer config file:\n%s" + % pfile, "Config error") + return False + + if not bfile: + self.message("Config file did not contain a board file " + "include statement.", "Config error") + return False + else: + if not self.pgBoard.loadConfigFile(bfile): + self.message("There was a problem loading the board config file:\n%s" + % bfile, "Config error") + return False + + return True + + def getConfigFileNames(self, fn): + pfile = None + bfile = None + path = os.path.join(cmdFolder, fn) + try: + cfgBuffer = list(open(path)) + except: + self.message("Unable to process config file %s." % fn, "File error") + return None, None + + for ln in cfgBuffer: + if not ln.lstrip().startswith("#include"): + continue + + m = reInclude.search(ln) + if m: + t = m.groups() + if len(t) == 1: + if "printer." in t[0]: + if pfile: + self.message("Multiple printer file include statements.\n" + "Ignoring %s." % ln, "Config error", + wx.OK + wx.ICON_WARNING) + else: + pfile = os.path.join(cmdFolder, t[0]) + elif "board." in t[0]: + if bfile: + self.message("Multiple board file include statements.\n" + "Ignoring %s." % ln, "Config error", + wx.OK + wx.ICON_WARNING) + else: + bfile = os.path.join(cmdFolder, t[0]) + else: + self.message("Unable to parse include statement:\n%s" % ln, + "Config error") + + return pfile, bfile + + def onSaveConfig(self, evt): + fn = os.path.join(cmdFolder, "config.h") + try: + fp = open(fn, 'w') + except: + self.message("Unable to open config.h for output.", "File error") + return False + + bfn = self.pgBoard.getFileName() + if self.pgBoard.isModified() and self.pgBoard.isValid(): + if not self.pgBoard.saveConfigFile(bfn): + return False + else: + self.pgBoard.generateTempTables() + + pfn = self.pgPrinter.getFileName() + if self.pgPrinter.isModified() and self.pgPrinter.isValid(): + if not self.pgPrinter.saveConfigFile(pfn): + return False + + prefix = cmdFolder + os.path.sep + lpfx = len(prefix) + + if bfn.startswith(prefix): + rbfn = bfn[lpfx:] + else: + rbfn = bfn + + if pfn.startswith(prefix): + rpfn = pfn[lpfx:] + else: + rpfn = pfn + + fp.write("\n") + fp.write("// Configuration for controller board.\n") + fp.write("#include \"%s\"\n" % rbfn) + fp.write("\n") + fp.write("// Configuration for printer board.\n") + fp.write("#include \"%s\"\n" % rpfn) + + fp.close() + + self.checkEnableLoadConfig() + return True + + def onBuild(self, evt): + self.onBuildorUpload(True) + + def onUpload(self, evt): + self.onBuildorUpload(False) + + def onBuildorUpload(self, buildFlag): + if not (self.pgPrinter.hasData() or self.pgBoard.hasData()): + dlg = wx.MessageDialog(self, "Data needs to be loaded. " + "Click Yes to load config.h.", + "Data missing", + wx.YES_NO | wx.NO_DEFAULT | wx.ICON_INFORMATION) + rc = dlg.ShowModal() + dlg.Destroy() + if rc != wx.ID_YES: + return + + self.loadConfigFile("config.h") + else: + if self.pgPrinter.isModified(): + dlg = wx.MessageDialog(self, "Printer data needs to be saved. Click " + "Yes to save printer configuration.", + "Changes pending", + wx.YES_NO | wx.NO_DEFAULT | wx.ICON_INFORMATION) + rc = dlg.ShowModal() + dlg.Destroy() + if rc != wx.ID_YES: + return + + if self.protPrtFile: + rc = self.onSavePrinterConfigAs(None) + else: + rc = self.onSavePrinterConfig(None) + if not rc: + return + + if self.pgBoard.isModified(): + dlg = wx.MessageDialog(self, "Board data needs to be saved. Click " + "Yes to save board configuration.", + "Changes pending", + wx.YES_NO | wx.NO_DEFAULT | wx.ICON_INFORMATION) + rc = dlg.ShowModal() + dlg.Destroy() + if rc != wx.ID_YES: + return + + if self.protBrdFile: + rc = self.onSaveBoardConfigAs(None) + else: + rc = self.onSaveBoardConfig(None) + if not rc: + return + + if not self.verifyConfigLoaded(): + dlg = wx.MessageDialog(self, "Loaded configuration does not match the " + "config.h file. Click Yes to save config.h.", + "Configuration changed", + wx.YES_NO | wx.NO_DEFAULT | wx.ICON_INFORMATION) + rc = dlg.ShowModal() + dlg.Destroy() + if rc != wx.ID_YES: + return + + if not self.onSaveConfig(None): + return + + f_cpu, cpu = self.pgBoard.getCPUInfo() + if not cpu: + dlg = wx.MessageDialog(self, "Unable to determine CPU type.", + "CPU type error", wx.OK | wx.ICON_ERROR) + dlg.ShowModal() + dlg.Destroy() + return + + if not f_cpu: + dlg = wx.MessageDialog(self, "Unable to determine CPU clock rate.", + "CPU clock rate error", wx.OK | wx.ICON_ERROR) + dlg.ShowModal() + dlg.Destroy() + return + + if buildFlag: + dlg = Build(self, self.settings, f_cpu, cpu) + dlg.ShowModal() + dlg.Destroy() + self.checkEnableUpload() + else: + dlg = Upload(self, self.settings, f_cpu, cpu) + dlg.ShowModal() + dlg.Destroy() + + def verifyConfigLoaded(self): + pfile, bfile = self.getConfigFileNames("config.h") + lpfile = self.pgPrinter.getFileName() + lbfile = self.pgBoard.getFileName() + + return ((pfile == lpfile) and (bfile == lbfile)) + + def onEditSettings(self, evt): + dlg = SettingsDlg(self, self.settings) + rc = dlg.ShowModal() + dlg.Destroy() + + def onHelp(self, evt): + self.message("Find help by hovering slowly over the buttons and text " + "fields. Tooltip should appear, explaining things.", + "Find help", style = wx.OK) + + def onReportProblem(self, evt): + import urllib + import webbrowser + import subprocess + from sys import platform + + # Testing allowed URLs up to 32 kB in size. Longer URLs are simply chopped. + mailRecipients ="reply+0004dc756da9f0641af0a3834c580ad5be469f4f6b" \ + "5d4cfc92cf00000001118c958a92a169ce051faa8c@" \ + "reply.github.com,mah@jump-ing.de" + mailSubject = "Teacup problem report" + mailBody = "Please answer these questions before hitting \"send\":\n\n" \ + "What did you try to do?\n\n\n" \ + "What did you expect to happen?\n\n\n" \ + "What happened instead?\n\n\n\n" \ + "To allow developers to help, configuration files are " \ + "attached, with help comments stripped:\n" + + for f in self.pgBoard.getFileName(), self.pgPrinter.getFileName(): + if not f: + mailBody += "\n(no file loaded)\n" + continue + + mailBody += "\n" + os.path.basename(f) + ":\n" + mailBody += "----------------------------------------------\n" + try: + fc = open(f).read() + fc = reHelpText.sub("", fc) + mailBody += fc + except: + mailBody += "(could not read this file)\n" + mailBody += "----------------------------------------------\n" + + url = "mailto:" + urllib.quote(mailRecipients) + \ + "?subject=" + urllib.quote(mailSubject) + \ + "&body=" + urllib.quote(mailBody) + + # This is a work around a bug in gvfs-open coming with (at least) Ubuntu + # 15.04. gvfs-open would open mailto:///user@example.com instead of + # the requested mailto:user@example.com. + if platform.startswith("linux"): + try: + subprocess.check_output(["gvfs-open", "--help"]) + + # Broken gvfs-open exists, so it might be used. + # Try to open the URL directly. + for urlOpener in "thunderbird", "evolution", "firefox", "mozilla", \ + "epiphany", "konqueror", "chromium-browser", \ + "google-chrome": + try: + subprocess.check_output([urlOpener, url], stderr=subprocess.STDOUT) + return + except: + pass + except: + pass + + webbrowser.open_new(url) + + def onAbout(self, evt): + # Get the contributors' top 10 with something like this: + # export B=experimental + # git log $B | grep "Author:" | sort | uniq | while \ + # read A; do N=$(git log $B | grep "$A" | wc -l); echo "$N $A"; done | \ + # sort -rn + self.message("Teacup Firmware is a 3D Printer and CNC machine controlling " + "firmware with emphasis on performance, efficiency and " + "outstanding quality. What Teacup does, shall it do very well." + "\n\n\n" + "Lots of people hard at work! Top 10 contributors:\n\n" + " Markus Hitter (542 commits)\n" + " Michael Moon (322 commits)\n" + " Phil Hord (55 commits)\n" + " Jeff Bernardis (51 commits)\n" + " Markus Amsler (47 commits)\n" + " David Forrest (27 commits)\n" + " Jim McGee (15 commits)\n" + " Ben Jackson (12 commits)\n" + " Bas Laarhoven (10 commits)\n" + " Stephan Walter (9 commits)\n" + " Roland Brochard (3 commits)\n" + " Jens Ch. Restemeier (3 commits)\n", + "About Teacup", style = wx.OK) + + def message(self, text, title, style = wx.OK + wx.ICON_ERROR): + dlg = wx.MessageDialog(self, text, title, style) + dlg.ShowModal() + dlg.Destroy() + + +def StartGui(teacupFolder): + global cmdFolder + + cmdFolder = teacupFolder + app = wx.App(False) + frame = ConfigFrame() + frame.Show(True) + app.MainLoop()