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