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