printrun-src/printrun/printcore.py

changeset 15
0bbb006204fc
child 46
cce0af6351f0
equal deleted inserted replaced
14:51bf56ba3c10 15:0bbb006204fc
1 #!/usr/bin/env python
2
3 # This file is part of the Printrun suite.
4 #
5 # Printrun is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # Printrun is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with Printrun. If not, see <http://www.gnu.org/licenses/>.
17
18 __version__ = "2015.03.10"
19
20 from serialWrapper import Serial, SerialException, PARITY_ODD, PARITY_NONE
21 from select import error as SelectError
22 import threading
23 from Queue import Queue, Empty as QueueEmpty
24 import time
25 import platform
26 import os
27 import sys
28 stdin, stdout, stderr = sys.stdin, sys.stdout, sys.stderr
29 reload(sys).setdefaultencoding('utf8')
30 sys.stdin, sys.stdout, sys.stderr = stdin, stdout, stderr
31 import logging
32 import traceback
33 import errno
34 import socket
35 import re
36 from functools import wraps
37 from collections import deque
38 from printrun import gcoder
39 from .utils import install_locale, decode_utf8
40 install_locale('pronterface')
41
42 def locked(f):
43 @wraps(f)
44 def inner(*args, **kw):
45 with inner.lock:
46 return f(*args, **kw)
47 inner.lock = threading.Lock()
48 return inner
49
50 def control_ttyhup(port, disable_hup):
51 """Controls the HUPCL"""
52 if platform.system() == "Linux":
53 if disable_hup:
54 os.system("stty -F %s -hup" % port)
55 else:
56 os.system("stty -F %s hup" % port)
57
58 def enable_hup(port):
59 control_ttyhup(port, False)
60
61 def disable_hup(port):
62 control_ttyhup(port, True)
63
64 class printcore():
65 def __init__(self, port = None, baud = None, dtr=None):
66 """Initializes a printcore instance. Pass the port and baud rate to
67 connect immediately"""
68 self.baud = None
69 self.dtr = None
70 self.port = None
71 self.analyzer = gcoder.GCode()
72 # Serial instance connected to the printer, should be None when
73 # disconnected
74 self.printer = None
75 # clear to send, enabled after responses
76 # FIXME: should probably be changed to a sliding window approach
77 self.clear = 0
78 # The printer has responded to the initial command and is active
79 self.online = False
80 # is a print currently running, true if printing, false if paused
81 self.printing = False
82 self.mainqueue = None
83 self.priqueue = Queue(0)
84 self.queueindex = 0
85 self.lineno = 0
86 self.resendfrom = -1
87 self.paused = False
88 self.sentlines = {}
89 self.log = deque(maxlen = 10000)
90 self.sent = []
91 self.writefailures = 0
92 self.tempcb = None # impl (wholeline)
93 self.recvcb = None # impl (wholeline)
94 self.sendcb = None # impl (wholeline)
95 self.preprintsendcb = None # impl (wholeline)
96 self.printsendcb = None # impl (wholeline)
97 self.layerchangecb = None # impl (wholeline)
98 self.errorcb = None # impl (wholeline)
99 self.startcb = None # impl ()
100 self.endcb = None # impl ()
101 self.onlinecb = None # impl ()
102 self.loud = False # emit sent and received lines to terminal
103 self.tcp_streaming_mode = False
104 self.greetings = ['start', 'Grbl ']
105 self.wait = 0 # default wait period for send(), send_now()
106 self.read_thread = None
107 self.stop_read_thread = False
108 self.send_thread = None
109 self.stop_send_thread = False
110 self.print_thread = None
111 if port is not None and baud is not None:
112 self.connect(port, baud)
113 self.xy_feedrate = None
114 self.z_feedrate = None
115
116 def logError(self, error):
117 if self.errorcb:
118 try: self.errorcb(error)
119 except: logging.error(traceback.format_exc())
120 else:
121 logging.error(error)
122
123 @locked
124 def disconnect(self):
125 """Disconnects from printer and pauses the print
126 """
127 if self.printer:
128 if self.read_thread:
129 self.stop_read_thread = True
130 if threading.current_thread() != self.read_thread:
131 self.read_thread.join()
132 self.read_thread = None
133 if self.print_thread:
134 self.printing = False
135 self.print_thread.join()
136 self._stop_sender()
137 try:
138 self.printer.close()
139 except socket.error:
140 pass
141 except OSError:
142 pass
143 self.printer = None
144 self.online = False
145 self.printing = False
146
147 @locked
148 def connect(self, port = None, baud = None, dtr=None):
149 """Set port and baudrate if given, then connect to printer
150 """
151 if self.printer:
152 self.disconnect()
153 if port is not None:
154 self.port = port
155 if baud is not None:
156 self.baud = baud
157 if dtr is not None:
158 self.dtr = dtr
159 if self.port is not None and self.baud is not None:
160 # Connect to socket if "port" is an IP, device if not
161 host_regexp = re.compile("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$|^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$")
162 is_serial = True
163 if ":" in port:
164 bits = port.split(":")
165 if len(bits) == 2:
166 hostname = bits[0]
167 try:
168 port = int(bits[1])
169 if host_regexp.match(hostname) and 1 <= port <= 65535:
170 is_serial = False
171 except:
172 pass
173 self.writefailures = 0
174 if not is_serial:
175 self.printer_tcp = socket.socket(socket.AF_INET,
176 socket.SOCK_STREAM)
177 self.printer_tcp.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
178 self.timeout = 0.25
179 self.printer_tcp.settimeout(1.0)
180 try:
181 self.printer_tcp.connect((hostname, port))
182 self.printer_tcp.settimeout(self.timeout)
183 self.printer = self.printer_tcp.makefile()
184 except socket.error as e:
185 if(e.strerror is None): e.strerror=""
186 self.logError(_("Could not connect to %s:%s:") % (hostname, port) +
187 "\n" + _("Socket error %s:") % e.errno +
188 "\n" + e.strerror)
189 self.printer = None
190 self.printer_tcp = None
191 return
192 else:
193 disable_hup(self.port)
194 self.printer_tcp = None
195 try:
196 self.printer = Serial(port = self.port,
197 baudrate = self.baud,
198 timeout = 0.25,
199 parity = PARITY_ODD)
200 self.printer.close()
201 self.printer.parity = PARITY_NONE
202 try: #this appears not to work on many platforms, so we're going to call it but not care if it fails
203 self.printer.setDTR(dtr);
204 except:
205 #self.logError(_("Could not set DTR on this platform")) #not sure whether to output an error message
206 pass
207 self.printer.open()
208 except SerialException as e:
209 self.logError(_("Could not connect to %s at baudrate %s:") % (self.port, self.baud) +
210 "\n" + _("Serial error: %s") % e)
211 self.printer = None
212 return
213 except IOError as e:
214 self.logError(_("Could not connect to %s at baudrate %s:") % (self.port, self.baud) +
215 "\n" + _("IO error: %s") % e)
216 self.printer = None
217 return
218 self.stop_read_thread = False
219 self.read_thread = threading.Thread(target = self._listen)
220 self.read_thread.start()
221 self._start_sender()
222
223 def reset(self):
224 """Reset the printer
225 """
226 if self.printer and not self.printer_tcp:
227 self.printer.setDTR(1)
228 time.sleep(0.2)
229 self.printer.setDTR(0)
230
231 def _readline(self):
232 try:
233 try:
234 line = self.printer.readline()
235 if self.printer_tcp and not line:
236 raise OSError(-1, "Read EOF from socket")
237 except socket.timeout:
238 return ""
239
240 if len(line) > 1:
241 self.log.append(line)
242 if self.recvcb:
243 try: self.recvcb(line)
244 except: self.logError(traceback.format_exc())
245 if self.loud: logging.info("RECV: %s" % line.rstrip())
246 return line
247 except SelectError as e:
248 if 'Bad file descriptor' in e.args[1]:
249 self.logError(_(u"Can't read from printer (disconnected?) (SelectError {0}): {1}").format(e.errno, decode_utf8(e.strerror)))
250 return None
251 else:
252 self.logError(_(u"SelectError ({0}): {1}").format(e.errno, decode_utf8(e.strerror)))
253 raise
254 except SerialException as e:
255 self.logError(_(u"Can't read from printer (disconnected?) (SerialException): {0}").format(decode_utf8(str(e))))
256 return None
257 except socket.error as e:
258 self.logError(_(u"Can't read from printer (disconnected?) (Socket error {0}): {1}").format(e.errno, decode_utf8(e.strerror)))
259 return None
260 except OSError as e:
261 if e.errno == errno.EAGAIN: # Not a real error, no data was available
262 return ""
263 self.logError(_(u"Can't read from printer (disconnected?) (OS Error {0}): {1}").format(e.errno, e.strerror))
264 return None
265
266 def _listen_can_continue(self):
267 if self.printer_tcp:
268 return not self.stop_read_thread and self.printer
269 return (not self.stop_read_thread
270 and self.printer
271 and self.printer.isOpen())
272
273 def _listen_until_online(self):
274 while not self.online and self._listen_can_continue():
275 self._send("M105")
276 if self.writefailures >= 4:
277 logging.error(_("Aborting connection attempt after 4 failed writes."))
278 return
279 empty_lines = 0
280 while self._listen_can_continue():
281 line = self._readline()
282 if line is None: break # connection problem
283 # workaround cases where M105 was sent before printer Serial
284 # was online an empty line means read timeout was reached,
285 # meaning no data was received thus we count those empty lines,
286 # and once we have seen 15 in a row, we just break and send a
287 # new M105
288 # 15 was chosen based on the fact that it gives enough time for
289 # Gen7 bootloader to time out, and that the non received M105
290 # issues should be quite rare so we can wait for a long time
291 # before resending
292 if not line:
293 empty_lines += 1
294 if empty_lines == 15: break
295 else: empty_lines = 0
296 if line.startswith(tuple(self.greetings)) \
297 or line.startswith('ok') or "T:" in line:
298 self.online = True
299 if self.onlinecb:
300 try: self.onlinecb()
301 except: self.logError(traceback.format_exc())
302 return
303
304 def _listen(self):
305 """This function acts on messages from the firmware
306 """
307 self.clear = True
308 if not self.printing:
309 self._listen_until_online()
310 while self._listen_can_continue():
311 line = self._readline()
312 if line is None:
313 break
314 if line.startswith('DEBUG_'):
315 continue
316 if line.startswith(tuple(self.greetings)) or line.startswith('ok'):
317 self.clear = True
318 if line.startswith('ok') and "T:" in line and self.tempcb:
319 # callback for temp, status, whatever
320 try: self.tempcb(line)
321 except: self.logError(traceback.format_exc())
322 elif line.startswith('Error'):
323 self.logError(line)
324 # Teststrings for resend parsing # Firmware exp. result
325 # line="rs N2 Expected checksum 67" # Teacup 2
326 if line.lower().startswith("resend") or line.startswith("rs"):
327 for haystack in ["N:", "N", ":"]:
328 line = line.replace(haystack, " ")
329 linewords = line.split()
330 while len(linewords) != 0:
331 try:
332 toresend = int(linewords.pop(0))
333 self.resendfrom = toresend
334 break
335 except:
336 pass
337 self.clear = True
338 self.clear = True
339
340 def _start_sender(self):
341 self.stop_send_thread = False
342 self.send_thread = threading.Thread(target = self._sender)
343 self.send_thread.start()
344
345 def _stop_sender(self):
346 if self.send_thread:
347 self.stop_send_thread = True
348 self.send_thread.join()
349 self.send_thread = None
350
351 def _sender(self):
352 while not self.stop_send_thread:
353 try:
354 command = self.priqueue.get(True, 0.1)
355 except QueueEmpty:
356 continue
357 while self.printer and self.printing and not self.clear:
358 time.sleep(0.001)
359 self._send(command)
360 while self.printer and self.printing and not self.clear:
361 time.sleep(0.001)
362
363 def _checksum(self, command):
364 return reduce(lambda x, y: x ^ y, map(ord, command))
365
366 def startprint(self, gcode, startindex = 0):
367 """Start a print, gcode is an array of gcode commands.
368 returns True on success, False if already printing.
369 The print queue will be replaced with the contents of the data array,
370 the next line will be set to 0 and the firmware notified. Printing
371 will then start in a parallel thread.
372 """
373 if self.printing or not self.online or not self.printer:
374 return False
375 self.queueindex = startindex
376 self.mainqueue = gcode
377 self.printing = True
378 self.lineno = 0
379 self.resendfrom = -1
380 self._send("M110", -1, True)
381 if not gcode or not gcode.lines:
382 return True
383 self.clear = False
384 resuming = (startindex != 0)
385 self.print_thread = threading.Thread(target = self._print,
386 kwargs = {"resuming": resuming})
387 self.print_thread.start()
388 return True
389
390 def cancelprint(self):
391 self.pause()
392 self.paused = False
393 self.mainqueue = None
394 self.clear = True
395
396 # run a simple script if it exists, no multithreading
397 def runSmallScript(self, filename):
398 if filename is None: return
399 f = None
400 try:
401 with open(filename) as f:
402 for i in f:
403 l = i.replace("\n", "")
404 l = l[:l.find(";")] # remove comments
405 self.send_now(l)
406 except:
407 pass
408
409 def pause(self):
410 """Pauses the print, saving the current position.
411 """
412 if not self.printing: return False
413 self.paused = True
414 self.printing = False
415
416 # try joining the print thread: enclose it in try/except because we
417 # might be calling it from the thread itself
418 try:
419 self.print_thread.join()
420 except RuntimeError, e:
421 if e.message == "cannot join current thread":
422 pass
423 else:
424 self.logError(traceback.format_exc())
425 except:
426 self.logError(traceback.format_exc())
427
428 self.print_thread = None
429
430 # saves the status
431 self.pauseX = self.analyzer.abs_x
432 self.pauseY = self.analyzer.abs_y
433 self.pauseZ = self.analyzer.abs_z
434 self.pauseE = self.analyzer.abs_e
435 self.pauseF = self.analyzer.current_f
436 self.pauseRelative = self.analyzer.relative
437
438 def resume(self):
439 """Resumes a paused print.
440 """
441 if not self.paused: return False
442 if self.paused:
443 # restores the status
444 self.send_now("G90") # go to absolute coordinates
445
446 xyFeedString = ""
447 zFeedString = ""
448 if self.xy_feedrate is not None:
449 xyFeedString = " F" + str(self.xy_feedrate)
450 if self.z_feedrate is not None:
451 zFeedString = " F" + str(self.z_feedrate)
452
453 self.send_now("G1 X%s Y%s%s" % (self.pauseX, self.pauseY,
454 xyFeedString))
455 self.send_now("G1 Z" + str(self.pauseZ) + zFeedString)
456 self.send_now("G92 E" + str(self.pauseE))
457
458 # go back to relative if needed
459 if self.pauseRelative: self.send_now("G91")
460 # reset old feed rate
461 self.send_now("G1 F" + str(self.pauseF))
462
463 self.paused = False
464 self.printing = True
465 self.print_thread = threading.Thread(target = self._print,
466 kwargs = {"resuming": True})
467 self.print_thread.start()
468
469 def send(self, command, wait = 0):
470 """Adds a command to the checksummed main command queue if printing, or
471 sends the command immediately if not printing"""
472
473 if self.online:
474 if self.printing:
475 self.mainqueue.append(command)
476 else:
477 self.priqueue.put_nowait(command)
478 else:
479 self.logError(_("Not connected to printer."))
480
481 def send_now(self, command, wait = 0):
482 """Sends a command to the printer ahead of the command queue, without a
483 checksum"""
484 if self.online:
485 self.priqueue.put_nowait(command)
486 else:
487 self.logError(_("Not connected to printer."))
488
489 def _print(self, resuming = False):
490 self._stop_sender()
491 try:
492 if self.startcb:
493 # callback for printing started
494 try: self.startcb(resuming)
495 except:
496 self.logError(_("Print start callback failed with:") +
497 "\n" + traceback.format_exc())
498 while self.printing and self.printer and self.online:
499 self._sendnext()
500 self.sentlines = {}
501 self.log.clear()
502 self.sent = []
503 if self.endcb:
504 # callback for printing done
505 try: self.endcb()
506 except:
507 self.logError(_("Print end callback failed with:") +
508 "\n" + traceback.format_exc())
509 except:
510 self.logError(_("Print thread died due to the following error:") +
511 "\n" + traceback.format_exc())
512 finally:
513 self.print_thread = None
514 self._start_sender()
515
516 def process_host_command(self, command):
517 """only ;@pause command is implemented as a host command in printcore, but hosts are free to reimplement this method"""
518 command = command.lstrip()
519 if command.startswith(";@pause"):
520 self.pause()
521
522 def _sendnext(self):
523 if not self.printer:
524 return
525 while self.printer and self.printing and not self.clear:
526 time.sleep(0.001)
527 # Only wait for oks when using serial connections or when not using tcp
528 # in streaming mode
529 if not self.printer_tcp or not self.tcp_streaming_mode:
530 self.clear = False
531 if not (self.printing and self.printer and self.online):
532 self.clear = True
533 return
534 if self.resendfrom < self.lineno and self.resendfrom > -1:
535 self._send(self.sentlines[self.resendfrom], self.resendfrom, False)
536 self.resendfrom += 1
537 return
538 self.resendfrom = -1
539 if not self.priqueue.empty():
540 self._send(self.priqueue.get_nowait())
541 self.priqueue.task_done()
542 return
543 if self.printing and self.queueindex < len(self.mainqueue):
544 (layer, line) = self.mainqueue.idxs(self.queueindex)
545 gline = self.mainqueue.all_layers[layer][line]
546 if self.layerchangecb and self.queueindex > 0:
547 (prev_layer, prev_line) = self.mainqueue.idxs(self.queueindex - 1)
548 if prev_layer != layer:
549 try: self.layerchangecb(layer)
550 except: self.logError(traceback.format_exc())
551 if self.preprintsendcb:
552 if self.queueindex + 1 < len(self.mainqueue):
553 (next_layer, next_line) = self.mainqueue.idxs(self.queueindex + 1)
554 next_gline = self.mainqueue.all_layers[next_layer][next_line]
555 else:
556 next_gline = None
557 gline = self.preprintsendcb(gline, next_gline)
558 if gline is None:
559 self.queueindex += 1
560 self.clear = True
561 return
562 tline = gline.raw
563 if tline.lstrip().startswith(";@"): # check for host command
564 self.process_host_command(tline)
565 self.queueindex += 1
566 self.clear = True
567 return
568
569 # Strip comments
570 tline = gcoder.gcode_strip_comment_exp.sub("", tline).strip()
571 if tline:
572 self._send(tline, self.lineno, True)
573 self.lineno += 1
574 if self.printsendcb:
575 try: self.printsendcb(gline)
576 except: self.logError(traceback.format_exc())
577 else:
578 self.clear = True
579 self.queueindex += 1
580 else:
581 self.printing = False
582 self.clear = True
583 if not self.paused:
584 self.queueindex = 0
585 self.lineno = 0
586 self._send("M110", -1, True)
587
588 def _send(self, command, lineno = 0, calcchecksum = False):
589 # Only add checksums if over serial (tcp does the flow control itself)
590 if calcchecksum and not self.printer_tcp:
591 prefix = "N" + str(lineno) + " " + command
592 command = prefix + "*" + str(self._checksum(prefix))
593 if "M110" not in command:
594 self.sentlines[lineno] = command
595 if self.printer:
596 self.sent.append(command)
597 # run the command through the analyzer
598 gline = None
599 try:
600 gline = self.analyzer.append(command, store = False)
601 except:
602 logging.warning(_("Could not analyze command %s:") % command +
603 "\n" + traceback.format_exc())
604 if self.loud:
605 logging.info("SENT: %s" % command)
606 if self.sendcb:
607 try: self.sendcb(command, gline)
608 except: self.logError(traceback.format_exc())
609 try:
610 self.printer.write(str(command + "\n"))
611 if self.printer_tcp:
612 try:
613 self.printer.flush()
614 except socket.timeout:
615 pass
616 self.writefailures = 0
617 except socket.error as e:
618 if e.errno is None:
619 self.logError(_(u"Can't write to printer (disconnected ?):") +
620 "\n" + traceback.format_exc())
621 else:
622 self.logError(_(u"Can't write to printer (disconnected?) (Socket error {0}): {1}").format(e.errno, decode_utf8(e.strerror)))
623 self.writefailures += 1
624 except SerialException as e:
625 self.logError(_(u"Can't write to printer (disconnected?) (SerialException): {0}").format(decode_utf8(str(e))))
626 self.writefailures += 1
627 except RuntimeError as e:
628 self.logError(_(u"Socket connection broken, disconnected. ({0}): {1}").format(e.errno, decode_utf8(e.strerror)))
629 self.writefailures += 1

mercurial