chiark / gitweb /
Merge branch 'SteamEngine' of github.com:daid/Cura into SteamEngine
[cura.git] / Cura / gui / printWindow.py
1 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
2
3 import threading
4 import re
5 import subprocess
6 import sys
7 import time
8 import platform
9 import os
10 import power
11 import datetime
12
13 import wx
14 from wx.lib import buttons
15
16 from Cura.gui.util import webcam
17 from Cura.gui.util import taskbar
18 from Cura.util import machineCom
19 from Cura.util import gcodeInterpreter
20 from Cura.util import resources
21 from Cura.util import profile
22
23 #The printProcessMonitor is used from the main GUI python process. This monitors the printing python process.
24 # This class also handles starting of the 2nd process for printing and all communications with it.
25 class printProcessMonitor():
26         def __init__(self, callback = None):
27                 self.handle = None
28                 self._state = 'CLOSED'
29                 self._z = 0.0
30                 self._callback = callback
31                 self._id = -1
32
33         def loadFile(self, filename, id):
34                 if self.handle is None:
35                         if platform.system() == "Darwin" and hasattr(sys, 'frozen'):
36                                 cmdList = [os.path.join(os.path.dirname(sys.executable), 'Cura')]
37                         else:
38                                 cmdList = [sys.executable, '-m', 'Cura.cura']
39                         cmdList.append('-r')
40                         cmdList.append(filename)
41                         if platform.system() == "Darwin":
42                                 if platform.machine() == 'i386':
43                                         cmdList.insert(0, 'arch')
44                                         cmdList.insert(1, '-i386')
45                         #Save the preferences before starting the print window so we use the proper machine settings.
46                         profile.savePreferences(profile.getPreferencePath())
47                         self.handle = subprocess.Popen(cmdList, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
48                         self.thread = threading.Thread(target=self.Monitor)
49                         self.thread.start()
50                 else:
51                         self.handle.stdin.write('LOAD:%s\n' % filename)
52                 self._id = id
53
54         def Monitor(self):
55                 p = self.handle
56                 line = p.stdout.readline()
57                 while len(line) > 0:
58                         line = line.rstrip()
59                         try:
60                                 if line.startswith('Z:'):
61                                         self._z = float(line[2:])
62                                         self._callCallback()
63                                 elif line.startswith('STATE:'):
64                                         self._state = line[6:]
65                                         self._callCallback()
66                                 else:
67                                         print '>' + line.rstrip()
68                         except:
69                                 print sys.exc_info()
70                         line = p.stdout.readline()
71                 line = p.stderr.readline()
72                 while len(line) > 0:
73                         print '>>' + line.rstrip()
74                         line = p.stderr.readline()
75                 p.communicate()
76                 self.handle = None
77                 self.thread = None
78
79         def getID(self):
80                 return self._id
81
82         def getZ(self):
83                 return self._z
84
85         def getState(self):
86                 return self._state
87
88         def _callCallback(self):
89                 if self._callback is not None:
90                         self._callback()
91
92 def startPrintInterface(filename):
93         #startPrintInterface is called from the main script when we want the printer interface to run in a separate process.
94         # It needs to run in a separate process, as any running python code blocks the GCode sender python code (http://wiki.python.org/moin/GlobalInterpreterLock).
95
96         sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
97
98         app = wx.App(False)
99         resources.setupLocalization(profile.getPreference('language'))
100         printWindowHandle = printWindow()
101         printWindowHandle.Show(True)
102         printWindowHandle.Raise()
103         printWindowHandle.OnConnect(None)
104         t = threading.Thread(target=printWindowHandle.LoadGCodeFile, args=(filename,))
105         t.daemon = True
106         t.start()
107         app.MainLoop()
108
109 class PrintCommandButton(buttons.GenBitmapButton):
110         def __init__(self, parent, commandList, bitmapFilename, size=(20, 20)):
111                 self.bitmap = wx.Bitmap(resources.getPathForImage(bitmapFilename))
112                 super(PrintCommandButton, self).__init__(parent.directControlPanel, -1, self.bitmap, size=size)
113
114                 self.commandList = commandList
115                 self.parent = parent
116
117                 self.SetBezelWidth(1)
118                 self.SetUseFocusIndicator(False)
119
120                 self.Bind(wx.EVT_BUTTON, self.OnClick)
121
122         def OnClick(self, e):
123                 if self.parent.machineCom is None or self.parent.machineCom.isPrinting():
124                         return;
125                 for cmd in self.commandList:
126                         self.parent.machineCom.sendCommand(cmd)
127                 e.Skip()
128
129
130 class printWindow(wx.Frame):
131         "Main user interface window"
132
133         def __init__(self):
134                 super(printWindow, self).__init__(None, -1, title=_("Printing"))
135                 t = time.time()
136                 self.machineCom = None
137                 self.gcode = None
138                 self.gcodeList = None
139                 self.sendList = []
140                 self.temp = None
141                 self.bedTemp = None
142                 self.bufferLineCount = 4
143                 self.sendCnt = 0
144                 self.feedrateRatioOuterWall = 1.0
145                 self.feedrateRatioInnerWall = 1.0
146                 self.feedrateRatioFill = 1.0
147                 self.feedrateRatioSupport = 1.0
148                 self.pause = False
149                 self.termHistory = []
150                 self.termHistoryIdx = 0
151
152                 self.cam = None
153
154                 self.SetSizer(wx.BoxSizer())
155                 self.panel = wx.Panel(self)
156                 self.GetSizer().Add(self.panel, 1, flag=wx.EXPAND)
157                 self.sizer = wx.GridBagSizer(2, 2)
158                 self.panel.SetSizer(self.sizer)
159
160                 sb = wx.StaticBox(self.panel, label=_("Statistics"))
161                 boxsizer = wx.StaticBoxSizer(sb, wx.VERTICAL)
162
163                 self.powerWarningText = wx.StaticText(parent=self.panel,
164                         id=-1,
165                         label=_("Your computer is running on battery power.\nConnect your computer to AC power or your print might not finish."),
166                         style=wx.ALIGN_CENTER)
167                 self.powerWarningText.SetBackgroundColour('red')
168                 self.powerWarningText.SetForegroundColour('white')
169                 boxsizer.AddF(self.powerWarningText, flags=wx.SizerFlags().Expand().Border(wx.BOTTOM, 10))
170                 self.powerManagement = power.PowerManagement()
171                 self.powerWarningTimer = wx.Timer(self)
172                 self.Bind(wx.EVT_TIMER, self.OnPowerWarningChange, self.powerWarningTimer)
173                 self.OnPowerWarningChange(None)
174                 self.powerWarningTimer.Start(10000)
175
176                 self.statsText = wx.StaticText(self.panel, -1, _("Filament: ####.##m #.##g\nEstimated print time: #####:##\nMachine state:\nDetecting baudrateXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"))
177                 boxsizer.Add(self.statsText, flag=wx.LEFT, border=5)
178
179                 self.sizer.Add(boxsizer, pos=(0, 0), span=(7, 1), flag=wx.EXPAND)
180
181                 self.connectButton = wx.Button(self.panel, -1, _("Connect"))
182                 #self.loadButton = wx.Button(self.panel, -1, 'Load')
183                 self.printButton = wx.Button(self.panel, -1, _("Print"))
184                 self.pauseButton = wx.Button(self.panel, -1, _("Pause"))
185                 self.cancelButton = wx.Button(self.panel, -1, _("Cancel print"))
186                 self.machineLogButton = wx.Button(self.panel, -1, _("Error log"))
187                 self.progress = wx.Gauge(self.panel, -1)
188
189                 self.sizer.Add(self.connectButton, pos=(1, 1), flag=wx.EXPAND)
190                 #self.sizer.Add(self.loadButton, pos=(1,1), flag=wx.EXPAND)
191                 self.sizer.Add(self.printButton, pos=(2, 1), flag=wx.EXPAND)
192                 self.sizer.Add(self.pauseButton, pos=(3, 1), flag=wx.EXPAND)
193                 self.sizer.Add(self.cancelButton, pos=(4, 1), flag=wx.EXPAND)
194                 self.sizer.Add(self.machineLogButton, pos=(5, 1), flag=wx.EXPAND)
195                 self.sizer.Add(self.progress, pos=(7, 0), span=(1, 7), flag=wx.EXPAND)
196
197                 nb = wx.Notebook(self.panel)
198                 self.tabs = nb
199                 self.sizer.Add(nb, pos=(0, 2), span=(7, 4), flag=wx.EXPAND)
200
201                 self.temperaturePanel = wx.Panel(nb)
202                 sizer = wx.GridBagSizer(2, 2)
203                 self.temperaturePanel.SetSizer(sizer)
204
205                 self.temperatureSelect = wx.SpinCtrl(self.temperaturePanel, -1, '0', size=(21 * 3, 21), style=wx.SP_ARROW_KEYS)
206                 self.temperatureSelect.SetRange(0, 400)
207                 self.temperatureHeatUp = wx.Button(self.temperaturePanel, -1, str(int(profile.getProfileSettingFloat('print_temperature'))) + 'C')
208                 self.bedTemperatureLabel = wx.StaticText(self.temperaturePanel, -1, _("BedTemp:"))
209                 self.bedTemperatureSelect = wx.SpinCtrl(self.temperaturePanel, -1, '0', size=(21 * 3, 21), style=wx.SP_ARROW_KEYS)
210                 self.bedTemperatureSelect.SetRange(0, 400)
211                 self.bedTemperatureLabel.Show(False)
212                 self.bedTemperatureSelect.Show(False)
213
214                 self.temperatureGraph = temperatureGraph(self.temperaturePanel)
215
216                 sizer.Add(wx.StaticText(self.temperaturePanel, -1, _("Temp:")), pos=(0, 0))
217                 sizer.Add(self.temperatureSelect, pos=(0, 1))
218                 sizer.Add(self.temperatureHeatUp, pos=(0, 2))
219                 sizer.Add(self.bedTemperatureLabel, pos=(1, 0))
220                 sizer.Add(self.bedTemperatureSelect, pos=(1, 1))
221                 sizer.Add(self.temperatureGraph, pos=(2, 0), span=(1, 3), flag=wx.EXPAND)
222                 sizer.AddGrowableRow(2)
223                 sizer.AddGrowableCol(2)
224
225                 nb.AddPage(self.temperaturePanel, 'Temp')
226
227                 self.directControlPanel = wx.Panel(nb)
228
229                 sizer = wx.GridBagSizer(2, 2)
230                 self.directControlPanel.SetSizer(sizer)
231                 sizer.Add(PrintCommandButton(self, ['G91', 'G1 Y100 F6000', 'G90'], 'print-move-y100.png'), pos=(0, 3))
232                 sizer.Add(PrintCommandButton(self, ['G91', 'G1 Y10 F6000', 'G90'], 'print-move-y10.png'), pos=(1, 3))
233                 sizer.Add(PrintCommandButton(self, ['G91', 'G1 Y1 F6000', 'G90'], 'print-move-y1.png'), pos=(2, 3))
234
235                 sizer.Add(PrintCommandButton(self, ['G91', 'G1 Y-1 F6000', 'G90'], 'print-move-y-1.png'), pos=(4, 3))
236                 sizer.Add(PrintCommandButton(self, ['G91', 'G1 Y-10 F6000', 'G90'], 'print-move-y-10.png'), pos=(5, 3))
237                 sizer.Add(PrintCommandButton(self, ['G91', 'G1 Y-100 F6000', 'G90'], 'print-move-y-100.png'), pos=(6, 3))
238
239                 sizer.Add(PrintCommandButton(self, ['G91', 'G1 X-100 F6000', 'G90'], 'print-move-x-100.png'), pos=(3, 0))
240                 sizer.Add(PrintCommandButton(self, ['G91', 'G1 X-10 F6000', 'G90'], 'print-move-x-10.png'), pos=(3, 1))
241                 sizer.Add(PrintCommandButton(self, ['G91', 'G1 X-1 F6000', 'G90'], 'print-move-x-1.png'), pos=(3, 2))
242
243                 sizer.Add(PrintCommandButton(self, ['G28 X0 Y0'], 'print-move-home.png'), pos=(3, 3))
244
245                 sizer.Add(PrintCommandButton(self, ['G91', 'G1 X1 F6000', 'G90'], 'print-move-x1.png'), pos=(3, 4))
246                 sizer.Add(PrintCommandButton(self, ['G91', 'G1 X10 F6000', 'G90'], 'print-move-x10.png'), pos=(3, 5))
247                 sizer.Add(PrintCommandButton(self, ['G91', 'G1 X100 F6000', 'G90'], 'print-move-x100.png'), pos=(3, 6))
248
249                 sizer.Add(PrintCommandButton(self, ['G91', 'G1 Z10 F200', 'G90'], 'print-move-z10.png'), pos=(0, 8))
250                 sizer.Add(PrintCommandButton(self, ['G91', 'G1 Z1 F200', 'G90'], 'print-move-z1.png'), pos=(1, 8))
251                 sizer.Add(PrintCommandButton(self, ['G91', 'G1 Z0.1 F200', 'G90'], 'print-move-z0.1.png'), pos=(2, 8))
252
253                 sizer.Add(PrintCommandButton(self, ['G28 Z0'], 'print-move-home.png'), pos=(3, 8))
254
255                 sizer.Add(PrintCommandButton(self, ['G91', 'G1 Z-0.1 F200', 'G90'], 'print-move-z-0.1.png'), pos=(4, 8))
256                 sizer.Add(PrintCommandButton(self, ['G91', 'G1 Z-1 F200', 'G90'], 'print-move-z-1.png'), pos=(5, 8))
257                 sizer.Add(PrintCommandButton(self, ['G91', 'G1 Z-10 F200', 'G90'], 'print-move-z-10.png'), pos=(6, 8))
258
259                 sizer.Add(PrintCommandButton(self, ['G92 E0', 'G1 E2 F120'], 'extrude.png', size=(60, 20)), pos=(1, 10),
260                         span=(1, 3), flag=wx.EXPAND)
261                 sizer.Add(PrintCommandButton(self, ['G92 E0', 'G1 E-2 F120'], 'retract.png', size=(60, 20)), pos=(2, 10),
262                         span=(1, 3), flag=wx.EXPAND)
263
264                 nb.AddPage(self.directControlPanel, _("Jog"))
265
266                 self.speedPanel = wx.Panel(nb)
267                 sizer = wx.GridBagSizer(2, 2)
268                 self.speedPanel.SetSizer(sizer)
269
270                 self.outerWallSpeedSelect = wx.SpinCtrl(self.speedPanel, -1, '100', size=(21 * 3, 21), style=wx.SP_ARROW_KEYS)
271                 self.outerWallSpeedSelect.SetRange(5, 1000)
272                 self.innerWallSpeedSelect = wx.SpinCtrl(self.speedPanel, -1, '100', size=(21 * 3, 21), style=wx.SP_ARROW_KEYS)
273                 self.innerWallSpeedSelect.SetRange(5, 1000)
274                 self.fillSpeedSelect = wx.SpinCtrl(self.speedPanel, -1, '100', size=(21 * 3, 21), style=wx.SP_ARROW_KEYS)
275                 self.fillSpeedSelect.SetRange(5, 1000)
276                 self.supportSpeedSelect = wx.SpinCtrl(self.speedPanel, -1, '100', size=(21 * 3, 21), style=wx.SP_ARROW_KEYS)
277                 self.supportSpeedSelect.SetRange(5, 1000)
278
279                 sizer.Add(wx.StaticText(self.speedPanel, -1, _("Outer wall:")), pos=(0, 0))
280                 sizer.Add(self.outerWallSpeedSelect, pos=(0, 1))
281                 sizer.Add(wx.StaticText(self.speedPanel, -1, "%"), pos=(0, 2))
282                 sizer.Add(wx.StaticText(self.speedPanel, -1, _("Inner wall:")), pos=(1, 0))
283                 sizer.Add(self.innerWallSpeedSelect, pos=(1, 1))
284                 sizer.Add(wx.StaticText(self.speedPanel, -1, "%"), pos=(1, 2))
285                 sizer.Add(wx.StaticText(self.speedPanel, -1, _("Fill:")), pos=(2, 0))
286                 sizer.Add(self.fillSpeedSelect, pos=(2, 1))
287                 sizer.Add(wx.StaticText(self.speedPanel, -1, "%"), pos=(2, 2))
288                 sizer.Add(wx.StaticText(self.speedPanel, -1, _("Support:")), pos=(3, 0))
289                 sizer.Add(self.supportSpeedSelect, pos=(3, 1))
290                 sizer.Add(wx.StaticText(self.speedPanel, -1, "%"), pos=(3, 2))
291
292                 nb.AddPage(self.speedPanel, _("Speed"))
293
294                 self.termPanel = wx.Panel(nb)
295                 sizer = wx.GridBagSizer(2, 2)
296                 self.termPanel.SetSizer(sizer)
297
298                 f = wx.Font(8, wx.FONTFAMILY_MODERN, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False)
299                 self.termLog = wx.TextCtrl(self.termPanel, style=wx.TE_MULTILINE | wx.TE_DONTWRAP)
300                 self.termLog.SetFont(f)
301                 self.termLog.SetEditable(0)
302                 self.termInput = wx.TextCtrl(self.termPanel, style=wx.TE_PROCESS_ENTER)
303                 self.termInput.SetFont(f)
304
305                 sizer.Add(self.termLog, pos=(0, 0), flag=wx.EXPAND)
306                 sizer.Add(self.termInput, pos=(1, 0), flag=wx.EXPAND)
307                 sizer.AddGrowableCol(0)
308                 sizer.AddGrowableRow(0)
309
310                 nb.AddPage(self.termPanel, _("Term"))
311
312                 self.sizer.AddGrowableRow(6)
313                 self.sizer.AddGrowableCol(3)
314
315                 self.Bind(wx.EVT_CLOSE, self.OnClose)
316                 self.connectButton.Bind(wx.EVT_BUTTON, self.OnConnect)
317                 #self.loadButton.Bind(wx.EVT_BUTTON, self.OnLoad)
318                 self.printButton.Bind(wx.EVT_BUTTON, self.OnPrint)
319                 self.pauseButton.Bind(wx.EVT_BUTTON, self.OnPause)
320                 self.cancelButton.Bind(wx.EVT_BUTTON, self.OnCancel)
321                 self.machineLogButton.Bind(wx.EVT_BUTTON, self.OnMachineLog)
322
323                 self.Bind(wx.EVT_BUTTON, lambda e: (self.temperatureSelect.SetValue(int(profile.getProfileSettingFloat('print_temperature'))), self.machineCom.sendCommand("M104 S%d" % (int(profile.getProfileSettingFloat('print_temperature'))))), self.temperatureHeatUp)
324                 self.Bind(wx.EVT_SPINCTRL, self.OnTempChange, self.temperatureSelect)
325                 self.Bind(wx.EVT_SPINCTRL, self.OnBedTempChange, self.bedTemperatureSelect)
326
327                 self.Bind(wx.EVT_SPINCTRL, self.OnSpeedChange, self.outerWallSpeedSelect)
328                 self.Bind(wx.EVT_SPINCTRL, self.OnSpeedChange, self.innerWallSpeedSelect)
329                 self.Bind(wx.EVT_SPINCTRL, self.OnSpeedChange, self.fillSpeedSelect)
330                 self.Bind(wx.EVT_SPINCTRL, self.OnSpeedChange, self.supportSpeedSelect)
331                 self.Bind(wx.EVT_TEXT_ENTER, self.OnTermEnterLine, self.termInput)
332                 self.termInput.Bind(wx.EVT_CHAR, self.OnTermKey)
333
334                 self.Layout()
335                 self.Fit()
336                 self.Centre()
337
338                 self.statsText.SetMinSize(self.statsText.GetSize())
339
340                 self.UpdateButtonStates()
341
342                 #self.UpdateProgress()
343                 self._thread = threading.Thread(target=self._stdinMonitor)
344                 self._thread.daemon = True
345                 self._thread.start()
346
347                 if webcam.hasWebcamSupport():
348                         #Need to call the camera class on the GUI thread, or else it won't work. Shame as it hangs the GUI for about 2 seconds.
349                         wx.CallAfter(self._webcamCheck)
350
351         def _stdinMonitor(self):
352                 while True:
353                         line = sys.stdin.readline().rstrip()
354                         if line.startswith('LOAD:'):
355                                 if not self.LoadGCodeFile(line[5:]):
356                                         print 'LOADFAILED\n'
357
358         def _webcamCheck(self):
359                 self.cam = webcam.webcam()
360                 if self.cam.hasCamera():
361                         self.camPage = wx.Panel(self.tabs)
362                         sizer = wx.GridBagSizer(2, 2)
363                         self.camPage.SetSizer(sizer)
364
365                         self.timelapsEnable = wx.CheckBox(self.camPage, -1, _("Enable timelapse movie recording"))
366                         self.timelapsSavePath = wx.TextCtrl(self.camPage, -1, os.path.expanduser('~/timelaps_' + datetime.datetime.now().strftime('%Y-%m-%d_%H-%M') + '.mpg'))
367                         sizer.Add(self.timelapsEnable, pos=(0, 0), span=(1, 2), flag=wx.EXPAND)
368                         sizer.Add(self.timelapsSavePath, pos=(1, 0), span=(1, 2), flag=wx.EXPAND)
369
370                         pages = self.cam.propertyPages()
371                         self.cam.buttons = [self.timelapsEnable, self.timelapsSavePath]
372                         for page in pages:
373                                 button = wx.Button(self.camPage, -1, page)
374                                 button.index = pages.index(page)
375                                 sizer.Add(button, pos=(2, pages.index(page)))
376                                 button.Bind(wx.EVT_BUTTON, self.OnPropertyPageButton)
377                                 self.cam.buttons.append(button)
378
379                         self.campreviewEnable = wx.CheckBox(self.camPage, -1, _("Show preview"))
380                         sizer.Add(self.campreviewEnable, pos=(3, 0), span=(1, 2), flag=wx.EXPAND)
381
382                         self.camPreview = wx.Panel(self.camPage)
383                         sizer.Add(self.camPreview, pos=(4, 0), span=(1, 2), flag=wx.EXPAND)
384
385                         self.tabs.AddPage(self.camPage, _("Camera"))
386                         self.camPreview.timer = wx.Timer(self)
387                         self.Bind(wx.EVT_TIMER, self.OnCameraTimer, self.camPreview.timer)
388                         self.camPreview.timer.Start(500)
389                         self.camPreview.Bind(wx.EVT_ERASE_BACKGROUND, self.OnCameraEraseBackground)
390                 else:
391                         self.cam = None
392
393         def OnCameraTimer(self, e):
394                 if not self.campreviewEnable.GetValue():
395                         return
396                 if self.machineCom is not None and self.machineCom.isPrinting():
397                         return
398                 self.cam.takeNewImage()
399                 self.camPreview.Refresh()
400
401         def OnCameraEraseBackground(self, e):
402                 dc = e.GetDC()
403                 if not dc:
404                         dc = wx.ClientDC(self)
405                         rect = self.GetUpdateRegion().GetBox()
406                         dc.SetClippingRect(rect)
407                 dc.SetBackground(wx.Brush(self.camPreview.GetBackgroundColour(), wx.SOLID))
408                 if self.cam.getLastImage() is not None:
409                         self.camPreview.SetMinSize((self.cam.getLastImage().GetWidth(), self.cam.getLastImage().GetHeight()))
410                         self.camPage.Fit()
411                         dc.DrawBitmap(self.cam.getLastImage(), 0, 0)
412                 else:
413                         dc.Clear()
414
415         def OnPropertyPageButton(self, e):
416                 self.cam.openPropertyPage(e.GetEventObject().index)
417
418         def UpdateButtonStates(self):
419                 self.connectButton.Enable(self.machineCom is None or self.machineCom.isClosedOrError())
420                 #self.loadButton.Enable(self.machineCom == None or not (self.machineCom.isPrinting() or self.machineCom.isPaused()))
421                 self.printButton.Enable(self.machineCom is not None and self.machineCom.isOperational() and not (
422                 self.machineCom.isPrinting() or self.machineCom.isPaused()))
423                 self.temperatureHeatUp.Enable(self.machineCom is not None and self.machineCom.isOperational() and not (
424                 self.machineCom.isPrinting() or self.machineCom.isPaused()))
425                 self.pauseButton.Enable(
426                         self.machineCom is not None and (self.machineCom.isPrinting() or self.machineCom.isPaused()))
427                 if self.machineCom is not None and self.machineCom.isPaused():
428                         self.pauseButton.SetLabel(_("Resume"))
429                 else:
430                         self.pauseButton.SetLabel(_("Pause"))
431                 self.cancelButton.Enable(
432                         self.machineCom is not None and (self.machineCom.isPrinting() or self.machineCom.isPaused()))
433                 self.temperatureSelect.Enable(self.machineCom is not None and self.machineCom.isOperational())
434                 self.bedTemperatureSelect.Enable(self.machineCom is not None and self.machineCom.isOperational())
435                 self.directControlPanel.Enable(
436                         self.machineCom is not None and self.machineCom.isOperational() and not self.machineCom.isPrinting())
437                 self.machineLogButton.Show(self.machineCom is not None and self.machineCom.isClosedOrError())
438                 if self.cam is not None:
439                         for button in self.cam.buttons:
440                                 button.Enable(self.machineCom is None or not self.machineCom.isPrinting())
441
442         def UpdateProgress(self):
443                 status = ""
444                 if self.gcode is None:
445                         status += _("Loading gcode...\n")
446                 else:
447                         status += _("Filament: %(amount).2fm %(weight).2fg\n") % {'amount': self.gcode.extrusionAmount / 1000, 'weight': self.gcode.calculateWeight() * 1000}
448                         cost = self.gcode.calculateCost()
449                         if cost is not None:
450                                 status += _("Filament cost: %s\n") % (cost)
451                         #status += "Estimated print time: %02d:%02d\n" % (int(self.gcode.totalMoveTimeMinute / 60), int(self.gcode.totalMoveTimeMinute % 60))
452                 if self.machineCom is None or not self.machineCom.isPrinting():
453                         self.progress.SetValue(0)
454                         if self.gcodeList is not None:
455                                 status += 'Line: -/%d\n' % (len(self.gcodeList))
456                 else:
457                         printTime = self.machineCom.getPrintTime() / 60
458                         printTimeLeft = self.machineCom.getPrintTimeRemainingEstimate()
459                         status += 'Line: %d/%d %d%%\n' % (self.machineCom.getPrintPos(), len(self.gcodeList),
460                                                           self.machineCom.getPrintPos() * 100 / len(self.gcodeList))
461                         if self.currentZ > 0:
462                                 status += 'Height: %0.1f\n' % (self.currentZ)
463                         status += 'Print time: %02d:%02d\n' % (int(printTime / 60), int(printTime % 60))
464                         if printTimeLeft is None:
465                                 status += 'Print time left: Unknown\n'
466                         else:
467                                 status += 'Print time left: %02d:%02d\n' % (int(printTimeLeft / 60), int(printTimeLeft % 60))
468                         self.progress.SetValue(self.machineCom.getPrintPos())
469                         taskbar.setProgress(self, self.machineCom.getPrintPos(), len(self.gcodeList))
470                 if self.machineCom is not None:
471                         if self.machineCom.getTemp() > 0:
472                                 status += 'Temp: %s\n' % (' ,'.join(map(str, self.machineCom.getTemp())))
473                         if self.machineCom.getBedTemp() > 0:
474                                 status += 'Bed Temp: %d\n' % (self.machineCom.getBedTemp())
475                                 self.bedTemperatureLabel.Show(True)
476                                 self.bedTemperatureSelect.Show(True)
477                                 self.temperaturePanel.Layout()
478                         status += 'Machine state:%s\n' % (self.machineCom.getStateString())
479
480                 self.statsText.SetLabel(status.strip())
481
482         def OnConnect(self, e):
483                 if self.machineCom is not None:
484                         self.machineCom.close()
485                 self.machineCom = machineCom.MachineCom(callbackObject=self)
486                 self.UpdateButtonStates()
487                 taskbar.setBusy(self, True)
488
489         def OnLoad(self, e):
490                 pass
491
492         def OnPrint(self, e):
493                 if self.machineCom is None or not self.machineCom.isOperational():
494                         return
495                 if self.gcodeList is None:
496                         return
497                 if self.machineCom.isPrinting():
498                         return
499                 self.currentZ = -1
500                 if self.cam is not None and self.timelapsEnable.GetValue():
501                         self.cam.startTimelapse(self.timelapsSavePath.GetValue())
502                 self.machineCom.printGCode(self.gcodeList)
503                 self.UpdateButtonStates()
504
505         def OnCancel(self, e):
506                 self.pauseButton.SetLabel('Pause')
507                 self.machineCom.cancelPrint()
508                 self.machineCom.sendCommand("M84")
509                 self.machineCom.sendCommand("M104 S0")
510                 self.UpdateButtonStates()
511
512         def OnPause(self, e):
513                 if self.machineCom.isPaused():
514                         self.machineCom.setPause(False)
515                 else:
516                         self.machineCom.setPause(True)
517
518         def OnMachineLog(self, e):
519                 LogWindow('\n'.join(self.machineCom.getLog()))
520
521         def OnClose(self, e):
522                 global printWindowHandle
523                 printWindowHandle = None
524                 if self.machineCom is not None:
525                         self.machineCom.close()
526                 self.Destroy()
527
528         def OnTempChange(self, e):
529                 self.machineCom.sendCommand("M104 S%d" % (self.temperatureSelect.GetValue()))
530
531         def OnBedTempChange(self, e):
532                 self.machineCom.sendCommand("M140 S%d" % (self.bedTemperatureSelect.GetValue()))
533
534         def OnSpeedChange(self, e):
535                 if self.machineCom is None:
536                         return
537                 self.machineCom.setFeedrateModifier('WALL-OUTER', self.outerWallSpeedSelect.GetValue() / 100.0)
538                 self.machineCom.setFeedrateModifier('WALL-INNER', self.innerWallSpeedSelect.GetValue() / 100.0)
539                 self.machineCom.setFeedrateModifier('FILL', self.fillSpeedSelect.GetValue() / 100.0)
540                 self.machineCom.setFeedrateModifier('SUPPORT', self.supportSpeedSelect.GetValue() / 100.0)
541
542         def AddTermLog(self, line):
543                 if len(self.termLog.GetValue()) > 10000:
544                         self.termLog.SetValue(self.termLog.GetValue()[-10000:])
545                 self.termLog.SetInsertionPointEnd()
546                 self.termLog.AppendText(unicode(line, 'utf-8', 'replace'))
547                 #l = self.termLog.GetLastPosition()     # if needed (windows? mac?)
548                 #self.termLog.ShowPosition(l)
549
550         def OnTermEnterLine(self, e):
551                 line = self.termInput.GetValue()
552                 if line == '':
553                         return
554                 self.termLog.AppendText('>%s\n' % (line))
555                 self.machineCom.sendCommand(line)
556                 self.termHistory.append(line)
557                 self.termHistoryIdx = len(self.termHistory)
558                 self.termInput.SetValue('')
559
560         def OnTermKey(self, e):
561                 if len(self.termHistory) > 0:
562                         if e.GetKeyCode() == wx.WXK_UP:
563                                 self.termHistoryIdx -= 1
564                                 if self.termHistoryIdx < 0:
565                                         self.termHistoryIdx = len(self.termHistory) - 1
566                                 self.termInput.SetValue(self.termHistory[self.termHistoryIdx])
567                         if e.GetKeyCode() == wx.WXK_DOWN:
568                                 self.termHistoryIdx -= 1
569                                 if self.termHistoryIdx >= len(self.termHistory):
570                                         self.termHistoryIdx = 0
571                                 self.termInput.SetValue(self.termHistory[self.termHistoryIdx])
572                 e.Skip()
573
574         def OnPowerWarningChange(self, e):
575                 type = self.powerManagement.get_providing_power_source_type()
576                 if type == power.POWER_TYPE_AC and self.powerWarningText.IsShown():
577                         self.powerWarningText.Hide()
578                         self.panel.Layout()
579                         self.Layout()
580                 elif type != power.POWER_TYPE_AC and not self.powerWarningText.IsShown():
581                         self.powerWarningText.Show()
582                         self.panel.Layout()
583                         self.Layout()
584
585         def LoadGCodeFile(self, filename):
586                 if self.machineCom is not None and self.machineCom.isPrinting():
587                         return False
588                 #Send an initial M110 to reset the line counter to zero.
589                 prevLineType = lineType = 'CUSTOM'
590                 gcodeList = ["M110"]
591                 for line in open(filename, 'r'):
592                         if line.startswith(';TYPE:'):
593                                 lineType = line[6:].strip()
594                         if ';' in line:
595                                 line = line[0:line.find(';')]
596                         line = line.strip()
597                         if len(line) > 0:
598                                 if prevLineType != lineType:
599                                         gcodeList.append((line, lineType, ))
600                                 else:
601                                         gcodeList.append(line)
602                                 prevLineType = lineType
603                 gcode = gcodeInterpreter.gcode()
604                 gcode.load(gcodeList)
605                 #print "Loaded: %s (%d)" % (filename, len(gcodeList))
606                 self.filename = filename
607                 self.gcode = gcode
608                 self.gcodeList = gcodeList
609
610                 wx.CallAfter(self.progress.SetRange, len(gcodeList))
611                 wx.CallAfter(self.UpdateButtonStates)
612                 wx.CallAfter(self.UpdateProgress)
613                 return True
614
615         def sendLine(self, lineNr):
616                 if lineNr >= len(self.gcodeList):
617                         return False
618                 line = self.gcodeList[lineNr]
619                 try:
620                         if ('M104' in line or 'M109' in line) and 'S' in line:
621                                 n = int(re.search('S([0-9]*)', line).group(1))
622                                 wx.CallAfter(self.temperatureSelect.SetValue, n)
623                         if ('M140' in line or 'M190' in line) and 'S' in line:
624                                 n = int(re.search('S([0-9]*)', line).group(1))
625                                 wx.CallAfter(self.bedTemperatureSelect.SetValue, n)
626                 except:
627                         print "Unexpected error:", sys.exc_info()
628                 checksum = reduce(lambda x, y: x ^ y, map(ord, "N%d%s" % (lineNr, line)))
629                 self.machineCom.sendCommand("N%d%s*%d" % (lineNr, line, checksum))
630                 return True
631
632         def mcLog(self, message):
633                 #print message
634                 pass
635
636         def mcTempUpdate(self, temp, bedTemp, targetTemp, bedTargetTemp):
637                 self.temperatureGraph.addPoint(temp, targetTemp, bedTemp, bedTargetTemp)
638                 wx.CallAfter(self._mcTempUpdate, temp, bedTemp, targetTemp, bedTargetTemp)
639
640         def _mcTempUpdate(self, temp, bedTemp, targetTemp, bedTargetTemp):
641                 if self.temperatureSelect.GetValue() != targetTemp[0] and wx.Window.FindFocus() != self.temperatureSelect:
642                         self.temperatureSelect.SetValue(targetTemp[0])
643                 if self.bedTemperatureSelect.GetValue() != bedTargetTemp and wx.Window.FindFocus() != self.bedTemperatureSelect:
644                         self.bedTemperatureSelect.SetValue(bedTargetTemp)
645
646         def mcStateChange(self, state):
647                 if self.machineCom is not None:
648                         if state == self.machineCom.STATE_OPERATIONAL and self.cam is not None:
649                                 self.cam.endTimelapse()
650                         if state == self.machineCom.STATE_OPERATIONAL:
651                                 taskbar.setBusy(self, False)
652                         if self.machineCom.isClosedOrError():
653                                 taskbar.setBusy(self, False)
654                         if self.machineCom.isPaused():
655                                 taskbar.setPause(self, True)
656                         if self.machineCom.isClosedOrError():
657                                 print 'STATE:CLOSED'
658                         elif self.machineCom.isPrinting():
659                                 print 'STATE:PRINTING'
660                         else:
661                                 print 'STATE:IDLE'
662                 wx.CallAfter(self.UpdateButtonStates)
663                 wx.CallAfter(self.UpdateProgress)
664
665         def mcMessage(self, message):
666                 wx.CallAfter(self.AddTermLog, message)
667
668         def mcProgress(self, lineNr):
669                 wx.CallAfter(self.UpdateProgress)
670
671         def mcZChange(self, newZ):
672                 self.currentZ = newZ
673                 print 'Z:%f' % newZ
674                 if self.cam is not None:
675                         wx.CallAfter(self.cam.takeNewImage)
676                         wx.CallAfter(self.camPreview.Refresh)
677
678
679 class temperatureGraph(wx.Panel):
680         def __init__(self, parent):
681                 super(temperatureGraph, self).__init__(parent)
682
683                 self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
684                 self.Bind(wx.EVT_SIZE, self.OnSize)
685                 self.Bind(wx.EVT_PAINT, self.OnDraw)
686
687                 self.lastDraw = time.time() - 1.0
688                 self.points = []
689                 self.backBuffer = None
690                 self.addPoint([0]*16, [0]*16, 0, 0)
691                 self.SetMinSize((320, 200))
692
693         def OnEraseBackground(self, e):
694                 pass
695
696         def OnSize(self, e):
697                 if self.backBuffer is None or self.GetSize() != self.backBuffer.GetSize():
698                         self.backBuffer = wx.EmptyBitmap(*self.GetSizeTuple())
699                         self.UpdateDrawing(True)
700
701         def OnDraw(self, e):
702                 dc = wx.BufferedPaintDC(self, self.backBuffer)
703
704         def UpdateDrawing(self, force=False):
705                 now = time.time()
706                 if not force and now - self.lastDraw < 1.0:
707                         return
708                 self.lastDraw = now
709                 dc = wx.MemoryDC()
710                 dc.SelectObject(self.backBuffer)
711                 dc.Clear()
712                 dc.SetFont(wx.SystemSettings.GetFont(wx.SYS_SYSTEM_FONT))
713                 w, h = self.GetSizeTuple()
714                 bgLinePen = wx.Pen('#A0A0A0')
715                 tempPen = wx.Pen('#FF4040')
716                 tempSPPen = wx.Pen('#FFA0A0')
717                 tempPenBG = wx.Pen('#FFD0D0')
718                 bedTempPen = wx.Pen('#4040FF')
719                 bedTempSPPen = wx.Pen('#A0A0FF')
720                 bedTempPenBG = wx.Pen('#D0D0FF')
721
722                 #Draw the background up to the current temperatures.
723                 x0 = 0
724                 t0 = [0] * len(self.points[0][0])
725                 bt0 = 0
726                 tSP0 = 0
727                 btSP0 = 0
728                 for temp, tempSP, bedTemp, bedTempSP, t in self.points:
729                         x1 = int(w - (now - t))
730                         for x in xrange(x0, x1 + 1):
731                                 for n in xrange(0, len(temp)):
732                                         t = float(x - x0) / float(x1 - x0 + 1) * (temp[n] - t0[n]) + t0[n]
733                                         dc.SetPen(tempPenBG)
734                                         dc.DrawLine(x, h, x, h - (t * h / 300))
735                                 bt = float(x - x0) / float(x1 - x0 + 1) * (bedTemp - bt0) + bt0
736                                 dc.SetPen(bedTempPenBG)
737                                 dc.DrawLine(x, h, x, h - (bt * h / 300))
738                         t0 = temp
739                         bt0 = bedTemp
740                         tSP0 = tempSP
741                         btSP0 = bedTempSP
742                         x0 = x1 + 1
743
744                 #Draw the grid
745                 for x in xrange(w, 0, -30):
746                         dc.SetPen(bgLinePen)
747                         dc.DrawLine(x, 0, x, h)
748                 tmpNr = 0
749                 for y in xrange(h - 1, 0, -h * 50 / 300):
750                         dc.SetPen(bgLinePen)
751                         dc.DrawLine(0, y, w, y)
752                         dc.DrawText(str(tmpNr), 0, y - dc.GetFont().GetPixelSize().GetHeight())
753                         tmpNr += 50
754                 dc.DrawLine(0, 0, w, 0)
755                 dc.DrawLine(0, 0, 0, h)
756
757                 #Draw the main lines
758                 x0 = 0
759                 t0 = [0] * len(self.points[0][0])
760                 bt0 = 0
761                 tSP0 = [0] * len(self.points[0][0])
762                 btSP0 = 0
763                 for temp, tempSP, bedTemp, bedTempSP, t in self.points:
764                         x1 = int(w - (now - t))
765                         for x in xrange(x0, x1 + 1):
766                                 for n in xrange(0, len(temp)):
767                                         t = float(x - x0) / float(x1 - x0 + 1) * (temp[n] - t0[n]) + t0[n]
768                                         tSP = float(x - x0) / float(x1 - x0 + 1) * (tempSP[n] - tSP0[n]) + tSP0[n]
769                                         dc.SetPen(tempSPPen)
770                                         dc.DrawPoint(x, h - (tSP * h / 300))
771                                         dc.SetPen(tempPen)
772                                         dc.DrawPoint(x, h - (t * h / 300))
773                                 bt = float(x - x0) / float(x1 - x0 + 1) * (bedTemp - bt0) + bt0
774                                 btSP = float(x - x0) / float(x1 - x0 + 1) * (bedTempSP - btSP0) + btSP0
775                                 dc.SetPen(bedTempSPPen)
776                                 dc.DrawPoint(x, h - (btSP * h / 300))
777                                 dc.SetPen(bedTempPen)
778                                 dc.DrawPoint(x, h - (bt * h / 300))
779                         t0 = temp
780                         bt0 = bedTemp
781                         tSP0 = tempSP
782                         btSP0 = bedTempSP
783                         x0 = x1 + 1
784
785                 del dc
786                 self.Refresh(eraseBackground=False)
787                 self.Update()
788
789                 if len(self.points) > 0 and (time.time() - self.points[0][4]) > w + 20:
790                         self.points.pop(0)
791
792         def addPoint(self, temp, tempSP, bedTemp, bedTempSP):
793                 if bedTemp is None:
794                         bedTemp = 0
795                 if bedTempSP is None:
796                         bedTempSP = 0
797                 self.points.append((temp[:], tempSP[:], bedTemp, bedTempSP, time.time()))
798                 wx.CallAfter(self.UpdateDrawing)
799
800
801 class LogWindow(wx.Frame):
802         def __init__(self, logText):
803                 super(LogWindow, self).__init__(None, title="Machine log")
804                 self.textBox = wx.TextCtrl(self, -1, logText, style=wx.TE_MULTILINE | wx.TE_DONTWRAP | wx.TE_READONLY)
805                 self.SetSize((500, 400))
806                 self.Centre()
807                 self.Show(True)