Teacup_Firmware/configtool/gui.py

631 lines
20 KiB
Python

import sys
import time
try:
import wx
if int(wx.__version__[0]) < 4:
print("Configtool needs wxPython 4.x. Please check https://wxpython.org/pages/downloads/.")
time.sleep(10)
sys.exit(-1)
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 https://wxpython.org/pages/downloads/."
)
time.sleep(10)
sys.exit(-1)
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 https://wxpython.org/pages/downloads/.")
time.sleep(10)
sys.exit(-1)
import os.path
from configtool.data import reHelpText
from configtool.decoration import Decoration
from configtool.settings import Settings
from configtool.settingsdlg import 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
class ConfigFrame(wx.Frame):
def __init__(self, settings):
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.settings.app = self
self.settings.font = wx.Font(8, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL,
wx.FONTWEIGHT_BOLD)
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(self.settings.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(self.settings.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(self.settings.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(self.settings.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(self.settings.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(self.settings.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 = self.settings.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()
def StartGui(settings):
app = wx.App(False)
frame = ConfigFrame(settings)
frame.Show(True)
app.MainLoop()