chiark / gitweb /
Add pause button to printing interface, and auto pause when M0 or M1 is hit.
[cura.git] / Cura / gui / printWindow.py
1 from __future__ import absolute_import\r
2 import __init__\r
3 \r
4 import wx, threading, re, subprocess, sys, os\r
5 from wx.lib import buttons\r
6 \r
7 from gui import icon\r
8 from gui import toolbarUtil\r
9 from util import machineCom\r
10 from util import profile\r
11 from util import gcodeInterpreter\r
12 \r
13 printWindowMonitorHandle = None\r
14 \r
15 def printFile(filename):\r
16         global printWindowMonitorHandle\r
17         if printWindowMonitorHandle == None:\r
18                 printWindowMonitorHandle = printProcessMonitor()\r
19         printWindowMonitorHandle.loadFile(filename)\r
20 \r
21 \r
22 def startPrintInterface(filename):\r
23         #startPrintInterface is called from the main script when we want the printer interface to run in a seperate process.\r
24         # 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).\r
25         app = wx.App(False)\r
26         printWindowHandle = printWindow()\r
27         printWindowHandle.Show(True)\r
28         printWindowHandle.Raise()\r
29         printWindowHandle.OnConnect(None)\r
30         printWindowHandle.LoadGCodeFile(filename)\r
31         app.MainLoop()\r
32 \r
33 class printProcessMonitor():\r
34         def __init__(self):\r
35                 self.handle = None\r
36         \r
37         def loadFile(self, filename):\r
38                 if self.handle == None:\r
39                         self.handle = subprocess.Popen([sys.executable, sys.argv[0], '-r', filename], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)\r
40                         self.thread = threading.Thread(target=self.Monitor)\r
41                         self.thread.start()\r
42                 else:\r
43                         self.handle.stdin.write(filename + '\n')\r
44         \r
45         def Monitor(self):\r
46                 p = self.handle\r
47                 line = p.stdout.readline()\r
48                 while(len(line) > 0):\r
49                         print line.rstrip()\r
50                         line = p.stdout.readline()\r
51                 p.wait()\r
52                 self.handle = None\r
53                 self.thread = None\r
54 \r
55 class PrintCommandButton(buttons.GenBitmapButton):\r
56         def __init__(self, parent, command, bitmapFilename, size=(20,20)):\r
57                 self.bitmap = toolbarUtil.getBitmapImage(bitmapFilename)\r
58                 super(PrintCommandButton, self).__init__(parent.directControlPanel, -1, self.bitmap, size=size)\r
59 \r
60                 self.command = command\r
61                 self.parent = parent\r
62 \r
63                 self.SetBezelWidth(1)\r
64                 self.SetUseFocusIndicator(False)\r
65 \r
66                 self.Bind(wx.EVT_BUTTON, self.OnClick)\r
67 \r
68         def OnClick(self, e):\r
69                 self.parent.sendCommand("G91")\r
70                 self.parent.sendCommand(self.command)\r
71                 self.parent.sendCommand("G90")\r
72                 e.Skip()\r
73 \r
74 class printWindow(wx.Frame):\r
75         "Main user interface window"\r
76         def __init__(self):\r
77                 super(printWindow, self).__init__(None, -1, title='Printing')\r
78                 self.machineCom = None\r
79                 self.machineConnected = False\r
80                 self.thread = None\r
81                 self.gcode = None\r
82                 self.gcodeList = None\r
83                 self.sendList = []\r
84                 self.printIdx = None\r
85                 self.temp = None\r
86                 self.bedTemp = None\r
87                 self.bufferLineCount = 4\r
88                 self.sendCnt = 0\r
89                 self.feedrateRatioOuterWall = 1.0\r
90                 self.feedrateRatioInnerWall = 1.0\r
91                 self.feedrateRatioFill = 1.0\r
92                 self.feedrateRatioSupport = 1.0\r
93                 self.pause = False\r
94 \r
95                 #self.SetIcon(icon.getMainIcon())\r
96                 \r
97                 self.SetSizer(wx.BoxSizer())\r
98                 self.panel = wx.Panel(self)\r
99                 self.GetSizer().Add(self.panel, 1, flag=wx.EXPAND)\r
100                 self.sizer = wx.GridBagSizer(2, 2)\r
101                 self.panel.SetSizer(self.sizer)\r
102                 \r
103                 sb = wx.StaticBox(self.panel, label="Statistics")\r
104                 boxsizer = wx.StaticBoxSizer(sb, wx.VERTICAL)\r
105                 self.statsText = wx.StaticText(self.panel, -1, "Filament: ####.##m #.##g\nPrint time: #####:##")\r
106                 boxsizer.Add(self.statsText, flag=wx.LEFT, border=5)\r
107                 \r
108                 self.sizer.Add(boxsizer, pos=(0,0), span=(5,1), flag=wx.EXPAND)\r
109                 \r
110                 self.connectButton = wx.Button(self.panel, -1, 'Connect')\r
111                 #self.loadButton = wx.Button(self.panel, -1, 'Load GCode')\r
112                 self.printButton = wx.Button(self.panel, -1, 'Print GCode')\r
113                 self.pauseButton = wx.Button(self.panel, -1, 'Pause')\r
114                 self.cancelButton = wx.Button(self.panel, -1, 'Cancel print')\r
115                 self.progress = wx.Gauge(self.panel, -1)\r
116                 \r
117                 self.sizer.Add(self.connectButton, pos=(0,1))\r
118                 #self.sizer.Add(self.loadButton, pos=(1,1))\r
119                 self.sizer.Add(self.printButton, pos=(2,1))\r
120                 self.sizer.Add(self.pauseButton, pos=(3,1))\r
121                 self.sizer.Add(self.cancelButton, pos=(4,1))\r
122                 self.sizer.Add(self.progress, pos=(5,0), span=(1,2), flag=wx.EXPAND)\r
123 \r
124                 nb = wx.Notebook(self.panel)\r
125                 self.sizer.Add(nb, pos=(0,3), span=(7,4))\r
126                 \r
127                 self.temperaturePanel = wx.Panel(nb)\r
128                 sizer = wx.GridBagSizer(2, 2)\r
129                 self.temperaturePanel.SetSizer(sizer)\r
130 \r
131                 self.temperatureSelect = wx.SpinCtrl(self.temperaturePanel, -1, '0', size=(21*3,21), style=wx.SP_ARROW_KEYS)\r
132                 self.temperatureSelect.SetRange(0, 400)\r
133                 self.bedTemperatureLabel = wx.StaticText(self.temperaturePanel, -1, "BedTemp:")\r
134                 self.bedTemperatureSelect = wx.SpinCtrl(self.temperaturePanel, -1, '0', size=(21*3,21), style=wx.SP_ARROW_KEYS)\r
135                 self.bedTemperatureSelect.SetRange(0, 400)\r
136                 self.bedTemperatureLabel.Show(False)\r
137                 self.bedTemperatureSelect.Show(False)\r
138                 \r
139                 sizer.Add(wx.StaticText(self.temperaturePanel, -1, "Temp:"), pos=(0,0))\r
140                 sizer.Add(self.temperatureSelect, pos=(0,1))\r
141                 sizer.Add(self.bedTemperatureLabel, pos=(1,0))\r
142                 sizer.Add(self.bedTemperatureSelect, pos=(1,1))\r
143 \r
144                 nb.AddPage(self.temperaturePanel, 'Temp')\r
145 \r
146                 self.directControlPanel = wx.Panel(nb)\r
147                 \r
148                 sizer = wx.GridBagSizer(2, 2)\r
149                 self.directControlPanel.SetSizer(sizer)\r
150                 sizer.Add(PrintCommandButton(self, 'G1 Y100 F6000', 'print-move-y100.png'), pos=(0,3))\r
151                 sizer.Add(PrintCommandButton(self, 'G1 Y10 F6000', 'print-move-y10.png'), pos=(1,3))\r
152                 sizer.Add(PrintCommandButton(self, 'G1 Y1 F6000', 'print-move-y1.png'), pos=(2,3))\r
153 \r
154                 sizer.Add(PrintCommandButton(self, 'G1 Y-1 F6000', 'print-move-y-1.png'), pos=(4,3))\r
155                 sizer.Add(PrintCommandButton(self, 'G1 Y-10 F6000', 'print-move-y-10.png'), pos=(5,3))\r
156                 sizer.Add(PrintCommandButton(self, 'G1 Y-100 F6000', 'print-move-y-100.png'), pos=(6,3))\r
157 \r
158                 sizer.Add(PrintCommandButton(self, 'G1 X-100 F6000', 'print-move-x-100.png'), pos=(3,0))\r
159                 sizer.Add(PrintCommandButton(self, 'G1 X-10 F6000', 'print-move-x-10.png'), pos=(3,1))\r
160                 sizer.Add(PrintCommandButton(self, 'G1 X-1 F6000', 'print-move-x-1.png'), pos=(3,2))\r
161 \r
162                 sizer.Add(PrintCommandButton(self, 'G28 X0 Y0', 'print-move-home.png'), pos=(3,3))\r
163 \r
164                 sizer.Add(PrintCommandButton(self, 'G1 X1 F6000', 'print-move-x1.png'), pos=(3,4))\r
165                 sizer.Add(PrintCommandButton(self, 'G1 X10 F6000', 'print-move-x10.png'), pos=(3,5))\r
166                 sizer.Add(PrintCommandButton(self, 'G1 X100 F6000', 'print-move-x100.png'), pos=(3,6))\r
167 \r
168                 sizer.Add(PrintCommandButton(self, 'G1 Z10 F200', 'print-move-z10.png'), pos=(0,7))\r
169                 sizer.Add(PrintCommandButton(self, 'G1 Z1 F200', 'print-move-z1.png'), pos=(1,7))\r
170                 sizer.Add(PrintCommandButton(self, 'G1 Z0.1 F200', 'print-move-z0.1.png'), pos=(2,7))\r
171 \r
172                 sizer.Add(PrintCommandButton(self, 'G28 Z0', 'print-move-home.png'), pos=(3,7))\r
173 \r
174                 sizer.Add(PrintCommandButton(self, 'G1 Z-0.1 F200', 'print-move-z-0.1.png'), pos=(4,7))\r
175                 sizer.Add(PrintCommandButton(self, 'G1 Z-1 F200', 'print-move-z-1.png'), pos=(5,7))\r
176                 sizer.Add(PrintCommandButton(self, 'G1 Z-10 F200', 'print-move-z-10.png'), pos=(6,7))\r
177 \r
178                 nb.AddPage(self.directControlPanel, 'Jog')\r
179 \r
180                 self.speedPanel = wx.Panel(nb)\r
181                 sizer = wx.GridBagSizer(2, 2)\r
182                 self.speedPanel.SetSizer(sizer)\r
183 \r
184                 self.outerWallSpeedSelect = wx.SpinCtrl(self.speedPanel, -1, '100', size=(21*3,21), style=wx.SP_ARROW_KEYS)\r
185                 self.outerWallSpeedSelect.SetRange(5, 1000)\r
186                 self.innerWallSpeedSelect = wx.SpinCtrl(self.speedPanel, -1, '100', size=(21*3,21), style=wx.SP_ARROW_KEYS)\r
187                 self.innerWallSpeedSelect.SetRange(5, 1000)\r
188                 self.fillSpeedSelect = wx.SpinCtrl(self.speedPanel, -1, '100', size=(21*3,21), style=wx.SP_ARROW_KEYS)\r
189                 self.fillSpeedSelect.SetRange(5, 1000)\r
190                 self.supportSpeedSelect = wx.SpinCtrl(self.speedPanel, -1, '100', size=(21*3,21), style=wx.SP_ARROW_KEYS)\r
191                 self.supportSpeedSelect.SetRange(5, 1000)\r
192                 \r
193                 sizer.Add(wx.StaticText(self.speedPanel, -1, "Outer wall:"), pos=(0,0))\r
194                 sizer.Add(self.outerWallSpeedSelect, pos=(0,1))\r
195                 sizer.Add(wx.StaticText(self.speedPanel, -1, "%"), pos=(0,2))\r
196                 sizer.Add(wx.StaticText(self.speedPanel, -1, "Inner wall:"), pos=(1,0))\r
197                 sizer.Add(self.innerWallSpeedSelect, pos=(1,1))\r
198                 sizer.Add(wx.StaticText(self.speedPanel, -1, "%"), pos=(1,2))\r
199                 sizer.Add(wx.StaticText(self.speedPanel, -1, "Fill:"), pos=(2,0))\r
200                 sizer.Add(self.fillSpeedSelect, pos=(2,1))\r
201                 sizer.Add(wx.StaticText(self.speedPanel, -1, "%"), pos=(2,2))\r
202                 sizer.Add(wx.StaticText(self.speedPanel, -1, "Support:"), pos=(3,0))\r
203                 sizer.Add(self.supportSpeedSelect, pos=(3,1))\r
204                 sizer.Add(wx.StaticText(self.speedPanel, -1, "%"), pos=(3,2))\r
205 \r
206                 nb.AddPage(self.speedPanel, 'Speed')\r
207 \r
208                 self.sizer.AddGrowableRow(3)\r
209                 self.sizer.AddGrowableCol(0)\r
210                 \r
211                 self.Bind(wx.EVT_CLOSE, self.OnClose)\r
212                 self.connectButton.Bind(wx.EVT_BUTTON, self.OnConnect)\r
213                 #self.loadButton.Bind(wx.EVT_BUTTON, self.OnLoad)\r
214                 self.printButton.Bind(wx.EVT_BUTTON, self.OnPrint)\r
215                 self.pauseButton.Bind(wx.EVT_BUTTON, self.OnPause)\r
216                 self.cancelButton.Bind(wx.EVT_BUTTON, self.OnCancel)\r
217                 \r
218                 self.Bind(wx.EVT_SPINCTRL, self.OnTempChange, self.temperatureSelect)\r
219                 self.Bind(wx.EVT_SPINCTRL, self.OnBedTempChange, self.bedTemperatureSelect)\r
220 \r
221                 self.Bind(wx.EVT_SPINCTRL, self.OnSpeedChange, self.outerWallSpeedSelect)\r
222                 self.Bind(wx.EVT_SPINCTRL, self.OnSpeedChange, self.innerWallSpeedSelect)\r
223                 self.Bind(wx.EVT_SPINCTRL, self.OnSpeedChange, self.fillSpeedSelect)\r
224                 self.Bind(wx.EVT_SPINCTRL, self.OnSpeedChange, self.supportSpeedSelect)\r
225                 \r
226                 self.Layout()\r
227                 self.Fit()\r
228                 self.Centre()\r
229 \r
230                 self.UpdateButtonStates()\r
231                 self.UpdateProgress()\r
232         \r
233         def UpdateButtonStates(self):\r
234                 self.connectButton.Enable(not self.machineConnected)\r
235                 #self.loadButton.Enable(self.printIdx == None)\r
236                 self.printButton.Enable(self.machineConnected and self.gcodeList != None and self.printIdx == None)\r
237                 self.pauseButton.Enable(self.printIdx != None)\r
238                 self.cancelButton.Enable(self.printIdx != None)\r
239                 self.temperatureSelect.Enable(self.machineConnected)\r
240                 self.bedTemperatureSelect.Enable(self.machineConnected)\r
241                 self.directControlPanel.Enable(self.machineConnected)\r
242         \r
243         def UpdateProgress(self):\r
244                 status = ""\r
245                 if self.gcode != None:\r
246                         status += "Filament: %.2fm %.2fg\n" % (self.gcode.extrusionAmount / 1000, self.gcode.calculateWeight() * 1000)\r
247                         cost = self.gcode.calculateCost()\r
248                         if cost != False:\r
249                                 status += "Filament cost: %s\n" % (cost)\r
250                         status += "Print time: %02d:%02d\n" % (int(self.gcode.totalMoveTimeMinute / 60), int(self.gcode.totalMoveTimeMinute % 60))\r
251                 if self.printIdx == None:\r
252                         self.progress.SetValue(0)\r
253                         if self.gcodeList != None:\r
254                                 status += 'Line: -/%d\n' % (len(self.gcodeList))\r
255                 else:\r
256                         status += 'Line: %d/%d\n' % (self.printIdx, len(self.gcodeList))\r
257                         self.progress.SetValue(self.printIdx)\r
258                 if self.temp != None:\r
259                         status += 'Temp: %d\n' % (self.temp)\r
260                 if self.bedTemp != None and self.bedTemp > 0:\r
261                         status += 'Bed Temp: %d\n' % (self.bedTemp)\r
262                         self.bedTemperatureLabel.Show(True)\r
263                         self.bedTemperatureSelect.Show(True)\r
264                         self.temperaturePanel.Layout()\r
265                 self.statsText.SetLabel(status.strip())\r
266                 self.Layout()\r
267         \r
268         def OnConnect(self, e):\r
269                 if self.machineCom != None:\r
270                         self.machineCom.close()\r
271                         self.thread.join()\r
272                 self.machineCom = machineCom.MachineCom()\r
273                 self.thread = threading.Thread(target=self.PrinterMonitor)\r
274                 self.thread.start()\r
275                 self.UpdateButtonStates()\r
276         \r
277         def OnLoad(self, e):\r
278                 pass\r
279         \r
280         def OnPrint(self, e):\r
281                 if not self.machineConnected:\r
282                         return\r
283                 if self.gcodeList == None:\r
284                         return\r
285                 if self.printIdx != None:\r
286                         return\r
287                 self.printIdx = 1\r
288                 self.sendLine(0)\r
289                 self.sendCnt = self.bufferLineCount\r
290                 self.UpdateButtonStates()\r
291         \r
292         def OnCancel(self, e):\r
293                 self.printIdx = None\r
294                 self.pause = False\r
295                 self.pauseButton.SetLabel('Pause')\r
296                 self.sendCommand("M84")\r
297                 self.UpdateButtonStates()\r
298         \r
299         def OnPause(self, e):\r
300                 if self.pause:\r
301                         self.pause = False\r
302                         self.sendLine(self.printIdx)\r
303                         self.printIdx += 1\r
304                         self.pauseButton.SetLabel('Pause')\r
305                 else:\r
306                         self.pause = True\r
307                         self.pauseButton.SetLabel('Resume')\r
308         \r
309         def OnClose(self, e):\r
310                 global printWindowHandle\r
311                 printWindowHandle = None\r
312                 if self.machineCom != None:\r
313                         self.machineCom.close()\r
314                         self.thread.join()\r
315                 self.Destroy()\r
316 \r
317         def OnTempChange(self, e):\r
318                 self.sendCommand("M104 S%d" % (self.temperatureSelect.GetValue()))\r
319 \r
320         def OnBedTempChange(self, e):\r
321                 self.sendCommand("M140 S%d" % (self.bedTemperatureSelect.GetValue()))\r
322         \r
323         def OnSpeedChange(self, e):\r
324                 self.feedrateRatioOuterWall = self.outerWallSpeedSelect.GetValue() / 100.0\r
325                 self.feedrateRatioInnerWall = self.innerWallSpeedSelect.GetValue() / 100.0\r
326                 self.feedrateRatioFill = self.fillSpeedSelect.GetValue() / 100.0\r
327                 self.feedrateRatioSupport = self.supportSpeedSelect.GetValue() / 100.0\r
328 \r
329         def LoadGCodeFile(self, filename):\r
330                 if self.printIdx != None:\r
331                         return\r
332                 #Send an initial M110 to reset the line counter to zero.\r
333                 lineType = 'CUSTOM'\r
334                 gcodeList = ["M110"]\r
335                 typeList = [lineType]\r
336                 for line in open(filename, 'r'):\r
337                         if line.startswith(';TYPE:'):\r
338                                 lineType = line[6:].strip()\r
339                         if ';' in line:\r
340                                 line = line[0:line.find(';')]\r
341                         line = line.strip()\r
342                         if len(line) > 0:\r
343                                 gcodeList.append(line)\r
344                                 typeList.append(lineType)\r
345                 gcode = gcodeInterpreter.gcode()\r
346                 gcode.loadList(gcodeList)\r
347                 print "Loaded: %s (%d)" % (filename, len(gcodeList))\r
348                 self.progress.SetRange(len(gcodeList))\r
349                 self.gcode = gcode\r
350                 self.gcodeList = gcodeList\r
351                 self.typeList = typeList\r
352                 self.UpdateButtonStates()\r
353                 self.UpdateProgress()\r
354                 \r
355         def sendCommand(self, cmd):\r
356                 if self.machineConnected:\r
357                         if self.printIdx == None or self.pause:\r
358                                 self.machineCom.sendCommand(cmd)\r
359                         else:\r
360                                 self.sendList.append(cmd)\r
361 \r
362         def sendLine(self, lineNr):\r
363                 if lineNr >= len(self.gcodeList):\r
364                         return False\r
365                 line = self.gcodeList[lineNr]\r
366                 if self.typeList[lineNr] == 'WALL-OUTER':\r
367                         line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self.feedrateRatioOuterWall)), line)\r
368                 if self.typeList[lineNr] == 'WALL-INNER':\r
369                         line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self.feedrateRatioInnerWall)), line)\r
370                 if self.typeList[lineNr] == 'FILL':\r
371                         line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self.feedrateRatioFill)), line)\r
372                 if self.typeList[lineNr] == 'SUPPORT':\r
373                         line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self.feedrateRatioSupport)), line)\r
374                 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (lineNr, line)))\r
375                 self.machineCom.sendCommand("N%d%s*%d" % (lineNr, line, checksum))\r
376                 if line == 'M0' or line == 'M1':\r
377                         self.OnPause(None)\r
378                 return True\r
379 \r
380         def PrinterMonitor(self):\r
381                 while True:\r
382                         line = self.machineCom.readline()\r
383                         if line == None:\r
384                                 self.machineConnected = False\r
385                                 wx.CallAfter(self.UpdateButtonStates)\r
386                                 return\r
387                         if self.machineConnected:\r
388                                 while self.sendCnt > 0 and not self.pause:\r
389                                         self.sendLine(self.printIdx)\r
390                                         self.printIdx += 1\r
391                                         self.sendCnt -= 1\r
392                         elif line.startswith("start"):\r
393                                 self.machineConnected = True\r
394                                 wx.CallAfter(self.UpdateButtonStates)\r
395                         if 'T:' in line:\r
396                                 self.temp = float(re.search("[0-9\.]*", line.split('T:')[1]).group(0))\r
397                                 if 'B:' in line:\r
398                                         self.bedTemp = float(re.search("[0-9\.]*", line.split('B:')[1]).group(0))\r
399                                 wx.CallAfter(self.UpdateProgress)\r
400                         if line == '':  #When we have a communication "timeout" and we're not sending gcode, then read the temperature.\r
401                                 if self.printIdx == None or self.pause:\r
402                                         self.machineCom.sendCommand("M105")\r
403                         if self.printIdx != None:\r
404                                 if line.startswith("ok"):\r
405                                         if len(self.sendList) > 0:\r
406                                                 self.machineCom.sendCommand(self.sendList.pop(0))\r
407                                         elif self.pause:\r
408                                                 self.sendCnt += 1\r
409                                         else:\r
410                                                 if self.sendLine(self.printIdx):\r
411                                                         self.printIdx += 1\r
412                                                 else:\r
413                                                         self.printIdx = None\r
414                                                         wx.CallAfter(self.UpdateButtonStates)\r
415                                                 wx.CallAfter(self.UpdateProgress)\r
416                                 elif "resend" in line.lower() or "rs" in line:\r
417                                         try:\r
418                                                 lineNr=int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])\r
419                                         except:\r
420                                                 if "rs" in line:\r
421                                                         lineNr=int(line.split()[1])\r
422                                         self.printIdx = lineNr\r
423                                         #we should actually resend the line here, but we also get an "ok" for each error from Marlin. And thus we'll resend on the OK.\r
424 \r