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