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