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